Skip to content
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

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

PhoenixIra
Copy link

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.


Copy link
Collaborator

@Toptachamann Toptachamann left a 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>
Copy link
Collaborator

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>
Copy link
Collaborator

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);
Copy link
Collaborator

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)
{
Copy link
Collaborator

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)
Copy link
Collaborator

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)
Copy link
Collaborator

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()
Copy link
Collaborator

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"
Copy link
Collaborator

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);
Copy link
Collaborator

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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Collaborator

@AlexandruValeanu AlexandruValeanu left a 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;
Copy link
Collaborator

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);
Copy link
Collaborator

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()
Copy link
Collaborator

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)
Copy link
Collaborator

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
Copy link
Collaborator

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()}
Copy link
Collaborator

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()
Copy link
Collaborator

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
Copy link
Collaborator

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;
Copy link
Collaborator

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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where's the header?

Copy link
Collaborator

@AlexandruValeanu AlexandruValeanu left a 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.

@PhoenixIra PhoenixIra force-pushed the chordalDecompositionPR branch 2 times, most recently from a068021 to 0544d8e Compare July 31, 2018 08:23
@PhoenixIra
Copy link
Author

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.
Furthermore I also changed the factory methods to constructors and merged master into the branch (and squashed my commits)

{

// resulting decomposition
protected Graph<Integer, DefaultEdge> decomposition;
Copy link
Member

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).

Copy link
Author

@PhoenixIra PhoenixIra Aug 3, 2018

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.

Copy link
Collaborator

@Toptachamann Toptachamann left a 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();
Copy link
Collaborator

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?

Copy link
Member

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).

Copy link
Collaborator

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

Copy link
Member

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
Copy link
Collaborator

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
Copy link
Collaborator

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
Copy link
Collaborator

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
Copy link
Collaborator

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?

Copy link
Author

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());
Copy link
Collaborator

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>
Copy link
Collaborator

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
Copy link
Collaborator

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
Copy link
Collaborator

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
Copy link
Collaborator

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?

Copy link
Author

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).
*/
Copy link
Collaborator

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}
Copy link
Collaborator

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
Copy link
Collaborator

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.
Copy link
Collaborator

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
Copy link
Collaborator

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>
Copy link
Collaborator

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?

Copy link
Author

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|)).

Copy link
Collaborator

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.

Copy link
Author

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.

Copy link
Collaborator

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.

Copy link
Author

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
 */

Copy link
Collaborator

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));
Copy link
Collaborator

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));
Copy link
Collaborator

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>();
Copy link
Collaborator

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);
Copy link
Collaborator

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){
Copy link
Collaborator

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){
Copy link
Collaborator

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
Copy link
Collaborator

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)
Copy link
Collaborator

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.

Copy link
Collaborator

@jkinable jkinable left a 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 and TreeDecomposition? 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>
Copy link
Collaborator

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?

Copy link
Author

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.
Copy link
Collaborator

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;
Copy link
Collaborator

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;
Copy link
Collaborator

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())
Copy link
Collaborator

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()
Copy link
Collaborator

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()
Copy link
Collaborator

@jkinable jkinable Sep 12, 2018

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
Copy link
Collaborator

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.

@jkinable
Copy link
Collaborator

jkinable commented Oct 1, 2018

@PhoenixIra any updates on this PR?

@PhoenixIra
Copy link
Author

I did not had much time the last weeks, but I will try to work on this the next days.

@PhoenixIra
Copy link
Author

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants