NOPT042 Constraint programming: Tutorial 8 - Global constraints¶
Example: Magic sequence¶
A magic sequence of length $n$ is a sequence of integers $x_0,\dots,x_{n−1}$ between $0$ and $n-1$, such that for all $i\in\{0,\dots,n-1\}$, the number $i$ occurs exactly $x_i$-times in the sequence. For instance, 6,2,1,0,0,0,1,0,0,0 is a magic sequence since 0 occurs 6 times in it, 1 occurs twice, etc.
(Problem from the book.)
Let's maximize the sum of the numbers in the sequence.
!time picat magic-sequence/magic-sequence.pi 8
CPU time 0.001 seconds. Backtracks: 20 [4,2,1,0,1,0,0,0] real 0m0.026s user 0m0.015s sys 0m0.006s
The constraint global_cardinality¶
global_cardinality(List, Pairs)
Let List be a list of integer-domain variables
[X1, . . ., Xd], and Pairs be a list of pairs [K1-V1, . . ., Kn-Vn], where each key
Ki is a unique integer, and each Vi is an integer-domain variable. The constraint is true if
every element of List is equal to some key, and, for each pair Ki-Vi, exactly Vi elements
of List are equal to Ki. This constraint can be defined as follows:
global_cardinality(List,Pairs) =>
foreach($Key-V in Pairs)
sum([B : E in List, B#<=>(E#=Key)]) #= V
end.
---from the guide
!cat magic-sequence/magic-sequence.pi
/*********************************************************** Adapted from magic_sequence.pi from Constraint Solving and Planning with Picat, Springer by Neng-Fa Zhou, Hakan Kjellerstrand, and Jonathan Fruhman ***********************************************************/ import cp. main([N]) => N := N.to_int, magic_sequence(N,Sequence), println(Sequence). magic_sequence(N, Sequence) => Sequence = new_list(N), Sequence :: 0..N-1, % create list: [0-Sequence[1], 1-Sequence[2], ...] Pairs = [$I-Sequence[I+1] : I in 0..N-1], global_cardinality(Sequence,Pairs), time2(solve(Sequence)).
!time picat magic-sequence/magic-sequence2.pi 64
!time picat magic-sequence/magic-sequence2.pi 400
CPU time 0.008 seconds. Backtracks: 7 [60,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0] real 0m0.042s user 0m0.034s sys 0m0.004s
CPU time 0.256 seconds. Backtracks: 7 [396,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0] real 0m7.417s user 0m7.071s sys 0m0.344s
!cat magic-sequence/magic-sequence2.pi
/*********************************************************** Adapted from magic_sequence.pi from Constraint Solving and Planning with Picat, Springer by Neng-Fa Zhou, Hakan Kjellerstrand, and Jonathan Fruhman ***********************************************************/ import cp. main([N]) => N := N.to_int, magic_sequence(N,Sequence), println(Sequence). magic_sequence(N, Sequence) => Sequence = new_list(N), Sequence :: 0..N-1, % % create list: [0-Sequence[1], 1-Sequence[2], ...] Pairs = [$I-Sequence[I+1] : I in 0..N-1], global_cardinality(Sequence,Pairs), % extra/redudant (implicit) constraints to speed up the model N #= sum(Sequence), Integers = [I : I in 0..N-1], scalar_product(Integers, Sequence, N), time2(solve([ff], Sequence)).
!time picat magic-sequence/magic-sequence3.pi 400
!time picat magic-sequence/magic-sequence3.pi 1024
CPU time 0.103 seconds. Backtracks: 7 [396,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0] real 0m0.333s user 0m0.268s sys 0m0.053s
CPU time 0.487 seconds. Backtracks: 7 [1020,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0] real 0m4.072s user 0m3.797s sys 0m0.272s
!cat magic-sequence/magic-sequence3.pi
/*********************************************************** Adapted from magic_sequence.pi from Constraint Solving and Planning with Picat, Springer by Neng-Fa Zhou, Hakan Kjellerstrand, and Jonathan Fruhman ***********************************************************/ import cp. main([N]) => N := N.to_int, magic_sequence(N,Sequence), println(Sequence). magic_sequence(N, Sequence) => Sequence = new_list(N), Sequence :: 0..N-1, % extra/redudant (implicit) constraints to speed up the model N #= sum(Sequence), Integers = [I : I in 0..N-1], scalar_product(Integers, Sequence, N), % % create list: [0-Sequence[1], 1-Sequence[2], ...] Pairs = [$I-Sequence[I+1] : I in 0..N-1], global_cardinality(Sequence,Pairs), time2(solve([ff], Sequence)).
The order of constraints¶
The order might matter to the solver, the above model is an example. When the cp solver parses a constraint, it tries to reduce their domains. The implicit (redundant) constranint using scalar_product can reduce the model a lot, which is much better to do before parsing global_cardinality. (Note: e.g. MiniZinc doesn't preserve the order of constraints during compilation, the behaviour is a bit unpredictable.)
(Heuristic: easy constraints and constraints that are strong [in reducing the search space] should go first??)
Example: Knight tour¶
Given an integer $N$, plan a tour of the knight on an $N\times N$ chessboard such that the knight visits every field exactly once and then returns to the starting field. You can assume that $N$ is even.
Hint: For a matrix M is a matrix, use M.vars() to extract its elements into a list.
!picat knight-tour/knight-tour.pi 8
CPU time 0.001 seconds. Backtracks: 0
x = {{11,12,13,10,15,21,22,14},{3,20,26,18,23,4,30,6},{2,1,29,5,27,32,8,7},{19,9,37,34,35,40,16,38},{43,17,50,53,52,28,24,46},{51,25,49,61,60,36,62,31},{59,33,57,58,63,64,45,39},{42,41,44,54,55,56,48,47}}
X:
11 12 13 10 15 21 22 14
3 20 26 18 23 4 30 6
2 1 29 5 27 32 8 7
19 9 37 34 35 40 16 38
43 17 50 53 52 28 24 46
51 25 49 61 60 36 62 31
59 33 57 58 63 64 45 39
42 41 44 54 55 56 48 47
Tour:
1 62 5 10 13 24 55 8
4 11 2 63 6 9 14 23
61 64 35 12 25 56 7 54
34 3 26 59 36 15 22 57
39 60 37 18 27 58 53 16
30 33 40 43 46 17 50 21
41 38 31 28 19 48 45 52
32 29 42 47 44 51 20 49
The circuit constraint¶
The constraint circuit(L) requires that the list $L$ represents a permutation of $1,\dots,n$ consisting of a single cycle, i.e., the graph with edges $i\to L[i]$ is a cycle. A similar constraint is subcircuit(L) which requires that elements for which $L[i]\neq i$ form a cycle.
!cat knight-tour/knight-tour.pi
/***********************************************************
Adapted from
knight_tour.pi
from Constraint Solving and Planning with Picat, Springer
by Neng-Fa Zhou, Hakan Kjellerstrand, and Jonathan Fruhman
***********************************************************/
import cp.
main([N]) =>
N := N.to_int,
knight(N,X),
println(x=X),
println("X:"),
print_matrix(X),
extract_tour(X,Tour),
println("Tour:"),
print_matrix(Tour).
% Knight's tour for even N*N.
knight(N, X) =>
X = new_array(N,N),
X :: 1..N*N,
XVars = X.vars(),
% restrict the domains of each square
foreach (I in 1..N, J in 1..N)
D = [-1,-2,1,2],
Dom = [(I+A-1)*N + J+B : A in D, B in D,
abs(A) + abs(B) == 3,
member(I+A,1..N), member(J+B,1..N)],
Dom.length > 0,
X[I,J] :: Dom
end,
circuit(XVars),
time2(solve([ff,split],XVars)).
extract_tour(X,Tour) =>
N = X.length,
Tour = new_array(N,N),
K = 1,
Tour[1,1] := K,
Next = X[1,1],
while (K < N*N)
K := K + 1,
I = 1+((Next-1) div N),
J = 1+((Next-1) mod N),
Tour[I,J] := K,
Next := X[I,J]
end.
print_matrix(M) =>
N = M.length,
V = (N*N).to_string().length,
Format = "% " ++ (V+1).to_string() ++ "d",
foreach(I in 1..N)
foreach(J in 1..N)
printf(Format,M[I,J])
end,
nl
end,
nl.