New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Nice tree decomposition for chordal graph #616
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've left a couple of comments about Javadoc and code style. Will have a closer look soon.
* <li>for every edge $e \in E(G)$, there is a node $t \in V(T)$ with $e \subseteq b(v)$</li> | ||
* <li>for all vertices $v \in V(G)$ the set $\{t \in V(T) | v \in b(t)\}$ is non-empty and | ||
* connected in $T$</li> | ||
* </ul> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's rephrase this definition a little bit:
* A tree-decomposition of a graph $G = (V, E)$ is a pair $(X, T) = (\{X_i\ |\ i\in I\}, (I, F))$ where
* $X = \{X_i\ |\ i \in I \}$ is a family of subsets of $V$, and $T = (I, F)$ is a tree, such that
* <ul>
* <li>Union of the sets $X_i$ equals to $V$</li>
* <li>for every edge $e = (u,v)$ there exists a set $X_i$ such that $u \in X_i$ and $v \in X_i$</li>
* <li>if both $X_i$ and $X_j$ contain a vertex $v$ then every set $X_k$ on the simple path from $X_i$ to $X_j$
* contains vertex $v$.</li>
* </ul>
This is a verbose variations of the definition introduced in the "Better Algorithms for the Pathwidth and Treewidth of Graphs".
* <br> | ||
* A nice tree decomposition is a special tree decomposition, which satisfies the properties: | ||
* <ul> | ||
* <li>for root $r \in V(T)$ and leaf $l \in V(T): b(r)=b(t)=\emptyset$</li> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you reference an article you took this definition from? I can only find definitions where root's and leaf nodes' bags have a single vertex rather that zero vertices.
protected NiceDecompositionBuilder() | ||
{ | ||
// creating objects | ||
decomposition = new DefaultDirectedGraph<Integer, DefaultEdge>(DefaultEdge.class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove generic type parameters
* @return a nice decomposition builder for the graph if the graph was chordal, else null | ||
*/ | ||
public static <V, E> ChordalNiceDecompositionBuilder<V, E> create(Graph<V, E> graph) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same thing could be achieved without static methods. You can throw an IllegalArgumentException if the graph is not chordal.
* @param vertex the vertex whose predecessors in order are to be returned. | ||
* @return the predecessors of {@code vertex} in order defines by {@code map}. | ||
*/ | ||
private Set<V> getOrderPredecessors(Map<V, Integer> map, V vertex) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we don't need to constant time lookups, this can be optimized to return a List. Please, do the same thing with the original method in ChordalityInspector.
} | ||
} | ||
|
||
private Graph<Integer, DefaultEdge> makeUnconnectedGraph(int n) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename to makeDisconnectedGraph(int n)
} | ||
|
||
@Test | ||
public void testUnconnectedGraph() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
&& union.size() == map.get(second).size()) | ||
continue; //join node! | ||
} | ||
assertFalse("Vertex Set "+current+" is not a valid node for a nice decomposition" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be replaced with fail()
+ "\nin decomposition "+decomposition | ||
+ "\nwith map "+map, true); //no valid node! | ||
} | ||
assertTrue(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this?
* @param perfectEliminationOrder the perfect elimination order of the graph | ||
* @return a nice decomposition builder for the graph if the graph was chordal, else null | ||
*/ | ||
public static <V, E> ChordalNiceDecompositionBuilder<V, E> create( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few small(-ish) comments.
NiceDecompositionBuilder<V> | ||
{ | ||
// the chordal graph | ||
Graph<V, E> graph; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why the fields aren't private?
decomposition.addEdge(node, vertexChildLeft); | ||
decomposition.addEdge(node, vertexChildRight); | ||
|
||
return new Pair<Integer, Integer>(vertexChildLeft, vertexChildRight); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You do not need the type parameters. return new Pair<>(vertexChildLeft, vertexChildRight);
also works.
* | ||
* @return unused integer | ||
*/ | ||
private Integer getNextInteger() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as @Toptachamann. Please only use Integer
when simply int
won't do.
* @return the newly created node; or null and no change if either {@code forgottenElement} is | ||
* in b({@code node}) or {@code node} is not a leaf. | ||
*/ | ||
protected Integer addForget(V forgottenElement, Integer node) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I can tell you use the method only once in ChordalNiceDecompositionBuilder
. There you have this line: forgetNodeMap.put(vertex, decompNode);
If you return null
then vertex
will be removed from the map. Is that the intended behaviour? If this was by design than I guess you can use Integer
here.
* | ||
* @param introducedElement the element, which is introduced | ||
* @param node the node, which becomes an introduce node | ||
* @return the newly created node; or null and no change if either {@code introducedElement} is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use {@code null} for null in the docs.
} | ||
|
||
/** | ||
* Returns the root of the decomposition {@code getDecomposition()} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Get the root of the decomposition computed by {@code getDecomposition()}
* | ||
* @return the root the decomposition | ||
*/ | ||
public Integer getRoot() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
int
@@ -0,0 +1,6 @@ | |||
|
|||
/** | |||
* Tree Decomposition related algorithms |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clash with existing package-info. There's no need to change it.
@@ -0,0 +1,130 @@ | |||
package org.jgrapht.alg.decompostion; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where's the header?
@@ -0,0 +1,115 @@ | |||
package org.jgrapht.alg.decompostion; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where's the header?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, please fix the conflict. It would be a good idea to sync your fork with jgrapht/master.
a068021
to
0544d8e
Compare
I used non-open lecture notes, where nice decomposition was defined with leaves and roots with empty sets. But since it was not difficult to change it, i did so. |
{ | ||
|
||
// resulting decomposition | ||
protected Graph<Integer, DefaultEdge> decomposition; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am wondering why not use directly something like Graph<Set<V>, DefaultEdge>
for the decomposition (and for the result of the decomposition).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was also my first attempt, but a decomposition (especially a nice decomposition) can have multiple nodes with same sets. Just look at the definition of a join node, where the join node and both of their children have the same set. But the graph class (or more precise: the vertex set class) would consider these three nodes the same. So my two options was to either use a Set with modified equal/hashvalue getter, a wrapper class or a Map from Node to Sets. Since the last one is equal to the formal definition, I opt for this method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've left again some mostly stylistic comments
this.graph = graph; | ||
this.perfectOrder = inspec.getPerfectEliminationOrder(); | ||
vertexInOrder = getVertexInOrder(); | ||
computeNiceDecomposition(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we run the algorithm at the construction time or after the first call to some getter method? @jkinable @jsichi @d-michail what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't currently have a standard pattern for this (some algs do it one way, some the other). But lazy usually turns out to be more flexible than eager in the long run.
As a related point, this is not really an instance of the Builder pattern, so I don't think we should use Builder in the name. Following the pattern set by the existing classes in this package, it should just be ChordalNiceDecomposition (and NiceDecompositionBase for the abstract class).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since a decomposition of a graph and a tree decomposition of a graph are different things, it’s better to name it ChordalNiceTreeDecomposition and NiceTreeDecompositionBase
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point.
private void computeNiceDecomposition() | ||
{ | ||
// map from vertices to decomposition-nodes where the decomposition-node has all its | ||
// predecessors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's mention that here the decomposition nodes are forget nodes and they also contain corresponding vertices:
/*
* Map from the vertices of the graph to the forget nodes from the decomposition tree. The mapping
* is one-to-one and each decomposition node contains corresponding vertex and all its predecessors.
*/
// predecessors | ||
Map<V, Integer> forgetNodeMap = new HashMap<>(graph.vertexSet().size()); | ||
|
||
//iterate over the perfect order |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// iterator over the perfect elimination order
// get the predecessors regarding the perfect elimination order | ||
List<V> predecessors = getOrderPredecessors(vertexInOrder, vertex); | ||
|
||
// calculate nearest successors according to perfect elimination order |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nearest predecessor
lastVertex = predecessor; | ||
} | ||
|
||
// get node with clique of last vertex, else we use the last node |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the current vertex has no predecessors, then we can use any decomposition node to build a path to, can't we?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct. Thats why I implicial just use the last one here.
*/ | ||
protected void leafClosure() | ||
{ | ||
Set<Integer> vertices = new HashSet<Integer>(decomposition.vertexSet()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and here
* <li>if both $X_i$ and $X_j$ contain a vertex $v$ then every set $X_k$ on the simple path from $X_i$ to $X_j$ | ||
* contains vertex $v$.</li> | ||
* </ul> | ||
* <br> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's mention the mapping b
or replace its usages below with the sets X_i. I would choose the first alternative since it is convenient to think of the decomposition tree as the mapping from tree nodes to the sets containing vertices from the initial graph.
* An abstract builder class for nice tree decompositions, which builds the tree decomposition in a | ||
* top-down manner. | ||
* <p> | ||
* A tree-decomposition of a graph $G = (V, E)$ is a pair $(X, T) = (\{X_i\ |\ i\in I\}, (I, F))$ where |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add space at the beginning
import org.jgrapht.graph.*; | ||
|
||
/** | ||
* An abstract builder class for nice tree decompositions, which builds the tree decomposition in a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An abstract class for building a nice tree decomposition in a top-down manner.
* <p> | ||
* The complexity of this algorithm is in $\mathcal{O}(|V|(|V|+|E|))$.<br> | ||
* Consider the every node in the nice tree decomposition: There are exactly $|V|$ many forget | ||
* nodes. There are at most $2|V|$ additionally nodes because of a join nodes. Every join node |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably, exactly |V| - 1 forget nodes since for the root we do not add a forget node. And are there at most 2|V| - 2 auxiliary nodes produced by adding join nodes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, yes. First one is due to changing the condition from |b(root)|=0 to |b(root)|=1, second one was just an overapproximation, but I changed both accordingly.
* Calculate the nearest predecessor according to the perfect elimination order. | ||
* The nearest predecessor means here the predecessor of the vertex, for which the forget node | ||
* was created last and thus is the highest predecessor (and nearest to the vertex). | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a regular comment, not a javadoc
import org.jgrapht.graph.*; | ||
|
||
/** | ||
* Tests for the {@link NiceDecompositionBuilder} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class was renamed
private Map<V, Integer> vertexInOrder; | ||
|
||
/** | ||
* Constructor for the nice decomposition builder of chordal graphs. The constructor |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Constructor for the nice tree decomposition algorithm
} | ||
|
||
/** | ||
* Constructor for the nice decomposition builder of chordal graphs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
} | ||
|
||
/** | ||
* Computes the nice decomposition of the graph. We iterate over the perfect elimination order |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice decomposition -> nice tree decomposition
* introduced vertex. Since this introduced vertex is part of the clique of the least ancestor | ||
* forget node, the corresponding bag of the introduce node is smaller than the set of neighbors of | ||
* this vertex. Thus the time complexity is in $\mathcal{O}(|V|(|V|+|E|))$. | ||
* <p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We generate |V| - 1
forget nodes and build |V|-1
paths to previously added nodes in the tree. There is at most one join node generated per such path, therefore there are at most |V|-1
join nodes. At least we are calling addJoinNode once per vertex from the perfect elimination order.
I don't quite understand why the running time of the algorithm is O(V(V + E)). Computing order predecessors and finding one with the highest index contributes O(V + E) overall. Creating and adding forget nodes run in O(V+ E) since the sum of the cardinalities of all predecessors sets equals to |E|. I'm uncertain about the running time complexity of deleting the nodes while building and finishing the paths. Every vertex can be O(deg(v)) times the higher predecessor, consequently it can be deleted O(deg(v)) times (deleting a node = adding "introduce" node). Deleting a node takes O(deg(v)) time since we a dealing with bags which contain cliques. This is done for every vertex, that's why the running time is (V*vertex_max_deg^2 + E) which is O(VE^2) worst case. Can we have a better complexity analysis?
Another point is that we have O(V^2) upper bound on introduce nodes, but adding one introduce node runs in O(vertex_max_deg) time because of set copying. So, the running time is at least O(V^2*vertex_max_deg). Can we aggregate this somehow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not quite get your first point. But for the second one, I do not agree. Adding one introduce node runs in O(|b(parent)|) - which is important, since in this case, the parent consits of a clique. Thus adding an introduce node for the vertex v costs in this case O(|Neighbour(v)|). Which is far less then O(vertex_max_deg). But I guess, I can try to do another analysis. Maybe this one is more elegant.
First lets proof the following lemma: every bag of a node in the tree decomposition consists of a clique. Proof by induction: the roots bag is obviously a (trivial) clique. Additionally, adding a forget node produces a node with a clique by construction, because the bag consists of the forgotten element and its predecessors.
Adding join nodes and adding introduce nodes only adds nodes, which bags are a subset of their parents bag. Since every subset of a clique is a clique, their bags are a clique by induction hypothesis.
Now there are at most |V| different pathes from the root to a leaf, because every vertex can create at most one new path. Consider any of them. For now we ignore the join nodes. Because the tree is a tree decomposition, every vertex v can be introduced and forgotten at most ones. Adding a forget node generates a node with a clique of v. Adding an introduce node, generates a node with a clique of v, whithout v (because v is in the bag of its parent and the parents bag is a clique). Thus the cost of adding a forget node or adding an introduce node for one v is in O(|Neighbour(v)|). Now every vertex has at most one introduce node and one forget node on this path, which adds up to O(|V|+|E|) for one path. Since there are at most |V| different paths, we have O(|V|(|V|+|E|)).
Now considering the join nodes: There are 2*(|V|-1) join nodes, everyone can be generated with at most vertex_max_deg sized bags, thus they add up to O(|V|*vertex_max_deg). Simmilar calculating the toIntroduce sets add up to O(|V|*vertex_max_deg). In total again O(|V|(|V|+|E|)).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now the analysis is clear for me, thanks! One more issue to resolve: why there are 2*(V-1) joins (at most)? We are adding one join node per forget node, consequently there are at most (V-1) join nodes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. There are (V-1) join nodes, but adding join nodes generate 2 nodes. But these two nodes are not (necessary) join nodes according to the definition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All right, thanks! Let's use the following Javadoc:
/**
* Builds a nice tree decomposition of chordal graphs. See {@link NiceTreeDecompositionBase} for
* an explanation of the nice tree decomposition.
* <p>
* For a given perfect elimination order of the graph $G = (V, E)$ every vertex in $V(G)$ has an associated
* index in this order. The predecessors of a vertex $x \in V(G)$ are those vertices incident to $x$ that
* have smaller index than $x$ (a vertex can have no predecessors). This class uses a perfect elimination
* order produced by {@link ChordalityInspector} to iteratively add vertices of $G$ to some nodes in the
* tree $T$. Initially, it adds a bag to the decomposition with the first vertex from the perfect elimination
* order as a root of the tree. Then for all remaining vertices $v \in V(G)$ this class generates a node (bag)
* $s \in I(T)$ in the tree $T$ which contains vertex $v$ and all its predecessors. Then it builds a path from
* $s \in I(T)$ to $t \in I(T)$, where $s$ is the node where the predecessor of $v$ with the highest index was
* added to the decomposition for the first time. If the vertex $v$ has no predecessors, a path is built to an
* arbitrary node from the tree $T$. The bags on the resulting path between $s$ and $t$ contain only cliques.
* <p>
* The time complexity of this algorithm is $\mathcal{O}(|V|(|V|+|E|))$. In the decomposition produced by this
* class there are exactly $|V|-1$ many forget nodes since they are generated for every vertex in the $V(G)$
* except for the vertex added as a root. There are at most $|V|-1$ additionally added join nodes which are
* generated during the connection of forget nodes to the tree. Every join node creates one additional
* root-to-leaf path. Therefore, there are $\mathcal{O}(|V|)$ root-to-leaf paths in the resulting decomposition.
* Every root-to-left path can contain $\mathcal{O}(|V|)$ introduce nodes, which yields $\mathcal{O}(|V|^2)$
* upper bound on introduce nodes. Now considering the bags of the introduce nodes. Bags of every root-to-leaf
* path can contain at most $|V|$ distinct vertices of the initial graph. Every vertex $v$ can be introduced and
* forgotten on a path at most once. The sizes of these introduce and forget nodes are $\mathcal{O}(deg(v) + 1)$.
* Consequently, every root-to-leaf path is constructed in $\mathcal{O}(|V| + |E|)$ time. Since there are
* $\mathcal{O}(|V|)$ root-to-leaf paths, the total running time is $\mathcal{O}(|V|(|V| + |E|))$.
* <p>
* The algorithm used to construct a nice tree decomposition is a non-recursive adaption of algorithm 2
* from here: <br>
* Hans L. Bodlaender, Arie M.C.A. Koster, Treewidth computations I. Upper bounds, Information and
* Computation, Volume 208, Issue 3, 2010, Pages 259-275,
*
* @author Ira Justus Fesefeldt (PhoenixIra)
* @author Timofey Chudakov
*
* @since June 2018
*
* @param <V> the vertex type of the graph
* @param <E> the edge type of the graph
*/
I've used your time analysis and mentioned a few relevant definitions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the javadoc! I changed a small part regarding the forget nodes, which I came up with after you mentioned the join nodes: join nodes double existing nodes and adds a join node. Thus we can have 2(|V|-1) many forget nodes by doubling every forget node ones:
/**
* Builds a nice tree decomposition of chordal graphs. See {@link NiceTreeDecompositionBase} for an
* explanation of the nice tree decomposition.
* <p>
* For a given perfect elimination order of the graph $G = (V, E)$ every vertex in $V(G)$ has an
* associated index in this order. The predecessors of a vertex $x \in V(G)$ are those vertices
* incident to $x$ that have smaller index than $x$ (a vertex can have no predecessors). This class
* uses a perfect elimination order produced by {@link ChordalityInspector} to iteratively add
* vertices of $G$ to some nodes in the tree $T$. Initially, it adds a bag to the decomposition with
* the first vertex from the perfect elimination order as a root of the tree. Then for all remaining
* vertices $v \in V(G)$ this class generates a node (bag) $s \in I(T)$ in the tree $T$ which
* contains vertex $v$ and all its predecessors. Then it builds a path from $s \in I(T)$ to $t \in
* I(T)$, where $s$ is the node where the predecessor of $v$ with the highest index was added to the
* decomposition for the first time. If the vertex $v$ has no predecessors, a path is built to an
* arbitrary node from the tree $T$. The bags on the resulting path between $s$ and $t$ contain only
* cliques.
* <p>
* The time complexity of this algorithm is $\mathcal{O}(|V|(|V|+|E|))$. In the decomposition
* produced by this class there are at most $|V|-1$ additionally added join nodes which are
* generated during the connection of forget nodes to the tree. The algorithm adds exactly $|V|-1$
* many forget nodes since they are generated for every vertex in the $V(G)$ except for the vertex
* added as a root. They may be doubled by join nodes, which adds up to at most $2(|V|-1)$ forget
* nodes. Every join node creates one additional root-to-leaf path. Therefore, there are
* $\mathcal{O}(|V|)$ root-to-leaf paths in the resulting decomposition. Every root-to-leaf path can
* contain $\mathcal{O}(|V|)$ introduce nodes, which yields $\mathcal{O}(|V|^2)$ upper bound on
* introduce nodes. Now considering the bags of the introduce nodes. Bags of every root-to-leaf path
* can contain at most $|V|$ distinct vertices of the initial graph. Every vertex $v$ can be
* introduced and forgotten on a path at most once. The sizes of these introduce and forget nodes
* are $\mathcal{O}(deg(v) + 1)$. Consequently, every root-to-leaf path is constructed in
* $\mathcal{O}(|V| + |E|)$ time. Since there are $\mathcal{O}(|V|)$ root-to-leaf paths, the total
* running time is $\mathcal{O}(|V|(|V| + |E|))$.
* <p>
* The algorithm used to construct a nice tree decomposition is a non-recursive adaption of
* algorithm 2 from here: <br>
* Hans L. Bodlaender, Arie M.C.A. Koster, Treewidth computations I. Upper bounds, Information and
* Computation, Volume 208, Issue 3, 2010, Pages 259-275,
*
* @author Ira Justus Fesefeldt (PhoenixIra)
* @author Timofey Chudakov
*
* @since June 2018
*
* @param <V> the vertex type of the graph
* @param <E> the edge type of the graph
*/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That’s right
{ | ||
V next = successor.get(0); | ||
queue.add(next); | ||
Set<W> union = new HashSet<W>(map.get(current)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove generic type parameter
V second = successor.get(1); | ||
queue.add(first); | ||
queue.add(second); | ||
Set<W> union = new HashSet<W>(map.get(current)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
Set<W> oldVertexSet = oldGraph.vertexSet(); | ||
for(W w : oldVertexSet) | ||
{ | ||
Set<V> keySet = new HashSet<V>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And here
if(keySet.isEmpty()) | ||
return false; | ||
//connected | ||
Graph<V,E> subgraph = new AsSubgraph<V,E>(decomposition,keySet); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And here
* @param map the map of the tree decomposition | ||
* @param root the root of the tree decomposition | ||
*/ | ||
public static <V,E,W> boolean isNiceDecomposition(Graph<V,E> decomposition, Map<V,Set<W>> map, V root){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's rename this method to isNiceTreeDecomposition
.
Also, why can't we call the method isTreeDecomposition
from this one, so that we test, that the input tree is (a) a tree decomposition and (b) it satisfies the properties of nice tree decomposition. If so, the user can simultaneously verify the tree decomposition properties and whether a tree decomposition is nice.
Another point is that the verification of the simple and nice tree decomposition properties can be placed in GraphTests
. This will include creating a class TreeDecomposition
to contain the decomposition tree, node map and root node. Then, the verification methods will look like GraphTests.isTreeDecomposition(Graph<V, E> graph, TreeDecomposition<V> decomposition)
and GraphTests.isNiceTreeDecomposition(Graph<V, E> graph, TreeDecomposition<V> decomposition)
. @jsichi @jkinable @d-michail @AlexandruValeanu what do you think about this? Another question is whether we need to parameterize the nice tree decomposition algorithm with the types of produced vertices and edges, i.e. the user might want to produce not integer vertices and specify a supplier for this. So, the resulting class might look like either TreeDecomposition<V>
or TreeDecomposition<V, I, E>
.
* @param decomposition the tree decomposition | ||
* @param map the map from vertices to the tree decomposition to the sets of the tree decomposition | ||
*/ | ||
public static <V,E,W,F> boolean isDecomposition(Graph<W,F> oldGraph, Graph<V,E> decomposition, Map<V,Set<W>> map){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's rename this method to isTreeDecomposition
* | ||
* @author Ira Justus Fesefeldt (PhoenixIra) | ||
*/ | ||
public class ChordalityNiceTreeDecompositionTest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the class that is being tested is ChordalNiceTreeDecomposition
, this one should be named ChordalNiceTreeDecompositionTest
Queue<V> queue = new ArrayDeque<>(); | ||
|
||
//test and add root | ||
if(map.get(root).size() != 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should use braces with the if
statements when their body consists of a single line. So, please, add braces in the single line if
statements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your submission. I have left various comments. Please have a look. This is definitely an interesting contribution to the library.
Several other questions:
-
It seems that ChordalNiceTreeDecomposition can be used to compute the treewidth of a graph. If so, let's add the method
public int getTreewidth()
. Similarly getPathwidth if the tree is a path graph (return -1 if the tree ain't a path) -
Currently the javadoc is very heavy on the math notation. Do you think we could add a short introductory paragraph which avoids some of the math? The papers/wikipedia page dealing with this topic don't seem to be of much help. I'm currently traveling so I don't have access to my books. I was hoping we could add something like:
Class for constructing nice tree decompositions from (undirected?) graphs. A tree decomposition of a graph is... A nice tree decomposition is a special type of tree decomposition. A tree decomposition is nice if...
Nice tree decompositions are primarily used to...
Such a short paragraph gives a more gentle introduction to the concept and makes it more accessible. After the gentle introduction we can proceed with the formal definitions using math notation.
-
Do we need interface classes such as
TreeDecompositionAlgorithm
andTreeDecomposition
? The answer to this question depends on whether there are alternative algorithms to compute TreeDecompositions, or whether there exist alternative types of TreeDecompositions (a 'nice' tree decomposition seems to be a special type, so I guess the answer is 'yes'). Since my knowledge about TreeDecompositions is very limited, I'm not the best person to judge this. Similarly, does it make sense to introduce a more general interface GraphDecomposition? -
Can you suggest some follow up work we can do in this area? E.g. what other decompositions/alternative implementations are missing? If possible, reference specific papers. We will add it to our Future work page.
* | ||
* @param <V> the vertices of the graph | ||
*/ | ||
abstract public class NiceTreeDecompositionBase<V> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the rational behind having this abstract base class: is it likely that this class will be extended by other classes? Can you name examples? If not, then I would recommend merging it with the other class. Are there other algorithms which benefit from this abstract class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most papers reference to an algorithm which can transform every graph G with treewidth k into a nice tree decomposition of width k. (Kloks, Ton: Treewidth - Computations and Approximations, Lecture Notes in Computer Science, 1994, p.149, Lemma 13.1.2). The case distinction looks similar to the methods of this base class to me.
Additionally I have already implemented an algorithm for interval graphs (as an interval representation) using the base class. The idea of the algorithm is briefly mentioned in this paper (Fomin F.V., Heggernes P., Mihai R., Mixed search number and linear-width of interval and split graphs, In: Brandstädt A., Kratsch D., Müller H. (eds), Graph-Theoretic Concepts in Computer Science, 2007, p.306).
|
||
/** | ||
* Constructor for the nice tree decomposition of chordal graphs. | ||
* The tree decomposition is calculated lazy when its needed for the first time. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lazy ->lazily
ChordalityInspector<V, E> inspec = new ChordalityInspector<>(graph); | ||
if (!inspec.isChordal()) | ||
throw new IllegalArgumentException("The given graph is not chordal."); | ||
this.graph = graph; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this implementation work for all types of graphs (directed/undirected)? If so, use this.graph=Objects.requireNonNull(graph)
else
this.graph=GraphTests.requireUndirectedGraph(graph)
NiceTreeDecompositionBase<V> | ||
{ | ||
// the chordal graph | ||
private Graph<V, E> graph; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's make this final
protected int addForgetNode(V forgottenElement, int node) | ||
{ | ||
//check precondition | ||
if (!Graphs.successorListOf(decomposition, node).isEmpty()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider changing these if statements to assert statements. That way this method is more efficient in production code. Same in other places
* | ||
* @return unused integer node for the decomposition | ||
*/ | ||
private int getNextInteger() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using a SupplierUtil.createIntegerSupplier(). This can also be provided directly to the decomposition graph which allows you to invoke decomposition.addVertex()
* | ||
* @return the computed tree decomposition graph | ||
*/ | ||
public Graph<Integer, DefaultEdge> getDecomposition() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't you return a Graph<Set<V>, DefaultEdge>
? In jgrapht, vertices can be anything, e.g. sets or even graphs! Similarly, getRoot could return a Set. Then there's no need for a getMap() method.
I noticed that @d-michail suggested the exact same thing. To avoid confusion, it might be worth to include a shorter version of your response to @d-michail comment somewhere in the javadoc, or as a comment directly in the code of the corresponding method.
Also, I'm not sure I understand why a tree decompisition can have duplicate nodes (i.e. bags with the exam same subset of vertices)? None of the examples I found featured a tree decomposition with duplicate vertices? (a) could you give an example? (b) can't these duplicate nodes be eliminated?
/** | ||
* An abstract class for building a nice tree decomposition in a top-down manner. | ||
* <p> | ||
* A tree decomposition of a graph $G = (V, E)$ is a pair $(X, T) = (\{X_i\ |\ i\in I\}, (I, F))$ where |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really like this definition of a tree decomposition. I've seen it in Bodlaender's paper, but it's quite hard to parse. What do you think of the definition found here:
https://math.mit.edu/~apost/courses/18.204-2016/18.204_Gerrod_Voigt_final_paper.pdf
This seems much easier to read, and it immediately defines the concept of a 'bag' which you used somewhere else but didn't define.
@PhoenixIra any updates on this PR? |
I did not had much time the last weeks, but I will try to work on this the next days. |
I think a class TreeDecomposition<V, I, E> might be useful. Giving the option to change the node type of the tree decomposition can also be useful: especially for nice tree decompositions you may want to have nodes of the type forget, introduce and join instead of checking it manually with set operations. But where should this class be located? |
Regarding the issue #586 we created also a nice tree decomposition builder for chordal graphs, which computes a nice tree decomposition with size and time complexity in O(|V|(|V|+|E|)).
We used the idea of an algorithm which outputs a non-nice tree decomposition of size and time complexity in O(|V|+|E|), but adapting it to nice tree decomposition gives here an additional cost (which we do not get for interval graphs).
We also squashed the commit history for a cleaner history and with better readable commits.
HISTORY.md
orCONTRIBUTORS.md