Skip to content

Commit c22082e

Browse files
committed
cluster predicate - unidirectional graph reachability matrix
Questions/notes: 1. Is there an autoformatter? 2. I'm not sure about the `multiroot` suffix, should it be `anyroot`? 3. Also, is `cluster` the best name? Is `constellations`/`sccs` better? I needed this some time back, and having this would have saved me quite some time. Essentially, this ended up consisting of two parts: 1. `reachable_multiroot()` - a seemingly rather boringly straight-forward generalization of the `reachable()`, which, instead of enforcing that the subgraph is reachable from _the_ root node (given by index, non-optional), and thus forcing there to be a single joint subgraph, instead takes (zero or more) roots as a mask. The rest of the logic is generalized accordingly. 2. `cluster()` predicate. This one took **_quite_** a bit of rewrites to fully distill it to it's purest+simplest+fastest form. :) The (final) basic idea is to, basically, give each node a unique "color", and then set the nodes, linked together by selected edges, to the same color, thus ensuring that the nodes, that are not in any way connected, don't have the same "color", and vice versa. Implementation-wise, instead of color, for each node, we track the index of the disjoint subgraph to which this node belongs, and it turned out that tracking that by tracking the root node of each subgraph is the best approach (referred to as root). So there's 3 parts: 1. Splitting disjoint subgraphs: we then defer to `reachable_multiroot()` to enfore that each disjoint subgraph actually has a root node, thus different disjoint subgraphs have different roots. 2. Coalescing same-color subgraphs: nodes, that are linked together by selected edge, have the same root. 3. And then we actually compute reachability matrix based on whether or not two nodes have same root (belong to the same disjoint subgraph) **Now, this is controversial. I've decided to return the reachability matrix, *NOT* the subgraph index. While sparse data structure seems better, it immediately requires symmetry breaking constraint, which is tricky (i have written it.), and `--all-solutions` actually ends up being *MUCH* slower (`O^3`?) than just returning reachability matrix... The directed version would need to return the matrix, so this is also future-proofing for consistency sake** I have an exhaustive test harness for the `cluster()` (but not the rest), it would not have been possible to refine the predicate so much otherwise, please see https://github.com/LebedevRI/minizinc-graph-reachability-matrix-predicate.mzn/tree/proof-of-concept/unidirectional
1 parent 809109a commit c22082e

25 files changed

+13815
-0
lines changed

share/minizinc/std/cluster.mzn

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
include "fzn_cluster_int.mzn";
2+
include "fzn_cluster_int_reif.mzn";
3+
include "fzn_cluster_enum.mzn";
4+
include "fzn_cluster_enum_reif.mzn";
5+
6+
/** @group globals.graph
7+
Clusterize the subgraph \a ns and \a es of a given undirected graph into disjoint subgraphs,
8+
and produce a \a r node reachability matrix.
9+
10+
@param N: the number of nodes in the given graph
11+
@param E: the number of edges in the given graph
12+
@param from: the leaving node 1..\a N for each edge
13+
@param to: the entering node 1..\a N for each edge
14+
@param r: node reachability matrix
15+
@param ns: a Boolean for each node whether it is in the subgraph
16+
@param es: a Boolean for each edge whether it is in the subgraph
17+
*/
18+
predicate cluster(int: N, int: E, array[int] of int: from, array[int] of int: to,
19+
array[int,int] of var bool: r,
20+
array[int] of var bool: ns, array[int] of var bool: es) =
21+
assert(index_set(from) = 1..E,"cluster: index set of from must be 1..\(E)") /\
22+
assert(index_set(to) = 1..E,"cluster: index set of to must be 1..\(E)") /\
23+
assert(index_set(ns) = 1..N,"cluster: index set of ns must be 1..\(N)") /\
24+
assert(index_set(es) = 1..E,"cluster: index set of es must be 1..\(E)") /\
25+
fzn_cluster(N,E,from,to,r,ns,es);
26+
27+
/** @group globals.graph
28+
Clusterize the subgraph \a ns and \a es of a given undirected graph into disjoint subgraphs,
29+
and produce a \a r node reachability matrix.
30+
31+
@param from: the leaving node for each edge
32+
@param to: the entering node for each edge
33+
@param r: node reachability matrix
34+
@param ns: a Boolean for each node whether it is in the subgraph
35+
@param es: a Boolean for each edge whether it is in the subgraph
36+
*/
37+
predicate cluster(array[$$E] of $$N: from, array[$$E] of $$N: to,
38+
array[$$N,$$N] of var bool: r,
39+
array[$$N] of var bool: ns, array[$$E] of var bool: es) =
40+
let {
41+
int: NUM_NODES = card(index_set(ns));
42+
} in
43+
assert(index_set(from) = index_set(to),"cluster: index set of from and to must be identical") /\
44+
assert(index_set(from) = index_set(es),"cluster: index set of from and es must be identical") /\
45+
assert(index_set_1of2(r) = index_set_2of2(r),"cluster: nodes in from must be in index set of r") /\
46+
assert(dom_array(from) subset index_set_1of2(r),"cluster: nodes in from must be in index set of r") /\
47+
assert(dom_array(to) subset index_set_1of2(r),"cluster: nodes in to must be in index set of r") /\
48+
assert(dom_array(from) subset index_set(ns),"cluster: nodes in from must be in index set of ns") /\
49+
assert(dom_array(to) subset index_set(ns),"cluster: nodes in to must be in index set of ns") /\
50+
fzn_cluster(from,to,r,ns,es);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
include "reachable_multiroot.mzn";
2+
3+
predicate fzn_cluster(array[$$E] of $$N: from, array[$$E] of $$N: to,
4+
array[$$N,$$N] of var bool: r,
5+
array[$$N] of var bool: ns, array[$$E] of var bool: es) =
6+
let {
7+
array[index_set(ns)] of var index_set(ns): NodeRoot;
8+
array[index_set(ns)] of var bool: NodeRootnessMask;
9+
array[index_set(ns)] of bool: ALL_GRAPH_NODES = [ true | n in index_set(ns) ];
10+
} in
11+
symmetry_breaking_constraint(forall (j,i in index_set(ns) where i > j)(
12+
r[i,j] == r[j,i]
13+
)) /\
14+
forall (c in index_set(ns))(
15+
r[c,c] == true
16+
) /\
17+
forall (j,i in index_set(ns) where i > j)(
18+
r[j,i] -> (ns[j] /\ ns[i])
19+
) /\
20+
reachable_multiroot(from,to,NodeRootnessMask,ALL_GRAPH_NODES,es) /\
21+
forall (n in index_set(ns))(
22+
NodeRootnessMask[n] == (NodeRoot[n] == n)
23+
) /\
24+
forall (e in index_set(es))(
25+
es[e] -> (NodeRoot[from[e]] == NodeRoot[to[e]])
26+
) /\
27+
forall (j,i in index_set(ns) where i > j)(
28+
r[j,i] == (NodeRoot[i] == NodeRoot[j])
29+
);
30+
31+
%-----------------------------------------------------------------------------%
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
predicate fzn_cluster_reif(array[$$E] of $$N: from, array[$$E] of $$N: to,
2+
array[$$N,$$N] of var bool: r, array[$$N] of var bool: ns,
3+
array[$$E] of var bool: es, var bool: b) =
4+
abort("Reified cluster constraint is not supported");
5+
6+
%-----------------------------------------------------------------------------%
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
include "reachable_multiroot.mzn";
2+
3+
predicate fzn_cluster(int: N, int: E, array[int] of int: from, array[int] of int: to,
4+
array[int,int] of var bool: r, array[int] of var bool: ns, array[int] of var bool: es) =
5+
let {
6+
array[1..N] of var 1..N: NodeRoot;
7+
array[1..N] of var bool: NodeRootnessMask;
8+
array[1..N] of bool: ALL_GRAPH_NODES = [ true | n in 1..N ];
9+
} in
10+
symmetry_breaking_constraint(forall (j,i in 1..N where i > j)(
11+
r[i,j] == r[j,i]
12+
)) /\
13+
forall (c in 1..N)(
14+
r[c,c] == true
15+
) /\
16+
forall (j,i in 1..N where i > j)(
17+
r[j,i] -> (ns[j] /\ ns[i])
18+
) /\
19+
reachable_multiroot(N,E,from,to,NodeRootnessMask,ALL_GRAPH_NODES,es) /\
20+
forall (n in 1..N)(
21+
NodeRootnessMask[n] == (NodeRoot[n] == n)
22+
) /\
23+
forall (e in 1..E)(
24+
es[e] -> (NodeRoot[from[e]] == NodeRoot[to[e]])
25+
) /\
26+
forall (j,i in 1..N where i > j)(
27+
r[j,i] == (NodeRoot[i] == NodeRoot[j])
28+
);
29+
30+
%-----------------------------------------------------------------------------%
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
predicate fzn_cluster_reif(int: N, int: E, array[int] of int: from, array[int] of int: to,
2+
array[int,int] of var bool: r, array[int] of var bool: ns,
3+
array[int] of var bool: es, var bool: b) =
4+
abort("Reified cluster constraint is not supported");
5+
6+
%-----------------------------------------------------------------------------%
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
include "subgraph.mzn";
2+
3+
predicate fzn_dreachable_multiroot(array[$$E] of $$N: from, array[$$E] of $$N: to,
4+
array[$$N] of var bool: rs, array[$$N] of var bool: ns, array[$$E] of var bool: es) =
5+
let {
6+
array[index_set(ns)] of var 0..card(index_set(ns))-1: dist; /* distance from root */
7+
array[index_set(ns)] of var index_set(ns): parent; /* parent */
8+
} in
9+
forall(n in index_set(ns)) % root nodes must be chosen
10+
(rs[n] -> ns[n]) /\
11+
forall(n in index_set(ns)) % self-parent is the same as either non-chosen node or root node
12+
((parent[n] = n) == ((not ns[n]) \/ rs[n])) /\
13+
forall(n in index_set(ns)) % each non-self-parent node is one more step removed from a root than its parent node
14+
(parent[n] != n -> dist[n] = dist[parent[n]] + 1) /\
15+
forall(n in index_set(ns)) % each non-self-parent node must have a chosen edge from its parent
16+
(parent[n] != n -> exists(e in index_set(from) where to[e] = n)(es[e] /\ from[e] = parent[n])) /\
17+
% nodes connected by chosen edges must be chosen
18+
subgraph(from,to,ns,es);
19+
20+
%-----------------------------------------------------------------------------%
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
predicate fzn_dreachable_multiroot_reif(array[$$E] of $$N: from, array[$$E] of $$N: to,
2+
array[$$N] of var bool: rs, array[$$N] of var bool: ns, array[$$E] of var bool: es,
3+
var bool: b) =
4+
abort("Reified dreachable_multiroot constraint is not supported");
5+
6+
%-----------------------------------------------------------------------------%
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
include "subgraph.mzn";
2+
3+
predicate fzn_dreachable_multiroot(int: N, int: E, array[int] of int: from, array[int] of int: to,
4+
array[int] of var bool: rs, array[int] of var bool: ns, array[int] of var bool: es) =
5+
let {
6+
array[1..N] of var 0..N-1: dist; /* distance from root */
7+
array[1..N] of var 1..N: parent; /* parent */
8+
} in
9+
forall(n in 1..N) % root nodes must be chosen
10+
(rs[n] -> ns[n]) /\
11+
forall(n in 1..N) % self-parent is the same as either non-chosen node or root node
12+
((parent[n] = n) == ((not ns[n]) \/ rs[n])) /\
13+
forall(n in 1..N) % each non-self-parent node is one more step removed from a root than its parent node
14+
(parent[n] != n -> dist[n] = dist[parent[n]] + 1) /\
15+
forall(n in 1..N) % each non-self-parent node must have a chosen edge from its parent
16+
(parent[n] != n -> exists(e in 1..E where to[e] = n)(es[e] /\ from[e] = parent[n])) /\
17+
% nodes connected by chosen edges must be chosen
18+
subgraph(N,E,from,to,ns,es);
19+
20+
%-----------------------------------------------------------------------------%
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
predicate fzn_dreachable_multiroot_reif(int: N, int: E, array[int] of int: from, array[int] of int: to,
2+
array[int] of var bool: rs, array[int] of var bool: ns, array[int] of var bool: es,
3+
var bool: b) =
4+
abort("Reified dreachable_multiroot constraint is not supported");
5+
6+
%-----------------------------------------------------------------------------%
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
include "subgraph.mzn";
2+
3+
predicate fzn_reachable_multiroot(array[$$E] of $$N: from, array[$$E] of $$N: to,
4+
array[$$N] of var bool: rs, array[$$N] of var bool: ns, array[$$E] of var bool: es) =
5+
let {
6+
int: E = length(es);
7+
set of int: NODE = index_set(ns);
8+
array[1..2*E] of NODE: dfrom = from ++ to;
9+
array[1..2*E] of NODE: dto = to ++ from;
10+
array[1..2*E] of var bool: des = es ++ es;
11+
} in
12+
/* duplicate the edges so that we can use directed graph reachability */
13+
fzn_dreachable_multiroot(dfrom,dto,rs,ns,des);
14+
15+
%-----------------------------------------------------------------------------%

0 commit comments

Comments
 (0)