2

There is an undirected graph (V,E), weights on the edges w : E → N, a target k ∈ N, and a threshold O ∈ N. Find a k-vertices tree of the graph of weight less than the threshold. In other words, select k vertices and k - 1 edges from V and E respectively such that they constitute a tree, and the sum of the weights of the selected edges are less than O.

Write an ASP program that takes V , E, w, k, and O as input, and finds a selection of edges satisfying the constraints, or outputs ‘unsatisfiable’ if the constraints cannot be satisfied. Selecting the edges implicitly induces a selection of the vertices, so there is no need for the selected vertices to be explicitly displayed.

An instance to this problem is provided through predicates vertex/1, weight/3, target/1, and threshold/1. All edges have weights, so statements of the form weight(a, b, 10). can be used to declare the existence of an edge between vertices a and b at the same time as declaring their weight, and there is no need for any redundant edge/2 predicate.

I tried the following:

% instance
vertex ( v1 ). vertex ( v2 ). vertex ( v3 ). 
vertex ( v4 ). vertex ( v5 ). vertex ( v6 ). 
vertex ( v7 ). vertex ( v8 ). vertex ( v9 ).
weight ( v1 , v2 ,3). weight ( v1 , v3 ,3). 
weight ( v2 , v4 ,1). weight ( v2 , v5 ,5). 
weight ( v3 , v4 ,3). weight ( v3 , v6 ,4). 
weight ( v4 , v5 ,4). weight ( v4 , v7 ,1). 
weight ( v5 , v7 ,7). 
weight ( v6 , v7 ,2). weight ( v6 , v8 ,2). 
weight ( v7 , v9 ,3). 
weight ( v8 , v9 ,2).
target (4).
threshold (4).

% encoding
(P-1) {select(X, Y) : weight(X, Y, Z)} (Q-1) :- target(P), target(Q).
sum(S) :- S = #sum {W,X,Y : select(X,Y), weight(X,Y,W); W,X,Z : select(X,Z), weight(X,Z,W) }.
:- sum(S),threshold(M), S > M.
:- select(A,B), select(C,D), A == C ; A == D ; B == C ; B == D. 

#show select/2.

And I get the following output:

clingo version 5.5.0
Reading from stdin
Solving...
Answer: 1
select(v2,v4) select(v4,v7) select(v6,v7)
Answer: 2
select(v2,v4) select(v4,v7) select(v6,v8)
Answer: 3
select(v2,v4) select(v4,v7) select(v8,v9)
SATISFIABLE

Models       : 3
Calls        : 1
Time         : 0.013s (Solving: 0.00s 1st Model: 0.00s Unsat: 0.00s)
CPU Time     : 0.000s

I was expecting just

select(v2,v4) select(v4,v7) select(v6,v7)

because the others clearly are not tress.

I think this is because of the problematic line:

:- select(A,B), select(C,D), A == C ; A == D ; B == C ; B == D.

How do I correct this?

DuDa
  • 3,718
  • 4
  • 16
  • 36
weak_at_math
  • 105
  • 7
  • Ok, a first attempt is to limit the number of selected nodes to target which will give the right output for the example. But it does not force a tree. – DuDa Nov 10 '20 at 10:55
  • You are right, although it will not work (I think) if the target was changed to 5 and everything else stays the same? Because the target is based on the weight, and isn't related to the number of nodes. But I could be wrong. – weak_at_math Nov 10 '20 at 11:02

1 Answers1

1

Ok, that was rather complicated. I'm pretty sure my solution is not perfect, I'm a beginner too.

Before we start with the code, let's check up the question once more: The requirement is to select k nodes and k-1 edges. If you think about it a bit this can form exactly two patterns: one connected tree or multiple non-connected graphs where there is at least one cycle. So if you make sure not to have a cycle you get one connected tree.

I added some nodes to the facts to check if a tree was formed or if the cheap unconnected cycle was found, and for doing so I had to change target and threshold to higher values.

enter image description here1

#const n = 5.

vertex ( v1; v2; v3; v4; v5; v6; v7; v8; v9 ).
vertex ( m1; m2; m3 ). 
weight ( v1 , v2 ,3). weight ( v1 , v3 ,3). 
weight ( v2 , v4 ,1). weight ( v2 , v5 ,5). 
weight ( v3 , v4 ,3). weight ( v3 , v6 ,4). 
weight ( v4 , v5 ,4). weight ( v4 , v7 ,1). 
weight ( v5 , v7 ,7). 
weight ( v6 , v7 ,2). weight ( v6 , v8 ,2). 
weight ( v7 , v9 ,3). 
weight ( v8 , v9 ,2).
weight ( m1 , m2 ,0).
weight ( m2 , m3 ,0).
weight ( m3 , m1 ,0).
target (n).
threshold (6).

And now comes the code, followed by an explanation.

% select subset of nodes and vertices
(P) {select(X) : vertex(X)} (P) :- target(P).
(P-1) {select(X, Y) : weight(X, Y, Z)} (Q-1) :- target(P), target(Q).
     
% postion does not matter in an undirected graph.
directed(A,B):-select(A,B).
directed(B,A):-select(A,B).

% for every selected edge all nodes are selected
:- directed(A,_), vertex(A), not select(A).

% for every selected node there exists at least one edge
:- select(A), {directed(A,B):vertex(B)}0.

% select a direction for each selected edge
{dir(A,B);dir(B,A)}==1 :- select(A,B). 

% force them in an order
{ found(X,1..n) } == 1 :- select(X).
{ found(X,N):select(X) } == 1 :- N = 1..n.
% reject if one edge does not follow the order 
:- found(X,NX), found(Y,NY),  dir(X,Y), NY<NX.
% reject if 2 different edges end in the same vertex 
:- dir(X,Z), dir(Y,Z), X!=Y.

:- threshold(M), M < #sum {W,X,Y : select(X,Y), weight(X,Y,W); W,X,Z : select(X,Z), weight(X,Z,W) }.

#show select/2.

Explanation:

  • To make it easier for me I added the selected vertices in the select/1 predicate.
  • Since dealing with undirected graphs always has to check both postions I added the directed/2 predicate which is a directed graph version of the selected edges.
  • Next I made sure every selected vertex has a selected edge and vice versa.
  • Now comes the complicated part: to detect cycles. For this I forced every selected edge in one of its two directions by using the predicate dir/2. Testing for a tree is easier in a directed graph.
  • Next I put an order found/2 to the vertices. The directed edges dir/2 where only allowed to go with this order. This forces cycles to a certain behavior.
  • Now comes the cycle destroyer: if the selected graph has a cycle then two edges from dir/2 will end in the same vertex. REJECT. If this was just an unlucky guess from clingo then it will find a luckier guess which fullfills this criterion.
  • Computation of the sum was copy and paste from you.

The output is 16 times

select(v2,v4) select(v4,v7) select(v6,v7) select(v6,v8)

The dublicates come from the fact that the order of the vertices in found/2 can differ but still get the same result.

DuDa
  • 3,718
  • 4
  • 16
  • 36
  • Wow thank you! Just a question, where in the code are you assigning a value for n? I went through the code a few times, but couldn't find the assignment. Also, I double checked it on clingo, and it does complain about the value of n not being assigned. Reading from stdin -:36:11-15: info: interval undefined: 1..n -:37:38-42: info: interval undefined: 1..n Any suggestions? – weak_at_math Nov 10 '20 at 19:38
  • oh sorry, I did alter the code in the post to make it shorter and forgot about it. I added the `#const n = 5.` on top of the facts. Note `target (n).` as well – DuDa Nov 10 '20 at 19:42
  • Great, that works flawlessly! A follow up question for my understanding. Instead of assigning a numeric value to n directly, is it possible to get this value from threshold? For example, if I mention the fact threshold(4)., is it possible to extract the value '4' from this predicate and assign it to a variable n? I tried n :- threshold(n)., but it is obviously wrong. I come from an object oriented programming background, so the syntax and flow of ASP programs are really confusing to me. – weak_at_math Nov 10 '20 at 20:19
  • Thanks again! Just that these answers will be automarked by a computer, and a human is unlikely to even see it. The facts will just be supplied to the program (which means, target4) will be a fact), and since setting a constant value and then using target(n) is not standard input (according to the problem statement), the test cases will eventually fail. Damn, I really hate automarking! Oh, and in my previous comment, I meant to say target(n) instead of threshold(n). – weak_at_math Nov 10 '20 at 20:44
  • Done, thanks again! You are a guardian angel, and a genius too! – weak_at_math Nov 10 '20 at 21:21