Find all connected components given the connections in a graph (Java)
I'm building a program in Java that simulates a network of moving nodes. When nodes collide there is a certain probability that they will establish a connection. I'm trying to store all the nodes in the graph in a way that makes the connected components obvious, so I'm storing it as a Set of Lists where each list is a component and the set contains all components. (Each component must have size greater than 1.)
The complete code is on github: https://github.com/IceCreamYou/ViralSpread
But here's the important part:
// Store the components in the "temp" variable while we calculate them.
// Each list is a component; the set contains all components.
HashSet<LinkedList<Person>> temp = new HashSet<LinkedList<Person>>();
// hardConnections is a set of all connections between the nodes.
// Each connection appears only once (e.g. if there is a connection from A to B
// there will not be a connection from B to A -- however each connection is
// considered bidirectional).
for (Person[] connection : hardConnections) {
// Each element of hardConnections is an array
// containing two connected nodes.
// "Person" is the node class.
Person a = connection[0], b = connection[1];
boolean bFound = false, aFound = false;
// If we've already added one of the nodes to a component,
// add the other one to it.
for (LinkedList<Person> component : temp) {
boolean bInComponent = false, aInComponent = false;
for (Person p : component) {
if (p.samePosition(b)) {
bInComponent = true;
}
if (p.samePosition(a)) {
aInComponent = true;
}
}
if (bInComponent && !aInComponent) {
component.add(a);
bFound = true;
break;
}
else if (!bInComponent && aInComponent) {
component.add(b);
aFound = true;
break;
}
else if (bInComponent && aInComponent) {
aFound = true;
bFound = true;
break;
}
}
// If neither node is in a component, create a new component containing both nodes.
if (!bFound && !aFound) {
LinkedList<Person> newComponent = new LinkedList<Person>();
newComponent.add(b);
newComponent.add(a);
temp.add(newComponent);
}
}
components = temp;
p.samePosition() checks if the nodes have the same X and Y position. I'm using that method instead of equals() just in case the problem is related to equality checks failing somehow, even though I haven't overridden anything in the Person class (including equals(), hashCode(), etc.).
This seems to work most of the time. Unfortunately sometimes after a new connection is added to a component from an existing component, this code calculates the new single component as being two components of incorrect sizes.
I'm aware that the problem could also be that I'm calculating the connections incorrectly. However, the connections display correctly on the screen, and there are the right number of them (in the set -- not just from counting on the screen) so the error is probably not there. Just in case though, here's the code that calculates the connecti开发者_运维知识库ons. It's a little less generic than the code above so probably harder to read.
// "infected" and "infector" are the nodes.
// sameColor is true if the nodes have the same type.
// Nodes with the same type should retain their connections
// (which are also to nodes of the same type) while if the nodes have different types
// then the "infected" node will lose its connections because
// it will no longer be the same type as those connections.
// Note that nodes of different types should never have connections to each other.
boolean sameColor = (infected.getVirus().equalsVirus(infector.getVirus()));
boolean sameColorConnectionExists = false;
// As above, hardConnections is a set of connections between nodes (the Person class)
Iterator<Person[]> it = hardConnections.iterator();
while (it.hasNext()) {
Person[] connection = it.next();
if (sameColor) {
if ((infected.equals(connection[0]) && infector.equals(connection[1])) ||
(infected.equals(connection[1]) && infector.equals(connection[0]))) {
sameColorConnectionExists = true;
}
}
// Remove connections to the connected person.
else if (infected.equals(connection[0]) || infected.equals(connection[1])) {
it.remove();
}
}
// Create a new connection if the nodes were of different types or
// if they are the same type but no connection exists between them.
if (!sameColor || !sameColorConnectionExists) {
hardConnections.add(new Person[] {infected, infector});
}
// After this function returns, the infected node is changed to the type of the infector.
Basically what's happening there is we walk through the set of connections and remove any connections to the infected node that aren't from nodes of the same type. Then if a connection between the infected and infecting nodes doesn't exist, we add one.
I can't figure out where the bug is here... any help (or even other tips / comments on the project itself) would be appreciated. If you're looking at the code in github, the code in question is in Statistics.java functions updateConnections() and updateComponents(). updateConnections() is called from Person.java in transferVirus().
Thanks.
UPDATE
I've been outputting debugging information in an attempt to figure out what's going on. I've isolated one collision that causes incorrect fragmentation. In the data below, the first part shows the edges of the graph, and the second part shows the connected components and how many nodes are in them. As you can see, after the second collision, there is an extra "dark gray" component that shouldn't be there. There is only one "dark gray" component and it has 4 nodes in it. (Screenshot: http://screencast.com/t/f9PIzjuk )
----
blue:(489, 82) blue:(471, 449)
blue:(416, 412) blue:(471, 449)
pink:(207, 172) pink:(204, 132)
dark gray:(51, 285) dark gray:(635, 278)
dark gray:(635, 278) dark gray:(746, 369)
dark gray:(51, 285) dark gray:(737, 313)
dark gray:(737, 313) dark gray:(635, 278)
cyan:(2, 523) cyan:(473, 273)
3 blue
2 pink
4 dark gray
2 cyan
----
blue:(514, 79) blue:(535, 435)
dark gray:(725, 326) dark gray:(717, 365)
blue:(404, 404) blue:(535, 435)
pink:(197, 186) pink:(236, 127)
dark gray:(35, 283) dark gray:(619, 271)
dark gray:(619, 271) dark gray:(717, 365)
dark gray:(35, 283) dark gray:(725, 326)
dark gray:(725, 326) dark gray:(619, 271)
cyan:(1, 505) cyan:(490, 288)
3 blue
2 pink
4 dark gray
2 dark gray
2 cyan
----
After thinking more about what information I needed in order to debug this without producing an overwhelming amount of information, I produced this result of a snapshot during the simulation:
red:(227, 344) red:(257, 318)
red:(643, 244) red:(437, 140)
red:(437, 140) red:(257, 318)
orange:(573, 485) orange:(255, 143)
orange:(255, 143) orange:(20, 86)
red:(227, 344) red:(437, 140)
orange:(727, 494) orange:(573, 485)
3 red
red:(257, 318)
red:(227, 344)
red:(437, 140)
4 orange
orange:(255, 143)
orange:(573, 485)
orange:(20, 86)
orange:(727, 494)
2 red
red:(437, 140)
red:(643, 244)
If you follow the logic here:
- Look at the first connection. There are no components yet, so create a new one with
red:(227, 344), red:(257, 318)
. - Look at the second connection. There is only one component and none of the nodes in the component match either of the nodes in the connection, so create a second component with
red:(227, 344), red:(257, 318)
. This is wrong because there are other nodes later in the list that connect the nodes in the current connection to the original component. - Look at the third connection. The second node matches a node in the first component, so add the first node to the first component.
- ...and so on.
So now I know that my algorithm for finding connected components is actually wrong. I guess I need to do some research on that. My first thought is to take the following approach:
- Create a copy of the list of connections.
- When we look at the first pair, we don't have any components yet, so add the nodes in the first connection to a new (first) component and remove that connection from the copy.
- Look at each successive connection. If one of the nodes in the connection is in the first component:
- Add its nodes to the component
- Remove that connection from the copy
- Go back to the last connection with nodes that we added to the component, and check each connection between that connection and the current connection to see if they connect to the component now. If they do, add them to the component and remove them from the copy. Do this recursively.
- When we get to the end, repeat the process starting with the (now reduced) copy instead of the list of all connections.
Any thoughts on that approach would be appreciated.
Per comments on the OP above --
For anyone interested, I completely redesigned the algorithm (which essentially maps a set of unique two-way edges to a set of connected components [size > 2]). You can find it in the github repo at https://github.com/IceCreamYou/ViralSpread/blob/master/Statistics.java
精彩评论