diff --git a/src/main/java/com/github/btrekkie/LICENSE b/src/main/java/com/github/btrekkie/LICENSE new file mode 100644 index 000000000..b9f0f2eb5 --- /dev/null +++ b/src/main/java/com/github/btrekkie/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 btrekkie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/main/java/com/github/btrekkie/README.md b/src/main/java/com/github/btrekkie/README.md new file mode 100644 index 000000000..d7a917212 --- /dev/null +++ b/src/main/java/com/github/btrekkie/README.md @@ -0,0 +1,9 @@ +Original readme --> https://github.com/btrekkie/dynamic-connectivity/ + +I am experimenting with using dynamic connectivity to help with Baritone builder 2 + +https://en.wikipedia.org/wiki/Dynamic_connectivity + +If the experiments in this repo go well, I want to integrate it +into https://github.com/cabaletta/baritone/tree/builder-2/src/main/java/baritone/builder (i think MIT is +compatible with LGPL so this is ok) \ No newline at end of file diff --git a/src/main/java/com/github/btrekkie/connectivity/Augmentation.java b/src/main/java/com/github/btrekkie/connectivity/Augmentation.java new file mode 100644 index 000000000..4c9a4f8d7 --- /dev/null +++ b/src/main/java/com/github/btrekkie/connectivity/Augmentation.java @@ -0,0 +1,39 @@ +package com.github.btrekkie.connectivity; + +/** + * A combining function for taking the augmentations associated with a set of ConnVertices and reducing them to a single + * result. The function is a binary operation for combining two values into one. For example, given vertices with + * augmentations A1, A2, A3, and A4, the combined result may be obtained by computing + * combine(combine(combine(A1, A2), A3), A4). In order for an augmentation result to be meaningful, the combining + * function must be commutative, meaning combine(x, y) is equivalent to combine(y, x), and associative, meaning + * combine(x, combine(y, z)) is equivalent to combine(combine(x, y), z). + *

+ * If a ConnGraph represents a game map, then one example of an augmentation would be the amount of gold accessible from + * a certain location. Each vertex would be augmented with the amount of gold in that location, and the combining + * function would add the two amounts of gold passed in as arguments. Another example would be the strongest monster + * that can reach a particular location. Each vertex with at least one monster would be augmented with a pointer to the + * strongest monster at that location, and the combining function would return the stronger of the two monsters passed + * in as arguments. A third example would be the number of locations accessible from a given vertex. Each vertex would + * be augmented with the number 1, and the combining function would add the two numbers of vertices passed in as + * arguments. + *

+ * ConnGraph treats two augmentation values X and Y as interchangeable if they are equal, as in + * X != null ? X.equals(Y) : Y == null. The same holds for two combined augmentation values, and for one combined + * augmentation value and one augmentation value. + *

+ * See the comments for ConnGraph. + */ +public interface Augmentation { + /** + * Returns the result of combining the specified values into one. Each argument is either the augmentation + * information associated with a vertex, or the result of a previous call to "combine". + *

+ * Note that a value of null is never passed in to indicate the absence of augmentation information. The fact that + * ConnGraph.getVertexAugmentation, for example, may return null when there is no associated augmentation might lead + * you to believe that a null argument indicates the absence of augmentation information, but again, it does not. A + * null argument can only mean that a vertex is explicitly associated with null augmentation information, due to a + * prior call to ConnGraph.setVertexAugmentation(vertex, null), or that the "combine" method previously returned + * null. + */ + public Object combine(Object value1, Object value2); +} diff --git a/src/main/java/com/github/btrekkie/connectivity/ConnEdge.java b/src/main/java/com/github/btrekkie/connectivity/ConnEdge.java new file mode 100644 index 000000000..1b8ed65e3 --- /dev/null +++ b/src/main/java/com/github/btrekkie/connectivity/ConnEdge.java @@ -0,0 +1,65 @@ +package com.github.btrekkie.connectivity; + +/** + * Represents an edge in a ConnGraph, at the level of the edge (i.e. at the lowest level i for which G_i contains the + * edge). Every graph edge has exactly one corresponding ConnEdge object, regardless of the number of levels it appears + * in. See the comments for the implementation of ConnGraph. + *

+ * ConnEdges are stored in the linked lists suggested by EulerTourVertex.graphListHead and + * EulerTourVertex.forestListHead. Each ConnEdge is in two linked lists, so care must be taken when traversing the + * linked lists. prev1 and next1 are the links for the list starting at vertex1.graphListHead or vertex1.forestListHead, + * while prev2 and next2 are the links for vertex2. But the vertex1 and vertex2 fields of a given edge are different + * from the vertex1 and vertex2 fields of the linked edges. For example, the edge after next1 is not necessarily + * next1.next1. It depends on whether next1.vertex1 is the same as vertex1. If next1.vertex1 == vertex1, then the edge + * after next1 is next1.next1, but otherwise, it is next1.next2. + */ +class ConnEdge { + /** + * The edge's first endpoint (at the same level as the edge). + */ + public EulerTourVertex vertex1; + + /** + * The edge's second endpoint (at the same level as the edge). + */ + public EulerTourVertex vertex2; + + /** + * The EulerTourEdge object describing the edge's presence in an Euler tour tree, at the same level as the edge, or + * null if the edge is not in the Euler tour forest F_i. + */ + public EulerTourEdge eulerTourEdge; + + /** + * The edge preceding this in a linked list of same-level edges adjacent to vertex1, if any. The edge is either part + * of a list of non-forest edges starting with vertex1.graphListHead, or part of a list of forest edges starting + * with vertex1.forestListHead. Note that this list excludes any edges that also appear in lower levels. + */ + public ConnEdge prev1; + + /** + * The edge succeeding this in a linked list of same-level edges adjacent to vertex1, if any. The edge is either + * part of a list of non-forest edges starting with vertex1.graphListHead, or part of a list of forest edges + * starting with vertex1.forestListHead. Note that this list excludes any edges that also appear in lower levels. + */ + public ConnEdge next1; + + /** + * The edge preceding this in a linked list of same-level edges adjacent to vertex2, if any. The edge is either part + * of a list of non-forest edges starting with vertex2.graphListHead, or part of a list of forest edges starting + * with vertex2.forestListHead. Note that this list excludes any edges that also appear in lower levels. + */ + public ConnEdge prev2; + + /** + * The edge succeeding this in a linked list of same-level edges adjacent to vertex2, if any. The edge is either + * part of a list of non-forest edges starting with vertex2.graphListHead, or part of a list of forest edges + * starting with vertex2.forestListHead. Note that this list excludes any edges that also appear in lower levels. + */ + public ConnEdge next2; + + public ConnEdge(EulerTourVertex vertex1, EulerTourVertex vertex2) { + this.vertex1 = vertex1; + this.vertex2 = vertex2; + } +} diff --git a/src/main/java/com/github/btrekkie/connectivity/ConnGraph.java b/src/main/java/com/github/btrekkie/connectivity/ConnGraph.java new file mode 100644 index 000000000..2a813153f --- /dev/null +++ b/src/main/java/com/github/btrekkie/connectivity/ConnGraph.java @@ -0,0 +1,1239 @@ +package com.github.btrekkie.connectivity; + +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongSet; +import it.unimi.dsi.fastutil.longs.LongSets; + +import java.util.*; + +/** + * Implements an undirected graph with dynamic connectivity. It supports adding and removing edges and determining + * whether two vertices are connected - whether there is a path between them. Adding and removing edges take O(log^2 N) + * amortized time with high probability, while checking whether two vertices are connected takes O(log N) time with high + * probability. It uses O(V log V + E) space, where V is the number of vertices and E is the number of edges. Note that + * a ConnVertex may appear in multiple ConnGraphs, with a different set of adjacent vertices in each graph. + *

+ * ConnGraph optionally supports arbitrary augmentation. Each vertex may have an associated augmentation, or value. + * Given a vertex V, ConnGraph can quickly report the result of combining the augmentations of all of the vertices in + * the connected component containing V, using a combining function provided to the constructor. For example, if a + * ConnGraph represents a game map, then given the location of the player, we can quickly determine the amount of gold + * the player can access, or the strongest monster that can reach him. Augmentation does not affect the running time or + * space of ConnGraph in terms of big O notation, assuming the augmentation function takes a constant amount of time and + * the augmentation takes a constant amount of space. Retrieving the combined augmentation for a connected component + * takes O(log N) time with high probability. (Although ConnGraph does not directly support augmenting edges, this can + * also be accomplished, by imputing each edge's augmentation to an adjacent vertex.) + *

+ * When a vertex no longer has any adjacent edges, and it has no augmentation information, ConnGraph stops keeping track + * of the vertex. This reduces the time and space bounds of the ConnGraph, and it enables the ConnVertex to be garbage + * collected. If you know you are finished with a vertex, and that vertex has an augmentation, then you should call + * removeVertexAugmentation on the vertex, so that the graph can release it. + *

+ * As a side note, it would be more proper if ConnGraph had a generic type parameter indicating the type of the + * augmentation values. However, it is expected that it is more common not to use augmentation, so by not using a type + * parameter, we make usage of the ConnGraph class more convenient and less confusing in the common case. + */ +/* ConnGraph is implemented using a data structure described in + * http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.89.919&rep=rep1&type=pdf (Holm, de Lichtenberg, and Thorup + * (1998): Poly-Logarithmic Deterministic Fully-Dynamic Algorithms for Connectivity, Minimum Spanning Tree, 2-Edge, and + * Biconnectivity). However, ConnGraph does not include the optimization of using a B-tree in the top level, so queries + * take O(log N) time rather than O(log N / log log N) time. + * + * This implementation is actually based on a slightly modified description of the data structure given in + * https://ocw.mit.edu/courses/6-851-advanced-data-structures-spring-2012/resources/session-20-dynamic-graphs-ii/ . The + * description in the video differs from the data structure in the paper in that the levels are numbered in reverse + * order, the constraint on tree sizes is different, and the augmentation uses booleans in place of edges. In addition, + * the video defines subgraphs G_i. The change in the constraint on tree sizes is beneficial because it makes it easier + * to delete vertices. + * + * Note that the data structure described in the video is faulty. In the procedure for deleting an edge, it directs us + * to push down some edges. When we push an edge from level i to level i - 1, we would need to add the edge to + * F_{i - 1}, if the endpoints were not already connected in G_{i - 1}. However, this would violate the invariant that + * F_i be a superset of F_{i - 1}. To fix this problem, before searching for a replacement edge in level i, ConnGraph + * first pushes all level-i edges in the relevant tree down to level i - 1 and adds them to F_{i - 1}, as in the + * original paper. That way, when we subsequently push down edges, we can safely add them to G_{i - 1} without also + * adding them to F_{i - 1}. In order to do this efficiently, each vertex stores a second adjacency list, consisting of + * the level-i edges that are in F_i. In addition, we augment each Euler tour tree node with an a second boolean, + * indicating whether the subtree rooted at the node contains a canonical visit to a vertex with at least one level-i + * edge that is in F_i. + * + * The implementation of rerooting an Euler tour tree described in the video lecture appears to be incorrect as well. It + * breaks the references to the vertices' first and last visits. To fix this, we do not store references to the + * vertices' first and last visits. Instead, we have each vertex store a reference to an arbitrary visit to that vertex. + * We also maintain edge objects for each of the edges in the Euler tours. Each such edge stores a pointer to the two + * visits that precede the traversal of the edge in the Euler tour. These do not change when we perform a reroot. The + * remove edge operation then requires a pointer to the edge object, rather than pointers to the vertices. Given the + * edge object, we can splice out the range of nodes between the two visits that precede the edge. + * + * Rather than explicitly giving each edge a level number, the level numbers are implicit through links from each level + * to the level below it. For purposes of analysis, the level number of the top level is equal to + * maxLogVertexCountSinceRebuild, the ceiling of log base 2 of the maximum number of vertices in the graph since the + * last rebuild operation. Once the ratio between the maximum number of vertices since the last rebuild and the current + * number of vertices becomes large enough, we rebuild the data structure. This ensures that the level number of the top + * level is O(log V). + * + * Most methods' time bounds are probabilistic. For example, "connected" takes O(log N) time with high probability. The + * 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 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/ . + * According to that video, in a hash map with chaining, if the hash function is totally random, then the longest chain + * length is O(log N / log log N) with high probability. A totally random hash function is a slightly different concept + * than having ConnVertex.hashCode() return a random value, due to the particular definition of "hash function" used in + * the video. Nevertheless, the analysis is the same. A random hashCode() implementation ultimately results in + * independently hashing each entry to a random bucket, which is equivalent to a totally random hash function. + * + * However, the claim depends on certain features of the implementation of HashMap, gleaned from reading the source + * code. In particular, it assumes that HashMap resolves collisions using chaining. (Newer versions of Java sometimes + * store the entries that hash to the same bucket in binary search trees rather than linked lists, but this can't hurt + * the asymptotic performance.) Note that the implementation of HashMap transforms the return value of hashCode(), by + * "spreading" the higher-order bits to lower-order positions. However, this transform is a permutation of the integers. + * If the input to a transform is selected uniformly at random, and the transform is a permutation, than the output also + * has a uniform random distribution. + */ +public class ConnGraph { + /** + * The difference between ceiling of log base 2 of the maximum number of vertices in the graph since the last call + * to rebuild() or clear() and ceiling of log base 2 of the current number of vertices, at or above which we call + * rebuild(). (There is special handling for 0 vertices.) + */ + private static final int REBUILD_CHANGE = 2; + + /** + * The maximum number of vertices we can store in a ConnGraph. This is limited by the fact that EulerTourNode.size + * is an int. Since the size of an Euler tour tree is one less than twice the number of vertices in the tree, the + * number of vertices may be at most (int)((((long)Integer.MAX_VALUE) + 1) / 2). + *

+ * Of course, we could simply change the "size" field to be a long. But more fundamentally, the number of vertices + * is limited by the fact that vertexInfo and VertexInfo.edges use HashMaps. Using a HashMap becomes problematic at + * around Integer.MAX_VALUE entries. HashMap buckets entries based on 32-bit hash codes, so in principle, it can + * only hash the entries to at most 2^32 buckets. In order to support a significantly greater limit on the number of + * vertices, we would need to use a more elaborate mapping approach. + */ + private static final int MAX_VERTEX_COUNT = 1 << 30; + + /** + * The augmentation function for the graph, if any. + */ + private final Augmentation augmentation; + + /** + * A map from each vertex in this graph to information about the vertex in this graph. If a vertex has no adjacent + * edges and no associated augmentation, we remove it from vertexInfo, to save time and space. Lookups take O(1) + * expected time and O(log N / log log N) time with high probability, because vertexInfo is a HashMap, and + * ConnVertex.hashCode() returns a random integer. + */ + private Long2ObjectOpenHashMap vertexInfo = new Long2ObjectOpenHashMap<>(); + + /** + * Ceiling of log base 2 of the maximum number of vertices in this graph since the last rebuild. This is 0 if that + * number is 0. + */ + private int maxLogVertexCountSinceRebuild; + + /** + * Constructs a new ConnGraph with no augmentation. + */ + public ConnGraph() { + augmentation = null; + } + + /** + * Constructs an augmented ConnGraph, using the specified function to combine augmentation values. + */ + public ConnGraph(Augmentation augmentation) { + this.augmentation = augmentation; + } + + /** + * Equivalent implementation is contractual. + */ + private void assertIsAugmented() { + if (augmentation == null) { + throw new RuntimeException( + "You may only call augmentation-related methods on ConnGraph if the graph is augmented, i.e. if an " + + "Augmentation was passed to the constructor"); + } + } + + /** + * Returns the VertexInfo containing information about the specified vertex in this graph. If the vertex is not in + * this graph (i.e. it does not have an entry in vertexInfo), this method adds it to the graph, and creates a + * VertexInfo object for it. + */ + private VertexInfo ensureInfo(long vertex) { + VertexInfo info = vertexInfo.get(vertex); + if (info != null) { + return info; + } + + if (vertexInfo.size() == MAX_VERTEX_COUNT) { + throw new RuntimeException( + "Sorry, ConnGraph has too many vertices to perform this operation. ConnGraph does not support " + + "storing more than ~2^30 vertices at a time."); + } + + EulerTourVertex eulerTourVertex = new EulerTourVertex(); + EulerTourNode node = new EulerTourNode(eulerTourVertex, augmentation); + eulerTourVertex.arbitraryVisit = node; + node.left = EulerTourNode.LEAF; + node.right = EulerTourNode.LEAF; + node.augment(); + + info = new VertexInfo(eulerTourVertex); + vertexInfo.put(vertex, info); + if (vertexInfo.size() > 1 << maxLogVertexCountSinceRebuild) { + maxLogVertexCountSinceRebuild++; + } + return info; + } + + /** + * Takes the specified vertex out of this graph. We should call this method as soon as a vertex does not have any + * adjacent edges and does not have any augmentation information. This method assumes that the vertex is currently + * in the graph. + */ + private void remove(long vertex) { + vertexInfo.remove(vertex); + if (vertexInfo.size() << REBUILD_CHANGE <= 1 << maxLogVertexCountSinceRebuild) { + rebuild(); + } + } + + /** + * Collapses an adjacency list (either graphListHead or forestListHead) for an EulerTourVertex into the adjacency + * list for an EulerTourVertex that represents the same underlying ConnVertex, but at a higher level. This has the + * effect of prepending the list for the lower level to the beginning of the list for the higher level, and + * replacing all links to the lower-level vertex in the ConnEdges with links to the higher-level vertex. + * + * @param head The first node in the list for the higher-level vertex. + * @param lowerHead The first node in the list for the lower-level vertex. + * @param vertex The higher-level vertex. + * @param lowerVertex The lower-level vertex. + * @return The head of the combined linked list. + */ + private ConnEdge collapseEdgeList( + ConnEdge head, ConnEdge lowerHead, EulerTourVertex vertex, EulerTourVertex lowerVertex) { + if (lowerHead == null) { + return head; + } + + ConnEdge prevLowerEdge = null; + ConnEdge lowerEdge = lowerHead; + while (lowerEdge != null) { + prevLowerEdge = lowerEdge; + if (lowerEdge.vertex1 == lowerVertex) { + lowerEdge.vertex1 = vertex; + lowerEdge = lowerEdge.next1; + } else { + lowerEdge.vertex2 = vertex; + lowerEdge = lowerEdge.next2; + } + } + + if (prevLowerEdge.vertex1 == vertex) { + prevLowerEdge.next1 = head; + } else { + prevLowerEdge.next2 = head; + } + if (head != null) { + if (head.vertex1 == vertex) { + head.prev1 = prevLowerEdge; + } else { + head.prev2 = prevLowerEdge; + } + } + return lowerHead; + } + + /** + * Equivalent implementation is contractual. + *

+ * 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) { + for (EulerTourNode parent = node; parent != null; parent = parent.parent) { + if (!parent.augmentFlags()) { + break; + } + } + } + + /** + * Rebuilds the data structure so that the number of levels is at most the ceiling of log base 2 of the number of + * vertices in the graph (or zero in the case of zero vertices). The current implementation of rebuild() takes + * O(V + E) time, assuming a constant difference between maxLogVertexCountSinceRebuild and the result of the + * logarithm. + */ + private void rebuild() { + // Rebuild the graph by collapsing the top deleteCount + 1 levels into the top level + + if (vertexInfo.isEmpty()) { + maxLogVertexCountSinceRebuild = 0; + return; + } + int deleteCount = 0; + while (2 * vertexInfo.size() <= 1 << maxLogVertexCountSinceRebuild) { + maxLogVertexCountSinceRebuild--; + deleteCount++; + } + if (deleteCount == 0) { + return; + } + + for (VertexInfo info : vertexInfo.values()) { + EulerTourVertex vertex = info.vertex; + EulerTourVertex lowerVertex = vertex; + for (int i = 0; i < deleteCount; i++) { + lowerVertex = lowerVertex.lowerVertex; + if (lowerVertex == null) { + break; + } + + vertex.graphListHead = + collapseEdgeList(vertex.graphListHead, lowerVertex.graphListHead, vertex, lowerVertex); + if (lowerVertex.forestListHead != null) { + // Change the eulerTourEdge links + ConnEdge lowerEdge = lowerVertex.forestListHead; + while (lowerEdge != null) { + if (lowerEdge.vertex1 == lowerVertex) { + // We'll address this edge when we visit lowerEdge.vertex2 + lowerEdge = lowerEdge.next1; + } else { + EulerTourEdge edge = lowerEdge.eulerTourEdge.higherEdge; + for (int j = 0; j < i; j++) { + edge = edge.higherEdge; + } + lowerEdge.eulerTourEdge = edge; + lowerEdge = lowerEdge.next2; + } + } + + vertex.forestListHead = + collapseEdgeList(vertex.forestListHead, lowerVertex.forestListHead, vertex, lowerVertex); + } + } + + if (lowerVertex != null) { + lowerVertex = lowerVertex.lowerVertex; + } + vertex.lowerVertex = lowerVertex; + if (lowerVertex != null) { + lowerVertex.higherVertex = vertex; + } + augmentAncestorFlags(vertex.arbitraryVisit); + } + } + + /** + * Adds the specified edge to the graph adjacency list of edge.vertex1, as in EulerTourVertex.graphListHead. + * Assumes it is not currently in any lists, except possibly the graph adjacency list of edge.vertex2. + */ + private void addToGraphLinkedList1(ConnEdge edge) { + edge.prev1 = null; + edge.next1 = edge.vertex1.graphListHead; + if (edge.next1 != null) { + if (edge.next1.vertex1 == edge.vertex1) { + edge.next1.prev1 = edge; + } else { + edge.next1.prev2 = edge; + } + } + edge.vertex1.graphListHead = edge; + } + + /** + * Adds the specified edge to the graph adjacency list of edge.vertex2, as in EulerTourVertex.graphListHead. + * Assumes it is not currently in any lists, except possibly the graph adjacency list of edge.vertex1. + */ + private void addToGraphLinkedList2(ConnEdge edge) { + edge.prev2 = null; + edge.next2 = edge.vertex2.graphListHead; + if (edge.next2 != null) { + if (edge.next2.vertex1 == edge.vertex2) { + edge.next2.prev1 = edge; + } else { + edge.next2.prev2 = edge; + } + } + edge.vertex2.graphListHead = edge; + } + + /** + * Adds the specified edge to the graph adjacency lists of edge.vertex1 and edge.vertex2, as in + * EulerTourVertex.graphListHead. Assumes it is not currently in any lists. + */ + private void addToGraphLinkedLists(ConnEdge edge) { + addToGraphLinkedList1(edge); + addToGraphLinkedList2(edge); + } + + /** + * Adds the specified edge to the forest adjacency lists of edge.vertex1 and edge.vertex2, as in + * EulerTourVertex.forestListHead. Assumes it is not currently in any lists. + */ + private void addToForestLinkedLists(ConnEdge edge) { + edge.prev1 = null; + edge.next1 = edge.vertex1.forestListHead; + if (edge.next1 != null) { + if (edge.next1.vertex1 == edge.vertex1) { + edge.next1.prev1 = edge; + } else { + edge.next1.prev2 = edge; + } + } + edge.vertex1.forestListHead = edge; + + edge.prev2 = null; + edge.next2 = edge.vertex2.forestListHead; + if (edge.next2 != null) { + if (edge.next2.vertex1 == edge.vertex2) { + edge.next2.prev1 = edge; + } else { + edge.next2.prev2 = edge; + } + } + edge.vertex2.forestListHead = edge; + } + + /** + * Removes the specified edge from an adjacency list of edge.vertex1, as in graphListHead and forestListHead. + * Assumes it is initially in exactly one of the lists for edge.vertex1. + */ + private void removeFromLinkedList1(ConnEdge edge) { + if (edge.prev1 != null) { + if (edge.prev1.vertex1 == edge.vertex1) { + edge.prev1.next1 = edge.next1; + } else { + edge.prev1.next2 = edge.next1; + } + } else if (edge == edge.vertex1.graphListHead) { + edge.vertex1.graphListHead = edge.next1; + } else { + edge.vertex1.forestListHead = edge.next1; + } + if (edge.next1 != null) { + if (edge.next1.vertex1 == edge.vertex1) { + edge.next1.prev1 = edge.prev1; + } else { + edge.next1.prev2 = edge.prev1; + } + } + } + + /** + * Removes the specified edge from an adjacency list of edge.vertex2, as in graphListHead and forestListHead. + * Assumes it is initially in exactly one of the lists for edge.vertex2. + */ + private void removeFromLinkedList2(ConnEdge edge) { + if (edge.prev2 != null) { + if (edge.prev2.vertex1 == edge.vertex2) { + edge.prev2.next1 = edge.next2; + } else { + edge.prev2.next2 = edge.next2; + } + } else if (edge == edge.vertex2.graphListHead) { + edge.vertex2.graphListHead = edge.next2; + } else { + edge.vertex2.forestListHead = edge.next2; + } + if (edge.next2 != null) { + if (edge.next2.vertex1 == edge.vertex2) { + edge.next2.prev1 = edge.prev2; + } else { + edge.next2.prev2 = edge.prev2; + } + } + } + + /** + * Removes the specified edge from the adjacency lists of edge.vertex1 and edge.vertex2, as in graphListHead and + * forestListHead. Assumes it is initially in exactly one of the lists for edge.vertex1 and exactly one of the lists + * for edge.vertex2. + */ + private void removeFromLinkedLists(ConnEdge edge) { + removeFromLinkedList1(edge); + removeFromLinkedList2(edge); + } + + /** + * Add an edge between the specified vertices to the Euler tour forest F_i. Assumes that the edge's endpoints are + * initially in separate trees. Returns the created edge. + */ + private EulerTourEdge addForestEdge(EulerTourVertex vertex1, EulerTourVertex vertex2) { + // We need to be careful about where we split and where we add and remove nodes, so as to avoid breaking any + // EulerTourEdge.visit* fields + EulerTourNode root = vertex2.arbitraryVisit.root(); + EulerTourNode max = root.max(); + if (max.vertex != vertex2) { + // Reroot + EulerTourNode min = root.min(); + if (max.vertex.arbitraryVisit == max) { + max.vertex.arbitraryVisit = min; + augmentAncestorFlags(min); + augmentAncestorFlags(max); + } + root = max.remove(); + EulerTourNode[] splitRoots = root.split(vertex2.arbitraryVisit); + root = splitRoots[1].concatenate(splitRoots[0]); + EulerTourNode newNode = new EulerTourNode(vertex2, root.augmentationFunc); + newNode.left = EulerTourNode.LEAF; + newNode.right = EulerTourNode.LEAF; + newNode.isRed = true; + EulerTourNode parent = root.max(); + parent.right = newNode; + newNode.parent = parent; + root = newNode.fixInsertion(); + max = newNode; + } + + EulerTourNode[] splitRoots = vertex1.arbitraryVisit.root().split(vertex1.arbitraryVisit); + EulerTourNode before = splitRoots[0]; + EulerTourNode after = splitRoots[1]; + EulerTourNode newNode = new EulerTourNode(vertex1, root.augmentationFunc); + before.concatenate(root, newNode).concatenate(after); + 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. + * + * @param edge The edge. + * @param srcInfo The source vertex's info. + * @param destVertex The destination vertex, i.e. the edge's key in srcInfo.edges. + */ + private void addToEdgeMap(ConnEdge edge, VertexInfo srcInfo, long destVertex) { + srcInfo.edges.put(destVertex, edge); + } + + @Deprecated + public boolean addEdge(ConnVertex connVertex1, ConnVertex connVertex2) { + return addEdge(connVertex1.getIdentity(), connVertex2.getIdentity()); + } + + /** + * Adds an edge between the specified vertices, if such an edge is not already present. Taken together with + * removeEdge, this method takes O(log^2 N) amortized time with high probability. + * + * @return Whether there was no edge between the vertices. + */ + public boolean addEdge(long connVertex1, long connVertex2) { + if (connVertex1 == connVertex2) { + throw new IllegalArgumentException("Self-loops are not allowed"); + } + if (vertexInfo.size() >= MAX_VERTEX_COUNT - 1) { + throw new RuntimeException( + "Sorry, ConnGraph has too many vertices to perform this operation. ConnGraph does not support " + + "storing more than ~2^30 vertices at a time."); + } + VertexInfo info1 = ensureInfo(connVertex1); + if (info1.edges.containsKey(connVertex2)) { + return false; + } + VertexInfo info2 = ensureInfo(connVertex2); + + EulerTourVertex vertex1 = info1.vertex; + EulerTourVertex vertex2 = info2.vertex; + ConnEdge edge = new ConnEdge(vertex1, vertex2); + + if (vertex1.arbitraryVisit.root() == vertex2.arbitraryVisit.root()) { + addToGraphLinkedLists(edge); + } else { + addToForestLinkedLists(edge); + edge.eulerTourEdge = addForestEdge(vertex1, vertex2); + } + augmentAncestorFlags(vertex1.arbitraryVisit); + augmentAncestorFlags(vertex2.arbitraryVisit); + + addToEdgeMap(edge, info1, connVertex2); + addToEdgeMap(edge, info2, connVertex1); + return true; + } + + /** + * Returns vertex.lowerVertex. If this is null, ensureLowerVertex sets vertex.lowerVertex to a new vertex and + * returns it. + */ + private EulerTourVertex ensureLowerVertex(EulerTourVertex vertex) { + EulerTourVertex lowerVertex = vertex.lowerVertex; + if (lowerVertex == null) { + lowerVertex = new EulerTourVertex(); + EulerTourNode lowerNode = new EulerTourNode(lowerVertex, null); + lowerVertex.arbitraryVisit = lowerNode; + vertex.lowerVertex = lowerVertex; + lowerVertex.higherVertex = vertex; + + lowerNode.left = EulerTourNode.LEAF; + lowerNode.right = EulerTourNode.LEAF; + lowerNode.augment(); + } + return lowerVertex; + } + + /** + * Pushes all level-i forest edges in the tree rooted at the specified node down to level i - 1, and adds them to + * F_{i - 1}, where i is the level of the tree. + */ + private void pushForestEdges(EulerTourNode root) { + // Iterate over all of the nodes that have hasForestEdge == true + if (!root.hasForestEdge || root.size == 1) { + return; + } + EulerTourNode node; + for (node = root; node.left.hasForestEdge; node = node.left) ; + while (node != null) { + EulerTourVertex vertex = node.vertex; + ConnEdge edge = vertex.forestListHead; + if (edge != null) { + EulerTourVertex lowerVertex = ensureLowerVertex(vertex); + ConnEdge prevEdge = null; + while (edge != null) { + if (edge.vertex2 == vertex || edge.vertex2 == lowerVertex) { + // We address this edge when we visit edge.vertex1 + prevEdge = edge; + edge = edge.next2; + } else { + edge.vertex1 = lowerVertex; + edge.vertex2 = ensureLowerVertex(edge.vertex2); + EulerTourEdge lowerEdge = addForestEdge(edge.vertex1, edge.vertex2); + lowerEdge.higherEdge = edge.eulerTourEdge; + edge.eulerTourEdge = lowerEdge; + prevEdge = edge; + edge = edge.next1; + } + } + + // Prepend vertex.forestListHead to the beginning of lowerVertex.forestListHead + if (prevEdge.vertex1 == lowerVertex) { + prevEdge.next1 = lowerVertex.forestListHead; + } else { + prevEdge.next2 = lowerVertex.forestListHead; + } + if (lowerVertex.forestListHead != null) { + if (lowerVertex.forestListHead.vertex1 == lowerVertex) { + lowerVertex.forestListHead.prev1 = prevEdge; + } else { + lowerVertex.forestListHead.prev2 = prevEdge; + } + } + lowerVertex.forestListHead = vertex.forestListHead; + vertex.forestListHead = null; + augmentAncestorFlags(lowerVertex.arbitraryVisit); + } + + // Iterate to the next node with hasForestEdge == true, clearing hasForestEdge as we go + if (node.right.hasForestEdge) { + for (node = node.right; node.left.hasForestEdge; node = node.left) ; + } else { + node.hasForestEdge = false; + while (node.parent != null && node.parent.right == node) { + node = node.parent; + node.hasForestEdge = false; + } + node = node.parent; + } + } + } + + /** + * Searches for a level-i edge connecting a vertex in the tree rooted at the specified node to a vertex in another + * tree, where i is the level of the tree. This is a "replacement" edge because it replaces the edge that was + * previously connecting the two trees. We push any level-i edges we encounter that do not connect to another tree + * down to level i - 1, adding them to G_{i - 1}. This method assumes that root.hasForestEdge is false. + * + * @param root The root of the tree. + * @return The replacement edge, or null if there is no replacement edge. + */ + private ConnEdge findReplacementEdge(EulerTourNode root) { + // Iterate over all of the nodes that have hasGraphEdge == true + if (!root.hasGraphEdge) { + return null; + } + EulerTourNode node; + for (node = root; node.left.hasGraphEdge; node = node.left) ; + while (node != null) { + EulerTourVertex vertex = node.vertex; + ConnEdge edge = vertex.graphListHead; + if (edge != null) { + ConnEdge replacementEdge = null; + ConnEdge prevEdge = null; + while (edge != null) { + EulerTourVertex adjVertex; + ConnEdge nextEdge; + if (edge.vertex1 == vertex) { + adjVertex = edge.vertex2; + nextEdge = edge.next1; + } else { + adjVertex = edge.vertex1; + nextEdge = edge.next2; + } + + if (adjVertex.arbitraryVisit.root() != root) { + replacementEdge = edge; + break; + } + + // Remove the edge from the adjacency list of adjVertex. We will remove it from the adjacency list + // of "vertex" later. + if (edge.vertex1 == adjVertex) { + removeFromLinkedList1(edge); + } else { + removeFromLinkedList2(edge); + } + augmentAncestorFlags(adjVertex.arbitraryVisit); + + // Push the edge down to level i - 1 + edge.vertex1 = ensureLowerVertex(edge.vertex1); + edge.vertex2 = ensureLowerVertex(edge.vertex2); + + // Add the edge to the adjacency list of adjVertex.lowerVertex. We will add it to the adjacency list + // of lowerVertex later. + if (edge.vertex1 != vertex.lowerVertex) { + addToGraphLinkedList1(edge); + } else { + addToGraphLinkedList2(edge); + } + augmentAncestorFlags(adjVertex.lowerVertex.arbitraryVisit); + + prevEdge = edge; + edge = nextEdge; + } + + // Prepend the linked list up to prevEdge to the beginning of vertex.lowerVertex.graphListHead + if (prevEdge != null) { + EulerTourVertex lowerVertex = vertex.lowerVertex; + if (prevEdge.vertex1 == lowerVertex) { + prevEdge.next1 = lowerVertex.graphListHead; + } else { + prevEdge.next2 = lowerVertex.graphListHead; + } + if (lowerVertex.graphListHead != null) { + if (lowerVertex.graphListHead.vertex1 == lowerVertex) { + lowerVertex.graphListHead.prev1 = prevEdge; + } else { + lowerVertex.graphListHead.prev2 = prevEdge; + } + } + lowerVertex.graphListHead = vertex.graphListHead; + augmentAncestorFlags(lowerVertex.arbitraryVisit); + } + vertex.graphListHead = edge; + if (edge == null) { + augmentAncestorFlags(vertex.arbitraryVisit); + } else if (edge.vertex1 == vertex) { + edge.prev1 = null; + } else { + edge.prev2 = null; + } + + if (replacementEdge != null) { + return replacementEdge; + } + } + + // Iterate to the next node with hasGraphEdge == true. Note that nodes' hasGraphEdge fields can change as we + // push down edges. + if (node.right.hasGraphEdge) { + for (node = node.right; node.left.hasGraphEdge; node = node.left) ; + } else { + while (node.parent != null && (node.parent.right == node || !node.parent.hasGraphEdge)) { + node = node.parent; + } + node = node.parent; + } + } + return null; + } + + /** + * Removes the edge from srcInfo to destVertex from the edge map for srcInfo (srcInfo.edges), if it is present. + * Returns the edge that we removed, if any. + */ + private ConnEdge removeFromEdgeMap(VertexInfo srcInfo, long destVertex) { + ConnEdge edge = srcInfo.edges.remove(destVertex); + return edge; + } + + @Deprecated + public boolean removeEdge(ConnVertex vertex1, ConnVertex vertex2) { + return removeEdge(vertex1.getIdentity(), vertex2.getIdentity()); + } + + /** + * Removes the edge between the specified vertices, if there is such an edge. Taken together with addEdge, this + * method takes O(log^2 N) amortized time with high probability. + * + * @return Whether there was an edge between the vertices. + */ + public boolean removeEdge(long vertex1, long vertex2) { + if (vertex1 == vertex2) { + throw new IllegalArgumentException("Self-loops are not allowed"); + } + + VertexInfo info1 = vertexInfo.get(vertex1); + if (info1 == null) { + return false; + } + ConnEdge edge = removeFromEdgeMap(info1, vertex2); + if (edge == null) { + return false; + } + VertexInfo info2 = vertexInfo.get(vertex2); + removeFromEdgeMap(info2, vertex1); + + removeFromLinkedLists(edge); + augmentAncestorFlags(edge.vertex1.arbitraryVisit); + augmentAncestorFlags(edge.vertex2.arbitraryVisit); + + if (edge.eulerTourEdge != null) { + for (EulerTourEdge levelEdge = edge.eulerTourEdge; levelEdge != null; levelEdge = levelEdge.higherEdge) { + removeForestEdge(levelEdge); + } + edge.eulerTourEdge = null; + + // Search for a replacement edge + ConnEdge replacementEdge = null; + EulerTourVertex levelVertex1 = edge.vertex1; + EulerTourVertex levelVertex2 = edge.vertex2; + while (levelVertex1 != null) { + EulerTourNode root1 = levelVertex1.arbitraryVisit.root(); + EulerTourNode root2 = levelVertex2.arbitraryVisit.root(); + + // 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 + if (root1.size == 1 && levelVertex1.higherVertex != null) { + levelVertex1.higherVertex.lowerVertex = null; + } + if (root2.size == 1 && levelVertex2.higherVertex != null) { + levelVertex2.higherVertex.lowerVertex = null; + } + + levelVertex1 = levelVertex1.higherVertex; + levelVertex2 = levelVertex2.higherVertex; + } + + if (replacementEdge != null) { + // Add the replacement edge to all of the forests at or above the current level + removeFromLinkedLists(replacementEdge); + addToForestLinkedLists(replacementEdge); + EulerTourVertex replacementVertex1 = replacementEdge.vertex1; + EulerTourVertex replacementVertex2 = replacementEdge.vertex2; + augmentAncestorFlags(replacementVertex1.arbitraryVisit); + augmentAncestorFlags(replacementVertex2.arbitraryVisit); + EulerTourEdge lowerEdge = null; + while (replacementVertex1 != null) { + EulerTourEdge levelEdge = addForestEdge(replacementVertex1, replacementVertex2); + if (lowerEdge == null) { + replacementEdge.eulerTourEdge = levelEdge; + } else { + lowerEdge.higherEdge = levelEdge; + } + + lowerEdge = levelEdge; + replacementVertex1 = replacementVertex1.higherVertex; + replacementVertex2 = replacementVertex2.higherVertex; + } + } + } + + if (info1.edges.isEmpty() && !info1.vertex.hasAugmentation) { + remove(vertex1); + } + if (info2.edges.isEmpty() && !info2.vertex.hasAugmentation) { + remove(vertex2); + } + return true; + } + + @Deprecated + public boolean connected(ConnVertex vertex1, ConnVertex vertex2) { + return connected(vertex1.getIdentity(), vertex2.getIdentity()); + } + + /** + * Returns whether the specified vertices are connected - whether there is a path between them. Returns true if + * vertex1 == vertex2. This method takes O(log N) time with high probability. + */ + public boolean connected(long vertex1, long vertex2) { + if (vertex1 == vertex2) { + return true; + } + VertexInfo info1 = vertexInfo.get(vertex1); + if (info1 == null) { + return false; + } + VertexInfo info2 = vertexInfo.get(vertex2); + return info2 != null && info1.vertex.arbitraryVisit.root() == info2.vertex.arbitraryVisit.root(); + } + + /** + * Returns the vertices that are directly adjacent to the specified vertex. + */ + public LongSet adjacentVertices(long vertex) { + VertexInfo info = vertexInfo.get(vertex); + if (info != null) { + return info.edges.keySet(); + } else { + return LongSets.emptySet(); + } + } + + @Deprecated + public Object setVertexAugmentation(ConnVertex connVertex, Object vertexAugmentation) { + return setVertexAugmentation(connVertex.getIdentity(), vertexAugmentation); + } + + /** + * Sets the augmentation associated with the specified vertex. This method takes O(log N) time with high + * probability. + *

+ * Note that passing a null value for the second argument is not the same as removing the augmentation. For that, + * you need to call removeVertexAugmentation. + * + * @return The augmentation that was previously associated with the vertex. Returns null if it did not have any + * associated augmentation. + */ + public Object setVertexAugmentation(long connVertex, Object vertexAugmentation) { + assertIsAugmented(); + EulerTourVertex vertex = ensureInfo(connVertex).vertex; + Object oldAugmentation = vertex.augmentation; + if (!vertex.hasAugmentation || + (vertexAugmentation != null ? !vertexAugmentation.equals(oldAugmentation) : oldAugmentation != null)) { + vertex.augmentation = vertexAugmentation; + vertex.hasAugmentation = true; + for (EulerTourNode node = vertex.arbitraryVisit; node != null; node = node.parent) { + if (!node.augment()) { + break; + } + } + } + return oldAugmentation; + } + + @Deprecated + public Object removeVertexAugmentation(ConnVertex connVertex) { + return removeVertexAugmentation(connVertex.getIdentity()); + } + + /** + * Removes any augmentation associated with the specified vertex. This method takes O(log N) time with high + * probability. + * + * @return The augmentation that was previously associated with the vertex. Returns null if it did not have any + * associated augmentation. + */ + public Object removeVertexAugmentation(long connVertex) { + assertIsAugmented(); + VertexInfo info = vertexInfo.get(connVertex); + if (info == null) { + return null; + } + + EulerTourVertex vertex = info.vertex; + Object oldAugmentation = vertex.augmentation; + if (info.edges.isEmpty()) { + remove(connVertex); + } else if (vertex.hasAugmentation) { + vertex.augmentation = null; + vertex.hasAugmentation = false; + for (EulerTourNode node = vertex.arbitraryVisit; node != null; node = node.parent) { + if (!node.augment()) { + break; + } + } + } + return oldAugmentation; + } + + @Deprecated + public Object getVertexAugmentation(ConnVertex vertex) { + return getVertexAugmentation(vertex.getIdentity()); + } + + /** + * Returns the augmentation associated with the specified vertex. Returns null if it does not have any associated + * augmentation. At present, this method takes constant expected time. Contrast with getComponentAugmentation. + */ + public Object getVertexAugmentation(long vertex) { + assertIsAugmented(); + VertexInfo info = vertexInfo.get(vertex); + if (info != null) { + return info.vertex.augmentation; + } else { + return null; + } + } + + @Deprecated + public Object getComponentAugmentation(ConnVertex vertex) { + return getComponentAugmentation(vertex.getIdentity()); + } + + /** + * Returns the result of combining the augmentations associated with all of the vertices in the connected component + * containing the specified vertex. Returns null if none of those vertices has any associated augmentation. This + * method takes O(log N) time with high probability. + */ + public Object getComponentAugmentation(long vertex) { + assertIsAugmented(); + VertexInfo info = vertexInfo.get(vertex); + if (info != null) { + return info.vertex.arbitraryVisit.root().augmentation; + } else { + return null; + } + } + + @Deprecated + public boolean vertexHasAugmentation(ConnVertex vertex) { + return vertexHasAugmentation(vertex.getIdentity()); + } + + /** + * Returns whether the specified vertex has any associated augmentation. At present, this method takes constant + * expected time. Contrast with componentHasAugmentation. + */ + public boolean vertexHasAugmentation(long vertex) { + assertIsAugmented(); + VertexInfo info = vertexInfo.get(vertex); + if (info != null) { + return info.vertex.hasAugmentation; + } else { + return false; + } + } + + @Deprecated + public boolean componentHasAugmentation(ConnVertex vertex) { + return componentHasAugmentation(vertex.getIdentity()); + } + + /** + * Returns whether any of the vertices in the connected component containing the specified vertex has any associated + * augmentation. This method takes O(log N) time with high probability. + */ + public boolean componentHasAugmentation(long vertex) { + assertIsAugmented(); + VertexInfo info = vertexInfo.get(vertex); + if (info != null) { + return info.vertex.arbitraryVisit.root().hasAugmentation; + } else { + return false; + } + } + + /** + * Clears this graph, by removing all edges and vertices, and removing all augmentation information from the + * vertices. + */ + public void clear() { + // Note that we construct a new HashMap rather than calling vertexInfo.clear() in order to ensure a reduction in + // space + vertexInfo = new Long2ObjectOpenHashMap<>(); + maxLogVertexCountSinceRebuild = 0; + } + + /** + * 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. + */ + private void optimizeForestEdges() { + for (VertexInfo info : vertexInfo.values()) { + int level = maxLogVertexCountSinceRebuild; + EulerTourVertex vertex; + for (vertex = info.vertex; vertex.lowerVertex != null; vertex = vertex.lowerVertex) { + level--; + } + + while (vertex != null) { + EulerTourNode node = vertex.arbitraryVisit; + ConnEdge edge = vertex.forestListHead; + while (edge != null) { + if (vertex == edge.vertex2) { + // We'll address this edge when we visit edge.vertex1 + edge = edge.next2; + continue; + } + ConnEdge nextEdge = edge.next1; + + EulerTourVertex lowerVertex1 = vertex; + EulerTourVertex lowerVertex2 = edge.vertex2; + for (int lowerLevel = level - 1; lowerLevel > 0; lowerLevel--) { + // Compute the total size if we combine the Euler tour trees + int combinedSize = 1; + if (lowerVertex1.lowerVertex != null) { + combinedSize += lowerVertex1.lowerVertex.arbitraryVisit.root().size; + } else { + combinedSize++; + } + if (lowerVertex2.lowerVertex != null) { + combinedSize += lowerVertex2.lowerVertex.arbitraryVisit.root().size; + } else { + combinedSize++; + } + + // X EulerTourVertices = (2 * X - 1) EulerTourNodes + if (combinedSize > 2 * (1 << lowerLevel) - 1) { + break; + } + + lowerVertex1 = ensureLowerVertex(lowerVertex1); + lowerVertex2 = ensureLowerVertex(lowerVertex2); + EulerTourEdge lowerEdge = addForestEdge(lowerVertex1, lowerVertex2); + lowerEdge.higherEdge = edge.eulerTourEdge; + edge.eulerTourEdge = lowerEdge; + } + + if (lowerVertex1 != vertex) { + // We pushed the edge down at least one level + removeFromLinkedLists(edge); + augmentAncestorFlags(node); + augmentAncestorFlags(edge.vertex2.arbitraryVisit); + + edge.vertex1 = lowerVertex1; + edge.vertex2 = lowerVertex2; + addToForestLinkedLists(edge); + augmentAncestorFlags(lowerVertex1.arbitraryVisit); + augmentAncestorFlags(lowerVertex2.arbitraryVisit); + } + + edge = nextEdge; + } + + vertex = vertex.higherVertex; + level++; + } + } + } + + /** + * 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) ; + while (vertex != null) { + EulerTourNode node = vertex.arbitraryVisit; + ConnEdge edge = vertex.graphListHead; + while (edge != null) { + if (vertex == edge.vertex2) { + // We'll address this edge when we visit edge.vertex1 + edge = edge.next2; + continue; + } + ConnEdge nextEdge = edge.next1; + + // Use binary search to identify the lowest level where the two vertices are in the same connected + // component + int maxLevelsDown = 0; + EulerTourVertex lowerVertex1 = vertex.lowerVertex; + EulerTourVertex lowerVertex2 = edge.vertex2.lowerVertex; + while (lowerVertex1 != null && lowerVertex2 != null) { + maxLevelsDown++; + lowerVertex1 = lowerVertex1.lowerVertex; + lowerVertex2 = lowerVertex2.lowerVertex; + } + EulerTourVertex levelVertex1 = vertex; + EulerTourVertex levelVertex2 = edge.vertex2; + while (maxLevelsDown > 0) { + int levelsDown = (maxLevelsDown + 1) / 2; + lowerVertex1 = levelVertex1; + lowerVertex2 = levelVertex2; + for (int i = 0; i < levelsDown; i++) { + lowerVertex1 = lowerVertex1.lowerVertex; + lowerVertex2 = lowerVertex2.lowerVertex; + } + + if (lowerVertex1.arbitraryVisit.root() != lowerVertex2.arbitraryVisit.root()) { + maxLevelsDown = levelsDown - 1; + } else { + levelVertex1 = lowerVertex1; + levelVertex2 = lowerVertex2; + maxLevelsDown -= levelsDown; + } + } + + if (levelVertex1 != vertex) { + removeFromLinkedLists(edge); + augmentAncestorFlags(node); + augmentAncestorFlags(edge.vertex2.arbitraryVisit); + + edge.vertex1 = levelVertex1; + edge.vertex2 = levelVertex2; + addToGraphLinkedLists(edge); + augmentAncestorFlags(levelVertex1.arbitraryVisit); + augmentAncestorFlags(levelVertex2.arbitraryVisit); + } + + edge = nextEdge; + } + vertex = vertex.higherVertex; + } + } + } + + /** + * 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(); + } +} diff --git a/src/main/java/com/github/btrekkie/connectivity/ConnVertex.java b/src/main/java/com/github/btrekkie/connectivity/ConnVertex.java new file mode 100644 index 000000000..4e562f73f --- /dev/null +++ b/src/main/java/com/github/btrekkie/connectivity/ConnVertex.java @@ -0,0 +1,47 @@ +package com.github.btrekkie.connectivity; + +import java.util.Random; + +/** + * A vertex in a ConnGraph. See the comments for ConnGraph. + */ +public class ConnVertex { + /** + * The thread-local random number generator we use by default to set the "hash" field. + */ + private static final ThreadLocal random = new ThreadLocal() { + @Override + protected Random initialValue() { + return new Random(); + } + }; + + /** + * A randomly generated integer to use as the return value of hashCode(). ConnGraph relies on random hash codes for + * its performance guarantees. + */ + private final long hash; + + public ConnVertex() { + hash = random.get().nextLong(); + } + + /** + * Constructs a new ConnVertex. + * + * @param random The random number generator to use to produce a random hash code. ConnGraph relies on random hash + * codes for its performance guarantees. + */ + public ConnVertex(Random random) { + hash = random.nextLong(); + } + + @Override + public int hashCode() { + throw new UnsupportedOperationException(); + } + + public long getIdentity(){ + return hash; + } +} diff --git a/src/main/java/com/github/btrekkie/connectivity/EulerTourEdge.java b/src/main/java/com/github/btrekkie/connectivity/EulerTourEdge.java new file mode 100644 index 000000000..367399361 --- /dev/null +++ b/src/main/java/com/github/btrekkie/connectivity/EulerTourEdge.java @@ -0,0 +1,30 @@ +package com.github.btrekkie.connectivity; + +/** + * The representation of a forest edge in some Euler tour forest F_i at some particular level i. Each forest edge has + * one EulerTourEdge object for each level it appears in. See the comments for the implementation of ConnGraph. + */ +class EulerTourEdge { + /** + * One of the two visits preceding the edge in the Euler tour, in addition to visit2. (The node is at the same level + * as the EulerTourEdge.) + */ + public final EulerTourNode visit1; + + /** + * One of the two visits preceding the edge in the Euler tour, in addition to visit1. (The node is at the same level + * as the EulerTourEdge.) + */ + public final EulerTourNode visit2; + + /** + * The representation of this edge in the next-higher level. higherEdge is null if this edge is in the highest + * level. + */ + public EulerTourEdge higherEdge; + + public EulerTourEdge(EulerTourNode visit1, EulerTourNode visit2) { + this.visit1 = visit1; + this.visit2 = visit2; + } +} diff --git a/src/main/java/com/github/btrekkie/connectivity/EulerTourNode.java b/src/main/java/com/github/btrekkie/connectivity/EulerTourNode.java new file mode 100644 index 000000000..356f98733 --- /dev/null +++ b/src/main/java/com/github/btrekkie/connectivity/EulerTourNode.java @@ -0,0 +1,120 @@ +package com.github.btrekkie.connectivity; + +import com.github.btrekkie.red_black_node.RedBlackNode; + +/** + * A node in an Euler tour tree for ConnGraph (at some particular level i). See the comments for the implementation of + * ConnGraph. + */ +class EulerTourNode extends RedBlackNode { + /** + * The dummy leaf node. + */ + public static final EulerTourNode LEAF = new EulerTourNode(null, null); + + /** + * The vertex this node visits. + */ + public final EulerTourVertex vertex; + + /** + * The number of nodes in the subtree rooted at this node. + */ + public int size; + + /** + * Whether the subtree rooted at this node contains a node "node" for which + * node.vertex.arbitraryNode == node && node.vertex.graphListHead != null. + */ + public boolean hasGraphEdge; + + /** + * Whether the subtree rooted at this node contains a node "node" for which + * node.vertex.arbitraryNode == node && node.vertex.forestListHead != null. + */ + public boolean hasForestEdge; + + /** + * The combining function for combining user-provided augmentations. augmentationFunc is null if this node is not in + * the highest level. + */ + public final Augmentation augmentationFunc; + + /** + * The combined augmentation for the subtree rooted at this node. This is the result of combining the augmentation + * values node.vertex.augmentation for all nodes "node" in the subtree rooted at this node for which + * node.vertex.arbitraryVisit == node, using augmentationFunc. This is null if hasAugmentation is false. + */ + public Object augmentation; + + /** + * Whether the subtree rooted at this node contains at least one augmentation value. This indicates whether there is + * some node "node" in the subtree rooted at this node for which node.vertex.hasAugmentation is true and + * node.vertex.arbitraryVisit == node. + */ + public boolean hasAugmentation; + + public EulerTourNode(EulerTourVertex vertex, Augmentation augmentationFunc) { + this.vertex = vertex; + this.augmentationFunc = augmentationFunc; + } + + /** + * Like augment(), but only updates the augmentation fields hasGraphEdge and hasForestEdge. + */ + public boolean augmentFlags() { + boolean newHasGraphEdge = + left.hasGraphEdge || right.hasGraphEdge || (vertex.arbitraryVisit == this && vertex.graphListHead != null); + boolean newHasForestEdge = + left.hasForestEdge || right.hasForestEdge || + (vertex.arbitraryVisit == this && vertex.forestListHead != null); + if (newHasGraphEdge == hasGraphEdge && newHasForestEdge == hasForestEdge) { + return false; + } else { + hasGraphEdge = newHasGraphEdge; + hasForestEdge = newHasForestEdge; + return true; + } + } + + @Override + public boolean augment() { + int newSize = left.size + right.size + 1; + boolean augmentedFlags = augmentFlags(); + + Object newAugmentation = null; + boolean newHasAugmentation = false; + if (augmentationFunc != null) { + if (left.hasAugmentation) { + newAugmentation = left.augmentation; + newHasAugmentation = true; + } + if (vertex.hasAugmentation && vertex.arbitraryVisit == this) { + if (newHasAugmentation) { + newAugmentation = augmentationFunc.combine(newAugmentation, vertex.augmentation); + } else { + newAugmentation = vertex.augmentation; + newHasAugmentation = true; + } + } + if (right.hasAugmentation) { + if (newHasAugmentation) { + newAugmentation = augmentationFunc.combine(newAugmentation, right.augmentation); + } else { + newAugmentation = right.augmentation; + newHasAugmentation = true; + } + } + } + + if (newSize == size && !augmentedFlags && hasAugmentation == newHasAugmentation && + (newAugmentation != null ? newAugmentation.equals(augmentation) : augmentation == null)) { + return false; + } else { + size = newSize; + augmentation = newAugmentation; + hasAugmentation = newHasAugmentation; + return true; + } + } +} diff --git a/src/main/java/com/github/btrekkie/connectivity/EulerTourVertex.java b/src/main/java/com/github/btrekkie/connectivity/EulerTourVertex.java new file mode 100644 index 000000000..e2412e189 --- /dev/null +++ b/src/main/java/com/github/btrekkie/connectivity/EulerTourVertex.java @@ -0,0 +1,47 @@ +package com.github.btrekkie.connectivity; + +/** + * The representation of a ConnVertex at some particular level i. Each vertex has one EulerTourVertex object for each + * level it appears in. Note that different vertices may appear in different numbers of levels, as EulerTourVertex + * objects are only created for lower levels as needed. See the comments for the implementation of ConnGraph. + */ +class EulerTourVertex { + /** + * The representation of this edge in the next-lower level. lowerVertex is null if this is the lowest-level + * representation of this vertex. + */ + public EulerTourVertex lowerVertex; + + /** + * The representation of this edge in the next-higher level. This is null if this vertex is in the highest level. + */ + public EulerTourVertex higherVertex; + + /** + * An arbitrarily selected visit to the vertex in the Euler tour tree that contains it (at the same level as this). + */ + public EulerTourNode arbitraryVisit; + + /** + * The first edge in the linked list of level-i edges that are adjacent to the vertex in G_i, but are not in the + * Euler tour forest F_i, where i is the level of the vertex. Note that this list excludes any edges that also + * appear in lower levels. + */ + public ConnEdge graphListHead; + + /** + * The first edge in the linked list of level-i edges adjacent to the vertex that are in F_i, where i is the level + * of the vertex. Note that this list excludes any edges that also appear in lower levels. + */ + public ConnEdge forestListHead; + + /** + * The augmentation associated with this vertex, if any. This is null instead if higherVertex != null. + */ + public Object augmentation; + + /** + * Whether there is any augmentation associated with this vertex. This is false instead if higherVertex != null. + */ + public boolean hasAugmentation; +} diff --git a/src/main/java/com/github/btrekkie/connectivity/VertexInfo.java b/src/main/java/com/github/btrekkie/connectivity/VertexInfo.java new file mode 100644 index 000000000..ff9ff0b08 --- /dev/null +++ b/src/main/java/com/github/btrekkie/connectivity/VertexInfo.java @@ -0,0 +1,28 @@ +package com.github.btrekkie.connectivity; + +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; + +import java.util.HashMap; +import java.util.Map; + +/** + * Describes a ConnVertex, with respect to a particular ConnGraph. There is exactly one VertexInfo object per vertex in + * a given graph, regardless of how many levels the vertex is in. See the comments for the implementation of ConnGraph. + */ +class VertexInfo { + /** + * The representation of the vertex in the highest level. + */ + public EulerTourVertex vertex; + + /** + * A map from each ConnVertex adjacent to this vertex to the ConnEdge object for the edge connecting it to this + * vertex. Lookups take O(1) expected time and O(log N / log log N) time with high probability, because "edges" is a + * HashMap, and ConnVertex.hashCode() returns a random integer. + */ + public Long2ObjectOpenHashMap edges = new Long2ObjectOpenHashMap<>(); + + public VertexInfo(EulerTourVertex vertex) { + this.vertex = vertex; + } +} diff --git a/src/main/java/com/github/btrekkie/optimization_ideas.txt b/src/main/java/com/github/btrekkie/optimization_ideas.txt new file mode 100644 index 000000000..6feb84b0d --- /dev/null +++ b/src/main/java/com/github/btrekkie/optimization_ideas.txt @@ -0,0 +1,124 @@ +Thoughts concerning optimization: + +- I should not optimize until I have access to real-world input samples. Any + optimizations are bound to result in increased code complexity, which is not + worth it unless I can demonstrate a non-trivial reduction in running time in a + real-world setting. +- I should profile the program and see if there are any quick wins. +- Most obviously, I could implement the optimization of using a B-tree in the + top level, and see what effect this has on performance. It would definitely + improve the query time, and hopefully it wouldn't affect the update time too + much. +- If I do implement the B-tree optimization, I should also store a binary search + tree representation of the same forest in the top level, for the user-defined + augmentation. That way, updates will require O(log N) calls to + Augmentation.combine, rather than O(log^2 N / log log N) calls. We have no + control over how long Augmentation.combine takes, so we should minimize calls + to that method. It will take a bit of care to ensure that fetching the + augmentation for a connected component takes O(log N / log log N) time, + however. +- Alternatively, in each B-tree node, we could store a binary search tree that + combines the augmentation information for the items in that node. This might + even benefit ConnGraphs that do not have user-supplied augmentation. +- Actually, there is a way of speeding up queries by an O(log log N) factor that + should be cleaner and easier to implement than a B-tree. We could change each + Euler tour node in the top level to store its kth ancestor (e.g. if k = 3, + then each node stores a pointer to its great grandparent). Each time we change + a node's parent, we have to change the pointers for all of the descendants + that are less than k levels below the node - up to 2^k - 1 nodes. Since there + are O(log N) parent pointer changes per update, and updates already take + O(log^2 N) amortized time, we can afford a k value of up to lg lg N + O(1) + (but interestingly, not O(log log N)). However, I think this would be + significantly slower than a B-tree in practice. +- If I don't implement the B-tree optimization, there are a couple of small + optimizations I could try. First, I could move the field + EulerTourNode.augmentationFunc to EulerTourVertex, in order to save a little + bit of space. Second, I could create EulerTourNode and EulerTourVertex + subclasses specifically for the top level, and offload the fields related to + user augmentation to those subclasses. These changes seem inelegant, but it + might be worth checking the effect they have on performance. +- I looked at the heuristics recommended in + http://people.csail.mit.edu/karger/Papers/impconn.pdf (Iyer, et al. (2001): An + Experimental Study of Poly-Logarithmic Fully-Dynamic Connectivity Algorithms). + The first heuristic is: before pushing down all of the same-level forest + edges, which is an expensive operation, sample O(log N) same-level non-forest + edges to see if we can get lucky and find a replacement edge without pushing + anything. The second heuristic is to refrain from pushing edges in + sufficiently small components. + + The first heuristic seems reasonable. However, I don't get the second + heuristic. The concept seems to be basically the same as the first - don't + push down any edges if we can cheaply find a replacement edge. However, the + execution is cruder. Rather than limiting the number of edges to search, which + is closely related to the cost of the search, the second heuristic is based on + the number of vertices, which is not as closely related. +- I have a few ideas for implementing this first heuristic, which could be + attempted and their effects on performance measured. The first is that to + sample the graph edges in a semi-random order, I could augment each Euler tour + node with the sum across all canonical visits to vertices of the number of + adjacent same-level graph edges. Then, to obtain a sample, we select each + vertex with a probability proportional to this adjacency number. This is + fairly straightforward: at each node, we decide to go left, go right, or use + the current vertex in proportion to the augmentation. + + This is not exactly random, because after we select a vertex, we choose an + arbitrary adjacent edge. However, it seems close enough, and it's not easy to + do better. +- After sampling an edge, we should remove it from the adjacency list, to avoid + repeatedly sampling the same edge. We should then store it in an array of + edges we sampled, so that later, we can either re-add the edges to the + adjacency lists or push all of them down as necessary. +- If the sampling budget (i.e. the number of edges we intend to sample) is at + least the number of same-level graph edges, then we should forgo pushing down + any edges, regardless of whether we find a replacement edge. +- We don't need to sample edges from the smaller of the two post-cut trees. We + can sample them from the larger one if it has fewer same-level graph edges. + This increases our chances of finding a replacement edge if there is one. As + long as we don't push down any forest edges in the larger tree, we're safe. +- With an extra augmentation, we can determine whether there is probably a + replacement edge. This helps us because if there is probably no replacement + edge, then we can save some time by skipping edge sampling entirely. (If the + sampling budget is at least the number of same-level graph edges, then we + should also refrain from pushing down any edges, as in a previously mentioned + optimization.) + + Assign each ConnEdge a random integer ID. Store the XOR of the IDs of all + adjacent same-level graph edges in each of the vertices. Augment the Euler + tour trees with the XOR of those values for all canonical visits to vertices. + The XOR stored in a post-cut tree's root node is equal to the XOR of all of + the replacement edges' IDs, because each non-replacement edge is in two + adjacency lists and cancels itself out, while each replacement edge is in one + adjacency list. Thus, the XOR is 0 if there is no replacement edge, and it is + non-zero with probability 1 - 1 / 2^32 if there is at least one replacement + edge. +- If one of the post-cut trees has a same-level forest edge and the other does + not, and the difference in the number of same-level graph edges is not that + large, we should favor the one that does not, because it's expensive to push + forest edges. Also, there's no need to sample if we pick a tree that has no + same-level forest edges. +- I wonder if there's a good way to estimate the cost of pushing down a given + set of forest edges. For example, is there a strong correlation between the + number of forest edges and the cost of pushing them? We could use a larger + sampling budget the greater this cost is. +- During sampling, it might help to push down edges whose endpoints are + connected in the next-lower level, and to not count them against the sampling + budget. By paying for some of the edges, we're able to sample more edges, so + we're less likely to have to push down the forest edges. + + The downside is that checking whether the endpoints are in the same connected + component takes O(log N) time. To mitigate this, we should refrain from + attempting to push down edges until necessary. That is, we should spend the + entire sampling budget first, then search the edges we sampled for an edge + that we can push down. After finding one such edge, we should sample another + edge, then search for another edge to push down, etc. +- When we are done sampling edges, it might still be beneficial to iterate over + the rest of the same-level graph edges in a (semi-)random order. This does + increase the amount of time that iteration takes. However, using a random + order could be helpful if the sequence of updates has some sort of pattern + affecting the location of replacement edges. For example, if there is a + tendency to put replacement edges near the end of the Euler tour, or to + cluster replacement edges so that they are close to each other in the Euler + tour, then random iteration should tend to locate a replacement edge faster + than in-order iteration. (If we know from a previously mentioned optimization + that there is probably no replacement edge, then we shouldn't bother to + iterate over the edges in random order.) diff --git a/src/main/java/com/github/btrekkie/red_black_node/RedBlackNode.java b/src/main/java/com/github/btrekkie/red_black_node/RedBlackNode.java new file mode 100644 index 000000000..1484562cd --- /dev/null +++ b/src/main/java/com/github/btrekkie/red_black_node/RedBlackNode.java @@ -0,0 +1,1408 @@ +// from: https://github.com/btrekkie/RedBlackNode/blob/master/src/main/java/com/github/btrekkie/red_black_node/RedBlackNode.java +// also MIT: https://github.com/btrekkie/RedBlackNode/blob/master/LICENSE +package com.github.btrekkie.red_black_node; + +import java.lang.reflect.Array; +import java.util.*; + +/** + * A node in a red-black tree ( https://en.wikipedia.org/wiki/Red%E2%80%93black_tree ). Compared to a class like Java's + * TreeMap, RedBlackNode is a low-level data structure. The internals of a node are exposed as public fields, allowing + * clients to directly observe and manipulate the structure of the tree. This gives clients flexibility, although it + * also enables them to violate the red-black or BST properties. The RedBlackNode class provides methods for performing + * various standard operations, such as insertion and removal. + *

+ * Unlike most implementations of binary search trees, RedBlackNode supports arbitrary augmentation. By subclassing + * RedBlackNode, clients can add arbitrary data and augmentation information to each node. For example, if we were to + * use a RedBlackNode subclass to implement a sorted set, the subclass would have a field storing an element in the set. + * If we wanted to keep track of the number of non-leaf nodes in each subtree, we would store this as a "size" field and + * override augment() to update this field. All RedBlackNode methods (such as "insert" and remove()) call augment() as + * necessary to correctly maintain the augmentation information, unless otherwise indicated. + *

+ * The values of the tree are stored in the non-leaf nodes. RedBlackNode does not support use cases where values must be + * stored in the leaf nodes. It is recommended that all of the leaf nodes in a given tree be the same (black) + * RedBlackNode instance, to save space. The root of an empty tree is a leaf node, as opposed to null. + *

+ * For reference, a red-black tree is a binary search tree satisfying the following properties: + *

+ * - Every node is colored red or black. + * - The leaf nodes, which are dummy nodes that do not store any values, are colored black. + * - The root is black. + * - Both children of each red node are black. + * - Every path from the root to a leaf contains the same number of black nodes. + * + * @param The type of node in the tree. For example, we might have + * "class FooNode extends RedBlackNode>". + * @author Bill Jacobs + */ +public abstract class RedBlackNode> implements Comparable { + /** + * A Comparator that compares Comparable elements using their natural order. + */ + private static final Comparator> NATURAL_ORDER = new Comparator>() { + @Override + public int compare(Comparable value1, Comparable value2) { + return value1.compareTo(value2); + } + }; + + /** + * The parent of this node, if any. "parent" is null if this is a leaf node. + */ + public N parent; + + /** + * The left child of this node. "left" is null if this is a leaf node. + */ + public N left; + + /** + * The right child of this node. "right" is null if this is a leaf node. + */ + public N right; + + /** + * Whether the node is colored red, as opposed to black. + */ + public boolean isRed; + + /** + * Sets any augmentation information about the subtree rooted at this node that is stored in this node. For + * example, if we augment each node by subtree size (the number of non-leaf nodes in the subtree), this method would + * set the size field of this node to be equal to the size field of the left child plus the size field of the right + * child plus one. + *

+ * "Augmentation information" is information that we can compute about a subtree rooted at some node, preferably + * based only on the augmentation information in the node's two children and the information in the node. Examples + * of augmentation information are the sum of the values in a subtree and the number of non-leaf nodes in a subtree. + * Augmentation information may not depend on the colors of the nodes. + *

+ * This method returns whether the augmentation information in any of the ancestors of this node might have been + * affected by changes in this subtree since the last call to augment(). In the usual case, where the augmentation + * information depends only on the information in this node and the augmentation information in its immediate + * children, this is equivalent to whether the augmentation information changed as a result of this call to + * augment(). For example, in the case of subtree size, this returns whether the value of the size field prior to + * calling augment() differed from the size field of the left child plus the size field of the right child plus one. + * False positives are permitted. The return value is unspecified if we have not called augment() on this node + * before. + *

+ * This method may assume that this is not a leaf node. It may not assume that the augmentation information stored + * in any of the tree's nodes is correct. However, if the augmentation information stored in all of the node's + * descendants is correct, then the augmentation information stored in this node must be correct after calling + * augment(). + */ + public boolean augment() { + return false; + } + + /** + * Throws a RuntimeException if we detect that this node locally violates any invariants specific to this subclass + * of RedBlackNode. For example, if this stores the size of the subtree rooted at this node, this should throw a + * RuntimeException if the size field of this is not equal to the size field of the left child plus the size field + * of the right child plus one. Note that we may call this on a leaf node. + *

+ * assertSubtreeIsValid() calls assertNodeIsValid() on each node, or at least starts to do so until it detects a + * problem. assertNodeIsValid() should assume the node is in a tree that satisfies all properties common to all + * red-black trees, as assertSubtreeIsValid() is responsible for such checks. assertNodeIsValid() should be + * "downward-looking", i.e. it should ignore any information in "parent", and it should be "local", i.e. it should + * only check a constant number of descendants. To include "global" checks, such as verifying the BST property + * concerning ordering, override assertSubtreeIsValid(). assertOrderIsValid is useful for checking the BST + * property. + */ + public void assertNodeIsValid() { + + } + + /** + * Returns whether this is a leaf node. + */ + public boolean isLeaf() { + return left == null; + } + + /** + * Returns the root of the tree that contains this node. + */ + public N root() { + @SuppressWarnings("unchecked") + N node = (N) this; + while (node.parent != null) { + node = node.parent; + } + return node; + } + + /** + * Returns the first node in the subtree rooted at this node, if any. + */ + public N min() { + if (isLeaf()) { + return null; + } + @SuppressWarnings("unchecked") + N node = (N) this; + while (!node.left.isLeaf()) { + node = node.left; + } + return node; + } + + /** + * Returns the last node in the subtree rooted at this node, if any. + */ + public N max() { + if (isLeaf()) { + return null; + } + @SuppressWarnings("unchecked") + N node = (N) this; + while (!node.right.isLeaf()) { + node = node.right; + } + return node; + } + + /** + * Returns the node immediately before this in the tree that contains this node, if any. + */ + public N predecessor() { + if (!left.isLeaf()) { + N node; + for (node = left; !node.right.isLeaf(); node = node.right) ; + return node; + } else if (parent == null) { + return null; + } else { + @SuppressWarnings("unchecked") + N node = (N) this; + while (node.parent != null && node.parent.left == node) { + node = node.parent; + } + return node.parent; + } + } + + /** + * Returns the node immediately after this in the tree that contains this node, if any. + */ + public N successor() { + if (!right.isLeaf()) { + N node; + for (node = right; !node.left.isLeaf(); node = node.left) ; + return node; + } else if (parent == null) { + return null; + } else { + @SuppressWarnings("unchecked") + N node = (N) this; + while (node.parent != null && node.parent.right == node) { + node = node.parent; + } + return node.parent; + } + } + + /** + * Performs a left rotation about this node. This method assumes that !isLeaf() && !right.isLeaf(). It calls + * augment() on this node and on its resulting parent. However, it does not call augment() on any of the resulting + * parent's ancestors, because that is normally the responsibility of the caller. + * + * @return The return value from calling augment() on the resulting parent. + */ + public boolean rotateLeft() { + if (isLeaf() || right.isLeaf()) { + throw new IllegalArgumentException("The node or its right child is a leaf"); + } + N newParent = right; + right = newParent.left; + @SuppressWarnings("unchecked") + N nThis = (N) this; + if (!right.isLeaf()) { + right.parent = nThis; + } + newParent.parent = parent; + parent = newParent; + newParent.left = nThis; + if (newParent.parent != null) { + if (newParent.parent.left == this) { + newParent.parent.left = newParent; + } else { + newParent.parent.right = newParent; + } + } + augment(); + return newParent.augment(); + } + + /** + * Performs a right rotation about this node. This method assumes that !isLeaf() && !left.isLeaf(). It calls + * augment() on this node and on its resulting parent. However, it does not call augment() on any of the resulting + * parent's ancestors, because that is normally the responsibility of the caller. + * + * @return The return value from calling augment() on the resulting parent. + */ + public boolean rotateRight() { + if (isLeaf() || left.isLeaf()) { + throw new IllegalArgumentException("The node or its left child is a leaf"); + } + N newParent = left; + left = newParent.right; + @SuppressWarnings("unchecked") + N nThis = (N) this; + if (!left.isLeaf()) { + left.parent = nThis; + } + newParent.parent = parent; + parent = newParent; + newParent.right = nThis; + if (newParent.parent != null) { + if (newParent.parent.left == this) { + newParent.parent.left = newParent; + } else { + newParent.parent.right = newParent; + } + } + augment(); + return newParent.augment(); + } + + /** + * Performs red-black insertion fixup. To be more precise, this fixes a tree that satisfies all of the requirements + * of red-black trees, except that this may be a red child of a red node, and if this is the root, the root may be + * red. node.isRed must initially be true. This method assumes that this is not a leaf node. The method performs + * any rotations by calling rotateLeft() and rotateRight(). This method is more efficient than fixInsertion if + * "augment" is false or augment() might return false. + * + * @param augment Whether to set the augmentation information for "node" and its ancestors, by calling augment(). + */ + public void fixInsertionWithoutGettingRoot(boolean augment) { + if (!isRed) { + throw new IllegalArgumentException("The node must be red"); + } + boolean changed = augment; + if (augment) { + augment(); + } + + RedBlackNode node = this; + while (node.parent != null && node.parent.isRed) { + N parent = node.parent; + N grandparent = parent.parent; + if (grandparent.left.isRed && grandparent.right.isRed) { + grandparent.left.isRed = false; + grandparent.right.isRed = false; + grandparent.isRed = true; + + if (changed) { + changed = parent.augment(); + if (changed) { + changed = grandparent.augment(); + } + } + node = grandparent; + } else { + if (parent.left == node) { + if (grandparent.right == parent) { + parent.rotateRight(); + node = parent; + parent = node.parent; + } + } else if (grandparent.left == parent) { + parent.rotateLeft(); + node = parent; + parent = node.parent; + } + + if (parent.left == node) { + boolean grandparentChanged = grandparent.rotateRight(); + if (augment) { + changed = grandparentChanged; + } + } else { + boolean grandparentChanged = grandparent.rotateLeft(); + if (augment) { + changed = grandparentChanged; + } + } + + parent.isRed = false; + grandparent.isRed = true; + node = parent; + break; + } + } + + if (node.parent == null) { + node.isRed = false; + } + if (changed) { + for (node = node.parent; node != null; node = node.parent) { + if (!node.augment()) { + break; + } + } + } + } + + /** + * Performs red-black insertion fixup. To be more precise, this fixes a tree that satisfies all of the requirements + * of red-black trees, except that this may be a red child of a red node, and if this is the root, the root may be + * red. node.isRed must initially be true. This method assumes that this is not a leaf node. The method performs + * any rotations by calling rotateLeft() and rotateRight(). This method is more efficient than fixInsertion() if + * augment() might return false. + */ + public void fixInsertionWithoutGettingRoot() { + fixInsertionWithoutGettingRoot(true); + } + + /** + * Performs red-black insertion fixup. To be more precise, this fixes a tree that satisfies all of the requirements + * of red-black trees, except that this may be a red child of a red node, and if this is the root, the root may be + * red. node.isRed must initially be true. This method assumes that this is not a leaf node. The method performs + * any rotations by calling rotateLeft() and rotateRight(). + * + * @param augment Whether to set the augmentation information for "node" and its ancestors, by calling augment(). + * @return The root of the resulting tree. + */ + public N fixInsertion(boolean augment) { + fixInsertionWithoutGettingRoot(augment); + return root(); + } + + /** + * Performs red-black insertion fixup. To be more precise, this fixes a tree that satisfies all of the requirements + * of red-black trees, except that this may be a red child of a red node, and if this is the root, the root may be + * red. node.isRed must initially be true. This method assumes that this is not a leaf node. The method performs + * any rotations by calling rotateLeft() and rotateRight(). + * + * @return The root of the resulting tree. + */ + public N fixInsertion() { + fixInsertionWithoutGettingRoot(true); + return root(); + } + + /** + * Returns a Comparator that compares instances of N using their natural order, as in N.compareTo. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + private Comparator naturalOrder() { + Comparator comparator = (Comparator) NATURAL_ORDER; + return (Comparator) comparator; + } + + /** + * Inserts the specified node into the tree rooted at this node. Assumes this is the root. We treat newNode as a + * solitary node that does not belong to any tree, and we ignore its initial "parent", "left", "right", and isRed + * fields. + *

+ * If it is not efficient or convenient to find the location for a node using a Comparator, then you should manually + * add the node to the appropriate location, color it red, and call fixInsertion(). + * + * @param newNode The node to insert. + * @param allowDuplicates Whether to insert newNode if there is an equal node in the tree. To check whether we + * inserted newNode, check whether newNode.parent is null and the return value differs from newNode. + * @param comparator A comparator indicating where to put the node. If this is null, we use the nodes' natural + * order, as in N.compareTo. If you are passing null, then you must override the compareTo method, because the + * default implementation requires the nodes to already be in the same tree. + * @return The root of the resulting tree. + */ + public N insert(N newNode, boolean allowDuplicates, Comparator comparator) { + if (parent != null) { + throw new IllegalArgumentException("This is not the root of a tree"); + } + @SuppressWarnings("unchecked") + N nThis = (N) this; + if (isLeaf()) { + newNode.isRed = false; + newNode.left = nThis; + newNode.right = nThis; + newNode.parent = null; + newNode.augment(); + return newNode; + } + if (comparator == null) { + comparator = naturalOrder(); + } + + N node = nThis; + int comparison; + while (true) { + comparison = comparator.compare(newNode, node); + if (comparison < 0) { + if (!node.left.isLeaf()) { + node = node.left; + } else { + newNode.left = node.left; + newNode.right = node.left; + node.left = newNode; + newNode.parent = node; + break; + } + } else if (comparison > 0 || allowDuplicates) { + if (!node.right.isLeaf()) { + node = node.right; + } else { + newNode.left = node.right; + newNode.right = node.right; + node.right = newNode; + newNode.parent = node; + break; + } + } else { + newNode.parent = null; + return nThis; + } + } + newNode.isRed = true; + return newNode.fixInsertion(); + } + + /** + * Moves this node to its successor's former position in the tree and vice versa, i.e. sets the "left", "right", + * "parent", and isRed fields of each. This method assumes that this is not a leaf node. + * + * @return The node with which we swapped. + */ + private N swapWithSuccessor() { + N replacement = successor(); + boolean oldReplacementIsRed = replacement.isRed; + N oldReplacementLeft = replacement.left; + N oldReplacementRight = replacement.right; + N oldReplacementParent = replacement.parent; + + replacement.isRed = isRed; + replacement.left = left; + replacement.right = right; + replacement.parent = parent; + if (parent != null) { + if (parent.left == this) { + parent.left = replacement; + } else { + parent.right = replacement; + } + } + + @SuppressWarnings("unchecked") + N nThis = (N) this; + isRed = oldReplacementIsRed; + left = oldReplacementLeft; + right = oldReplacementRight; + if (oldReplacementParent == this) { + parent = replacement; + parent.right = nThis; + } else { + parent = oldReplacementParent; + parent.left = nThis; + } + + replacement.right.parent = replacement; + if (!replacement.left.isLeaf()) { + replacement.left.parent = replacement; + } + if (!right.isLeaf()) { + right.parent = nThis; + } + return replacement; + } + + /** + * Performs red-black deletion fixup. To be more precise, this fixes a tree that satisfies all of the requirements + * of red-black trees, except that all paths from the root to a leaf that pass through the sibling of this node have + * one fewer black node than all other root-to-leaf paths. This method assumes that this is not a leaf node. + */ + private void fixSiblingDeletion() { + RedBlackNode sibling = this; + boolean changed = true; + boolean haveAugmentedParent = false; + boolean haveAugmentedGrandparent = false; + while (true) { + N parent = sibling.parent; + if (sibling.isRed) { + parent.isRed = true; + sibling.isRed = false; + if (parent.left == sibling) { + changed = parent.rotateRight(); + sibling = parent.left; + } else { + changed = parent.rotateLeft(); + sibling = parent.right; + } + haveAugmentedParent = true; + haveAugmentedGrandparent = true; + } else if (!sibling.left.isRed && !sibling.right.isRed) { + sibling.isRed = true; + if (parent.isRed) { + parent.isRed = false; + break; + } else { + if (changed && !haveAugmentedParent) { + changed = parent.augment(); + } + N grandparent = parent.parent; + if (grandparent == null) { + break; + } else if (grandparent.left == parent) { + sibling = grandparent.right; + } else { + sibling = grandparent.left; + } + haveAugmentedParent = haveAugmentedGrandparent; + haveAugmentedGrandparent = false; + } + } else { + if (sibling == parent.left) { + if (!sibling.left.isRed) { + sibling.rotateLeft(); + sibling = sibling.parent; + } + } else if (!sibling.right.isRed) { + sibling.rotateRight(); + sibling = sibling.parent; + } + sibling.isRed = parent.isRed; + parent.isRed = false; + if (sibling == parent.left) { + sibling.left.isRed = false; + changed = parent.rotateRight(); + } else { + sibling.right.isRed = false; + changed = parent.rotateLeft(); + } + haveAugmentedParent = haveAugmentedGrandparent; + haveAugmentedGrandparent = false; + break; + } + } + + // Update augmentation info + N parent = sibling.parent; + if (changed && parent != null) { + if (!haveAugmentedParent) { + changed = parent.augment(); + } + if (changed && parent.parent != null) { + parent = parent.parent; + if (!haveAugmentedGrandparent) { + changed = parent.augment(); + } + if (changed) { + for (parent = parent.parent; parent != null; parent = parent.parent) { + if (!parent.augment()) { + break; + } + } + } + } + } + } + + /** + * Removes this node from the tree that contains it. The effect of this method on the fields of this node is + * unspecified. This method assumes that this is not a leaf node. This method is more efficient than remove() if + * augment() might return false. + *

+ * If the node has two children, we begin by moving the node's successor to its former position, by changing the + * successor's "left", "right", "parent", and isRed fields. + */ + public void removeWithoutGettingRoot() { + if (isLeaf()) { + throw new IllegalArgumentException("Attempted to remove a leaf node"); + } + N replacement; + if (left.isLeaf() || right.isLeaf()) { + replacement = null; + } else { + replacement = swapWithSuccessor(); + } + + N child; + if (!left.isLeaf()) { + child = left; + } else if (!right.isLeaf()) { + child = right; + } else { + child = null; + } + + if (child != null) { + // Replace this node with its child + child.parent = parent; + if (parent != null) { + if (parent.left == this) { + parent.left = child; + } else { + parent.right = child; + } + } + child.isRed = false; + + if (child.parent != null) { + N parent; + for (parent = child.parent; parent != null; parent = parent.parent) { + if (!parent.augment()) { + break; + } + } + } + } else if (parent != null) { + // Replace this node with a leaf node + N leaf = left; + N parent = this.parent; + N sibling; + if (parent.left == this) { + parent.left = leaf; + sibling = parent.right; + } else { + parent.right = leaf; + sibling = parent.left; + } + + if (!isRed) { + RedBlackNode siblingNode = sibling; + siblingNode.fixSiblingDeletion(); + } else { + while (parent != null) { + if (!parent.augment()) { + break; + } + parent = parent.parent; + } + } + } + + if (replacement != null) { + replacement.augment(); + for (N parent = replacement.parent; parent != null; parent = parent.parent) { + if (!parent.augment()) { + break; + } + } + } + + // Clear any previously existing links, so that we're more likely to encounter an exception if we attempt to + // access the removed node + parent = null; + left = null; + right = null; + isRed = true; + } + + /** + * Removes this node from the tree that contains it. The effect of this method on the fields of this node is + * unspecified. This method assumes that this is not a leaf node. + *

+ * If the node has two children, we begin by moving the node's successor to its former position, by changing the + * successor's "left", "right", "parent", and isRed fields. + * + * @return The root of the resulting tree. + */ + public N remove() { + if (isLeaf()) { + throw new IllegalArgumentException("Attempted to remove a leaf node"); + } + + // Find an arbitrary non-leaf node in the tree other than this node + N node; + if (parent != null) { + node = parent; + } else if (!left.isLeaf()) { + node = left; + } else if (!right.isLeaf()) { + node = right; + } else { + return left; + } + + removeWithoutGettingRoot(); + return node.root(); + } + + /** + * Returns the root of a perfectly height-balanced subtree containing the next "size" (non-leaf) nodes from + * "iterator", in iteration order. This method is responsible for setting the "left", "right", "parent", and isRed + * fields of the nodes, and calling augment() as appropriate. It ignores the initial values of the "left", "right", + * "parent", and isRed fields. + * + * @param iterator The nodes. + * @param size The number of nodes. + * @param height The "height" of the subtree's root node above the deepest leaf in the tree that contains it. Since + * insertion fixup is slow if there are too many red nodes and deleteion fixup is slow if there are too few red + * nodes, we compromise and have red nodes at every fourth level. We color a node red iff its "height" is equal + * to 1 mod 4. + * @param leaf The leaf node. + * @return The root of the subtree. + */ + private static > N createTree( + Iterator iterator, int size, int height, N leaf) { + if (size == 0) { + return leaf; + } else { + N left = createTree(iterator, (size - 1) / 2, height - 1, leaf); + N node = iterator.next(); + N right = createTree(iterator, size / 2, height - 1, leaf); + + node.isRed = height % 4 == 1; + node.left = left; + node.right = right; + if (!left.isLeaf()) { + left.parent = node; + } + if (!right.isLeaf()) { + right.parent = node; + } + + node.augment(); + return node; + } + } + + /** + * Returns the root of a perfectly height-balanced tree containing the specified nodes, in iteration order. This + * method is responsible for setting the "left", "right", "parent", and isRed fields of the nodes (excluding + * "leaf"), and calling augment() as appropriate. It ignores the initial values of the "left", "right", "parent", + * and isRed fields. + * + * @param nodes The nodes. + * @param leaf The leaf node. + * @return The root of the tree. + */ + public static > N createTree(Collection nodes, N leaf) { + int size = nodes.size(); + if (size == 0) { + return leaf; + } + + int height = 0; + for (int subtreeSize = size; subtreeSize > 0; subtreeSize /= 2) { + height++; + } + + N node = createTree(nodes.iterator(), size, height, leaf); + node.parent = null; + node.isRed = false; + return node; + } + + /** + * Concatenates to the end of the tree rooted at this node. To be precise, given that all of the nodes in this + * precede the node "pivot", which precedes all of the nodes in "last", this returns the root of a tree containing + * all of these nodes. This method destroys the trees rooted at "this" and "last". We treat "pivot" as a solitary + * node that does not belong to any tree, and we ignore its initial "parent", "left", "right", and isRed fields. + * This method assumes that this node and "last" are the roots of their respective trees. + *

+ * This method takes O(log N) time. It is more efficient than inserting "pivot" and then calling concatenate(last). + * It is considerably more efficient than inserting "pivot" and all of the nodes in "last". + */ + public N concatenate(N last, N pivot) { + // If the black height of "first", where first = this, is less than or equal to that of "last", starting at the + // root of "last", we keep going left until we reach a black node whose black height is equal to that of + // "first". Then, we make "pivot" the parent of that node and of "first", coloring it red, and perform + // insertion fixup on the pivot. If the black height of "first" is greater than that of "last", we do the + // mirror image of the above. + + if (parent != null) { + throw new IllegalArgumentException("This is not the root of a tree"); + } + if (last.parent != null) { + throw new IllegalArgumentException("\"last\" is not the root of a tree"); + } + + // Compute the black height of the trees + int firstBlackHeight = 0; + @SuppressWarnings("unchecked") + N first = (N) this; + for (N node = first; node != null; node = node.right) { + if (!node.isRed) { + firstBlackHeight++; + } + } + int lastBlackHeight = 0; + for (N node = last; node != null; node = node.right) { + if (!node.isRed) { + lastBlackHeight++; + } + } + + // Identify the children and parent of pivot + N firstChild = first; + N lastChild = last; + N parent; + if (firstBlackHeight <= lastBlackHeight) { + parent = null; + int blackHeight = lastBlackHeight; + while (blackHeight > firstBlackHeight) { + if (!lastChild.isRed) { + blackHeight--; + } + parent = lastChild; + lastChild = lastChild.left; + } + if (lastChild.isRed) { + parent = lastChild; + lastChild = lastChild.left; + } + } else { + parent = null; + int blackHeight = firstBlackHeight; + while (blackHeight > lastBlackHeight) { + if (!firstChild.isRed) { + blackHeight--; + } + parent = firstChild; + firstChild = firstChild.right; + } + if (firstChild.isRed) { + parent = firstChild; + firstChild = firstChild.right; + } + } + + // Add "pivot" to the tree + pivot.isRed = true; + pivot.parent = parent; + if (parent != null) { + if (firstBlackHeight < lastBlackHeight) { + parent.left = pivot; + } else { + parent.right = pivot; + } + } + pivot.left = firstChild; + if (!firstChild.isLeaf()) { + firstChild.parent = pivot; + } + pivot.right = lastChild; + if (!lastChild.isLeaf()) { + lastChild.parent = pivot; + } + + // Perform insertion fixup + return pivot.fixInsertion(); + } + + /** + * Concatenates the tree rooted at "last" to the end of the tree rooted at this node. To be precise, given that all + * of the nodes in this precede all of the nodes in "last", this returns the root of a tree containing all of these + * nodes. This method destroys the trees rooted at "this" and "last". It assumes that this node and "last" are the + * roots of their respective trees. This method takes O(log N) time. It is considerably more efficient than + * inserting all of the nodes in "last". + */ + public N concatenate(N last) { + if (parent != null || last.parent != null) { + throw new IllegalArgumentException("The node is not the root of a tree"); + } + if (isLeaf()) { + return last; + } else if (last.isLeaf()) { + @SuppressWarnings("unchecked") + N nThis = (N) this; + return nThis; + } else { + N node = last.min(); + last = node.remove(); + return concatenate(last, node); + } + } + + /** + * Splits the tree rooted at this node into two trees, so that the first element of the return value is the root of + * a tree consisting of the nodes that were before the specified node, and the second element of the return value is + * the root of a tree consisting of the nodes that were equal to or after the specified node. This method is + * destructive, meaning it does not preserve the original tree. It assumes that this node is the root and is in the + * same tree as splitNode. It takes O(log N) time. It is considerably more efficient than removing all of the + * nodes at or after splitNode and then creating a new tree from those nodes. + * + * @param The node at which to split the tree. + * @return An array consisting of the resulting trees. + */ + public N[] split(N splitNode) { + // To split the tree, we accumulate a pre-split tree and a post-split tree. We walk down the tree toward the + // position where we are splitting. Whenever we go left, we concatenate the right subtree with the post-split + // tree, and whenever we go right, we concatenate the pre-split tree with the left subtree. We use the + // concatenation algorithm described in concatenate(Object, Object). For the pivot, we use the last node where + // we went left in the case of a left move, and the last node where we went right in the case of a right move. + // + // The method uses the following variables: + // + // node: The current node in our walk down the tree. + // first: A node on the right spine of the pre-split tree. At the beginning of each iteration, it is the black + // node with the same black height as "node". If the pre-split tree is empty, this is null instead. + // firstParent: The parent of "first". If the pre-split tree is empty, this is null. Otherwise, this is the + // same as first.parent, unless first.isLeaf(). + // firstPivot: The node where we last went right, i.e. the next node to use as a pivot when concatenating with + // the pre-split tree. + // advanceFirst: Whether to set "first" to be its next black descendant at the end of the loop. + // last, lastParent, lastPivot, advanceLast: Analogous to "first", firstParent, firstPivot, and advanceFirst, + // but for the post-split tree. + if (parent != null) { + throw new IllegalArgumentException("This is not the root of a tree"); + } + if (isLeaf() || splitNode.isLeaf()) { + throw new IllegalArgumentException("The root or the split node is a leaf"); + } + + // Create an array containing the path from the root to splitNode + int depth = 1; + N parent; + for (parent = splitNode; parent.parent != null; parent = parent.parent) { + depth++; + } + if (parent != this) { + throw new IllegalArgumentException("The split node does not belong to this tree"); + } + RedBlackNode[] path = new RedBlackNode[depth]; + for (parent = splitNode; parent != null; parent = parent.parent) { + depth--; + path[depth] = parent; + } + + @SuppressWarnings("unchecked") + N node = (N) this; + N first = null; + N firstParent = null; + N last = null; + N lastParent = null; + N firstPivot = null; + N lastPivot = null; + while (!node.isLeaf()) { + boolean advanceFirst = !node.isRed && firstPivot != null; + boolean advanceLast = !node.isRed && lastPivot != null; + if ((depth + 1 < path.length && path[depth + 1] == node.left) || depth + 1 == path.length) { + // Left move + if (lastPivot == null) { + // The post-split tree is empty + last = node.right; + last.parent = null; + if (last.isRed) { + last.isRed = false; + lastParent = last; + last = last.left; + } + } else { + // Concatenate node.right and the post-split tree + if (node.right.isRed) { + node.right.isRed = false; + } else if (!node.isRed) { + lastParent = last; + last = last.left; + if (last.isRed) { + lastParent = last; + last = last.left; + } + advanceLast = false; + } + lastPivot.isRed = true; + lastPivot.parent = lastParent; + if (lastParent != null) { + lastParent.left = lastPivot; + } + lastPivot.left = node.right; + if (!lastPivot.left.isLeaf()) { + lastPivot.left.parent = lastPivot; + } + lastPivot.right = last; + if (!last.isLeaf()) { + last.parent = lastPivot; + } + last = lastPivot.left; + lastParent = lastPivot; + lastPivot.fixInsertionWithoutGettingRoot(false); + } + lastPivot = node; + node = node.left; + } else { + // Right move + if (firstPivot == null) { + // The pre-split tree is empty + first = node.left; + first.parent = null; + if (first.isRed) { + first.isRed = false; + firstParent = first; + first = first.right; + } + } else { + // Concatenate the post-split tree and node.left + if (node.left.isRed) { + node.left.isRed = false; + } else if (!node.isRed) { + firstParent = first; + first = first.right; + if (first.isRed) { + firstParent = first; + first = first.right; + } + advanceFirst = false; + } + firstPivot.isRed = true; + firstPivot.parent = firstParent; + if (firstParent != null) { + firstParent.right = firstPivot; + } + firstPivot.right = node.left; + if (!firstPivot.right.isLeaf()) { + firstPivot.right.parent = firstPivot; + } + firstPivot.left = first; + if (!first.isLeaf()) { + first.parent = firstPivot; + } + first = firstPivot.right; + firstParent = firstPivot; + firstPivot.fixInsertionWithoutGettingRoot(false); + } + firstPivot = node; + node = node.right; + } + + depth++; + + // Update "first" and "last" to be the nodes at the proper black height + if (advanceFirst) { + firstParent = first; + first = first.right; + if (first.isRed) { + firstParent = first; + first = first.right; + } + } + if (advanceLast) { + lastParent = last; + last = last.left; + if (last.isRed) { + lastParent = last; + last = last.left; + } + } + } + + // Add firstPivot to the pre-split tree + N leaf = node; + if (first == null) { + first = leaf; + } else { + firstPivot.isRed = true; + firstPivot.parent = firstParent; + if (firstParent != null) { + firstParent.right = firstPivot; + } + firstPivot.left = leaf; + firstPivot.right = leaf; + firstPivot.fixInsertionWithoutGettingRoot(false); + for (first = firstPivot; first.parent != null; first = first.parent) { + first.augment(); + } + first.augment(); + } + + // Add lastPivot to the post-split tree + lastPivot.isRed = true; + lastPivot.parent = lastParent; + if (lastParent != null) { + lastParent.left = lastPivot; + } + lastPivot.left = leaf; + lastPivot.right = leaf; + lastPivot.fixInsertionWithoutGettingRoot(false); + for (last = lastPivot; last.parent != null; last = last.parent) { + last.augment(); + } + last.augment(); + + @SuppressWarnings("unchecked") + N[] result = (N[]) Array.newInstance(getClass(), 2); + result[0] = first; + result[1] = last; + return result; + } + + /** + * Returns the lowest common ancestor of this node and "other" - the node that is an ancestor of both and is not the + * parent of a node that is an ancestor of both. Assumes that this is in the same tree as "other". Assumes that + * neither "this" nor "other" is a leaf node. This method may return "this" or "other". + *

+ * Note that while it is possible to compute the lowest common ancestor in O(P) time, where P is the length of the + * path from this node to "other", the "lca" method is not guaranteed to take O(P) time. If your application + * requires this, then you should write your own lowest common ancestor method. + */ + public N lca(N other) { + if (isLeaf() || other.isLeaf()) { + throw new IllegalArgumentException("One of the nodes is a leaf node"); + } + + // Compute the depth of each node + int depth = 0; + for (N parent = this.parent; parent != null; parent = parent.parent) { + depth++; + } + int otherDepth = 0; + for (N parent = other.parent; parent != null; parent = parent.parent) { + otherDepth++; + } + + // Go up to nodes of the same depth + @SuppressWarnings("unchecked") + N parent = (N) this; + N otherParent = other; + if (depth <= otherDepth) { + for (int i = otherDepth; i > depth; i--) { + otherParent = otherParent.parent; + } + } else { + for (int i = depth; i > otherDepth; i--) { + parent = parent.parent; + } + } + + // Find the LCA + while (parent != otherParent) { + parent = parent.parent; + otherParent = otherParent.parent; + } + if (parent != null) { + return parent; + } else { + throw new IllegalArgumentException("The nodes do not belong to the same tree"); + } + } + + /** + * Returns an integer comparing the position of this node in the tree that contains it with that of "other". Returns + * a negative number if this is earlier, a positive number if this is later, and 0 if this is at the same position. + * Assumes that this is in the same tree as "other". Assumes that neither "this" nor "other" is a leaf node. + *

+ * The base class's implementation takes O(log N) time. If a RedBlackNode subclass stores a value used to order the + * nodes, then it could override compareTo to compare the nodes' values, which would take O(1) time. + *

+ * Note that while it is possible to compare the positions of two nodes in O(P) time, where P is the length of the + * path from this node to "other", the default implementation of compareTo is not guaranteed to take O(P) time. If + * your application requires this, then you should write your own comparison method. + */ + @Override + public int compareTo(N other) { + if (isLeaf() || other.isLeaf()) { + throw new IllegalArgumentException("One of the nodes is a leaf node"); + } + + // The algorithm operates as follows: compare the depth of this node to that of "other". If the depth of + // "other" is greater, keep moving up from "other" until we find the ancestor at the same depth. Then, keep + // moving up from "this" and from that node until we reach the lowest common ancestor. The node that arrived + // from the left child of the common ancestor is earlier. The algorithm is analogous if the depth of "other" is + // not greater. + if (this == other) { + return 0; + } + + // Compute the depth of each node + int depth = 0; + RedBlackNode parent; + for (parent = this; parent.parent != null; parent = parent.parent) { + depth++; + } + int otherDepth = 0; + N otherParent; + for (otherParent = other; otherParent.parent != null; otherParent = otherParent.parent) { + otherDepth++; + } + + // Go up to nodes of the same depth + if (depth < otherDepth) { + otherParent = other; + for (int i = otherDepth - 1; i > depth; i--) { + otherParent = otherParent.parent; + } + if (otherParent.parent != this) { + otherParent = otherParent.parent; + } else if (left == otherParent) { + return 1; + } else { + return -1; + } + parent = this; + } else if (depth > otherDepth) { + parent = this; + for (int i = depth - 1; i > otherDepth; i--) { + parent = parent.parent; + } + if (parent.parent != other) { + parent = parent.parent; + } else if (other.left == parent) { + return -1; + } else { + return 1; + } + otherParent = other; + } else { + parent = this; + otherParent = other; + } + + // Keep going up until we reach the lowest common ancestor + while (parent.parent != otherParent.parent) { + parent = parent.parent; + otherParent = otherParent.parent; + } + if (parent.parent == null) { + throw new IllegalArgumentException("The nodes do not belong to the same tree"); + } + if (parent.parent.left == parent) { + return -1; + } else { + return 1; + } + } + + /** + * Throws a RuntimeException if the RedBlackNode fields of this are not correct for a leaf node. + */ + private void assertIsValidLeaf() { + if (left != null || right != null || parent != null || isRed) { + throw new RuntimeException("A leaf node's \"left\", \"right\", \"parent\", or isRed field is incorrect"); + } + } + + /** + * Throws a RuntimeException if the subtree rooted at this node does not satisfy the red-black properties, excluding + * the requirement that the root be black, or it contains a repeated node other than a leaf node. + * + * @param blackHeight The required number of black nodes in each path from this to a leaf node, including this and + * the leaf node. + * @param visited The nodes we have reached thus far, other than leaf nodes. This method adds the non-leaf nodes in + * the subtree rooted at this node to "visited". + */ + private void assertSubtreeIsValidRedBlack(int blackHeight, Set> visited) { + @SuppressWarnings("unchecked") + N nThis = (N) this; + if (left == null || right == null) { + assertIsValidLeaf(); + if (blackHeight != 1) { + throw new RuntimeException("Not all root-to-leaf paths have the same number of black nodes"); + } + return; + } else if (!visited.add(new Reference(nThis))) { + throw new RuntimeException("The tree contains a repeated non-leaf node"); + } else { + int childBlackHeight; + if (isRed) { + if ((!left.isLeaf() && left.isRed) || (!right.isLeaf() && right.isRed)) { + throw new RuntimeException("A red node has a red child"); + } + childBlackHeight = blackHeight; + } else if (blackHeight == 0) { + throw new RuntimeException("Not all root-to-leaf paths have the same number of black nodes"); + } else { + childBlackHeight = blackHeight - 1; + } + + if (!left.isLeaf() && left.parent != this) { + throw new RuntimeException("left.parent != this"); + } + if (!right.isLeaf() && right.parent != this) { + throw new RuntimeException("right.parent != this"); + } + RedBlackNode leftNode = left; + RedBlackNode rightNode = right; + leftNode.assertSubtreeIsValidRedBlack(childBlackHeight, visited); + rightNode.assertSubtreeIsValidRedBlack(childBlackHeight, visited); + } + } + + /** + * Calls assertNodeIsValid() on every node in the subtree rooted at this node. + */ + private void assertNodesAreValid() { + assertNodeIsValid(); + if (left != null) { + RedBlackNode leftNode = left; + RedBlackNode rightNode = right; + leftNode.assertNodesAreValid(); + rightNode.assertNodesAreValid(); + } + } + + /** + * Throws a RuntimeException if the subtree rooted at this node is not a valid red-black tree, e.g. if a red node + * has a red child or it contains a non-leaf node "node" for which node.left.parent != node. (If parent != null, + * it's okay if isRed is true.) This method is useful for debugging. See also assertSubtreeIsValid(). + */ + public void assertSubtreeIsValidRedBlack() { + if (isLeaf()) { + assertIsValidLeaf(); + } else { + if (parent == null && isRed) { + throw new RuntimeException("The root is red"); + } + + // Compute the black height of the tree + Set> nodes = new HashSet>(); + int blackHeight = 0; + @SuppressWarnings("unchecked") + N node = (N) this; + while (node != null) { + if (!nodes.add(new Reference(node))) { + throw new RuntimeException("The tree contains a repeated non-leaf node"); + } + if (!node.isRed) { + blackHeight++; + } + node = node.left; + } + + assertSubtreeIsValidRedBlack(blackHeight, new HashSet>()); + } + } + + /** + * Throws a RuntimeException if we detect a problem with the subtree rooted at this node, such as a red child of a + * red node or a non-leaf descendant "node" for which node.left.parent != node. This method is useful for + * debugging. RedBlackNode subclasses may want to override assertSubtreeIsValid() to call assertOrderIsValid. + */ + public void assertSubtreeIsValid() { + assertSubtreeIsValidRedBlack(); + assertNodesAreValid(); + } + + /** + * Throws a RuntimeException if the nodes in the subtree rooted at this node are not in the specified order or they + * do not lie in the specified range. Assumes that the subtree rooted at this node is a valid binary tree, i.e. it + * has no repeated nodes other than leaf nodes. + * + * @param comparator A comparator indicating how the nodes should be ordered. + * @param start The lower limit for nodes in the subtree, if any. + * @param end The upper limit for nodes in the subtree, if any. + */ + private void assertOrderIsValid(Comparator comparator, N start, N end) { + if (!isLeaf()) { + @SuppressWarnings("unchecked") + N nThis = (N) this; + if (start != null && comparator.compare(nThis, start) < 0) { + throw new RuntimeException("The nodes are not ordered correctly"); + } + if (end != null && comparator.compare(nThis, end) > 0) { + throw new RuntimeException("The nodes are not ordered correctly"); + } + RedBlackNode leftNode = left; + RedBlackNode rightNode = right; + leftNode.assertOrderIsValid(comparator, start, nThis); + rightNode.assertOrderIsValid(comparator, nThis, end); + } + } + + /** + * Throws a RuntimeException if the nodes in the subtree rooted at this node are not in the specified order. + * Assumes that this is a valid binary tree, i.e. there are no repeated nodes other than leaf nodes. This method is + * useful for debugging. RedBlackNode subclasses may want to override assertSubtreeIsValid() to call + * assertOrderIsValid. + * + * @param comparator A comparator indicating how the nodes should be ordered. If this is null, we use the nodes' + * natural order, as in N.compareTo. + */ + public void assertOrderIsValid(Comparator comparator) { + if (comparator == null) { + comparator = naturalOrder(); + } + assertOrderIsValid(comparator, null, null); + } +} + diff --git a/src/main/java/com/github/btrekkie/red_black_node/Reference.java b/src/main/java/com/github/btrekkie/red_black_node/Reference.java new file mode 100644 index 000000000..6fbd9ddeb --- /dev/null +++ b/src/main/java/com/github/btrekkie/red_black_node/Reference.java @@ -0,0 +1,34 @@ +// from: https://github.com/btrekkie/RedBlackNode/blob/master/src/main/java/com/github/btrekkie/red_black_node/Reference.java +// also MIT: https://github.com/btrekkie/RedBlackNode/blob/master/LICENSE +package com.github.btrekkie.red_black_node; + +/** + * Wraps a value using reference equality. In other words, two references are equal only if their values are the same + * object instance, as in ==. + * + * @param The type of value. + */ +class Reference { + /** + * The value this wraps. + */ + private final T value; + + public Reference(T value) { + this.value = value; + } + + public boolean equals(Object obj) { + if (!(obj instanceof Reference)) { + return false; + } + Reference reference = (Reference) obj; + return value == reference.value; + } + + @Override + public int hashCode() { + return System.identityHashCode(value); + } +} + diff --git a/src/main/java/com/github/leijurv/BetterBlockPos.java b/src/main/java/com/github/leijurv/BetterBlockPos.java new file mode 100644 index 000000000..8b4bdedd7 --- /dev/null +++ b/src/main/java/com/github/leijurv/BetterBlockPos.java @@ -0,0 +1,230 @@ +/* + * This file is part of Baritone. + * + * Baritone is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Baritone is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Baritone. If not, see . + */ + +package com.github.leijurv; + +import it.unimi.dsi.fastutil.HashCommon; + +/** + * A better BlockPos that has fewer hash collisions (and slightly more performant offsets) + *

+ * Is it really faster to subclass BlockPos and calculate a hash in the constructor like this, taking everything into account? + * Yes. 20% faster actually. It's called BETTER BlockPos for a reason. Source: + * Benchmark Spreadsheet + * + * @author leijurv + */ +public final class BetterBlockPos { + + public static final BetterBlockPos ORIGIN = new BetterBlockPos(0, 0, 0); + + public final int x; + public final int y; + public final int z; + + public BetterBlockPos(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + @Override + public int hashCode() { + return (int) longHash(x, y, z); + } + + public static long longHash(BetterBlockPos pos) { + return longHash(pos.x, pos.y, pos.z); + } + + public static final int NUM_X_BITS = 26; + public static final int NUM_Z_BITS = NUM_X_BITS; + public static final int NUM_Y_BITS = 9; // note: even though Y goes from 0 to 255, that doesn't mean 8 bits will "just work" because the deserializer assumes signed. i could change it for just Y to assume unsigned and leave X and Z as signed, however, we know that in 1.17 they plan to add negative Y. for that reason, the better approach is to give the extra bits to Y and leave it as signed. + // also, if 1.17 sticks with the current plan which is -64 to +320, we could have 9 bits for Y and a constant offset of -64 to change it to -128 to +256. + // that would result in the packed long representation of any valid coordinate still being a positive integer + // i like that property, so i will keep num_y_bits at 9 and plan for an offset in 1.17 + // it also gives 1 bit of wiggle room in case anything else happens in the future, so we are only using 63 out of 64 bits at the moment + public static final int Z_SHIFT = 0; + public static final int Y_SHIFT = Z_SHIFT + NUM_Z_BITS + 1; // 1 padding bit to make twos complement not overflow + public static final int X_SHIFT = Y_SHIFT + NUM_Y_BITS + 1; // and also here too + public static final long X_MASK = (1L << NUM_X_BITS) - 1L; // X doesn't need padding as the overflow carry bit is just discarded, like a normal long (-1) + (1) = 0 + public static final long Y_MASK = (1L << NUM_Y_BITS) - 1L; + public static final long Z_MASK = (1L << NUM_Z_BITS) - 1L; + + public static final long POST_ADDITION_MASK = X_MASK << X_SHIFT | Y_MASK << Y_SHIFT | Z_MASK << Z_SHIFT; // required to "manually inline" toLong(-1, -1, -1) here so that javac inserts proper ldc2_w instructions at usage points instead of getstatic + // what's this ^ mask for? + // it allows for efficient offsetting and manipulation of a long packed coordinate + // if we had three ints, x y z, it would be easy to do "y += 1" or "x -= 1" + // but how do you do those things if you have a long with x y and z all stuffed into one primitive? + // adding together two long coordinates actually works perfectly if both sides have X, Y, and Z as all positive, no issues at all + // but when Y or Z is negative, we run into an issue. consider 8 bits: negative one is 11111111 and one is 00000001 + // adding them together gives 00000000, zero, **but only because there isn't a 9th bit to carry into** + // if we had, instead, 00000000 11111111 + 00000000 00000001 we would rightly get 00000001 00000000 with the 1 being carried into the 9th position there + // this is exactly what happens. "toLong(0, 1, 0) + toLong(0, -1, 0)" ends up equaling toLong(1, 0, 0) while we'd rather it equal toLong(0, 0, 0) + // so, we simply mask out the unwanted result of the carry by inserting 1 bit of padding space (as added above) between each + // it used to be 000XXXXXXXXXXXXXXXXXXXXXXXXXXYYYYYYYYYZZZZZZZZZZZZZZZZZZZZZZZZZZ + // and now it is 0XXXXXXXXXXXXXXXXXXXXXXXXXX0YYYYYYYYY0ZZZZZZZZZZZZZZZZZZZZZZZZZZ + // we simply place the X Y and Z in slightly different sections of the long, putting a bit of space between each + // the mask ^ is 0111111111111111111111111110111111111011111111111111111111111111 + // using that example of (0,1,0) + (0,-1,0), here's what happens + // 0000000000000000000000000000000000001000000000000000000000000000 (this is X=0 Y=1 Z=0) + // + 0000000000000000000000000000111111111000000000000000000000000000 (this is X=0 Y=-1 Z=0) + // = 0000000000000000000000000001000000000000000000000000000000000000 + // the unwanted carry bit here ^ is no longer corrupting the least significant bit of X and making it 1! + // now it's just turning on the unused padding bit that we don't care about + // using the mask and bitwise and, we can easily and branchlessly turn off the padding bits just in case something overflow carried into them! + // 0000000000000000000000000001000000000000000000000000000000000000 (the result of the addition from earlier) + // & 0111111111111111111111111110111111111011111111111111111111111111 (this is POST_ADDITION_MASK) + // = 0000000000000000000000000000000000000000000000000000000000000000 + // POST_ADDITION_MASK retains the bits that actually form X, Y, and Z, but intentionally turns off the padding bits + // so, we can simply do "(toLong(0, 1, 0) + toLong(0, -1, 0)) & POST_ADDITION_MASK" and correctly get toLong(0, 0, 0) + // which is incredibly fast and efficient, an add then a bitwise AND against a constant + // and it doesn't require us to pull out X, Y, and Z, modify one of them, and put them all back into the long + // that's what the point of the mask is + + static { + if (POST_ADDITION_MASK != toLong(-1, -1, -1)) { + throw new IllegalStateException(POST_ADDITION_MASK + " " + toLong(-1, -1, -1)); // sanity check + } + } + + public long toLong() { + return toLong(this.x, this.y, this.z); + } + + public static BetterBlockPos fromLong(long serialized) { + return new BetterBlockPos(XfromLong(serialized), YfromLong(serialized), ZfromLong(serialized)); + } + + public static int XfromLong(long serialized) { + return (int) (serialized << (64 - X_SHIFT - NUM_X_BITS) >> (64 - NUM_X_BITS)); + } + + public static int YfromLong(long serialized) { + return (int) (serialized << (64 - Y_SHIFT - NUM_Y_BITS) >> (64 - NUM_Y_BITS)); + } + + public static int ZfromLong(long serialized) { + return (int) (serialized << (64 - Z_SHIFT - NUM_Z_BITS) >> (64 - NUM_Z_BITS)); + } + + public static long toLong(final int x, final int y, final int z) { + return ((long) x & X_MASK) << X_SHIFT | ((long) y & Y_MASK) << Y_SHIFT | ((long) z & Z_MASK) << Z_SHIFT; + } + + public static long offsetBy(long pos, int x, int y, int z) { + return (pos + toLong(x, y, z)) & BetterBlockPos.POST_ADDITION_MASK; + } + + public static final long HASHCODE_MURMUR_MASK = murmur64(-1); + public static final long ZOBRIST_MURMUR_MASK = murmur64(-2); + + public static long longHash(int x, int y, int z) { + return longHash(toLong(x, y, z)); + } + + public static long longHash(long packed) { + return murmur64(HASHCODE_MURMUR_MASK ^ packed); + } + + public static long murmur64(long h) { + return HashCommon.murmurHash3(h); + } + + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + if (o instanceof BetterBlockPos) { + BetterBlockPos oth = (BetterBlockPos) o; + return oth.x == x && oth.y == y && oth.z == z; + } + return false; + } + + public BetterBlockPos upPlusY() { + // this is unimaginably faster than blockpos.up + // that literally calls + // this.up(1) + // which calls this.offset(EnumFacing.UP, 1) + // which does return n == 0 ? this : new BlockPos(this.getX() + facing.getXOffset() * n, this.getY() + facing.getYOffset() * n, this.getZ() + facing.getZOffset() * n); + + // how many function calls is that? up(), up(int), offset(EnumFacing, int), new BlockPos, getX, getXOffset, getY, getYOffset, getZ, getZOffset + // that's ten. + // this is one function call. + return new BetterBlockPos(x, y + 1, z); + } + + public BetterBlockPos upPlusY(int amt) { + // see comment in up() + return amt == 0 ? this : new BetterBlockPos(x, y + amt, z); + } + + public BetterBlockPos downMinusY() { + // see comment in up() + return new BetterBlockPos(x, y - 1, z); + } + + public BetterBlockPos downMinusY(int amt) { + // see comment in up() + return amt == 0 ? this : new BetterBlockPos(x, y - amt, z); + } + + public BetterBlockPos northMinusZ() { + return new BetterBlockPos(x, y, z - 1); + } + + public BetterBlockPos northMinusZ(int amt) { + return amt == 0 ? this : new BetterBlockPos(x, y, z - amt); + } + + public BetterBlockPos southPlusZ() { + return new BetterBlockPos(x, y, z + 1); + } + + public BetterBlockPos southPlusZ(int amt) { + return amt == 0 ? this : new BetterBlockPos(x, y, z + amt); + } + + public BetterBlockPos eastPlusX() { + return new BetterBlockPos(x + 1, y, z); + } + + public BetterBlockPos eastPlusX(int amt) { + return amt == 0 ? this : new BetterBlockPos(x + amt, y, z); + } + + public BetterBlockPos westMinusX() { + return new BetterBlockPos(x - 1, y, z); + } + + public BetterBlockPos westMinusX(int amt) { + return amt == 0 ? this : new BetterBlockPos(x - amt, y, z); + } + + @Override + public String toString() { + return String.format( + "BetterBlockPos{x=%d,y=%d,z=%d}", + x, + y, + z + ); + } +} + diff --git a/src/main/java/com/github/leijurv/EulerTourForest.java b/src/main/java/com/github/leijurv/EulerTourForest.java new file mode 100644 index 000000000..8f938fd85 --- /dev/null +++ b/src/main/java/com/github/leijurv/EulerTourForest.java @@ -0,0 +1,866 @@ +package com.github.leijurv; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.IntStream; + +public class EulerTourForest { + + static long parentWalks; + static long parentCalls; + // https://web.stanford.edu/class/archive/cs/cs166/cs166.1166/lectures/17/Small17.pdf + // https://u.cs.biu.ac.il/~rodittl/p723-holm.pdf + // https://web.archive.org/web/20180725100607/https://infoscience.epfl.ch/record/99353/files/HenzingerK99.pdf + // https://en.wikipedia.org/wiki/Dynamic_connectivity#The_Level_structure + + public final BSTNode[] loopbacks; // a (v,v) fake edge is created per vertex and maintained at the appropriate location in the tree, to allow fast lookups of where "v" is, without having to rely on the presence or absence of tree edges connected to v + + public EulerTourForest(int n) { + this.loopbacks = IntStream.range(0, n).mapToObj(SplayNode::new).toArray(BSTNode[]::new); + } + + public TreeEdge link(int vertA, int vertB) { + if (connected(vertA, vertB)) { + throw new IllegalStateException(); + } + BSTNode outgoing = new SplayNode(vertA, vertB); + BSTNode incoming = new SplayNode(vertB, vertA); + BSTNode.barrelRollToLowest(loopbacks[vertA]); + BSTNode.barrelRollToLowest(loopbacks[vertB]); + BSTNode.concatenate(loopbacks[vertA], outgoing); // (a,a) ... (a,b) + BSTNode.concatenate(outgoing, loopbacks[vertB]); // (a,a) ... (a,b) (b,b) ... + BSTNode.concatenate(loopbacks[vertB], incoming); // (a,a) ... (a,b) (b,b) ... (b,a) + return new TreeEdge(incoming, outgoing); + } + + public void cut(TreeEdge edge) { + if (edge.owner() != this) { + throw new IllegalArgumentException(); + } + if (edge.cut) { + return; + } + edge.cut = true; + BSTNode outgoing = edge.left; + BSTNode incoming = edge.right; + if (incoming.src != outgoing.dst || incoming.dst != outgoing.src || outgoing == incoming || incoming.src == incoming.dst) { + throw new IllegalStateException(); + } + if (!connected(incoming.src, incoming.dst)) { + throw new IllegalStateException(); + } + BSTNode.barrelRollToLowest(outgoing); + BSTNodePair disconnected = incoming.disconnect(Direction.RIGHT); + if (disconnected.left.walkDescendant(Direction.LEFT) != outgoing || disconnected.right.walkDescendant(Direction.LEFT) != incoming) { + throw new IllegalStateException(); + } + if (loopbacks[incoming.src].walkAncestor() != disconnected.left || loopbacks[outgoing.src].walkAncestor() != disconnected.right) { + throw new IllegalStateException(); + } + outgoing.remove(); + incoming.remove(); + } + + public boolean connected(int vertA, int vertB) { + return loopbacks[vertA].walkAncestor() == loopbacks[vertB].walkAncestor(); + } + + public int size(int vert) { + return loopbacks[vert].walkAncestor().loopbackSize; + } + + public int[] walk(int vert) { + BSTNode root = loopbacks[vert].walkAncestor(); + int[] ret = new int[root.loopbackSize]; + int[] idx = {0}; + root.walk(node -> { + if (node.isLoopback()) { + ret[idx[0]++] = node.src; + } + }); + return ret; + } + + // redblacknode impl deleted here + + public static class SplayNode extends BSTNode { + + private SplayNode() { + super(); + } + + private SplayNode(int same) { + super(same); + } + + private SplayNode(int src, int dst) { + super(src, dst); + } + + public void splay() { + while (parent != null) { + BSTNode grandparent = parent.parent; + Direction myDir = whichChildAmI(); + if (grandparent != null) { + Direction parentDir = parent.whichChildAmI(); + if (myDir == parentDir) { + // see "Zig-zig step" of https://en.wikipedia.org/wiki/Splay_tree + grandparent.splayRotate(parentDir); + parent.splayRotate(myDir); + } else { + // see "Zig-zag step" of https://en.wikipedia.org/wiki/Splay_tree + parent.splayRotate(myDir); + grandparent.splayRotate(parentDir); + } + } else { + parent.splayRotate(myDir); + if (parent != null) { + throw new IllegalStateException(); + } + } + } + } + + @Override + protected BSTNode concatenateRoots(BSTNode right) { + if (this.parent != null || right.parent != null || this == right) { + throw new IllegalStateException(); + } + SplayNode newRoot = (SplayNode) right.walkDescendant(Direction.LEFT); + newRoot.splay(); + // newRoot is now the root of a splay tree containing all of right + // and, it is the LOWEST value of right, and left is assumed to be less than it, so now we can just attach left as its left child + // (since it is the lowest value of right, it cannot possibly have any left child already) + if (newRoot.getChild(Direction.LEFT) != null) { + throw new IllegalStateException(); + } + newRoot.setChild(Direction.LEFT, this); + newRoot.childUpdated(); + return newRoot; + } + + @Override + protected BSTNodePair disconnect(Direction remainOnSide) { + splay(); // SIGNIFICANTLY easier to split the tree in half, if we are the root node + BSTNode left = this; + BSTNode right = this; + // simple detach of one side + if (remainOnSide == Direction.LEFT) { + right = rightChild; + rightChild = null; + childUpdated(); + if (right != null) { + right.sizeMustBeAccurate(); + right.parent = null; + } + } else { + left = leftChild; + leftChild = null; + childUpdated(); + if (left != null) { + left.sizeMustBeAccurate(); + left.parent = null; + } + } + return new BSTNodePair(left, right); + } + + public void remove() { + splay(); + if (leftChild != null) { + leftChild.parent = null; + } + if (rightChild != null) { + rightChild.parent = null; + } + if (leftChild != null && rightChild != null) { + leftChild.concatenateRoots(rightChild); + } + } + } + + public abstract static class BSTNode { + + protected final int src; + protected final int dst; + + protected BSTNode leftChild; + protected BSTNode rightChild; + protected BSTNode parent; + + protected int loopbackSize; + + private BSTNode() { + this(-1); + } + + private BSTNode(int same) { + this.src = same; + this.dst = same; + this.loopbackSize = 1; + } + + private BSTNode(int src, int dst) { + if (src == dst) { + throw new IllegalArgumentException(); + } + this.src = src; + this.dst = dst; + this.loopbackSize = 0; + } + + protected void splayRotate(Direction dir) { + rotateSecret(dir); + } + + protected void redBlackRotate(Direction dir) { + rotateSecret(dir.opposite()); // i guess they just use different conventions? + } + + private void rotateSecret(Direction dir) { + // promote my "dir" child to my level, swap myself down to that level + + // see "Zig step" of https://en.wikipedia.org/wiki/Splay_tree + BSTNode child = this.getChild(dir); + BSTNode replacementChild = child.getChild(dir.opposite()); // stays at the same level, is just rotated to the other side of the tree + this.setChild(dir, replacementChild); + if (parent == null) { + child.parent = null; + } else { + parent.setChild(whichChildAmI(), child); + } + child.setChild(dir.opposite(), this); // e.g. my left child now has me as their right child + childUpdated(); + parent.childUpdated(); + } + + public static BSTNode concatenate(BSTNode left, BSTNode right) { + if (left == null) { + throw new IllegalStateException(); + } + if (right == null) { + return left; + } + return left.walkAncestor().concatenateRoots(right.walkAncestor()); + } + + protected void afterSwap(BSTNode other) { + + } + + protected void swapLocationWith(BSTNode other) { + if (other == this) { + throw new IllegalStateException(); + } + if (other.parent == this) { + other.swapLocationWith(this); + return; + } + if (parent == other) { + // grandpa + // other + // this otherChild + // left right + // and we want that to become + // grandpa + // this + // other otherChild + // left right + Direction dir = whichChildAmI(); // LEFT, in the above example + BSTNode otherChild = other.getChild(dir.opposite()); + BSTNode left = leftChild; + BSTNode right = rightChild; + if (other.parent == null) { // grandpa + parent = null; + } else { + other.parent.setChild(other.whichChildAmI(), this); + } + setChild(dir, other); + setChild(dir.opposite(), otherChild); + other.setChild(Direction.LEFT, left); + other.setChild(Direction.RIGHT, right); + other.childUpdated(); + childUpdated(); + } else { + Direction myDir = parent == null ? null : whichChildAmI(); + Direction otherDir = other.parent == null ? null : other.whichChildAmI(); + BSTNode tmpLeft = leftChild; + BSTNode tmpRight = rightChild; + + BSTNode tmpParent = parent; + if (other.parent == null) { // grandpa + parent = null; + } else { + other.parent.setChild(otherDir, this); + } + if (tmpParent == null) { + other.parent = null; + } else { + tmpParent.setChild(myDir, other); + } + + setChild(Direction.LEFT, other.leftChild); + setChild(Direction.RIGHT, other.rightChild); + other.setChild(Direction.LEFT, tmpLeft); + other.setChild(Direction.RIGHT, tmpRight); + calcSize(); + if (parent != null) { + parent.bubbleUpSize(); + } + other.calcSize(); + if (other.parent != null) { + other.parent.bubbleUpSize(); + } + } + afterSwap(other); + } + + protected abstract BSTNode concatenateRoots(BSTNode right); + + public static BSTNode barrelRollToLowest(BSTNode target) { + // 1. chop the tree in half, centered at target + // 2. reattach them in the opposite order + // in other words, "cut the deck but don't riffle" - leave "target" as the first node (NOT necessarily the root node) + BSTNodePair pair = target.disconnect(Direction.RIGHT); + if ((target instanceof SplayNode && pair.right != target) || pair.right.parent != null) { // splay to root only happens with a splay tree, obviously + throw new IllegalStateException(); + } + // target is now the lowest (leftmost) element of pair.right + BSTNode ret = BSTNode.concatenate(pair.right, pair.left); // target is now first, and everything else is still in order :D + // use concatenate and not concatenateRoots because pair.left could be null + if (ret == target && pair.left != null) { + throw new IllegalStateException(); + } + return ret; + } + + protected abstract BSTNodePair disconnect(Direction remainOnSide); // chops the tree in half, with "this" remaining on the side "remainOnSide" + + public abstract void remove(); + + protected void bubbleUpSize() { + int ns = calcSize(); + if (loopbackSize != ns) { + loopbackSize = ns; + if (parent != null) { + parent.bubbleUpSize(); + } + } + } + + protected void childUpdated() { + loopbackSize = calcSize(); + } + + protected void sizeMustBeAccurate() { + if (loopbackSize != calcSize()) { + throw new IllegalStateException(); + } + } + + protected int calcSize() { + int size = 0; + if (isLoopback()) { + size++; + } + if (rightChild != null) { + size += rightChild.loopbackSize; + } + if (leftChild != null) { + size += leftChild.loopbackSize; + } + return size; + } + + protected BSTNode getChild(Direction dir) { + return dir == Direction.LEFT ? leftChild : rightChild; + } + + protected void setChild(Direction dir, BSTNode newChild) { + if (newChild == this) { + throw new IllegalStateException(); + } + if (dir == Direction.LEFT) { + leftChild = newChild; + } else { + rightChild = newChild; + } + if (newChild != null) { + newChild.parent = this; + } + } + + protected Direction whichChildAmI() { + if (parent.leftChild == this) { + return Direction.LEFT; + } + if (parent.rightChild == this) { + return Direction.RIGHT; + } + throw new IllegalStateException(); + } + + protected BSTNode walkDescendant(Direction side) { + BSTNode child = getChild(side); + if (child == null) { + return this; + } else { + return child.walkDescendant(side); + } + } + + protected BSTNode walkAncestor() { + BSTNode walk = this; + while (walk.parent != null) { + parentWalks++; + walk = walk.parent; + } + parentCalls++; + return walk; + /*if (parent == null) { + return this; + } else { + return parent.walkAncestor(); + }*/ + } + + protected void walk(Consumer consumer) { + if (leftChild != null) { + leftChild.walk(consumer); + } + consumer.accept(this); + if (rightChild != null) { + rightChild.walk(consumer); + } + } + + protected BSTNode walkNext() { + if (rightChild != null) { + return rightChild.walkDescendant(Direction.LEFT); + } + BSTNode itr = this; + while (itr.parent != null && itr.whichChildAmI() == Direction.RIGHT) { + itr = itr.parent; + } + return itr.parent; + } + + protected boolean isAlone() { + return parent == null && leftChild == null && rightChild == null; + } + + public boolean isLoopback() { + return src == dst; + } + } + + public enum Direction { // TODO check if proguard converts this to an int + LEFT, RIGHT; + + public Direction opposite() { + return this == LEFT ? RIGHT : LEFT; + } + } + + private static class BSTNodePair { + + final BSTNode left; + final BSTNode right; + + private BSTNodePair(BSTNode left, BSTNode right) { + this.left = left; + this.right = right; + if ((left != null && left.parent != null) || (right != null && right.parent != null)) { + throw new IllegalStateException(); + } + } + } + + public class TreeEdge { + + private boolean cut; + final BSTNode left; + final BSTNode right; + + private TreeEdge(BSTNode left, BSTNode right) { + this.left = left; + this.right = right; + } + + private EulerTourForest owner() { + return EulerTourForest.this; + } + } + + private static void mustEq(BSTNode a, BSTNode b) { + if (a != b) { + throw new IllegalStateException(a + " " + b); + } + } + + public static void sanityCheck() { + for (Direction dir : Direction.values()) { + System.out.println("Testing zig " + dir); + // see "Zig step" of https://en.wikipedia.org/wiki/Splay_tree + SplayNode p = new SplayNode(); + SplayNode x = new SplayNode(); + SplayNode A = new SplayNode(); + SplayNode B = new SplayNode(); + SplayNode C = new SplayNode(); + p.setChild(dir, x); + p.setChild(dir.opposite(), C); + x.setChild(dir, A); + x.setChild(dir.opposite(), B); + + x.splay(); + + mustEq(p.parent, x); + mustEq(p.getChild(dir), B); + mustEq(p.getChild(dir.opposite()), C); + mustEq(x.parent, null); + mustEq(x.getChild(dir), A); + mustEq(x.getChild(dir.opposite()), p); + mustEq(A.parent, x); + mustEq(A.getChild(dir), null); + mustEq(A.getChild(dir.opposite()), null); + mustEq(B.parent, p); + mustEq(B.getChild(dir), null); + mustEq(B.getChild(dir.opposite()), null); + mustEq(C.parent, p); + mustEq(C.getChild(dir), null); + mustEq(C.getChild(dir.opposite()), null); + } + for (Direction dir : Direction.values()) { + System.out.println("Testing zig-zig " + dir); + // see "Zig-zig step" of https://en.wikipedia.org/wiki/Splay_tree + SplayNode g = new SplayNode(); + SplayNode p = new SplayNode(); + SplayNode x = new SplayNode(); + SplayNode A = new SplayNode(); + SplayNode B = new SplayNode(); + SplayNode C = new SplayNode(); + SplayNode D = new SplayNode(); + g.setChild(dir, p); + g.setChild(dir.opposite(), D); + p.setChild(dir, x); + p.setChild(dir.opposite(), C); + x.setChild(dir, A); + x.setChild(dir.opposite(), B); + + x.splay(); + + mustEq(g.parent, p); + mustEq(g.getChild(dir), C); + mustEq(g.getChild(dir.opposite()), D); + mustEq(p.parent, x); + mustEq(p.getChild(dir), B); + mustEq(p.getChild(dir.opposite()), g); + mustEq(x.parent, null); + mustEq(x.getChild(dir), A); + mustEq(x.getChild(dir.opposite()), p); + mustEq(A.parent, x); + mustEq(A.getChild(dir), null); + mustEq(A.getChild(dir.opposite()), null); + mustEq(B.parent, p); + mustEq(B.getChild(dir), null); + mustEq(B.getChild(dir.opposite()), null); + mustEq(C.parent, g); + mustEq(C.getChild(dir), null); + mustEq(C.getChild(dir.opposite()), null); + mustEq(D.parent, g); + mustEq(D.getChild(dir), null); + mustEq(D.getChild(dir.opposite()), null); + } + for (Direction dir : Direction.values()) { + System.out.println("Testing zig-zag " + dir); + // see "Zig-zag step" of https://en.wikipedia.org/wiki/Splay_tree + SplayNode g = new SplayNode(); + SplayNode p = new SplayNode(); + SplayNode x = new SplayNode(); + SplayNode A = new SplayNode(); + SplayNode B = new SplayNode(); + SplayNode C = new SplayNode(); + SplayNode D = new SplayNode(); + g.setChild(dir, p); + g.setChild(dir.opposite(), D); + p.setChild(dir, A); + p.setChild(dir.opposite(), x); + x.setChild(dir, B); + x.setChild(dir.opposite(), C); + + x.splay(); + + mustEq(g.parent, x); + mustEq(g.getChild(dir), C); + mustEq(g.getChild(dir.opposite()), D); + mustEq(p.parent, x); + mustEq(p.getChild(dir), A); + mustEq(p.getChild(dir.opposite()), B); + mustEq(x.parent, null); + mustEq(x.getChild(dir), p); + mustEq(x.getChild(dir.opposite()), g); + mustEq(A.parent, p); + mustEq(A.getChild(dir), null); + mustEq(A.getChild(dir.opposite()), null); + mustEq(B.parent, p); + mustEq(B.getChild(dir), null); + mustEq(B.getChild(dir.opposite()), null); + mustEq(C.parent, g); + mustEq(C.getChild(dir), null); + mustEq(C.getChild(dir.opposite()), null); + mustEq(D.parent, g); + mustEq(D.getChild(dir), null); + mustEq(D.getChild(dir.opposite()), null); + } + for (Direction GtoP : Direction.values()) { + for (Direction PtoX : Direction.values()) { + System.out.println("Testing connected swap " + GtoP + " " + PtoX); + SplayNode g = new SplayNode(); + SplayNode p = new SplayNode(); + SplayNode x = new SplayNode(); + SplayNode A = new SplayNode(); + SplayNode B = new SplayNode(); + SplayNode C = new SplayNode(); + SplayNode D = new SplayNode(); + g.setChild(GtoP, p); + g.setChild(GtoP.opposite(), D); + p.setChild(PtoX, x); + p.setChild(PtoX.opposite(), A); + x.setChild(Direction.LEFT, B); + x.setChild(Direction.RIGHT, C); + + /*x.black = true; + p.black = false;*/ + p.swapLocationWith(x); + + /*if (x.black || !p.black) { + throw new IllegalStateException(); + }*/ + mustEq(g.parent, null); + mustEq(g.getChild(GtoP), x); + mustEq(g.getChild(GtoP.opposite()), D); + mustEq(p.parent, x); + mustEq(p.getChild(Direction.LEFT), B); + mustEq(p.getChild(Direction.RIGHT), C); + mustEq(x.parent, g); + mustEq(x.getChild(PtoX), p); + mustEq(x.getChild(PtoX.opposite()), A); + mustEq(A.parent, x); + mustEq(A.getChild(Direction.LEFT), null); + mustEq(A.getChild(Direction.RIGHT), null); + mustEq(B.parent, p); + mustEq(B.getChild(Direction.LEFT), null); + mustEq(B.getChild(Direction.RIGHT), null); + mustEq(C.parent, p); + mustEq(C.getChild(Direction.LEFT), null); + mustEq(C.getChild(Direction.RIGHT), null); + mustEq(D.parent, g); + mustEq(D.getChild(Direction.LEFT), null); + mustEq(D.getChild(Direction.RIGHT), null); + } + } + for (Direction APtoA : Direction.values()) { + for (Direction BPtoB : Direction.values()) { + System.out.println("Testing disconnected swap " + APtoA + " " + BPtoB); + SplayNode ap = new SplayNode(); + SplayNode apoc = new SplayNode(); + SplayNode a = new SplayNode(); + SplayNode alc = new SplayNode(); + SplayNode arc = new SplayNode(); + SplayNode bp = new SplayNode(); + SplayNode bpoc = new SplayNode(); + SplayNode b = new SplayNode(); + SplayNode blc = new SplayNode(); + SplayNode brc = new SplayNode(); + ap.setChild(APtoA, a); + ap.setChild(APtoA.opposite(), apoc); + a.setChild(Direction.LEFT, alc); + a.setChild(Direction.RIGHT, arc); + bp.setChild(BPtoB, b); + bp.setChild(BPtoB.opposite(), bpoc); + b.setChild(Direction.LEFT, blc); + b.setChild(Direction.RIGHT, brc); + + /*a.black = true; + b.black = false;*/ + a.swapLocationWith(b); + + /*if (a.black || !b.black) { + throw new IllegalStateException(); + }*/ + mustEq(ap.parent, null); + mustEq(ap.getChild(APtoA), b); + mustEq(ap.getChild(APtoA.opposite()), apoc); + mustEq(apoc.parent, ap); + mustEq(apoc.getChild(Direction.LEFT), null); + mustEq(apoc.getChild(Direction.RIGHT), null); + mustEq(a.parent, bp); + mustEq(a.getChild(Direction.LEFT), blc); + mustEq(a.getChild(Direction.RIGHT), brc); + mustEq(alc.parent, b); + mustEq(alc.getChild(Direction.LEFT), null); + mustEq(alc.getChild(Direction.RIGHT), null); + mustEq(arc.parent, b); + mustEq(arc.getChild(Direction.LEFT), null); + mustEq(arc.getChild(Direction.RIGHT), null); + mustEq(bp.parent, null); + mustEq(bp.getChild(BPtoB), a); + mustEq(bp.getChild(BPtoB.opposite()), bpoc); + mustEq(bpoc.parent, bp); + mustEq(bpoc.getChild(Direction.LEFT), null); + mustEq(bpoc.getChild(Direction.RIGHT), null); + mustEq(b.parent, ap); + mustEq(b.getChild(Direction.LEFT), alc); + mustEq(b.getChild(Direction.RIGHT), arc); + mustEq(blc.parent, a); + mustEq(blc.getChild(Direction.LEFT), null); + mustEq(blc.getChild(Direction.RIGHT), null); + mustEq(brc.parent, a); + mustEq(brc.getChild(Direction.LEFT), null); + mustEq(brc.getChild(Direction.RIGHT), null); + } + } + { + Random rand = new Random(5021); + List> constructors = Arrays.asList(SplayNode::new/*, SplayNode::new*/); + for (int run = 0; run < 10; run++) { + int NODES = 10000; + Supplier toUse = constructors.get(run % constructors.size()); + List nodes = new ArrayList<>(); + { + BSTNode root = toUse.get(); + nodes.add(root); + for (int i = 1; i < NODES; i++) { + nodes.add(toUse.get()); + root = BSTNode.concatenate(root, nodes.get(i)); + } + } + int shuffledBy = 0; + for (int ii = 0; ii < 10000; ii++) { + if (rand.nextBoolean()) { + BSTNode root = nodes.get(rand.nextInt(NODES)); + if (root instanceof SplayNode) { + ((SplayNode) root).splay(); + if (root != nodes.get(rand.nextInt(NODES)).walkAncestor() || root.loopbackSize != NODES) { + throw new IllegalStateException(); + } + } else { + throw new IllegalStateException(); + } + } + if (rand.nextBoolean()) { + shuffledBy = rand.nextInt(NODES); + BSTNode root = BSTNode.barrelRollToLowest(nodes.get(shuffledBy)); + if (root != nodes.get(rand.nextInt(NODES)).walkAncestor() || root.loopbackSize != NODES) { + throw new IllegalStateException(); + } + } + if (rand.nextBoolean()) { + int pos = rand.nextBoolean() ? (shuffledBy + NODES + rand.nextInt(10) - 5) % NODES : rand.nextInt(NODES); + BSTNode remove = nodes.remove(pos); + NODES--; + remove.remove(); + if (shuffledBy > pos) { + shuffledBy--; + } + } + List order = new ArrayList<>(NODES); + nodes.get(rand.nextInt(NODES)).walkAncestor().walk(order::add); + for (int n = 0; n < NODES; n++) { + if (order.get(n) != nodes.get((n + shuffledBy) % NODES)) { + throw new IllegalStateException(); + } + order.get(n).sizeMustBeAccurate(); + if (order.get(n).walkNext() != (n < NODES - 1 ? order.get(n + 1) : null)) { + throw new IllegalStateException(); + } + } + } + } + } + { + // slide 22 of https://web.stanford.edu/class/archive/cs/cs166/cs166.1166/lectures/17/Small17.pdf + EulerTourForest forest = new EulerTourForest(11); + forest.link(0, 1); + forest.link(1, 3); + forest.link(2, 4); + forest.link(1, 2); + TreeEdge toCut = forest.link(0, 5); + forest.link(5, 6); + forest.link(6, 9); + forest.link(9, 10); + forest.link(9, 8); + forest.link(6, 7); + BSTNode.barrelRollToLowest(forest.loopbacks[0]); + if (!forest.checkForest(true).equals("abdbcecbafgjkjijghgf")) { + throw new IllegalStateException(); + } + forest.cut(toCut); + if (!forest.checkForest(true).equals("abdbcecb fgjkjijghg")) { + throw new IllegalStateException(); + } + } + { + // slide 26 of https://web.stanford.edu/class/archive/cs/cs166/cs166.1166/lectures/17/Small17.pdf + EulerTourForest forest = new EulerTourForest(11); + forest.link(2, 4); + TreeEdge toCut = forest.link(2, 1); + forest.link(1, 0); + forest.link(1, 3); + forest.link(2, 6); + forest.link(6, 9); + forest.link(9, 10); + forest.link(9, 8); + forest.link(6, 7); + forest.link(5, 6); + BSTNode.barrelRollToLowest(forest.loopbacks[2]); + if (!forest.checkForest(true).equals("cecbabdbcgjkjijghgfg")) { + throw new IllegalStateException(); + } + forest.cut(toCut); + if (!forest.checkForest(true).equals("babd cgjkjijghgfgce")) { + throw new IllegalStateException(); + } + } + } + + public String checkForest(boolean verbose) { + boolean[] seen = new boolean[loopbacks.length]; + StringBuilder ret = new StringBuilder(); + for (int vert = 0; vert < loopbacks.length; vert++) { + if (seen[vert]) { + continue; + } + List order = new ArrayList<>(); + loopbacks[vert].walkAncestor().walk(order::add); + for (int i = 0; i < order.size(); i++) { + if (verbose) { + System.out.print("(" + (char) ('a' + order.get(i).src) + "," + (char) ('a' + order.get(i).dst) + ") "); + } + if (order.get(i).dst != order.get((i + 1) % order.size()).src) { + throw new IllegalStateException(); + } + if (order.get(i).isLoopback()) { + seen[order.get(i).src] = true; + } else { + ret.append((char) ('a' + order.get(i).src)); + } + } + if (verbose) { + System.out.println(); + } + ret.append(" "); + if (!seen[vert]) { + throw new IllegalStateException(); + } + } + if (verbose) { + System.out.println(ret); + } + return ret.toString().trim(); + } +} diff --git a/src/main/java/com/github/leijurv/NavigableSurface.java b/src/main/java/com/github/leijurv/NavigableSurface.java new file mode 100644 index 000000000..f297ccc17 --- /dev/null +++ b/src/main/java/com/github/leijurv/NavigableSurface.java @@ -0,0 +1,163 @@ +package com.github.leijurv; + +import com.github.btrekkie.connectivity.ConnGraph; + +import java.util.OptionalInt; + +public class NavigableSurface { + // the encapsulation / separation of concerns is not great, but this is better for testing purposes than the fully accurate stuff in https://github.com/cabaletta/baritone/tree/builder-2/src/main/java/baritone/builder lol + public final int sizeX; + public final int sizeY; + public final int sizeZ; + + private final boolean[][][] blocks; + + private final ConnGraph connGraph; + + public NavigableSurface(int x, int y, int z) { + this.sizeX = x; + this.sizeY = y; + this.sizeZ = z; + this.blocks = new boolean[x][y][z]; + this.connGraph = new ConnGraph(Attachment::new); + } + + public static class Attachment { + public final int surfaceSize; + + public Attachment(Object a, Object b) { + this((Attachment) a, (Attachment) b); + } + + public Attachment(Attachment a, Attachment b) { + this.surfaceSize = a.surfaceSize + b.surfaceSize; + } + + public Attachment() { + this.surfaceSize = 1; + } + } + + public OptionalInt surfaceSize(BetterBlockPos pos) { // how big is the navigable surface from here? how many distinct coordinates can i walk to (in the future, the augmentation will probably have a list of those coordinates or something?) + Object data = connGraph.getComponentAugmentation(pos.toLong()); + if (data != null) { // i disagree with the intellij suggestion here i think it makes it worse + return OptionalInt.of(((Attachment) data).surfaceSize); + } else { + return OptionalInt.empty(); + } + } + + // so the idea is that as blocks are added and removed, we'll maintain where the player can stand, and what connections that has to other places + public void placeOrRemoveBlock(BetterBlockPos where, boolean place) { + // i think it makes sense to only have a single function, as both placing and breaking blocks can create/remove places the player could stand, as well as creating/removing connections between those places + boolean previously = getBlock(where); + if (previously == place) { + return; // this is already the case + } + blocks[where.x][where.y][where.z] = place; + // first let's set some vertex info for where the player can and cannot stand + for (int dy = -1; dy <= 1; dy++) { + BetterBlockPos couldHaveChanged = where.upPlusY(dy); + boolean currentlyAllowed = canPlayerStandIn(couldHaveChanged); + if (currentlyAllowed) { + // i'm sure this will get more complicated later + connGraph.setVertexAugmentation(couldHaveChanged.toLong(), new Attachment()); + } else { + connGraph.removeVertexAugmentation(couldHaveChanged.toLong()); + } + } + // then let's set the edges + for (int dy = -2; dy <= 1; dy++) { // -2 because of the jump condition for ascending + // i guess some of these can be skipped based on whether "place" is false or true, but, whatever this is just for testing + BetterBlockPos couldHaveChanged = where.upPlusY(dy); + computePossibleMoves(couldHaveChanged); + } + } + + public boolean canPlayerStandIn(BetterBlockPos where) { + return getBlockOrAir(where.downMinusY()) && !getBlockOrAir(where) && !getBlockOrAir(where.upPlusY()); + } + + public void computePossibleMoves(BetterBlockPos feet) { + boolean anySuccess = canPlayerStandIn(feet); + // even if all are fail, need to remove those edges from the graph, so don't return early + for (int[] move : MOVES) { + BetterBlockPos newFeet = feet.eastPlusX(move[0]).upPlusY(move[1]).southPlusZ(move[2]); + boolean thisSuccess = anySuccess; + thisSuccess &= canPlayerStandIn(newFeet); + if (move[1] == -1) { + // descend movement requires the player head to move through one extra block (newFeet must be 3 high not 2 high) + thisSuccess &= !getBlockOrAir(newFeet.upPlusY(2)); + } + if (move[1] == 1) { + // same idea but ascending instead of descending + thisSuccess &= !getBlockOrAir(feet.upPlusY(2)); + } + if (thisSuccess) { + if (connGraph.addEdge(feet.toLong(), newFeet.toLong())) { + //System.out.println("Player can now move between " + feet + " and " + newFeet); + } + } else { + if (connGraph.removeEdge(feet.toLong(), newFeet.toLong())) { + //System.out.println("Player can no longer move between " + feet + " and " + newFeet); + } + } + } + } + + public int requireSurfaceSize(int x, int y, int z) { + return surfaceSize(new BetterBlockPos(x, y, z)).getAsInt(); + } + + public boolean inRange(int x, int y, int z) { + return (x | y | z | (sizeX - (x + 1)) | (sizeY - (y + 1)) | (sizeZ - (z + 1))) >= 0; // ">= 0" is used here in the sense of "most significant bit is not set" + } + + public boolean getBlock(BetterBlockPos where) { + return blocks[where.x][where.y][where.z]; + } + + public boolean getBlockOrAir(BetterBlockPos where) { + if (!inRange(where.x, where.y, where.z)) { + return false; + } + return getBlock(where); + } + + public boolean connected(BetterBlockPos a, BetterBlockPos b) { + return connGraph.connected(a.toLong(), b.toLong()); + } + + public void placeBlock(BetterBlockPos where) { + placeOrRemoveBlock(where, true); + } + + public void placeBlock(int x, int y, int z) { + placeBlock(new BetterBlockPos(x, y, z)); + } + + public void removeBlock(BetterBlockPos where) { + placeOrRemoveBlock(where, false); + } + + public void removeBlock(int x, int y, int z) { + removeBlock(new BetterBlockPos(x, y, z)); + } + + private static final int[][] MOVES = { + {1, -1, 0}, + {-1, -1, 0}, + {0, -1, 1}, + {0, -1, -1}, + + {1, 0, 0}, + {-1, 0, 0}, + {0, 0, 1}, + {0, 0, -1}, + + {1, 1, 0}, + {-1, 1, 0}, + {0, 1, 1}, + {0, 1, -1}, + }; +} diff --git a/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderCollection.java b/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderCollection.java new file mode 100644 index 000000000..90374ca52 --- /dev/null +++ b/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderCollection.java @@ -0,0 +1,46 @@ +package com.github.btrekkie.arbitrary_order_collection; + +import java.util.Comparator; + +/** + * Provides objects ordered in an arbitrary, but consistent fashion, through the createValue() method. To determine the + * relative order of two values, use ArbitraryOrderValue.compareTo. We may only compare values on which we have not + * called "remove" that were created in the same ArbitraryOrderCollection instance. Note that despite the name, + * ArbitraryOrderCollection does not implement Collection. + */ +/* We implement an ArbitraryOrderCollection using a red-black tree. We order the nodes arbitrarily. + */ +public class ArbitraryOrderCollection { + /** + * The Comparator for ordering ArbitraryOrderNodes. + */ + private static final Comparator NODE_COMPARATOR = new Comparator() { + @Override + public int compare(ArbitraryOrderNode node1, ArbitraryOrderNode node2) { + return 0; + } + }; + + /** + * The root node of the tree. + */ + private ArbitraryOrderNode root = new ArbitraryOrderNode(); + + /** + * Adds and returns a new value for ordering. + */ + public ArbitraryOrderValue createValue() { + ArbitraryOrderNode node = new ArbitraryOrderNode(); + root = root.insert(node, true, NODE_COMPARATOR); + return new ArbitraryOrderValue(node); + } + + /** + * Removes the specified value from this collection. Assumes we obtained the value by calling createValue() on this + * instance of ArbitraryOrderCollection. After calling "remove" on a value, we may no longer compare it to other + * values. + */ + public void remove(ArbitraryOrderValue value) { + root = value.node.remove(); + } +} diff --git a/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderNode.java b/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderNode.java new file mode 100644 index 000000000..8a889bb9a --- /dev/null +++ b/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderNode.java @@ -0,0 +1,10 @@ +package com.github.btrekkie.arbitrary_order_collection; + +import com.github.btrekkie.red_black_node.RedBlackNode; + +/** + * A node in an ArbitraryOrderCollection tree. See ArbitraryOrderCollection. + */ +class ArbitraryOrderNode extends RedBlackNode { + +} diff --git a/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderValue.java b/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderValue.java new file mode 100644 index 000000000..160c4ee75 --- /dev/null +++ b/src/test/java/com/github/btrekkie/arbitrary_order_collection/ArbitraryOrderValue.java @@ -0,0 +1,21 @@ +package com.github.btrekkie.arbitrary_order_collection; + +/** + * A value in an ArbitraryOrderCollection. To determine the relative order of two values in the same collection, call + * compareTo. + */ +public class ArbitraryOrderValue implements Comparable { + /** + * The node that establishes this value's relative position. + */ + final ArbitraryOrderNode node; + + ArbitraryOrderValue(ArbitraryOrderNode node) { + this.node = node; + } + + @Override + public int compareTo(ArbitraryOrderValue other) { + return node.compareTo(other.node); + } +} diff --git a/src/test/java/com/github/btrekkie/arbitrary_order_collection/test/ArbitraryOrderCollectionTest.java b/src/test/java/com/github/btrekkie/arbitrary_order_collection/test/ArbitraryOrderCollectionTest.java new file mode 100644 index 000000000..ad36a5c62 --- /dev/null +++ b/src/test/java/com/github/btrekkie/arbitrary_order_collection/test/ArbitraryOrderCollectionTest.java @@ -0,0 +1,73 @@ +package com.github.btrekkie.arbitrary_order_collection.test; + +import com.github.btrekkie.arbitrary_order_collection.ArbitraryOrderCollection; +import com.github.btrekkie.arbitrary_order_collection.ArbitraryOrderValue; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class ArbitraryOrderCollectionTest { + /** + * Tests ArbitraryOrderCollection. + */ + @Test + public void test() { + ArbitraryOrderCollection collection = new ArbitraryOrderCollection(); + List values1 = new ArrayList(5); + ArbitraryOrderValue value = collection.createValue(); + assertEquals(0, value.compareTo(value)); + values1.add(value); + for (int i = 0; i < 4; i++) { + values1.add(collection.createValue()); + } + Collections.sort(values1); + List values2 = new ArrayList(10); + for (int i = 0; i < 10; i++) { + value = collection.createValue(); + values2.add(value); + } + for (int i = 0; i < 5; i++) { + collection.remove(values2.get(2 * i)); + } + assertEquals(0, values1.get(0).compareTo(values1.get(0))); + assertTrue(values1.get(0).compareTo(values1.get(1)) < 0); + assertTrue(values1.get(1).compareTo(values1.get(0)) > 0); + assertTrue(values1.get(4).compareTo(values1.get(2)) > 0); + assertTrue(values1.get(0).compareTo(values1.get(4)) < 0); + + collection = new ArbitraryOrderCollection(); + values1 = new ArrayList(1000); + for (int i = 0; i < 1000; i++) { + value = collection.createValue(); + values1.add(value); + } + for (int i = 0; i < 500; i++) { + collection.remove(values1.get(2 * i)); + } + values2 = new ArrayList(500); + for (int i = 0; i < 500; i++) { + values2.add(values1.get(2 * i + 1)); + } + for (int i = 0; i < 500; i++) { + values2.get(0).compareTo(values2.get(i)); + } + Collections.sort(values2); + for (int i = 0; i < 500; i++) { + collection.createValue(); + } + for (int i = 0; i < 499; i++) { + assertTrue(values2.get(i).compareTo(values2.get(i + 1)) < 0); + assertTrue(values2.get(i + 1).compareTo(values2.get(i)) > 0); + } + for (int i = 1; i < 500; i++) { + assertEquals(0, values2.get(i).compareTo(values2.get(i))); + assertTrue(values2.get(0).compareTo(values2.get(i)) < 0); + assertTrue(values2.get(i).compareTo(values2.get(0)) > 0); + } + } +} diff --git a/src/test/java/com/github/btrekkie/connectivity/test/ConnGraphTest.java b/src/test/java/com/github/btrekkie/connectivity/test/ConnGraphTest.java new file mode 100644 index 000000000..381edd7f6 --- /dev/null +++ b/src/test/java/com/github/btrekkie/connectivity/test/ConnGraphTest.java @@ -0,0 +1,1103 @@ +package com.github.btrekkie.connectivity.test; + +import com.github.btrekkie.connectivity.ConnGraph; +import com.github.btrekkie.connectivity.ConnVertex; +import com.github.leijurv.EulerTourForest; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import static org.junit.Assert.*; + +/* Note that most of the ConnGraphTest test methods use the one-argument ConnVertex constructor, in order to make their + * behavior more predictable. That way, there are consistent test results, and test failures are easier to debug. + */ +public class ConnGraphTest { + private static long toLong(int x, int y) { + return (long) x & 0xffffffffL | ((long) y & 0xffffffffL) << 32; + } + + @Test + public void testPerformanceOnRepeatedConnectionAndDisconnection() { + EulerTourForest.sanityCheck(); + for (int trial = 0; trial < 10; trial++) { + try { + Thread.sleep(2000); + System.gc(); + Thread.sleep(2000); + } catch (InterruptedException ex) {} + long setup = System.currentTimeMillis(); + ConnGraph graph = new ConnGraph((a, b) -> (Integer) a + (Integer) b); + int SZ = 1000; + for (int x = 0; x < SZ; x++) { + for (int y = 0; y < SZ; y++) { + graph.setVertexAugmentation(toLong(x, y), 1); // much faster to do this earlier idk + } + } + for (int x = 0; x < SZ; x++) { + if (x % 100 == 0) { + System.out.println("Indicating progress: connected row " + x); + } + for (int y = 0; y < SZ; y++) { + if (y != SZ - 1 && y != SZ / 2) { // leave graph disconnected in the center - two big areas with no connection + graph.addEdge(toLong(x, y), toLong(x, y + 1)); + } + if (x != SZ - 1) { + graph.addEdge(toLong(x, y), toLong(x + 1, y)); + } + } + } + System.out.println("Setup took " + (System.currentTimeMillis() - setup)); + System.out.println("Part size " + graph.getComponentAugmentation(0)); + + /* + // previous test for cutting in half + long a = System.currentTimeMillis(); + for (int x = 0; x < SZ; x++) { + int y = SZ / 2; + graph.removeEdge(vertices[x*SZ+y],vertices[x*SZ+y+1]); + System.out.println("Sz " + graph.getComponentAugmentation(vertices[0])); + } + System.out.println("Time: " + (System.currentTimeMillis() - a)); + */ + + // try connecting and disconnecting one edge + + for (int reconnectTrial = 0; reconnectTrial < 10; reconnectTrial++) { // then try connecting and disconnecting them + long start = System.currentTimeMillis(); + int x = SZ / 2; + int y = SZ / 2; + graph.addEdge(toLong(x, y), toLong(x, y + 1)); + long afterAdd = System.currentTimeMillis(); + System.out.println("Connected size " + graph.getComponentAugmentation(0)); + graph.removeEdge(toLong(x, y), toLong(x, y + 1)); + System.out.println("Disconnected size " + graph.getComponentAugmentation(0)); + System.out.println("Took " + (System.currentTimeMillis() - afterAdd) + " to remove and " + (afterAdd - start) + " to add"); + } + + System.out.println("entire row"); + + // now try connecting and disconnecting the entire row + + for (int reconnectTrial = 0; reconnectTrial < 10; reconnectTrial++) { // then try connecting and disconnecting them + long start = System.currentTimeMillis(); + int y = SZ / 2; + for (int x = 0; x < SZ; x++) { + graph.addEdge(toLong(x, y), toLong(x, y + 1)); + } + long afterAdd = System.currentTimeMillis(); + System.out.println("Connected size " + graph.getComponentAugmentation(0)); + for (int x = 0; x < SZ; x++) { + graph.removeEdge(toLong(x, y), toLong(x, y + 1)); + } + System.out.println("Disconnected size " + graph.getComponentAugmentation(0)); + System.out.println("Took " + (System.currentTimeMillis() - afterAdd) + " to remove and " + (afterAdd - start) + " to add"); + } + + // entire column + System.out.println("Part size " + graph.getComponentAugmentation(0)); + { + int y = SZ / 2; + for (int x = 0; x < SZ; x++) { + graph.addEdge(toLong(x, y), toLong(x, y + 1)); + } + } + System.out.println("Part size " + graph.getComponentAugmentation(0)); + long col = System.currentTimeMillis(); + { + int x = SZ / 2; + for (int y = 0; y < SZ; y++) { + graph.removeEdge(toLong(x, y), toLong(x + 1, y)); + } + } + System.out.println("Part size " + graph.getComponentAugmentation(0)); + System.out.println("Column took " + (System.currentTimeMillis() - col)); + } + } + + /** + * Tests ConnectivityGraph on a small forest and a binary tree-like subgraph. + */ + @Test + public void testForestAndBinaryTree() { + ConnGraph graph = new ConnGraph(); + Random random = new Random(6170); + ConnVertex vertex1 = new ConnVertex(random); + ConnVertex vertex2 = new ConnVertex(random); + assertTrue(graph.addEdge(vertex1, vertex2)); + ConnVertex vertex3 = new ConnVertex(random); + assertTrue(graph.addEdge(vertex3, vertex1)); + ConnVertex vertex4 = new ConnVertex(random); + assertTrue(graph.addEdge(vertex1, vertex4)); + ConnVertex vertex5 = new ConnVertex(random); + ConnVertex vertex6 = new ConnVertex(random); + ConnVertex vertex7 = new ConnVertex(random); + assertTrue(graph.addEdge(vertex6, vertex7)); + assertTrue(graph.addEdge(vertex6, vertex5)); + assertTrue(graph.addEdge(vertex4, vertex5)); + assertFalse(graph.addEdge(vertex1, vertex3)); + ConnVertex vertex8 = new ConnVertex(random); + ConnVertex vertex9 = new ConnVertex(random); + assertTrue(graph.addEdge(vertex8, vertex9)); + ConnVertex vertex10 = new ConnVertex(random); + assertTrue(graph.addEdge(vertex8, vertex10)); + assertFalse(graph.removeEdge(vertex7, vertex1)); + assertTrue(graph.connected(vertex1, vertex4)); + assertTrue(graph.connected(vertex1, vertex1)); + assertTrue(graph.connected(vertex1, vertex2)); + assertTrue(graph.connected(vertex3, vertex6)); + assertTrue(graph.connected(vertex7, vertex4)); + assertTrue(graph.connected(vertex8, vertex9)); + assertTrue(graph.connected(vertex5, vertex2)); + assertTrue(graph.connected(vertex8, vertex10)); + assertTrue(graph.connected(vertex9, vertex10)); + assertFalse(graph.connected(vertex1, vertex8)); + assertFalse(graph.connected(vertex2, vertex10)); + assertTrue(graph.removeEdge(vertex4, vertex5)); + assertTrue(graph.connected(vertex1, vertex3)); + assertTrue(graph.connected(vertex2, vertex4)); + assertTrue(graph.connected(vertex5, vertex6)); + assertTrue(graph.connected(vertex5, vertex7)); + assertTrue(graph.connected(vertex8, vertex9)); + assertTrue(graph.connected(vertex3, vertex3)); + assertFalse(graph.connected(vertex1, vertex5)); + assertFalse(graph.connected(vertex4, vertex7)); + assertFalse(graph.connected(vertex1, vertex8)); + assertFalse(graph.connected(vertex6, vertex9)); + + /*Set expectedAdjVertices = new HashSet(); + expectedAdjVertices.add(vertex2); + expectedAdjVertices.add(vertex3); + expectedAdjVertices.add(vertex4); + assertEquals(expectedAdjVertices, new HashSet(graph.adjacentVertices(vertex1))); + expectedAdjVertices.clear(); + expectedAdjVertices.add(vertex5); + expectedAdjVertices.add(vertex7); + assertEquals(expectedAdjVertices, new HashSet(graph.adjacentVertices(vertex6))); + assertEquals(Collections.singleton(vertex8), new HashSet(graph.adjacentVertices(vertex9))); + assertEquals(Collections.emptySet(), new HashSet(graph.adjacentVertices(new ConnVertex(random))));*/ + graph.optimize(); + + List vertices = new ArrayList(1000); + for (int i = 0; i < 1000; i++) { + vertices.add(new ConnVertex(random)); + } + for (int i = 0; i < 1000; i++) { + if (i > 0 && Integer.bitCount(i) <= 3) { + graph.addEdge(vertices.get(i), vertices.get((i - 1) / 2)); + } + } + for (int i = 0; i < 1000; i++) { + if (Integer.bitCount(i) > 3) { + graph.addEdge(vertices.get((i - 1) / 2), vertices.get(i)); + } + } + for (int i = 15; i < 31; i++) { + graph.removeEdge(vertices.get(i), vertices.get((i - 1) / 2)); + } + assertTrue(graph.connected(vertices.get(0), vertices.get(0))); + assertTrue(graph.connected(vertices.get(11), vertices.get(2))); + assertTrue(graph.connected(vertices.get(7), vertices.get(14))); + assertTrue(graph.connected(vertices.get(0), vertices.get(10))); + assertFalse(graph.connected(vertices.get(0), vertices.get(15))); + assertFalse(graph.connected(vertices.get(15), vertices.get(16))); + assertFalse(graph.connected(vertices.get(14), vertices.get(15))); + assertFalse(graph.connected(vertices.get(7), vertices.get(605))); + assertFalse(graph.connected(vertices.get(5), vertices.get(87))); + assertTrue(graph.connected(vertices.get(22), vertices.get(22))); + assertTrue(graph.connected(vertices.get(16), vertices.get(70))); + assertTrue(graph.connected(vertices.get(113), vertices.get(229))); + assertTrue(graph.connected(vertices.get(21), vertices.get(715))); + assertTrue(graph.connected(vertices.get(175), vertices.get(715))); + assertTrue(graph.connected(vertices.get(30), vertices.get(999))); + assertTrue(graph.connected(vertices.get(991), vertices.get(999))); + } + + /** + * Tests ConnectivityGraph on a small graph that has cycles. + */ + @Test + public void testSmallCycles() { + ConnGraph graph = new ConnGraph(); + Random random = new Random(6170); + ConnVertex vertex1 = new ConnVertex(random); + ConnVertex vertex2 = new ConnVertex(random); + ConnVertex vertex3 = new ConnVertex(random); + ConnVertex vertex4 = new ConnVertex(random); + ConnVertex vertex5 = new ConnVertex(random); + assertTrue(graph.addEdge(vertex1, vertex2)); + assertTrue(graph.addEdge(vertex2, vertex3)); + assertTrue(graph.addEdge(vertex1, vertex3)); + assertTrue(graph.addEdge(vertex2, vertex4)); + assertTrue(graph.addEdge(vertex3, vertex4)); + assertTrue(graph.addEdge(vertex4, vertex5)); + assertTrue(graph.connected(vertex5, vertex1)); + assertTrue(graph.connected(vertex1, vertex4)); + assertTrue(graph.removeEdge(vertex4, vertex5)); + assertFalse(graph.connected(vertex4, vertex5)); + assertFalse(graph.connected(vertex5, vertex1)); + assertTrue(graph.connected(vertex1, vertex4)); + assertTrue(graph.removeEdge(vertex1, vertex2)); + assertTrue(graph.removeEdge(vertex3, vertex4)); + assertTrue(graph.connected(vertex1, vertex4)); + assertTrue(graph.removeEdge(vertex2, vertex3)); + assertTrue(graph.connected(vertex1, vertex3)); + assertTrue(graph.connected(vertex2, vertex4)); + assertFalse(graph.connected(vertex1, vertex4)); + } + + /** + * Tests ConnectivityGraph on a grid-based graph. + */ + @Test + public void testGrid() { + ConnGraph graph = new ConnGraph(); + Random random = new Random(6170); + ConnVertex vertex = new ConnVertex(random); + assertTrue(graph.connected(vertex, vertex)); + + graph = new ConnGraph(SumAndMax.AUGMENTATION); + List> vertices = new ArrayList>(20); + for (int y = 0; y < 20; y++) { + List row = new ArrayList(20); + for (int x = 0; x < 20; x++) { + row.add(new ConnVertex(random)); + } + vertices.add(row); + } + for (int y = 0; y < 19; y++) { + for (int x = 0; x < 19; x++) { + assertTrue(graph.addEdge(vertices.get(y).get(x), vertices.get(y).get(x + 1))); + assertTrue(graph.addEdge(vertices.get(y).get(x), vertices.get(y + 1).get(x))); + } + } + graph.optimize(); + + assertTrue(graph.connected(vertices.get(0).get(0), vertices.get(15).get(12))); + assertTrue(graph.connected(vertices.get(0).get(0), vertices.get(18).get(19))); + assertFalse(graph.connected(vertices.get(0).get(0), vertices.get(19).get(19))); + assertFalse(graph.removeEdge(vertices.get(18).get(19), vertices.get(19).get(19))); + assertFalse(graph.removeEdge(vertices.get(0).get(0), vertices.get(2).get(2))); + + assertTrue(graph.removeEdge(vertices.get(12).get(8), vertices.get(11).get(8))); + assertTrue(graph.removeEdge(vertices.get(12).get(9), vertices.get(11).get(9))); + assertTrue(graph.removeEdge(vertices.get(12).get(8), vertices.get(12).get(7))); + assertTrue(graph.removeEdge(vertices.get(13).get(8), vertices.get(13).get(7))); + assertTrue(graph.removeEdge(vertices.get(13).get(8), vertices.get(14).get(8))); + assertTrue(graph.removeEdge(vertices.get(12).get(9), vertices.get(12).get(10))); + assertTrue(graph.removeEdge(vertices.get(13).get(9), vertices.get(13).get(10))); + assertTrue(graph.connected(vertices.get(2).get(1), vertices.get(12).get(8))); + assertTrue(graph.connected(vertices.get(12).get(8), vertices.get(13).get(9))); + assertTrue(graph.removeEdge(vertices.get(13).get(9), vertices.get(14).get(9))); + assertFalse(graph.connected(vertices.get(2).get(1), vertices.get(12).get(8))); + assertTrue(graph.connected(vertices.get(12).get(8), vertices.get(13).get(9))); + assertFalse(graph.connected(vertices.get(11).get(8), vertices.get(12).get(8))); + assertTrue(graph.connected(vertices.get(16).get(18), vertices.get(6).get(15))); + assertTrue(graph.removeEdge(vertices.get(12).get(9), vertices.get(12).get(8))); + assertTrue(graph.removeEdge(vertices.get(12).get(8), vertices.get(13).get(8))); + assertFalse(graph.connected(vertices.get(2).get(1), vertices.get(12).get(8))); + assertFalse(graph.connected(vertices.get(12).get(8), vertices.get(13).get(9))); + assertFalse(graph.connected(vertices.get(11).get(8), vertices.get(12).get(8))); + assertTrue(graph.connected(vertices.get(13).get(8), vertices.get(12).get(9))); + + assertTrue(graph.removeEdge(vertices.get(6).get(15), vertices.get(5).get(15))); + assertTrue(graph.removeEdge(vertices.get(6).get(15), vertices.get(7).get(15))); + assertTrue(graph.removeEdge(vertices.get(6).get(15), vertices.get(6).get(14))); + assertTrue(graph.removeEdge(vertices.get(6).get(15), vertices.get(6).get(16))); + assertFalse(graph.removeEdge(vertices.get(6).get(15), vertices.get(5).get(15))); + assertFalse(graph.connected(vertices.get(16).get(18), vertices.get(6).get(15))); + assertFalse(graph.connected(vertices.get(7).get(15), vertices.get(6).get(15))); + graph.addEdge(vertices.get(6).get(15), vertices.get(7).get(15)); + assertTrue(graph.connected(vertices.get(16).get(18), vertices.get(6).get(15))); + + for (int y = 1; y < 19; y++) { + for (int x = 1; x < 19; x++) { + graph.removeEdge(vertices.get(y).get(x), vertices.get(y).get(x + 1)); + graph.removeEdge(vertices.get(y).get(x), vertices.get(y + 1).get(x)); + } + } + + assertTrue(graph.addEdge(vertices.get(5).get(6), vertices.get(0).get(7))); + assertTrue(graph.addEdge(vertices.get(12).get(8), vertices.get(5).get(6))); + assertTrue(graph.connected(vertices.get(5).get(6), vertices.get(14).get(0))); + assertTrue(graph.connected(vertices.get(12).get(8), vertices.get(0).get(17))); + assertFalse(graph.connected(vertices.get(3).get(5), vertices.get(0).get(9))); + assertFalse(graph.connected(vertices.get(14).get(2), vertices.get(11).get(18))); + + assertNull(graph.getVertexAugmentation(vertices.get(13).get(8))); + assertNull(graph.getVertexAugmentation(vertices.get(6).get(4))); + assertNull(graph.getComponentAugmentation(vertices.get(13).get(8))); + assertNull(graph.getComponentAugmentation(vertices.get(6).get(4))); + assertFalse(graph.vertexHasAugmentation(vertices.get(13).get(8))); + assertFalse(graph.vertexHasAugmentation(vertices.get(6).get(4))); + assertFalse(graph.componentHasAugmentation(vertices.get(13).get(8))); + assertFalse(graph.componentHasAugmentation(vertices.get(6).get(4))); + } + + /** + * Tests a graph with a hub-and-spokes subgraph and a clique subgraph. + */ + @Test + public void testWheelAndClique() { + ConnGraph graph = new ConnGraph(SumAndMax.AUGMENTATION); + Random random = new Random(6170); + ConnVertex hub = new ConnVertex(random); + List spokes1 = new ArrayList(10); + List spokes2 = new ArrayList(10); + for (int i = 0; i < 10; i++) { + ConnVertex spoke1 = new ConnVertex(random); + ConnVertex spoke2 = new ConnVertex(random); + assertTrue(graph.addEdge(spoke1, spoke2)); + assertNull(graph.setVertexAugmentation(spoke1, new SumAndMax(i, i))); + assertNull(graph.setVertexAugmentation(spoke2, new SumAndMax(i, i + 10))); + spokes1.add(spoke1); + spokes2.add(spoke2); + } + for (int i = 0; i < 10; i++) { + assertTrue(graph.addEdge(spokes1.get(i), hub)); + } + for (int i = 0; i < 10; i++) { + assertTrue(graph.addEdge(hub, spokes2.get(i))); + } + + List clique = new ArrayList(10); + for (int i = 0; i < 10; i++) { + ConnVertex vertex = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(vertex, new SumAndMax(i, i + 20))); + clique.add(vertex); + } + for (int i = 0; i < 10; i++) { + for (int j = i + 1; j < 10; j++) { + assertTrue(graph.addEdge(clique.get(i), clique.get(j))); + } + } + assertTrue(graph.addEdge(hub, clique.get(0))); + + assertTrue(graph.connected(spokes1.get(5), clique.get(3))); + assertTrue(graph.connected(spokes1.get(3), spokes2.get(8))); + assertTrue(graph.connected(spokes1.get(4), spokes2.get(4))); + assertTrue(graph.connected(clique.get(5), hub)); + SumAndMax expectedAugmentation = new SumAndMax(135, 29); + assertEquals(expectedAugmentation, graph.getComponentAugmentation(spokes2.get(8))); + assertTrue(graph.componentHasAugmentation(spokes2.get(8))); + assertEquals(expectedAugmentation, graph.getComponentAugmentation(hub)); + assertEquals(expectedAugmentation, graph.getComponentAugmentation(clique.get(9))); + assertEquals(new SumAndMax(4, 4), graph.getVertexAugmentation(spokes1.get(4))); + assertTrue(graph.vertexHasAugmentation(spokes1.get(4))); + assertNull(graph.getVertexAugmentation(hub)); + assertFalse(graph.vertexHasAugmentation(hub)); + + assertTrue(graph.removeEdge(spokes1.get(5), hub)); + assertTrue(graph.connected(spokes1.get(5), clique.get(2))); + assertTrue(graph.connected(spokes1.get(5), spokes1.get(8))); + assertTrue(graph.connected(spokes1.get(5), spokes2.get(5))); + assertEquals(new SumAndMax(135, 29), graph.getComponentAugmentation(hub)); + assertTrue(graph.removeEdge(spokes2.get(5), hub)); + assertFalse(graph.connected(spokes1.get(5), clique.get(2))); + assertFalse(graph.connected(spokes1.get(5), spokes1.get(8))); + assertTrue(graph.connected(spokes1.get(5), spokes2.get(5))); + assertEquals(new SumAndMax(125, 29), graph.getComponentAugmentation(hub)); + assertTrue(graph.addEdge(spokes1.get(5), hub)); + assertTrue(graph.connected(spokes1.get(5), clique.get(2))); + assertTrue(graph.connected(spokes1.get(5), spokes1.get(8))); + assertTrue(graph.connected(spokes1.get(5), spokes2.get(5))); + assertEquals(new SumAndMax(135, 29), graph.getComponentAugmentation(hub)); + + assertTrue(graph.removeEdge(hub, clique.get(0))); + assertFalse(graph.connected(spokes1.get(3), clique.get(4))); + assertTrue(graph.connected(spokes2.get(7), hub)); + assertFalse(graph.connected(hub, clique.get(0))); + assertTrue(graph.connected(spokes2.get(9), spokes1.get(5))); + assertEquals(new SumAndMax(90, 19), graph.getComponentAugmentation(hub)); + assertEquals(new SumAndMax(90, 19), graph.getComponentAugmentation(spokes2.get(4))); + assertEquals(new SumAndMax(45, 29), graph.getComponentAugmentation(clique.get(1))); + + assertEquals(new SumAndMax(9, 29), graph.setVertexAugmentation(clique.get(9), new SumAndMax(-20, 4))); + for (int i = 0; i < 10; i++) { + assertEquals( + new SumAndMax(i, i + 10), graph.setVertexAugmentation(spokes2.get(i), new SumAndMax(i - 1, i))); + } + assertNull(graph.removeVertexAugmentation(hub)); + assertEquals(new SumAndMax(4, 4), graph.removeVertexAugmentation(spokes1.get(4))); + assertEquals(new SumAndMax(6, 7), graph.removeVertexAugmentation(spokes2.get(7))); + + assertEquals(new SumAndMax(70, 9), graph.getComponentAugmentation(hub)); + assertTrue(graph.componentHasAugmentation(hub)); + assertEquals(new SumAndMax(70, 9), graph.getComponentAugmentation(spokes1.get(6))); + assertEquals(new SumAndMax(16, 28), graph.getComponentAugmentation(clique.get(4))); + + assertTrue(graph.addEdge(hub, clique.get(1))); + expectedAugmentation = new SumAndMax(86, 28); + assertEquals(expectedAugmentation, graph.getComponentAugmentation(hub)); + assertTrue(graph.componentHasAugmentation(hub)); + assertEquals(expectedAugmentation, graph.getComponentAugmentation(spokes2.get(7))); + assertEquals(expectedAugmentation, graph.getComponentAugmentation(clique.get(3))); + + for (int i = 0; i < 10; i++) { + assertTrue(graph.removeEdge(hub, spokes1.get(i))); + if (i != 5) { + assertTrue(graph.removeEdge(hub, spokes2.get(i))); + } + } + assertFalse(graph.connected(hub, spokes1.get(8))); + assertFalse(graph.connected(hub, spokes2.get(4))); + assertTrue(graph.connected(hub, clique.get(5))); + + graph.clear(); + assertTrue(graph.addEdge(hub, spokes1.get(0))); + assertTrue(graph.addEdge(hub, spokes2.get(0))); + assertTrue(graph.addEdge(spokes1.get(0), spokes2.get(0))); + assertTrue(graph.connected(hub, spokes1.get(0))); + assertFalse(graph.connected(hub, spokes2.get(4))); + assertTrue(graph.connected(clique.get(5), clique.get(5))); + assertNull(graph.getComponentAugmentation(hub)); + assertNull(graph.getVertexAugmentation(spokes2.get(8))); + } + + /** + * Sets the matching between vertices.get(columnIndex) and vertices.get(columnIndex + 1) to the permutation + * suggested by newPermutation. See the comments for the implementation of testPermutations(). + * + * @param graph The graph. + * @param vertices The vertices. + * @param columnIndex The index of the column. + * @param oldPermutation The permutation for the current matching between vertices.get(columnIndex) and + * vertices.get(columnIndex + 1). setPermutation removes the edges in this matching. If there are currently no + * edges between those columns, then oldPermutation is null. + * @param newPermutation The permutation for the new matching. + * @return newPermutation. + */ + private int[] setPermutation( + ConnGraph graph, List> vertices, int columnIndex, + int[] oldPermutation, int[] newPermutation) { + List column1 = vertices.get(columnIndex); + List column2 = vertices.get(columnIndex + 1); + if (oldPermutation != null) { + for (int i = 0; i < oldPermutation.length; i++) { + assertTrue(graph.removeEdge(column1.get(i), column2.get(oldPermutation[i]))); + } + } + for (int i = 0; i < newPermutation.length; i++) { + assertTrue(graph.addEdge(column1.get(i), column2.get(newPermutation[i]))); + } + return newPermutation; + } + + /** + * Asserts that the specified permutation is the correct composite permutation for the specified column, i.e. that + * for all i, vertices.get(0).get(i) is in the same connected component as + * vertices.get(columnIndex + 1).get(expectedPermutation[i]). See the comments for the implementation of + * testPermutations(). + */ + private void checkPermutation( + ConnGraph graph, List> vertices, int columnIndex, int[] expectedPermutation) { + List firstColumn = vertices.get(0); + List column = vertices.get(columnIndex + 1); + for (int i = 0; i < expectedPermutation.length; i++) { + assertTrue(graph.connected(firstColumn.get(i), column.get(expectedPermutation[i]))); + } + } + + /** + * Asserts that the specified permutation differs from the correct composite permutation for the specified column in + * every position, i.e. that for all i, vertices.get(0).get(i) is in a different connected component from + * vertices.get(columnIndex + 1).get(wrongPermutation[i]). See the comments for the implementation of + * testPermutations(). + */ + private void checkWrongPermutation( + ConnGraph graph, List> vertices, int columnIndex, int[] wrongPermutation) { + List firstColumn = vertices.get(0); + List column = vertices.get(columnIndex + 1); + for (int i = 0; i < wrongPermutation.length; i++) { + assertFalse(graph.connected(firstColumn.get(i), column.get(wrongPermutation[i]))); + } + } + + /** + * Tests a graph in the style used to prove lower bounds on the performance of dynamic connectivity, as presented in + * https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-851-advanced-data-structures-spring-2012/lecture-videos/session-21-dynamic-connectivity-lower-bound/ . + */ + @Test + public void testPermutations() { + // The graph used in testPermutations() uses an 8 x 9 grid of vertices, such that vertices.get(i).get(j) is the + // vertex at row j, column i. There is a perfect matching between each pair of columns i and i + 1 - that is, + // there are eight non-adjacent edges from vertices in column i to vertices in column i + 1. These form a + // permutation, so that the element j of the permutation is the row number of the vertex in column i + 1 that is + // adjacent to the vertex at row j, column i. + ConnGraph graph = new ConnGraph(); + Random random = new Random(6170); + List> vertices = new ArrayList>(9); + for (int i = 0; i < 9; i++) { + List column = new ArrayList(8); + for (int j = 0; j < 8; j++) { + column.add(new ConnVertex(random)); + } + vertices.add(column); + } + + int[] permutation0 = setPermutation(graph, vertices, 0, null, new int[]{2, 5, 0, 4, 7, 1, 3, 6}); + int[] permutation1 = setPermutation(graph, vertices, 1, null, new int[]{6, 5, 0, 7, 1, 2, 4, 3}); + int[] permutation2 = setPermutation(graph, vertices, 2, null, new int[]{2, 1, 7, 5, 6, 0, 4, 3}); + int[] permutation3 = setPermutation(graph, vertices, 3, null, new int[]{5, 2, 4, 6, 3, 0, 7, 1}); + int[] permutation4 = setPermutation(graph, vertices, 4, null, new int[]{5, 0, 2, 7, 4, 3, 1, 6}); + int[] permutation5 = setPermutation(graph, vertices, 5, null, new int[]{4, 7, 0, 1, 3, 6, 2, 5}); + int[] permutation6 = setPermutation(graph, vertices, 6, null, new int[]{4, 5, 3, 1, 7, 6, 2, 0}); + int[] permutation7 = setPermutation(graph, vertices, 7, null, new int[]{6, 7, 3, 0, 5, 1, 2, 4}); + + permutation0 = setPermutation(graph, vertices, 0, permutation0, new int[]{7, 5, 3, 0, 4, 2, 1, 6}); + checkWrongPermutation(graph, vertices, 0, new int[]{5, 3, 0, 4, 2, 1, 6, 7}); + checkPermutation(graph, vertices, 0, new int[]{7, 5, 3, 0, 4, 2, 1, 6}); + permutation4 = setPermutation(graph, vertices, 4, permutation4, new int[]{2, 7, 0, 6, 5, 4, 1, 3}); + checkWrongPermutation(graph, vertices, 4, new int[]{7, 1, 6, 0, 5, 4, 3, 2}); + checkPermutation(graph, vertices, 4, new int[]{2, 7, 1, 6, 0, 5, 4, 3}); + permutation2 = setPermutation(graph, vertices, 2, permutation2, new int[]{3, 5, 6, 1, 4, 2, 7, 0}); + checkWrongPermutation(graph, vertices, 2, new int[]{6, 0, 7, 5, 3, 2, 4, 1}); + checkPermutation(graph, vertices, 2, new int[]{1, 6, 0, 7, 5, 3, 2, 4}); + permutation6 = setPermutation(graph, vertices, 6, permutation6, new int[]{4, 7, 1, 3, 6, 0, 5, 2}); + checkWrongPermutation(graph, vertices, 6, new int[]{7, 3, 0, 4, 2, 5, 1, 6}); + checkPermutation(graph, vertices, 6, new int[]{6, 7, 3, 0, 4, 2, 5, 1}); + permutation1 = setPermutation(graph, vertices, 1, permutation1, new int[]{2, 4, 0, 5, 6, 3, 7, 1}); + checkWrongPermutation(graph, vertices, 1, new int[]{3, 5, 2, 6, 0, 4, 7, 1}); + checkPermutation(graph, vertices, 1, new int[]{1, 3, 5, 2, 6, 0, 4, 7}); + permutation5 = setPermutation(graph, vertices, 5, permutation5, new int[]{5, 3, 2, 0, 7, 1, 6, 4}); + checkWrongPermutation(graph, vertices, 5, new int[]{5, 1, 0, 4, 3, 6, 7, 2}); + checkPermutation(graph, vertices, 5, new int[]{2, 5, 1, 0, 4, 3, 6, 7}); + permutation3 = setPermutation(graph, vertices, 3, permutation3, new int[]{1, 7, 3, 0, 4, 5, 6, 2}); + checkWrongPermutation(graph, vertices, 3, new int[]{7, 3, 6, 2, 0, 4, 1, 5}); + checkPermutation(graph, vertices, 3, new int[]{5, 7, 3, 6, 2, 0, 4, 1}); + permutation7 = setPermutation(graph, vertices, 7, permutation7, new int[]{4, 7, 5, 6, 2, 0, 1, 3}); + checkWrongPermutation(graph, vertices, 7, new int[]{2, 0, 6, 4, 7, 3, 1, 5}); + checkPermutation(graph, vertices, 7, new int[]{5, 2, 0, 6, 4, 7, 3, 1}); + } + + /** + * Tests a graph based on the United States. + */ + @Test + public void testUnitedStates() { + ConnGraph graph = new ConnGraph(SumAndMax.AUGMENTATION); + Random random = new Random(6170); + ConnVertex alabama = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(alabama, new SumAndMax(7, 1819))); + ConnVertex alaska = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(alaska, new SumAndMax(1, 1959))); + ConnVertex arizona = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(arizona, new SumAndMax(9, 1912))); + ConnVertex arkansas = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(arkansas, new SumAndMax(4, 1836))); + ConnVertex california = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(california, new SumAndMax(53, 1850))); + ConnVertex colorado = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(colorado, new SumAndMax(7, 1876))); + ConnVertex connecticut = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(connecticut, new SumAndMax(5, 1788))); + ConnVertex delaware = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(delaware, new SumAndMax(1, 1787))); + ConnVertex florida = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(florida, new SumAndMax(27, 1845))); + ConnVertex georgia = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(georgia, new SumAndMax(14, 1788))); + ConnVertex hawaii = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(hawaii, new SumAndMax(2, 1959))); + ConnVertex idaho = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(idaho, new SumAndMax(2, 1890))); + ConnVertex illinois = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(illinois, new SumAndMax(18, 1818))); + ConnVertex indiana = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(indiana, new SumAndMax(9, 1816))); + ConnVertex iowa = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(iowa, new SumAndMax(4, 1846))); + ConnVertex kansas = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(kansas, new SumAndMax(4, 1861))); + ConnVertex kentucky = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(kentucky, new SumAndMax(6, 1792))); + ConnVertex louisiana = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(louisiana, new SumAndMax(6, 1812))); + ConnVertex maine = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(maine, new SumAndMax(2, 1820))); + ConnVertex maryland = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(maryland, new SumAndMax(8, 1788))); + ConnVertex massachusetts = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(massachusetts, new SumAndMax(9, 1788))); + ConnVertex michigan = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(michigan, new SumAndMax(14, 1837))); + ConnVertex minnesota = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(minnesota, new SumAndMax(8, 1858))); + ConnVertex mississippi = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(mississippi, new SumAndMax(4, 1817))); + ConnVertex missouri = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(missouri, new SumAndMax(8, 1821))); + ConnVertex montana = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(montana, new SumAndMax(1, 1889))); + ConnVertex nebraska = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(nebraska, new SumAndMax(3, 1867))); + ConnVertex nevada = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(nevada, new SumAndMax(4, 1864))); + ConnVertex newHampshire = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(newHampshire, new SumAndMax(2, 1788))); + ConnVertex newJersey = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(newJersey, new SumAndMax(12, 1787))); + ConnVertex newMexico = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(newMexico, new SumAndMax(3, 1912))); + ConnVertex newYork = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(newYork, new SumAndMax(27, 1788))); + ConnVertex northCarolina = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(northCarolina, new SumAndMax(13, 1789))); + ConnVertex northDakota = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(northDakota, new SumAndMax(1, 1889))); + ConnVertex ohio = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(ohio, new SumAndMax(16, 1803))); + ConnVertex oklahoma = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(oklahoma, new SumAndMax(5, 1907))); + ConnVertex oregon = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(oregon, new SumAndMax(5, 1859))); + ConnVertex pennsylvania = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(pennsylvania, new SumAndMax(18, 1787))); + ConnVertex rhodeIsland = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(rhodeIsland, new SumAndMax(2, 1790))); + ConnVertex southCarolina = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(southCarolina, new SumAndMax(7, 1788))); + ConnVertex southDakota = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(southDakota, new SumAndMax(1, 1889))); + ConnVertex tennessee = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(tennessee, new SumAndMax(9, 1796))); + ConnVertex texas = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(texas, new SumAndMax(36, 1845))); + ConnVertex utah = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(utah, new SumAndMax(4, 1896))); + ConnVertex vermont = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(vermont, new SumAndMax(1, 1791))); + ConnVertex virginia = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(virginia, new SumAndMax(11, 1788))); + ConnVertex washington = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(washington, new SumAndMax(10, 1889))); + ConnVertex westVirginia = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(westVirginia, new SumAndMax(3, 1863))); + ConnVertex wisconsin = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(wisconsin, new SumAndMax(8, 1848))); + ConnVertex wyoming = new ConnVertex(random); + assertNull(graph.setVertexAugmentation(wyoming, new SumAndMax(1, 1890))); + + assertTrue(graph.addEdge(alabama, florida)); + assertTrue(graph.addEdge(alabama, georgia)); + assertTrue(graph.addEdge(alabama, mississippi)); + assertTrue(graph.addEdge(alabama, tennessee)); + assertTrue(graph.addEdge(arizona, california)); + assertTrue(graph.addEdge(arizona, colorado)); + assertTrue(graph.addEdge(arizona, nevada)); + assertTrue(graph.addEdge(arizona, newMexico)); + assertTrue(graph.addEdge(arizona, utah)); + assertTrue(graph.addEdge(arkansas, louisiana)); + assertTrue(graph.addEdge(arkansas, mississippi)); + assertTrue(graph.addEdge(arkansas, missouri)); + assertTrue(graph.addEdge(arkansas, oklahoma)); + assertTrue(graph.addEdge(arkansas, tennessee)); + assertTrue(graph.addEdge(arkansas, texas)); + assertTrue(graph.addEdge(california, nevada)); + assertTrue(graph.addEdge(california, oregon)); + assertTrue(graph.addEdge(colorado, kansas)); + assertTrue(graph.addEdge(colorado, nebraska)); + assertTrue(graph.addEdge(colorado, newMexico)); + assertTrue(graph.addEdge(colorado, oklahoma)); + assertTrue(graph.addEdge(colorado, utah)); + assertTrue(graph.addEdge(colorado, wyoming)); + assertTrue(graph.addEdge(connecticut, massachusetts)); + assertTrue(graph.addEdge(connecticut, newYork)); + assertTrue(graph.addEdge(connecticut, rhodeIsland)); + assertTrue(graph.addEdge(delaware, maryland)); + assertTrue(graph.addEdge(delaware, newJersey)); + assertTrue(graph.addEdge(delaware, pennsylvania)); + assertTrue(graph.addEdge(florida, georgia)); + assertTrue(graph.addEdge(georgia, northCarolina)); + assertTrue(graph.addEdge(georgia, southCarolina)); + assertTrue(graph.addEdge(georgia, tennessee)); + assertTrue(graph.addEdge(idaho, montana)); + assertTrue(graph.addEdge(idaho, nevada)); + assertTrue(graph.addEdge(idaho, oregon)); + assertTrue(graph.addEdge(idaho, utah)); + assertTrue(graph.addEdge(idaho, washington)); + assertTrue(graph.addEdge(idaho, wyoming)); + assertTrue(graph.addEdge(illinois, indiana)); + assertTrue(graph.addEdge(illinois, iowa)); + assertTrue(graph.addEdge(illinois, kentucky)); + assertTrue(graph.addEdge(illinois, missouri)); + assertTrue(graph.addEdge(illinois, wisconsin)); + assertTrue(graph.addEdge(indiana, kentucky)); + assertTrue(graph.addEdge(indiana, michigan)); + assertTrue(graph.addEdge(indiana, ohio)); + assertTrue(graph.addEdge(iowa, minnesota)); + assertTrue(graph.addEdge(iowa, missouri)); + assertTrue(graph.addEdge(iowa, nebraska)); + assertTrue(graph.addEdge(iowa, southDakota)); + assertTrue(graph.addEdge(iowa, wisconsin)); + assertTrue(graph.addEdge(kansas, missouri)); + assertTrue(graph.addEdge(kansas, nebraska)); + assertTrue(graph.addEdge(kansas, oklahoma)); + assertTrue(graph.addEdge(kentucky, missouri)); + assertTrue(graph.addEdge(kentucky, ohio)); + assertTrue(graph.addEdge(kentucky, tennessee)); + assertTrue(graph.addEdge(kentucky, virginia)); + assertTrue(graph.addEdge(kentucky, westVirginia)); + assertTrue(graph.addEdge(louisiana, mississippi)); + assertTrue(graph.addEdge(louisiana, texas)); + assertTrue(graph.addEdge(maine, newHampshire)); + assertTrue(graph.addEdge(maryland, pennsylvania)); + assertTrue(graph.addEdge(maryland, virginia)); + assertTrue(graph.addEdge(maryland, westVirginia)); + assertTrue(graph.addEdge(massachusetts, newHampshire)); + assertTrue(graph.addEdge(massachusetts, newYork)); + assertTrue(graph.addEdge(massachusetts, rhodeIsland)); + assertTrue(graph.addEdge(massachusetts, vermont)); + assertTrue(graph.addEdge(michigan, ohio)); + assertTrue(graph.addEdge(michigan, wisconsin)); + assertTrue(graph.addEdge(minnesota, northDakota)); + assertTrue(graph.addEdge(minnesota, southDakota)); + assertTrue(graph.addEdge(minnesota, wisconsin)); + assertTrue(graph.addEdge(mississippi, tennessee)); + assertTrue(graph.addEdge(missouri, nebraska)); + assertTrue(graph.addEdge(missouri, oklahoma)); + assertTrue(graph.addEdge(missouri, tennessee)); + assertTrue(graph.addEdge(montana, northDakota)); + assertTrue(graph.addEdge(montana, southDakota)); + assertTrue(graph.addEdge(montana, wyoming)); + assertTrue(graph.addEdge(nebraska, southDakota)); + assertTrue(graph.addEdge(nebraska, wyoming)); + assertTrue(graph.addEdge(nevada, oregon)); + assertTrue(graph.addEdge(nevada, utah)); + assertTrue(graph.addEdge(newHampshire, vermont)); + assertTrue(graph.addEdge(newJersey, newYork)); + assertTrue(graph.addEdge(newJersey, pennsylvania)); + assertTrue(graph.addEdge(newMexico, oklahoma)); + assertTrue(graph.addEdge(newMexico, texas)); + assertTrue(graph.addEdge(newMexico, utah)); + assertTrue(graph.addEdge(newYork, pennsylvania)); + assertTrue(graph.addEdge(newYork, vermont)); + assertTrue(graph.addEdge(northCarolina, southCarolina)); + assertTrue(graph.addEdge(northCarolina, tennessee)); + assertTrue(graph.addEdge(northCarolina, virginia)); + assertTrue(graph.addEdge(northDakota, southDakota)); + assertTrue(graph.addEdge(ohio, pennsylvania)); + assertTrue(graph.addEdge(ohio, westVirginia)); + assertTrue(graph.addEdge(oklahoma, texas)); + assertTrue(graph.addEdge(oregon, washington)); + assertTrue(graph.addEdge(pennsylvania, westVirginia)); + assertTrue(graph.addEdge(southDakota, wyoming)); + assertTrue(graph.addEdge(tennessee, virginia)); + assertTrue(graph.addEdge(utah, wyoming)); + assertTrue(graph.addEdge(virginia, westVirginia)); + + assertTrue(graph.connected(florida, washington)); + assertTrue(graph.connected(rhodeIsland, michigan)); + assertTrue(graph.connected(delaware, texas)); + assertFalse(graph.connected(alaska, newYork)); + assertFalse(graph.connected(hawaii, idaho)); + assertEquals(new SumAndMax(432, 1912), graph.getComponentAugmentation(newJersey)); + assertEquals(new SumAndMax(2, 1959), graph.getComponentAugmentation(hawaii)); + + // 2186: Aliens attack, split nation in two using lasers + assertTrue(graph.removeEdge(northDakota, minnesota)); + assertTrue(graph.removeEdge(southDakota, minnesota)); + assertTrue(graph.removeEdge(southDakota, iowa)); + assertTrue(graph.removeEdge(nebraska, iowa)); + assertTrue(graph.removeEdge(nebraska, missouri)); + assertTrue(graph.removeEdge(kansas, missouri)); + assertTrue(graph.removeEdge(oklahoma, missouri)); + assertTrue(graph.removeEdge(oklahoma, arkansas)); + assertTrue(graph.removeEdge(texas, arkansas)); + assertTrue(graph.connected(california, massachusetts)); + assertTrue(graph.connected(montana, virginia)); + assertTrue(graph.connected(idaho, southDakota)); + assertTrue(graph.connected(maine, tennessee)); + assertEquals(new SumAndMax(432, 1912), graph.getComponentAugmentation(vermont)); + assertTrue(graph.removeEdge(texas, louisiana)); + assertFalse(graph.connected(california, massachusetts)); + assertFalse(graph.connected(montana, virginia)); + assertTrue(graph.connected(idaho, southDakota)); + assertTrue(graph.connected(maine, tennessee)); + assertEquals(new SumAndMax(149, 1912), graph.getComponentAugmentation(wyoming)); + assertEquals(new SumAndMax(283, 1863), graph.getComponentAugmentation(vermont)); + + // 2254: California breaks off into ocean, secedes + assertTrue(graph.removeEdge(california, oregon)); + assertTrue(graph.removeEdge(california, nevada)); + assertTrue(graph.removeEdge(california, arizona)); + assertEquals(new SumAndMax(53, 1850), graph.removeVertexAugmentation(california)); + assertFalse(graph.connected(california, utah)); + assertFalse(graph.connected(california, oregon)); + assertNull(graph.getComponentAugmentation(california)); + assertEquals(new SumAndMax(96, 1912), graph.getComponentAugmentation(washington)); + assertEquals(new SumAndMax(283, 1863), graph.getComponentAugmentation(vermont)); + + // 2367: Nuclear armageddon + assertEquals(new SumAndMax(7, 1819), graph.removeVertexAugmentation(alabama)); + assertTrue(graph.removeEdge(alabama, florida)); + assertTrue(graph.removeEdge(alabama, georgia)); + assertTrue(graph.removeEdge(alabama, mississippi)); + assertTrue(graph.removeEdge(alabama, tennessee)); + assertEquals(new SumAndMax(1, 1959), graph.removeVertexAugmentation(alaska)); + assertEquals(new SumAndMax(9, 1912), graph.removeVertexAugmentation(arizona)); + assertTrue(graph.removeEdge(arizona, colorado)); + assertTrue(graph.removeEdge(arizona, nevada)); + assertTrue(graph.removeEdge(arizona, newMexico)); + assertTrue(graph.removeEdge(arizona, utah)); + assertEquals(new SumAndMax(4, 1836), graph.removeVertexAugmentation(arkansas)); + assertTrue(graph.removeEdge(arkansas, louisiana)); + assertTrue(graph.removeEdge(arkansas, mississippi)); + assertTrue(graph.removeEdge(arkansas, missouri)); + assertTrue(graph.removeEdge(arkansas, tennessee)); + assertEquals(new SumAndMax(7, 1876), graph.removeVertexAugmentation(colorado)); + assertTrue(graph.removeEdge(colorado, kansas)); + assertTrue(graph.removeEdge(colorado, nebraska)); + assertTrue(graph.removeEdge(colorado, newMexico)); + assertTrue(graph.removeEdge(colorado, oklahoma)); + assertTrue(graph.removeEdge(colorado, utah)); + assertTrue(graph.removeEdge(colorado, wyoming)); + assertEquals(new SumAndMax(5, 1788), graph.removeVertexAugmentation(connecticut)); + assertTrue(graph.removeEdge(connecticut, massachusetts)); + assertTrue(graph.removeEdge(connecticut, newYork)); + assertTrue(graph.removeEdge(connecticut, rhodeIsland)); + assertEquals(new SumAndMax(1, 1787), graph.removeVertexAugmentation(delaware)); + assertTrue(graph.removeEdge(delaware, maryland)); + assertTrue(graph.removeEdge(delaware, newJersey)); + assertTrue(graph.removeEdge(delaware, pennsylvania)); + assertEquals(new SumAndMax(27, 1845), graph.removeVertexAugmentation(florida)); + assertTrue(graph.removeEdge(florida, georgia)); + assertEquals(new SumAndMax(14, 1788), graph.removeVertexAugmentation(georgia)); + assertTrue(graph.removeEdge(georgia, northCarolina)); + assertTrue(graph.removeEdge(georgia, southCarolina)); + assertTrue(graph.removeEdge(georgia, tennessee)); + assertEquals(new SumAndMax(2, 1959), graph.removeVertexAugmentation(hawaii)); + assertEquals(new SumAndMax(2, 1890), graph.removeVertexAugmentation(idaho)); + assertTrue(graph.removeEdge(idaho, montana)); + assertTrue(graph.removeEdge(idaho, nevada)); + assertTrue(graph.removeEdge(idaho, oregon)); + assertTrue(graph.removeEdge(idaho, utah)); + assertTrue(graph.removeEdge(idaho, washington)); + assertTrue(graph.removeEdge(idaho, wyoming)); + assertEquals(new SumAndMax(18, 1818), graph.removeVertexAugmentation(illinois)); + assertTrue(graph.removeEdge(illinois, indiana)); + assertTrue(graph.removeEdge(illinois, iowa)); + assertTrue(graph.removeEdge(illinois, kentucky)); + assertTrue(graph.removeEdge(illinois, missouri)); + assertTrue(graph.removeEdge(illinois, wisconsin)); + assertEquals(new SumAndMax(9, 1816), graph.removeVertexAugmentation(indiana)); + assertTrue(graph.removeEdge(indiana, kentucky)); + assertTrue(graph.removeEdge(indiana, michigan)); + assertTrue(graph.removeEdge(indiana, ohio)); + assertEquals(new SumAndMax(4, 1846), graph.removeVertexAugmentation(iowa)); + assertTrue(graph.removeEdge(iowa, minnesota)); + assertTrue(graph.removeEdge(iowa, missouri)); + assertTrue(graph.removeEdge(iowa, wisconsin)); + assertEquals(new SumAndMax(4, 1861), graph.removeVertexAugmentation(kansas)); + assertTrue(graph.removeEdge(kansas, nebraska)); + assertTrue(graph.removeEdge(kansas, oklahoma)); + assertEquals(new SumAndMax(6, 1792), graph.removeVertexAugmentation(kentucky)); + assertTrue(graph.removeEdge(kentucky, missouri)); + assertTrue(graph.removeEdge(kentucky, ohio)); + assertTrue(graph.removeEdge(kentucky, tennessee)); + assertTrue(graph.removeEdge(kentucky, virginia)); + assertTrue(graph.removeEdge(kentucky, westVirginia)); + assertEquals(new SumAndMax(6, 1812), graph.removeVertexAugmentation(louisiana)); + assertTrue(graph.removeEdge(louisiana, mississippi)); + assertEquals(new SumAndMax(2, 1820), graph.removeVertexAugmentation(maine)); + assertTrue(graph.removeEdge(maine, newHampshire)); + assertEquals(new SumAndMax(8, 1788), graph.removeVertexAugmentation(maryland)); + assertTrue(graph.removeEdge(maryland, pennsylvania)); + assertTrue(graph.removeEdge(maryland, virginia)); + assertTrue(graph.removeEdge(maryland, westVirginia)); + assertEquals(new SumAndMax(9, 1788), graph.removeVertexAugmentation(massachusetts)); + assertTrue(graph.removeEdge(massachusetts, newHampshire)); + assertTrue(graph.removeEdge(massachusetts, newYork)); + assertTrue(graph.removeEdge(massachusetts, rhodeIsland)); + assertTrue(graph.removeEdge(massachusetts, vermont)); + assertEquals(new SumAndMax(14, 1837), graph.removeVertexAugmentation(michigan)); + assertTrue(graph.removeEdge(michigan, ohio)); + assertTrue(graph.removeEdge(michigan, wisconsin)); + assertEquals(new SumAndMax(8, 1858), graph.removeVertexAugmentation(minnesota)); + assertTrue(graph.removeEdge(minnesota, wisconsin)); + assertEquals(new SumAndMax(4, 1817), graph.removeVertexAugmentation(mississippi)); + assertTrue(graph.removeEdge(mississippi, tennessee)); + assertEquals(new SumAndMax(8, 1821), graph.removeVertexAugmentation(missouri)); + assertTrue(graph.removeEdge(missouri, tennessee)); + assertEquals(new SumAndMax(1, 1889), graph.removeVertexAugmentation(montana)); + assertTrue(graph.removeEdge(montana, northDakota)); + assertTrue(graph.removeEdge(montana, southDakota)); + assertTrue(graph.removeEdge(montana, wyoming)); + assertEquals(new SumAndMax(3, 1867), graph.removeVertexAugmentation(nebraska)); + assertTrue(graph.removeEdge(nebraska, southDakota)); + assertTrue(graph.removeEdge(nebraska, wyoming)); + assertEquals(new SumAndMax(4, 1864), graph.removeVertexAugmentation(nevada)); + assertTrue(graph.removeEdge(nevada, oregon)); + assertTrue(graph.removeEdge(nevada, utah)); + assertEquals(new SumAndMax(2, 1788), graph.removeVertexAugmentation(newHampshire)); + assertTrue(graph.removeEdge(newHampshire, vermont)); + assertEquals(new SumAndMax(12, 1787), graph.removeVertexAugmentation(newJersey)); + assertTrue(graph.removeEdge(newJersey, newYork)); + assertTrue(graph.removeEdge(newJersey, pennsylvania)); + assertEquals(new SumAndMax(3, 1912), graph.removeVertexAugmentation(newMexico)); + assertTrue(graph.removeEdge(newMexico, oklahoma)); + assertTrue(graph.removeEdge(newMexico, texas)); + assertTrue(graph.removeEdge(newMexico, utah)); + assertEquals(new SumAndMax(27, 1788), graph.removeVertexAugmentation(newYork)); + assertTrue(graph.removeEdge(newYork, pennsylvania)); + assertTrue(graph.removeEdge(newYork, vermont)); + assertEquals(new SumAndMax(13, 1789), graph.removeVertexAugmentation(northCarolina)); + assertTrue(graph.removeEdge(northCarolina, southCarolina)); + assertTrue(graph.removeEdge(northCarolina, tennessee)); + assertTrue(graph.removeEdge(northCarolina, virginia)); + assertEquals(new SumAndMax(1, 1889), graph.removeVertexAugmentation(northDakota)); + assertTrue(graph.removeEdge(northDakota, southDakota)); + assertEquals(new SumAndMax(16, 1803), graph.removeVertexAugmentation(ohio)); + assertTrue(graph.removeEdge(ohio, pennsylvania)); + assertTrue(graph.removeEdge(ohio, westVirginia)); + assertEquals(new SumAndMax(5, 1907), graph.removeVertexAugmentation(oklahoma)); + assertTrue(graph.removeEdge(oklahoma, texas)); + assertEquals(new SumAndMax(5, 1859), graph.removeVertexAugmentation(oregon)); + assertTrue(graph.removeEdge(oregon, washington)); + assertEquals(new SumAndMax(18, 1787), graph.removeVertexAugmentation(pennsylvania)); + assertTrue(graph.removeEdge(pennsylvania, westVirginia)); + assertEquals(new SumAndMax(2, 1790), graph.removeVertexAugmentation(rhodeIsland)); + assertEquals(new SumAndMax(7, 1788), graph.removeVertexAugmentation(southCarolina)); + assertEquals(new SumAndMax(1, 1889), graph.removeVertexAugmentation(southDakota)); + assertTrue(graph.removeEdge(southDakota, wyoming)); + assertEquals(new SumAndMax(9, 1796), graph.removeVertexAugmentation(tennessee)); + assertTrue(graph.removeEdge(tennessee, virginia)); + assertEquals(new SumAndMax(36, 1845), graph.removeVertexAugmentation(texas)); + assertEquals(new SumAndMax(4, 1896), graph.removeVertexAugmentation(utah)); + assertTrue(graph.removeEdge(utah, wyoming)); + assertEquals(new SumAndMax(1, 1791), graph.removeVertexAugmentation(vermont)); + assertEquals(new SumAndMax(11, 1788), graph.removeVertexAugmentation(virginia)); + assertTrue(graph.removeEdge(virginia, westVirginia)); + assertEquals(new SumAndMax(10, 1889), graph.removeVertexAugmentation(washington)); + assertEquals(new SumAndMax(3, 1863), graph.removeVertexAugmentation(westVirginia)); + assertEquals(new SumAndMax(8, 1848), graph.removeVertexAugmentation(wisconsin)); + assertEquals(new SumAndMax(1, 1890), graph.removeVertexAugmentation(wyoming)); + + assertFalse(graph.connected(georgia, newMexico)); + assertFalse(graph.connected(wisconsin, michigan)); + assertFalse(graph.connected(ohio, kentucky)); + assertFalse(graph.connected(alaska, connecticut)); + assertNull(graph.getComponentAugmentation(southDakota)); + assertNull(graph.getComponentAugmentation(arkansas)); + } + + /** + * Tests ConnectivityGraph on the graph for a dodecahedron. + */ + @Test + public void testDodecahedron() { + ConnGraph graph = new ConnGraph(); + Random random = new Random(6170); + ConnVertex vertex1 = new ConnVertex(random); + ConnVertex vertex2 = new ConnVertex(random); + ConnVertex vertex3 = new ConnVertex(random); + ConnVertex vertex4 = new ConnVertex(random); + ConnVertex vertex5 = new ConnVertex(random); + ConnVertex vertex6 = new ConnVertex(random); + ConnVertex vertex7 = new ConnVertex(random); + ConnVertex vertex8 = new ConnVertex(random); + ConnVertex vertex9 = new ConnVertex(random); + ConnVertex vertex10 = new ConnVertex(random); + ConnVertex vertex11 = new ConnVertex(random); + ConnVertex vertex12 = new ConnVertex(random); + ConnVertex vertex13 = new ConnVertex(random); + ConnVertex vertex14 = new ConnVertex(random); + ConnVertex vertex15 = new ConnVertex(random); + ConnVertex vertex16 = new ConnVertex(random); + ConnVertex vertex17 = new ConnVertex(random); + ConnVertex vertex18 = new ConnVertex(random); + ConnVertex vertex19 = new ConnVertex(random); + ConnVertex vertex20 = new ConnVertex(random); + + assertTrue(graph.addEdge(vertex1, vertex2)); + assertTrue(graph.addEdge(vertex1, vertex5)); + assertTrue(graph.addEdge(vertex1, vertex6)); + assertTrue(graph.addEdge(vertex2, vertex3)); + assertTrue(graph.addEdge(vertex2, vertex8)); + assertTrue(graph.addEdge(vertex3, vertex4)); + assertTrue(graph.addEdge(vertex3, vertex10)); + assertTrue(graph.addEdge(vertex4, vertex5)); + assertTrue(graph.addEdge(vertex4, vertex12)); + assertTrue(graph.addEdge(vertex5, vertex14)); + assertTrue(graph.addEdge(vertex6, vertex7)); + assertTrue(graph.addEdge(vertex6, vertex15)); + assertTrue(graph.addEdge(vertex7, vertex8)); + assertTrue(graph.addEdge(vertex7, vertex16)); + assertTrue(graph.addEdge(vertex8, vertex9)); + assertTrue(graph.addEdge(vertex9, vertex10)); + assertTrue(graph.addEdge(vertex9, vertex17)); + assertTrue(graph.addEdge(vertex10, vertex11)); + assertTrue(graph.addEdge(vertex11, vertex12)); + assertTrue(graph.addEdge(vertex11, vertex18)); + assertTrue(graph.addEdge(vertex12, vertex13)); + assertTrue(graph.addEdge(vertex13, vertex14)); + assertTrue(graph.addEdge(vertex13, vertex19)); + assertTrue(graph.addEdge(vertex14, vertex15)); + assertTrue(graph.addEdge(vertex15, vertex20)); + assertTrue(graph.addEdge(vertex16, vertex17)); + assertTrue(graph.addEdge(vertex16, vertex20)); + assertTrue(graph.addEdge(vertex17, vertex18)); + assertTrue(graph.addEdge(vertex18, vertex19)); + assertTrue(graph.addEdge(vertex19, vertex20)); + graph.optimize(); + + assertTrue(graph.connected(vertex1, vertex17)); + assertTrue(graph.connected(vertex7, vertex15)); + + assertTrue(graph.removeEdge(vertex5, vertex14)); + assertTrue(graph.removeEdge(vertex6, vertex15)); + assertTrue(graph.removeEdge(vertex7, vertex16)); + assertTrue(graph.removeEdge(vertex12, vertex13)); + assertTrue(graph.removeEdge(vertex16, vertex17)); + assertTrue(graph.connected(vertex1, vertex14)); + assertTrue(graph.connected(vertex4, vertex20)); + assertTrue(graph.connected(vertex14, vertex16)); + + assertTrue(graph.removeEdge(vertex18, vertex19)); + assertFalse(graph.connected(vertex1, vertex14)); + assertFalse(graph.connected(vertex4, vertex20)); + assertTrue(graph.connected(vertex14, vertex16)); + + graph.clear(); + graph.optimize(); + assertTrue(graph.connected(vertex7, vertex7)); + assertFalse(graph.connected(vertex1, vertex2)); + } + + /** + * Tests the zero-argument ConnVertex constructor. + */ + @Test + public void testDefaultConnVertexConstructor() { + ConnGraph graph = new ConnGraph(); + ConnVertex vertex1 = new ConnVertex(); + ConnVertex vertex2 = new ConnVertex(); + ConnVertex vertex3 = new ConnVertex(); + ConnVertex vertex4 = new ConnVertex(); + ConnVertex vertex5 = new ConnVertex(); + ConnVertex vertex6 = new ConnVertex(); + assertTrue(graph.addEdge(vertex1, vertex2)); + assertTrue(graph.addEdge(vertex2, vertex3)); + assertTrue(graph.addEdge(vertex1, vertex3)); + assertTrue(graph.addEdge(vertex4, vertex5)); + assertTrue(graph.connected(vertex1, vertex3)); + assertTrue(graph.connected(vertex4, vertex5)); + assertFalse(graph.connected(vertex1, vertex4)); + + graph.optimize(); + assertTrue(graph.removeEdge(vertex1, vertex3)); + assertTrue(graph.connected(vertex1, vertex3)); + assertTrue(graph.connected(vertex4, vertex5)); + assertFalse(graph.connected(vertex1, vertex4)); + assertTrue(graph.removeEdge(vertex1, vertex2)); + assertFalse(graph.connected(vertex1, vertex3)); + assertTrue(graph.connected(vertex4, vertex5)); + assertFalse(graph.connected(vertex1, vertex4)); + + //assertEquals(Collections.singleton(vertex3), new HashSet(graph.adjacentVertices(vertex2))); + //assertTrue(graph.adjacentVertices(vertex1).isEmpty()); + //assertTrue(graph.adjacentVertices(vertex6).isEmpty()); + } +} diff --git a/src/test/java/com/github/btrekkie/connectivity/test/SumAndMax.java b/src/test/java/com/github/btrekkie/connectivity/test/SumAndMax.java new file mode 100644 index 000000000..1b2a89b76 --- /dev/null +++ b/src/test/java/com/github/btrekkie/connectivity/test/SumAndMax.java @@ -0,0 +1,43 @@ +package com.github.btrekkie.connectivity.test; + +import com.github.btrekkie.connectivity.Augmentation; + +/** + * Stores two values: a sum and a maximum. Used for testing augmentation in ConnGraph. + */ +class SumAndMax { + /** + * An Augmentation that combines two SumAndMaxes into one. + */ + public static final Augmentation AUGMENTATION = new Augmentation() { + @Override + public Object combine(Object value1, Object value2) { + SumAndMax sumAndMax1 = (SumAndMax) value1; + SumAndMax sumAndMax2 = (SumAndMax) value2; + return new SumAndMax(sumAndMax1.sum + sumAndMax2.sum, Math.max(sumAndMax1.max, sumAndMax2.max)); + } + }; + + public final int sum; + + public final int max; + + public SumAndMax(int sum, int max) { + this.sum = sum; + this.max = max; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SumAndMax)) { + return false; + } + SumAndMax sumAndMax = (SumAndMax) obj; + return sum == sumAndMax.sum && max == sumAndMax.max; + } + + @Override + public int hashCode() { + return 31 * sum + max; + } +} diff --git a/src/test/java/com/github/btrekkie/interval_tree/IntervalTree.java b/src/test/java/com/github/btrekkie/interval_tree/IntervalTree.java new file mode 100644 index 000000000..aa0204517 --- /dev/null +++ b/src/test/java/com/github/btrekkie/interval_tree/IntervalTree.java @@ -0,0 +1,65 @@ +package com.github.btrekkie.interval_tree; + +/** + * An interval tree data structure, which supports adding or removing an interval and finding an arbitrary interval that + * contains a specified value. + */ +/* The interval tree is ordered in ascending order of the start an interval, with ties broken by the end of the + * interval. Each node is augmented with the maximum ending value of an interval in the subtree rooted at the node. + */ +public class IntervalTree { + /** + * The root node of the tree. + */ + private IntervalTreeNode root = IntervalTreeNode.LEAF; + + /** + * Adds the specified interval to this. + */ + public void addInterval(IntervalTreeInterval interval) { + root = root.insert(new IntervalTreeNode(interval), true, null); + } + + /** + * Removes the specified interval from this, if it is present. + * + * @param interval The interval. + * @return Whether the interval was present. + */ + public boolean removeInterval(IntervalTreeInterval interval) { + IntervalTreeNode node = root; + while (!node.isLeaf()) { + if (interval.start < node.interval.start) { + node = node.left; + } else if (interval.start > node.interval.start) { + node = node.right; + } else if (interval.end < node.interval.end) { + node = node.left; + } else if (interval.end > node.interval.end) { + node = node.right; + } else { + root = node.remove(); + return true; + } + } + return false; + } + + /** + * Returns an aribtrary IntervalTreeInterval in this that contains the specified value. Returns null if there is no + * such interval. + */ + public IntervalTreeInterval findInterval(double value) { + IntervalTreeNode node = root; + while (!node.isLeaf()) { + if (value >= node.interval.start && value <= node.interval.end) { + return node.interval; + } else if (value <= node.left.maxEnd) { + node = node.left; + } else { + node = node.right; + } + } + return null; + } +} diff --git a/src/test/java/com/github/btrekkie/interval_tree/IntervalTreeInterval.java b/src/test/java/com/github/btrekkie/interval_tree/IntervalTreeInterval.java new file mode 100644 index 000000000..182eb37c6 --- /dev/null +++ b/src/test/java/com/github/btrekkie/interval_tree/IntervalTreeInterval.java @@ -0,0 +1,32 @@ +package com.github.btrekkie.interval_tree; + +/** + * An inclusive range of values [start, end]. Two intervals are equal if they have the same starting and ending values. + */ +public class IntervalTreeInterval { + /** + * The smallest value in the range. + */ + public final double start; + + /** + * The largest value in the range. + */ + public final double end; + + public IntervalTreeInterval(double start, double end) { + if (start > end) { + throw new IllegalArgumentException("The end of the range must be at most the start"); + } + this.start = start; + this.end = end; + } + + public boolean equals(Object obj) { + if (!(obj instanceof IntervalTreeInterval)) { + return false; + } + IntervalTreeInterval interval = (IntervalTreeInterval) obj; + return start == interval.start && end == interval.end; + } +} diff --git a/src/test/java/com/github/btrekkie/interval_tree/IntervalTreeNode.java b/src/test/java/com/github/btrekkie/interval_tree/IntervalTreeNode.java new file mode 100644 index 000000000..7b6bffaa7 --- /dev/null +++ b/src/test/java/com/github/btrekkie/interval_tree/IntervalTreeNode.java @@ -0,0 +1,76 @@ +package com.github.btrekkie.interval_tree; + +import com.github.btrekkie.red_black_node.RedBlackNode; + +/** + * A node in an IntervalTree. See the comments for the implementation of IntervalTree. Its compareTo method orders + * nodes as suggested in the comments for the implementation of IntervalTree. + */ +class IntervalTreeNode extends RedBlackNode { + /** + * The dummy leaf node. + */ + public static final IntervalTreeNode LEAF = new IntervalTreeNode(); + + /** + * The interval stored in this node. + */ + public IntervalTreeInterval interval; + + /** + * The maximum ending value of an interval in the subtree rooted at this node. + */ + public double maxEnd; + + public IntervalTreeNode(IntervalTreeInterval interval) { + this.interval = interval; + maxEnd = interval.end; + } + + /** + * Constructs a new dummy leaf node. + */ + private IntervalTreeNode() { + interval = null; + maxEnd = Double.NEGATIVE_INFINITY; + } + + @Override + public boolean augment() { + double newMaxEnd = Math.max(interval.end, Math.max(left.maxEnd, right.maxEnd)); + if (newMaxEnd == maxEnd) { + return false; + } else { + maxEnd = newMaxEnd; + return true; + } + } + + @Override + public void assertNodeIsValid() { + double expectedMaxEnd; + if (isLeaf()) { + expectedMaxEnd = Double.NEGATIVE_INFINITY; + } else { + expectedMaxEnd = Math.max(interval.end, Math.max(left.maxEnd, right.maxEnd)); + } + if (maxEnd != expectedMaxEnd) { + throw new RuntimeException("The node's maxEnd does not match that of the children"); + } + } + + @Override + public int compareTo(IntervalTreeNode other) { + if (interval.start != interval.end) { + return Double.compare(interval.start, other.interval.start); + } else { + return Double.compare(interval.end, other.interval.end); + } + } + + @Override + public void assertSubtreeIsValid() { + super.assertSubtreeIsValid(); + assertOrderIsValid(null); + } +} diff --git a/src/test/java/com/github/btrekkie/interval_tree/test/IntervalTreeTest.java b/src/test/java/com/github/btrekkie/interval_tree/test/IntervalTreeTest.java new file mode 100644 index 000000000..5af2bd6d2 --- /dev/null +++ b/src/test/java/com/github/btrekkie/interval_tree/test/IntervalTreeTest.java @@ -0,0 +1,46 @@ +package com.github.btrekkie.interval_tree.test; + +import com.github.btrekkie.interval_tree.IntervalTree; +import com.github.btrekkie.interval_tree.IntervalTreeInterval; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class IntervalTreeTest { + /** + * Tests IntervalTree. + */ + @Test + public void test() { + IntervalTree tree = new IntervalTree(); + assertNull(tree.findInterval(0.5)); + assertNull(tree.findInterval(-1)); + tree.addInterval(new IntervalTreeInterval(5, 7)); + tree.addInterval(new IntervalTreeInterval(42, 48)); + tree.addInterval(new IntervalTreeInterval(-1, 2)); + tree.addInterval(new IntervalTreeInterval(6, 12)); + tree.addInterval(new IntervalTreeInterval(21, 23)); + assertTrue(tree.removeInterval(new IntervalTreeInterval(-1, 2))); + assertFalse(tree.removeInterval(new IntervalTreeInterval(-1, 2))); + tree.addInterval(new IntervalTreeInterval(-6, -2)); + assertEquals(new IntervalTreeInterval(6, 12), tree.findInterval(8)); + assertNull(tree.findInterval(0)); + assertEquals(new IntervalTreeInterval(21, 23), tree.findInterval(21)); + assertEquals(new IntervalTreeInterval(42, 48), tree.findInterval(48)); + IntervalTreeInterval interval = tree.findInterval(6.5); + assertTrue(new IntervalTreeInterval(5, 7).equals(interval) || new IntervalTreeInterval(6, 12).equals(interval)); + + tree = new IntervalTree(); + for (int i = 0; i < 500; i++) { + tree.addInterval(new IntervalTreeInterval(2 * i, 2 * i + 1)); + } + for (int i = 0; i < 250; i++) { + tree.removeInterval(new IntervalTreeInterval(4 * i + 2, 4 * i + 3)); + } + assertNull(tree.findInterval(123.5)); + assertEquals(new IntervalTreeInterval(124, 125), tree.findInterval(124.5)); + assertEquals(new IntervalTreeInterval(776, 777), tree.findInterval(776)); + assertEquals(new IntervalTreeInterval(0, 1), tree.findInterval(0.5)); + assertEquals(new IntervalTreeInterval(996, 997), tree.findInterval(997)); + } +} diff --git a/src/test/java/com/github/btrekkie/red_black_node/test/RedBlackNodeTest.java b/src/test/java/com/github/btrekkie/red_black_node/test/RedBlackNodeTest.java new file mode 100644 index 000000000..b487e54fe --- /dev/null +++ b/src/test/java/com/github/btrekkie/red_black_node/test/RedBlackNodeTest.java @@ -0,0 +1,174 @@ +package com.github.btrekkie.red_black_node.test; + +import org.junit.Test; + +import java.util.Comparator; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Tests RedBlackNode. Most of the testing for RedBlackNode takes place in TreeListTest, IntervalTreeTest, + * SubArrayMinTest, and ArbitraryOrderCollectionTest, which test realistic use cases of RedBlackNode. TreeListTest + * tests most of the RedBlackNode methods, while IntervalTreeTest tests the "insert" method, SubArrayMinTest tests + * "lca", ArbitraryOrderCollectionTest tests compareTo, and RedBlackNodeTest tests assertSubtreeIsValid() and + * assertOrderIsValid. + */ +public class RedBlackNodeTest { + /** + * Returns whether the subtree rooted at the specified node is valid, as in TestRedBlackNode.assertSubtreeIsValid(). + */ + private boolean isSubtreeValid(TestRedBlackNode node) { + try { + node.assertSubtreeIsValid(); + return true; + } catch (RuntimeException exception) { + return false; + } + } + + /** + * Returns whether the nodes in the subtree rooted at the specified node are ordered correctly, as in + * TestRedBlackNode.assertOrderIsValid. + * + * @param comparator A comparator indicating how the nodes should be ordered. If this is null, we use the nodes' + * natural ordering, as in TestRedBlackNode.compare. + */ + private boolean isOrderValid(TestRedBlackNode node, Comparator comparator) { + try { + node.assertOrderIsValid(null); + return true; + } catch (RuntimeException exception) { + return false; + } + } + + /** + * Tests RedBlackNode.assertSubtreeIsValid() and RedBlackNode.assertOrderIsValid. + */ + @Test + public void testAssertIsValid() { + // Create a perfectly balanced tree of height 3 + TestRedBlackNode node0 = new TestRedBlackNode(0); + TestRedBlackNode node1 = new TestRedBlackNode(1); + TestRedBlackNode node2 = new TestRedBlackNode(2); + TestRedBlackNode node3 = new TestRedBlackNode(3); + TestRedBlackNode node4 = new TestRedBlackNode(4); + TestRedBlackNode node5 = new TestRedBlackNode(5); + TestRedBlackNode node6 = new TestRedBlackNode(6); + node0.parent = node1; + node0.left = TestRedBlackNode.LEAF; + node0.right = TestRedBlackNode.LEAF; + node1.parent = node3; + node1.left = node0; + node1.right = node2; + node1.isRed = true; + node2.parent = node1; + node2.left = TestRedBlackNode.LEAF; + node2.right = TestRedBlackNode.LEAF; + node3.left = node1; + node3.right = node5; + node4.parent = node5; + node4.left = TestRedBlackNode.LEAF; + node4.right = TestRedBlackNode.LEAF; + node5.parent = node3; + node5.left = node4; + node5.right = node6; + node5.isRed = true; + node6.parent = node5; + node6.left = TestRedBlackNode.LEAF; + node6.right = TestRedBlackNode.LEAF; + + node3.left = node3; + node3.right = node3; + node3.parent = node3; + assertFalse(isSubtreeValid(node3)); + node3.left = node1; + node3.right = node5; + node3.parent = null; + + node0.parent = node3; + assertFalse(isSubtreeValid(node3)); + node0.parent = node1; + + node1.right = node0; + assertFalse(isSubtreeValid(node3)); + node1.right = node2; + + node5.isRed = false; + assertFalse(isSubtreeValid(node3)); + assertTrue(isSubtreeValid(node5)); + node5.isRed = true; + + node3.isRed = true; + assertFalse(isSubtreeValid(node3)); + assertTrue(isSubtreeValid(node5)); + node3.isRed = false; + + node0.isRed = true; + node2.isRed = true; + node4.isRed = true; + node6.isRed = true; + assertFalse(isSubtreeValid(node3)); + node0.isRed = false; + node2.isRed = false; + node4.isRed = false; + node6.isRed = false; + + TestRedBlackNode.LEAF.isRed = true; + assertFalse(isSubtreeValid(node3)); + TestRedBlackNode.LEAF.isRed = false; + + TestRedBlackNode.LEAF.isValid = false; + assertFalse(isSubtreeValid(node3)); + assertFalse(isSubtreeValid(TestRedBlackNode.LEAF)); + TestRedBlackNode.LEAF.isValid = true; + + node1.isValid = false; + assertFalse(isSubtreeValid(node3)); + node1.isValid = true; + + node3.value = 2; + node2.value = 3; + assertFalse(isOrderValid(node3, null)); + assertFalse( + isOrderValid(node3, new Comparator() { + @Override + public int compare(TestRedBlackNode node1, TestRedBlackNode node2) { + return node1.value - node2.value; + } + })); + node3.value = 3; + node2.value = 2; + + node2.value = 4; + node4.value = 2; + assertFalse(isOrderValid(node3, null)); + node2.value = 2; + node4.value = 4; + + node0.value = 1; + node1.value = 0; + assertFalse(isOrderValid(node3, null)); + node0.value = 0; + node1.value = 1; + + // Do all of the assertions for which the tree is supposed to be valid at the end, to make sure we didn't make a + // mistake undoing any of the modifications + assertTrue(isSubtreeValid(node3)); + assertTrue(isSubtreeValid(node1)); + assertTrue(isSubtreeValid(node0)); + assertTrue(isSubtreeValid(TestRedBlackNode.LEAF)); + assertTrue(isOrderValid(node3, null)); + assertTrue(isOrderValid(node1, null)); + assertTrue(isOrderValid(node0, null)); + assertTrue(isOrderValid(TestRedBlackNode.LEAF, null)); + assertTrue( + isOrderValid(node3, new Comparator() { + @Override + public int compare(TestRedBlackNode node1, TestRedBlackNode node2) { + return node1.value - node2.value; + } + })); + } +} diff --git a/src/test/java/com/github/btrekkie/red_black_node/test/TestRedBlackNode.java b/src/test/java/com/github/btrekkie/red_black_node/test/TestRedBlackNode.java new file mode 100644 index 000000000..86ab1cb3a --- /dev/null +++ b/src/test/java/com/github/btrekkie/red_black_node/test/TestRedBlackNode.java @@ -0,0 +1,46 @@ +package com.github.btrekkie.red_black_node.test; + +import com.github.btrekkie.red_black_node.RedBlackNode; + +/** + * A RedBlackNode for RedBlackNodeTest. + */ +class TestRedBlackNode extends RedBlackNode { + /** + * The dummy leaf node. + */ + public static final TestRedBlackNode LEAF = new TestRedBlackNode(); + + /** + * The value stored in this node. "value" is unspecified if this is a leaf node. + */ + public int value; + + /** + * Whether this node is considered valid, as in assertNodeIsValid(). + */ + public boolean isValid = true; + + public TestRedBlackNode(int value) { + this.value = value; + } + + /** + * Constructs a new dummy leaf node. + */ + private TestRedBlackNode() { + + } + + @Override + public void assertNodeIsValid() { + if (!isValid) { + throw new RuntimeException("isValid is false"); + } + } + + @Override + public int compareTo(TestRedBlackNode other) { + return value - other.value; + } +} diff --git a/src/test/java/com/github/btrekkie/sub_array_min/SubArrayMin.java b/src/test/java/com/github/btrekkie/sub_array_min/SubArrayMin.java new file mode 100644 index 000000000..7c8e1c4ba --- /dev/null +++ b/src/test/java/com/github/btrekkie/sub_array_min/SubArrayMin.java @@ -0,0 +1,99 @@ +package com.github.btrekkie.sub_array_min; + +/** + * A list of integers. SubArrayMin provides the ability to quickly determine the minimum value in a given sublist. + */ +/* We implement SubArrayMin using a red-black tree augmented by subtree size and minimum value. Using the subtree size + * augmentation, we can find the node at a given index. + */ +public class SubArrayMin { + /** + * The root node. + */ + private SubArrayMinNode root = SubArrayMinNode.LEAF; + + /** + * Appends the specified value to the end of the list. + */ + public void add(int value) { + SubArrayMinNode newNode = new SubArrayMinNode(value); + newNode.left = SubArrayMinNode.LEAF; + newNode.right = SubArrayMinNode.LEAF; + if (root.isLeaf()) { + root = newNode; + newNode.augment(); + } else { + SubArrayMinNode node = root.max(); + node.right = newNode; + newNode.parent = node; + newNode.isRed = true; + root = newNode.fixInsertion(); + } + } + + /** + * Returns the node for the element with the specified index. Assumes "index" is in the range [0, root.size). + */ + private SubArrayMinNode getNode(int index) { + if (index < 0 || index >= root.size) { + throw new IndexOutOfBoundsException("Index " + index + " is not in the range [0, " + root.size + ")"); + } + int rank = index; + SubArrayMinNode node = root; + while (rank != node.left.size) { + if (rank < node.left.size) { + node = node.left; + } else { + rank -= node.left.size + 1; + node = node.right; + } + } + return node; + } + + /** + * Returns the minimum value in the subarray starting at index startIndex and ending at index endIndex - 1, + * inclusive. Assumes startIndex < endIndex, and assumes this contains indices startIndex and endIndex - 1. + */ + public int min(int startIndex, int endIndex) { + if (startIndex >= endIndex) { + throw new IllegalArgumentException("The start index must be less than the end index"); + } + SubArrayMinNode start = getNode(startIndex); + SubArrayMinNode end = getNode(endIndex - 1); + SubArrayMinNode lca = start.lca(end); + + int min = Math.min(lca.value, Math.min(start.value, end.value)); + if (start != lca) { + if (start.right.min < min) { + min = start.right.min; + } + for (SubArrayMinNode node = start; node.parent != lca; node = node.parent) { + if (node.parent.left == node) { + if (node.parent.value < min) { + min = node.parent.value; + } + if (node.parent.right.min < min) { + min = node.parent.right.min; + } + } + } + } + if (end != lca) { + if (end.left.min < min) { + min = end.left.min; + } + for (SubArrayMinNode node = end; node.parent != lca; node = node.parent) { + if (node.parent.right == node) { + if (node.parent.value < min) { + min = node.parent.value; + } + if (node.parent.left.min < min) { + min = node.parent.left.min; + } + } + } + } + return min; + } +} diff --git a/src/test/java/com/github/btrekkie/sub_array_min/SubArrayMinNode.java b/src/test/java/com/github/btrekkie/sub_array_min/SubArrayMinNode.java new file mode 100644 index 000000000..e76860f09 --- /dev/null +++ b/src/test/java/com/github/btrekkie/sub_array_min/SubArrayMinNode.java @@ -0,0 +1,66 @@ +package com.github.btrekkie.sub_array_min; + +import com.github.btrekkie.red_black_node.RedBlackNode; + +/** + * A node in a SubArrayMin object. See the comments for the implementation of that class. + */ +class SubArrayMinNode extends RedBlackNode { + /** + * The dummy leaf node. + */ + public static final SubArrayMinNode LEAF = new SubArrayMinNode(); + + /** + * The element stored in the node. The value is unspecified if this is a leaf node. + */ + public final int value; + + /** + * The number of elements in the subtree rooted at this node. + */ + public int size; + + /** + * The minimum element in the subtree rooted at this node. This is Integer.MAX_VALUE if this is a leaf node. + */ + public int min; + + public SubArrayMinNode(int value) { + this.value = value; + } + + private SubArrayMinNode() { + value = 0; + min = Integer.MAX_VALUE; + } + + @Override + public boolean augment() { + int newSize = left.size + right.size + 1; + int newMin = Math.min(value, Math.min(left.min, right.min)); + if (newSize == size && newMin == min) { + return false; + } else { + size = newSize; + min = newMin; + return true; + } + } + + @Override + public void assertNodeIsValid() { + int expectedSize; + int expectedMin; + if (isLeaf()) { + expectedSize = 0; + expectedMin = Integer.MAX_VALUE; + } else { + expectedSize = left.size + right.size + 1; + expectedMin = Math.min(value, Math.min(left.min, right.min)); + } + if (size != expectedSize || min != expectedMin) { + throw new RuntimeException("The node's size or minimum value does not match that of the children"); + } + } +} diff --git a/src/test/java/com/github/btrekkie/sub_array_min/test/SubArrayMinTest.java b/src/test/java/com/github/btrekkie/sub_array_min/test/SubArrayMinTest.java new file mode 100644 index 000000000..95266b387 --- /dev/null +++ b/src/test/java/com/github/btrekkie/sub_array_min/test/SubArrayMinTest.java @@ -0,0 +1,41 @@ +package com.github.btrekkie.sub_array_min.test; + +import com.github.btrekkie.sub_array_min.SubArrayMin; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class SubArrayMinTest { + /** + * Tests SubArrayMin. + */ + @Test + public void test() { + SubArrayMin sam = new SubArrayMin(); + sam.add(12); + sam.add(42); + sam.add(-3); + sam.add(16); + sam.add(5); + sam.add(8); + sam.add(4); + assertEquals(-3, sam.min(0, 7)); + assertEquals(12, sam.min(0, 2)); + assertEquals(-3, sam.min(2, 4)); + assertEquals(12, sam.min(0, 1)); + assertEquals(5, sam.min(3, 6)); + assertEquals(4, sam.min(4, 7)); + + sam = new SubArrayMin(); + for (int i = 0; i < 1000; i++) { + sam.add(-Integer.bitCount(i)); + } + assertEquals(0, sam.min(0, 1)); + assertEquals(-4, sam.min(0, 30)); + assertEquals(-9, sam.min(0, 1000)); + assertEquals(-9, sam.min(123, 777)); + assertEquals(-8, sam.min(777, 888)); + assertEquals(-6, sam.min(777, 788)); + assertEquals(-9, sam.min(900, 1000)); + } +} diff --git a/src/test/java/com/github/leijurv/NavigableSurfaceTest.java b/src/test/java/com/github/leijurv/NavigableSurfaceTest.java new file mode 100644 index 000000000..d3d5e4980 --- /dev/null +++ b/src/test/java/com/github/leijurv/NavigableSurfaceTest.java @@ -0,0 +1,276 @@ +package com.github.leijurv; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; + +import static org.junit.Assert.assertEquals; + + +public class NavigableSurfaceTest { + @Test + public void testBasic() { + NavigableSurface surface = new NavigableSurface(10, 10, 10); + surface.placeBlock(0, 0, 0); + assertEquals(OptionalInt.empty(), surface.surfaceSize(new BetterBlockPos(0, 0, 0))); + assertEquals(1, surface.requireSurfaceSize(0, 1, 0)); + surface.placeBlock(1, 0, 0); + assertEquals(2, surface.requireSurfaceSize(0, 1, 0)); + surface.placeBlock(1, 0, 0); + surface.placeBlock(2, 0, 0); + surface.placeBlock(3, 0, 0); + surface.placeBlock(4, 0, 0); + surface.placeBlock(5, 0, 0); + // XXXXXX + assertEquals(6, surface.requireSurfaceSize(2, 1, 0)); + + surface.placeBlock(2, 1, 0); + assertEquals(OptionalInt.empty(), surface.surfaceSize(new BetterBlockPos(2, 1, 0))); + assertEquals(6, surface.requireSurfaceSize(2, 2, 0)); + + surface.placeBlock(2, 2, 0); + // X + // X + // XXXXXX + assertEquals(2, surface.requireSurfaceSize(0, 1, 0)); + assertEquals(1, surface.requireSurfaceSize(2, 3, 0)); + assertEquals(3, surface.requireSurfaceSize(3, 1, 0)); + + surface.placeBlock(1, 1, 0); + // X + // XX + // XXXXXX + assertEquals(3, surface.requireSurfaceSize(0, 1, 0)); + assertEquals(3, surface.requireSurfaceSize(3, 1, 0)); + + surface.placeBlock(3, 2, 0); + // XX + // XX + // XXXXXX + assertEquals(4, surface.requireSurfaceSize(0, 1, 0)); + assertEquals(2, surface.requireSurfaceSize(4, 1, 0)); + + surface.placeBlock(4, 1, 0); + // XX + // XX X + // XXXXXX + assertEquals(6, surface.requireSurfaceSize(0, 1, 0)); + assertEquals(OptionalInt.empty(), surface.surfaceSize(new BetterBlockPos(3, 1, 0))); + + surface.removeBlock(2, 2, 0); + // X + // XX X + // XXXXXX + assertEquals(6, surface.requireSurfaceSize(2, 2, 0)); + + surface.removeBlock(2, 1, 0); + // X + // X X + // XXXXXX + assertEquals(3, surface.requireSurfaceSize(1, 2, 0)); + assertEquals(3, surface.requireSurfaceSize(3, 3, 0)); + + surface.removeBlock(3, 2, 0); + // X X + // XXXXXX + assertEquals(6, surface.requireSurfaceSize(0, 1, 0)); + + surface.removeBlock(1, 0, 0); + // X X + // X XXXX + assertEquals(6, surface.requireSurfaceSize(0, 1, 0)); + + surface.removeBlock(1, 1, 0); + // X + // X XXXX + assertEquals(1, surface.requireSurfaceSize(0, 1, 0)); + assertEquals(4, surface.requireSurfaceSize(2, 1, 0)); + } + + private NavigableSurface makeFlatSurface(int SZ) { + NavigableSurface surface = new NavigableSurface(SZ, SZ, SZ); + for (int x = 0; x < SZ; x++) { + for (int z = 0; z < SZ; z++) { + surface.placeBlock(new BetterBlockPos(x, 0, z)); + } + } + assertEquals(SZ * SZ, surface.requireSurfaceSize(0, 1, 0)); + return surface; + } + + @Test + public void testSurfaceSmall() { + makeFlatSurface(10); + } + + @Test + public void testSurfaceMed() { + makeFlatSurface(100); + } + + /*@Test + public void testSurfaceBig() { // 10x more on each side, so 100x more nodes total. youd expect this to be about 100x slower than testSurfaceMed, but it's actually 200x slower. this is ideally just because each graph operation is O(log^2 n) so n operations is O(n log^2 n), and hopefully not because of something that otherwise scales superlinearly + makeFlatSurface(1000); + }*/ + // okay but its slow so we dont care + + @Test + public void testStep() { + int SZ = 100; + int lineAt = SZ / 2; + NavigableSurface surface = makeFlatSurface(SZ); + for (int x = 0; x < SZ; x++) { + surface.placeBlock(x, 1, lineAt); // doesn't block the player since you can step over 1 block + } + assertEquals(SZ * SZ, surface.requireSurfaceSize(0, 1, 0)); + } + + @Test + public void testBlocked() { + int SZ = 100; + int lineAt = SZ / 2; + NavigableSurface surface = makeFlatSurface(SZ); + for (int x = 0; x < SZ; x++) { + surface.placeBlock(x, 2, lineAt); // does block the player since you can't step over 2 blocks + } + assertEquals(SZ * lineAt, surface.requireSurfaceSize(0, 1, 0)); + assertEquals(SZ * (SZ - lineAt - 1), surface.requireSurfaceSize(0, 1, SZ - 1)); + assertEquals(SZ, surface.requireSurfaceSize(0, 3, lineAt)); + } + + private void fillSurfaceInOrderMaintainingConnection(NavigableSurface surface, BetterBlockPos maintainConnectionTo, List iterationOrder) { + outer: + while (true) { + for (BetterBlockPos candidate : iterationOrder) { + if (surface.getBlock(candidate)) { + continue; // already placed + } + // let's try placing + surface.placeBlock(candidate); + if (surface.connected(candidate.upPlusY(), maintainConnectionTo)) { + // success, placed a block while retaining the path down to the ground + continue outer; + } + // fail :( + surface.removeBlock(candidate); + } + return; + } + } + + @Test + public void testCastleWall() { + // build a single wall, but, never place a block that disconnects the surface + // (we expect to see a triangle) + int SZ = 20; + NavigableSurface surface = makeFlatSurface(SZ); + BetterBlockPos someOtherBlock = new BetterBlockPos(0, 1, 1); // won't be involved in the wall (since z=1) + List order = new ArrayList<>(); + for (int y = 0; y < SZ; y++) { + for (int x = 0; x < SZ; x++) { + order.add(new BetterBlockPos(x, y, 0)); + } + } + + fillSurfaceInOrderMaintainingConnection(surface, someOtherBlock, order); + + String shouldBe = "" + + "XX | | | \n" + + "XXX | | | \n" + + "XXXX | | | \n" + + "XXXXX | | | \n" + + "XXXXXX | | | \n" + + "XXXXXXX | | | \n" + + "XXXXXXXX | | | \n" + + "XXXXXXXXX | | | \n" + + "XXXXXXXXXX | | | \n" + + "XXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXXXXXXXXX | | | \n" + + "XXXXXXXXXXXXXXXXXXXX| | | \n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n"; // double row is because we started with a flat surface, so there is another flat row behind this one to step back into + assertEquals(shouldBe, reportAllFourWalls(surface)); + } + + @Test + public void testCastleFourWalls() { + // build four walls, but, never place a block that disconnects the surface + // (we expect to see a zigzag cut out) + int SZ = 20; + NavigableSurface surface = makeFlatSurface(SZ); + BetterBlockPos someOtherBlock = new BetterBlockPos(SZ / 2, 1, SZ / 2); // center of the courtyard + List order = new ArrayList<>(); + for (int y = 0; y < SZ; y++) { + for (int x = 0; x < SZ; x++) { + for (int z = 0; z < SZ; z++) { + boolean xOnEdge = x == 0 || x == SZ - 1; + boolean zOnEdge = z == 0 || z == SZ - 1; + if (!xOnEdge && !zOnEdge) { + continue; // in the courtyard + } + order.add(new BetterBlockPos(x, y, z)); + } + } + } + + fillSurfaceInOrderMaintainingConnection(surface, someOtherBlock, order); + + String shouldBe = "" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXX XXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXX XXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXX XXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXX XXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXX XXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXX XXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXX XXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXX XXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXX XXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXX XXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXX XXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XX XXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|X XXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX| XXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX | XXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXX | XXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXX |XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXX X|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n" + + "XXXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXXX|XXXXXXXXXXXXXXXXXX\n"; + assertEquals(shouldBe, reportAllFourWalls(surface)); + } + + private String reportAllFourWalls(NavigableSurface surface) { + StringBuilder report = new StringBuilder(); + for (int y = surface.sizeY - 1; y >= 0; y--) { + // make a report of what all four walls look like + for (int x = 0; x < surface.sizeX; x++) { + report.append(surface.getBlock(new BetterBlockPos(x, y, 0)) ? 'X' : ' '); + } + report.append('|'); + // start at 1 not 0 so that we don't repeat the last iteration of the previous loop (that would make the report look bad because the staircase would repeat one column for no reason) + for (int z = 1; z < surface.sizeZ; z++) { + report.append(surface.getBlock(new BetterBlockPos(surface.sizeX - 1, y, z)) ? 'X' : ' '); + } + report.append('|'); + // same deal for starting at -2 rather than -1 + for (int x = surface.sizeX - 2; x >= 0; x--) { + report.append(surface.getBlock(new BetterBlockPos(x, y, surface.sizeZ - 1)) ? 'X' : ' '); + } + report.append('|'); + // and same again + for (int z = surface.sizeZ - 2; z > 0; z--) { + report.append(surface.getBlock(new BetterBlockPos(0, y, z)) ? 'X' : ' '); + } + report.append('\n'); + } + return report.toString(); + } +}