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.