Compare commits

...

3 Commits
0.1.0 ... 0.1.3

Author SHA1 Message Date
William Jacobs
458cdd3ddd Fixed size calculation in optimizeForestEdges()
This fixes the code in ConnGraph.optimizeForestEdges() that computes the resulting size after combining two Euler tour trees into one. Technically, the old behavior was still correct, but only by accident; this change is still kind of a bug fix.
2019-03-18 13:21:26 -04:00
William Jacobs
05d678a189 Split removeEdge and optimize() into smaller methods
This moves a bit of functionality from ConnGraph.removeEdge and ConnGraph.optimize() into new methods, in order to improve readability.
2019-03-16 14:42:36 -04:00
William Jacobs
1536209eb3 Optimization: don't push forest edges if there are no non-forest edges
This optimizes the process of searching for a replacement edge to refrain from pushing forest edges down when a tree does not have any same-level non-forest edges. If a tree doesn't have any such edges, then there definitely isn't a replacement edge at that level, so we can avoid doing extra work.

Interestingly, with this optimization, ConnGraph reduces to a single Euler tour forest if the graph is a forest, with addEdge and removeEdge taking O(log N) (non-amortized) time with high probability.
2019-03-06 22:24:50 -05:00
6 changed files with 77 additions and 57 deletions

View File

@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.btrekkie.connectivity</groupId>
<artifactId>dynamic-connectivity</artifactId>
<version>0.1.0</version>
<version>0.1.3</version>
<name>dynamic-connectivity</name>
<description>Data structure for dynamic connectivity in undirected graphs</description>
<build>
@@ -43,7 +43,7 @@
<dependency>
<groupId>com.github.btrekkie</groupId>
<artifactId>RedBlackNode</artifactId>
<version>1.0.0</version>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>

View File

@@ -75,8 +75,8 @@ import java.util.Map;
* reason they are probabilistic is that they involve hash lookups, using the vertexInfo and VertexInfo.edges hash maps.
* Given that each ConnVertex has a random hash code, it is easy to demonstrate that lookups take O(1) expected time.
* Furthermore, I claim that they take O(log N / log log N) time with high probability. This claim is sufficient to
* establish that all time bounds that are at least O(log N / log log N) if we exclude the hash lookup can be sustained
* if we add the qualifier "with high probability."
* establish that all time bounds that are at least O(log N / log log N) if we exclude hash lookups can be sustained if
* we add the qualifier "with high probability."
*
* This claim is based on information presented in
* https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-851-advanced-data-structures-spring-2012/lecture-videos/session-10-dictionaries/ .
@@ -258,7 +258,7 @@ public class ConnGraph {
/**
* Equivalent implementation is contractual.
*
* This method is useful for when a node's lists (graphListHead or forestListHead) or a vertex's arbitrary visit
* This method is useful for when an EulerTourVertex's lists (graphListHead or forestListHead) or arbitrary visit
* change, as these affect the hasGraphEdge and hasForestEdge augmentations.
*/
private void augmentAncestorFlags(EulerTourNode node) {
@@ -505,6 +505,33 @@ public class ConnGraph {
return new EulerTourEdge(newNode, max);
}
/** Removes the specified edge from the Euler tour forest F_i. */
private void removeForestEdge(EulerTourEdge edge) {
EulerTourNode firstNode;
EulerTourNode secondNode;
if (edge.visit1.compareTo(edge.visit2) < 0) {
firstNode = edge.visit1;
secondNode = edge.visit2;
} else {
firstNode = edge.visit2;
secondNode = edge.visit1;
}
if (firstNode.vertex.arbitraryVisit == firstNode) {
EulerTourNode successor = secondNode.successor();
firstNode.vertex.arbitraryVisit = successor;
augmentAncestorFlags(firstNode);
augmentAncestorFlags(successor);
}
EulerTourNode root = firstNode.root();
EulerTourNode[] firstSplitRoots = root.split(firstNode);
EulerTourNode before = firstSplitRoots[0];
EulerTourNode[] secondSplitRoots = firstSplitRoots[1].split(secondNode.successor());
before.concatenate(secondSplitRoots[1]);
firstNode.removeWithoutGettingRoot();
}
/**
* Adds the specified edge to the edge map for srcInfo (srcInfo.edges). Assumes that the edge is not currently in
* the map.
@@ -794,31 +821,8 @@ public class ConnGraph {
augmentAncestorFlags(edge.vertex2.arbitraryVisit);
if (edge.eulerTourEdge != null) {
// Remove the edge from all of the Euler tour trees that contain it
for (EulerTourEdge levelEdge = edge.eulerTourEdge; levelEdge != null; levelEdge = levelEdge.higherEdge) {
EulerTourNode firstNode;
EulerTourNode secondNode;
if (levelEdge.visit1.compareTo(levelEdge.visit2) < 0) {
firstNode = levelEdge.visit1;
secondNode = levelEdge.visit2;
} else {
firstNode = levelEdge.visit2;
secondNode = levelEdge.visit1;
}
if (firstNode.vertex.arbitraryVisit == firstNode) {
EulerTourNode successor = secondNode.successor();
firstNode.vertex.arbitraryVisit = successor;
augmentAncestorFlags(firstNode);
augmentAncestorFlags(successor);
}
EulerTourNode root = firstNode.root();
EulerTourNode[] firstSplitRoots = root.split(firstNode);
EulerTourNode before = firstSplitRoots[0];
EulerTourNode[] secondSplitRoots = firstSplitRoots[1].split(secondNode.successor());
before.concatenate(secondSplitRoots[1]);
firstNode.removeWithoutGettingRoot();
removeForestEdge(levelEdge);
}
edge.eulerTourEdge = null;
@@ -829,17 +833,22 @@ public class ConnGraph {
while (levelVertex1 != null) {
EulerTourNode root1 = levelVertex1.arbitraryVisit.root();
EulerTourNode root2 = levelVertex2.arbitraryVisit.root();
EulerTourNode root;
if (root1.size < root2.size) {
root = root1;
} else {
root = root2;
}
pushForestEdges(root);
replacementEdge = findReplacementEdge(root);
if (replacementEdge != null) {
break;
// Optimization: if hasGraphEdge is false for one of the roots, then there definitely isn't a
// replacement edge at this level
if (root1.hasGraphEdge && root2.hasGraphEdge) {
EulerTourNode root;
if (root1.size < root2.size) {
root = root1;
} else {
root = root2;
}
pushForestEdges(root);
replacementEdge = findReplacementEdge(root);
if (replacementEdge != null) {
break;
}
}
// To save space, get rid of trees with one node
@@ -1039,18 +1048,10 @@ public class ConnGraph {
}
/**
* Attempts to optimize the internal representation of the graph so that future updates will take less time. This
* method does not affect how long queries such as "connected" will take. You may find it beneficial to call
* optimize() when there is some downtime. Note that this method generally increases the amount of space the
* ConnGraph uses, but not beyond the bound of O(V log V + E).
* Pushes all forest edges as far down as possible, so that any further pushes would violate the constraint on the
* size of connected components. The current implementation of this method takes O(V log^2 V) time.
*/
public void optimize() {
// The current implementation of optimize() takes O(V log^2 V + E log V log log V) time
rebuild();
// Greedily push each forest edge as far down as possible - to the lowest level where the constraint on the
// size of connected components isn't violated
private void optimizeForestEdges() {
for (VertexInfo info : vertexInfo.values()) {
int level = maxLogVertexCountSinceRebuild;
EulerTourVertex vertex;
@@ -1072,20 +1073,21 @@ public class ConnGraph {
EulerTourVertex lowerVertex1 = vertex;
EulerTourVertex lowerVertex2 = edge.vertex2;
for (int lowerLevel = level - 1; lowerLevel > 0; lowerLevel--) {
int size = 0;
// Compute the total size if we combine the Euler tour trees
int combinedSize = 1;
if (lowerVertex1.lowerVertex != null) {
size = lowerVertex1.lowerVertex.arbitraryVisit.root().size;
combinedSize += lowerVertex1.lowerVertex.arbitraryVisit.root().size;
} else {
size = 1;
combinedSize++;
}
if (lowerVertex2.lowerVertex != null) {
size += lowerVertex2.lowerVertex.arbitraryVisit.root().size;
combinedSize += lowerVertex2.lowerVertex.arbitraryVisit.root().size;
} else {
size++;
combinedSize++;
}
// X EulerTourVertices = (2 * X - 1) EulerTourNodes
if (size > 2 * (1 << lowerLevel) - 1) {
if (combinedSize > 2 * (1 << lowerLevel) - 1) {
break;
}
@@ -1116,8 +1118,13 @@ public class ConnGraph {
level++;
}
}
}
// Push each non-forest edge down to the lowest level where the endpoints are in the same connected component
/**
* Pushes each non-forest edge down to the lowest level where the endpoints are in the same connected component. The
* current implementation of this method takes O(V log V + E log V log log V) time.
*/
private void optimizeGraphEdges() {
for (VertexInfo info : vertexInfo.values()) {
EulerTourVertex vertex;
for (vertex = info.vertex; vertex.lowerVertex != null; vertex = vertex.lowerVertex);
@@ -1180,4 +1187,17 @@ public class ConnGraph {
}
}
}
/**
* Attempts to optimize the internal representation of the graph so that future updates will take less time. This
* method does not affect how long queries such as "connected" will take. You may find it beneficial to call
* optimize() when there is some downtime. Note that this method generally increases the amount of space the
* ConnGraph uses, but not beyond the bound of O(V log V + E).
*/
public void optimize() {
// The current implementation of optimize() takes O(V log^2 V + E log V log log V) time
rebuild();
optimizeForestEdges();
optimizeGraphEdges();
}
}

Binary file not shown.