So, for example, below is the graph from the Network Flow Lecture Notes #1:
g1.txt
SOURCE A SINK G EDGE A D 3 EDGE A B 3 EDGE B C 4 EDGE C A 3 EDGE C D 1 EDGE C E 2 EDGE D E 2 EDGE D F 6 EDGE E B 1 EDGE E G 1 EDGE F G 9 |
The file g2.txt adds an edge from A to C with a capacity of 1.
I have a program makerandom.cpp which takes one argument, a number of nodes, and creates a random graph with one source, one sink, and the given number of other nodes. There is a random edge between every pair of nodes (in a random direction) with a random capacity between zero and 10. There are edges from the source to random nodes with a 40% probability, and there are edges from the sink to random nodes with a 40% probability. Thus, this is a pretty dense graph which should be a challenge to our network flow programs.
Below is an example of a graph made with makerandom 5. I think we can all agree that finding the maximum flow through this graph will be a bit of a challenge. To help you, I've colored the edges in the minimum cut red:
g3.txt
SOURCE s SINK t EDGE n00 t 4.923 EDGE n01 n00 8.824 EDGE n00 n02 6.932 EDGE n00 n03 6.518 EDGE n00 n04 6.183 EDGE n01 t 8.471 EDGE n02 n01 4.929 EDGE n03 n01 5.566 EDGE n01 n04 6.661 EDGE s n02 6.263 EDGE n02 n03 0.741 EDGE n04 n02 5.840 EDGE n04 n03 4.417 EDGE s n04 8.033 |
Take a minute to study that graph for a bit. Try to convince yourself that the edges in the minimum cut have to be in any maximum flow through the graph. If you take my word for it that these edges compose the minimum cut, it's pretty easy to construct a maximum flow graph:
By the way, the maximum flow of g1.txt is 5 (see the first set of Network Flow lecture notes), and the maximum flow of g3.txt is 10.087.
class Node { public: string name; vector <class Edge *> adj; int visited; }; |
Each node has a name, an adjacency list of edges, and a visited field which helps us perform depth-first-search. We're using a vector instead of a list, because as it turns out, once we create an edge, we never delete it. Therefore, using a vector makes like easier than deques or lists. You have to say "class Edge" because the definition for an Edge is below that of a Node.
Edges are a little meatier. Each edge has a name, pointers to the two nodes which it connects, a pointer to its reverse edge, and three weights:
By setting edges up in this way, you maintain all three graphs -- original, residual and flow, with one set of nodes and edges. It makes life easier. Here is the Edge definition:
class Edge { public: string name; Node *n1; Node *n2; Edge *reverse; double original; double residual; double flow; }; |
Finally, we'll have a Graph class. We're going to start with the following definition:
class Graph { public: Graph(); ~Graph(); void Print(); Node *Get_Node(string &s); Edge *Get_Edge(Node *n1, Node *n2); Node *Source; Node *Sink; vector <Node *> Nodes; vector <Edge *> Edges; map <string, Node *> N_Map; map <string, Edge *> E_Map; }; |
There are some design decisions here, which I'd like to go over. First, look at the data. There's a source and a sink, and two vectors that contain pointers to all of the nodes and edges. I have these vectors so that whenever you want to perform an operation on all of the nodes or edges, you can do it with these vectors. They are also convenient because I can use them to delete nodes and edges in the destructor. In fact, I'll show my destructor now, since all it does is delete nodes and edges:
Graph::~Graph() { int i; for (i = 0; i < Nodes.size(); i++) delete(Nodes[i]); for (i = 0; i < Edges.size(); i++) delete(Edges[i]); } |
The maps N_Map and E_Map store nodes and edges by their names. They are only used when we read in the graph, because when we specify a node or an edge, it may exist already. To figure out whether it exists already, we construct a name and then check N_Map or E_Map.
To exemplify, here's the code for Get_Node(), which either finds a node and returns a pointer to it, or determines that the node doesn't exist, in which case it creates the node, puts it into Nodes and N_Map, and returns it:
Node *Graph::Get_Node(string &s) { map <string, Node *>::iterator nit; Node *n; nit = N_Map.find(s); if (nit != N_Map.end()) return nit->second; n = new Node; n->name = s; Nodes.push_back(n); N_Map.insert(make_pair(s, n)); return n; } |
Finally, our constructor reads graph files. For the moment, we're just going to have it create nodes and not edges:
Graph::Graph() { string s, f, t; double cap; Node *n1, *n2; Source = NULL; Sink = NULL; while (cin >> s) { if (s == "SOURCE") { cin >> s; if (Source != NULL) { fprintf(stderr, "Can't specify two sources\n"); exit(1); } Source = Get_Node(s); } else if (s == "SINK") { cin >> s; if (Sink != NULL) { fprintf(stderr, "Can't specify two sinks\n"); exit(1); } Sink = Get_Node(s); } else if (s == "EDGE") { cin >> f >> t >> cap; n1 = Get_Node(f); n2 = Get_Node(t); /* We're not creating edges yet */ } } if (Source == NULL) { fprintf(stderr, "No Source.\n"); exit(1); } if (Sink == NULL) { fprintf(stderr, "No Sink.\n"); exit(1); } } |
Finally, we have a print method that prints the nodes, and a main() that creates a graph and prints it. All of the above code is in netflow1.cpp:
void Graph::Print() { Node *n; int i; printf("Source: %s\n", Source->name.c_str()); printf("Sink: %s\n", Sink->name.c_str()); printf("Nodes: "); for (i = 0; i < Nodes.size(); i++) { n = Nodes[i]; printf(" %s", n->name.c_str()); } printf("\n"); } main() { Graph *g; g = new Graph(); g->Print(); } |
When we run this on g1.txt and g1.txt, it prints all of the node names, plus the source and sink:
UNIX> make netflow1 g++ -O -c netflow1.cpp g++ -O -o netflow1 netflow1.cpp UNIX> netflow1 < g1.txt Source: A Sink: G Nodes: A G D B C E F UNIX> netflow1 < g3.txt Source: s Sink: t Nodes: s t n00 n01 n02 n03 n04 UNIX>
Edge *Graph::Get_Edge(Node *n1, Node *n2) { map <string, Edge *>::iterator eit; Edge *e; string name; name = n1->name + "->"; name += n2->name; eit = E_Map.find(name); if (eit != E_Map.end()) return eit->second; e = new Edge; e->name = name; e->n1 = n1; e->n2 = n2; e->original = 0; e->residual = 0; e->flow = 0; e->reverse = NULL; Edges.push_back(e); E_Map.insert(make_pair(name, e)); return e; } |
We use Get_Edge() in our constructor, which is a little subtle:
Graph::Graph() { string s, f, t; double cap; Node *n1, *n2; Edge *e; Source = NULL; Sink = NULL; while (cin >> s) { if (s == "SOURCE") { cin >> s; if (Source != NULL) { fprintf(stderr, "Can't specify two sources\n"); exit(1); } Source = Get_Node(s); } else if (s == "SINK") { cin >> s; if (Sink != NULL) { fprintf(stderr, "Can't specify two sinks\n"); exit(1); } Sink = Get_Node(s); } else if (s == "EDGE") { cin >> f >> t >> cap; n1 = Get_Node(f); n2 = Get_Node(t); e = Get_Edge(n1, n2); e->original += cap; if (e->reverse == NULL) { e->reverse = Get_Edge(n2, n1); e->reverse->reverse = e; n1->adj.push_back(e); n2->adj.push_back(e->reverse); } } } if (Source == NULL) { fprintf(stderr, "No Source.\n"); exit(1); } if (Sink == NULL) { fprintf(stderr, "No Sink.\n"); exit(1); } } |
The subtlety is that we only put an edge onto its node's adjacency list when we first create it, which we test by testing whether e->reverse is NULL. If e->reverse is non-NULL, then we know that both the edge and the reverse edge have been created before, and therefore are already on their nodes' adjacency lists.
We test our program, we also use the input file g2.txt, which is identical to g1.txt, except there is an additional edge from A to C.
UNIX> make netflow2 g++ -O -c netflow2.cpp g++ -O -o netflow2 netflow2.cpp UNIX> netflow2 < g1.txt Source: A Sink: G Node A: (A->D:3.000) (A->B:3.000) (A->C:0.000) Node G: (G->E:0.000) (G->F:0.000) Node D: (D->A:0.000) (D->C:0.000) (D->E:2.000) (D->F:6.000) Node B: (B->A:0.000) (B->C:4.000) (B->E:0.000) Node C: (C->B:0.000) (C->A:3.000) (C->D:1.000) (C->E:2.000) Node E: (E->C:0.000) (E->D:0.000) (E->B:1.000) (E->G:1.000) Node F: (F->D:0.000) (F->G:9.000) UNIX> netflow2 < g2.txt Source: A Sink: G Node A: (A->D:3.000) (A->B:3.000) (A->C:1.000) Node G: (G->E:0.000) (G->F:0.000) Node D: (D->A:0.000) (D->C:0.000) (D->E:2.000) (D->F:6.000) Node B: (B->A:0.000) (B->C:4.000) (B->E:0.000) Node C: (C->B:0.000) (C->A:3.000) (C->D:1.000) (C->E:2.000) Node E: (E->C:0.000) (E->D:0.000) (E->B:1.000) (E->G:1.000) Node F: (F->D:0.000) (F->G:9.000) UNIX> netflow2 < g3.txt Source: s Sink: t Node s: (s->n02:6.263) (s->n04:8.033) Node t: (t->n00:0.000) (t->n01:0.000) Node n00: (n00->t:4.923) (n00->n01:0.000) (n00->n02:6.932) (n00->n03:6.518) (n00->n04:6.183) Node n01: (n01->n00:8.824) (n01->t:8.471) (n01->n02:0.000) (n01->n03:0.000) (n01->n04:6.661) Node n02: (n02->n00:0.000) (n02->n01:4.929) (n02->s:0.000) (n02->n03:0.741) (n02->n04:0.000) Node n03: (n03->n00:0.000) (n03->n01:5.566) (n03->n02:0.000) (n03->n04:0.000) Node n04: (n04->n00:0.000) (n04->n01:0.000) (n04->n02:5.840) (n04->n03:4.417) (n04->s:0.000) UNIX>
double Graph::Find_Max_Flow() { double total; double f; Edge *e; int i; for (i = 0; i < Edges.size(); i++) { e = Edges[i]; e->flow = 0; e->residual = e->original; } total = 0; while (1) { f = Find_Augmenting_Path(); if (f == 0) { return total; } else { total += f; } } } |
So that we can program incrementally, we write Find_Augmenting_Path() so that it simply calls a depth-first search to find a path from the source to the sink, and then it exits the program:
double Graph::Find_Augmenting_Path() { int i; for (i = 0; i < Nodes.size(); i++) Nodes[i]->visited = 0; if (DFS(Source)) { printf("Quitting.\n"); } exit(0); } |
Finally, our depth-first search finds a path to the sink and then returns, printing the edges along the path in reverse order:
int Graph::DFS(Node *n) { int i; Edge *e; double f; if (n->visited) return 0; n->visited = 1; if (n == Sink) return 1; for (i = 0; i < n->adj.size(); i++) { e = n->adj[i]; if (e->residual > 0) { if (DFS(e->n2)) { printf("Found a path to the sink. Edge %s.\n", e->name.c_str()); return 1; } } } return 0; } |
We call Find_Max_Flow() in main(), and it's time to test.
main() { Graph *g; double f; g = new Graph(); f = g->Find_Max_Flow(); printf("Max flow is %.3lf\n", f); } |
It should find a valid path from the source to the sink:
UNIX> make netflow3 g++ -O -c netflow3.cpp g++ -O -o netflow3 netflow3.cpp UNIX> netflow3 < g1.txt Found a path to the sink. Edge E->G. Found a path to the sink. Edge D->E. Found a path to the sink. Edge A->D. Quitting. UNIX> netflow3 < g3.txt Found a path to the sink. Edge n00->t. Found a path to the sink. Edge n01->n00. Found a path to the sink. Edge n02->n01. Found a path to the sink. Edge s->n02. Quitting. UNIX>Here are the paths:
int Graph::DFS(Node *n) { int i; Edge *e; double f; if (n->visited) return 0; n->visited = 1; if (n == Sink) return 1; for (i = 0; i < n->adj.size(); i++) { e = n->adj[i]; if (e->residual > 0) { if (DFS(e->n2)) { Path.push_back(e); return 1; } } } return 0; } |
In Find_Augmenting_Path(), we process the path, figuring out the flow and then modifying the flow and residual graphs accordingly:
double Graph::Find_Augmenting_Path() { int i; double f; Edge *e; for (i = 0; i < Nodes.size(); i++) Nodes[i]->visited = 0; Path.clear(); if (DFS(Source)) { f = Path[0]->residual; for (i = 1; i < Path.size(); i++) { if (Path[i]->residual < f) f = Path[i]->residual; } for (i = 0; i < Path.size(); i++) { e = Path[i]; e->residual -= f; e->flow += f; e->reverse->residual += f; } printf("Found path with flow of %.3lf:", f); for (i = Path.size()-1; i >= 0; i--) printf(" %s", Path[i]->name.c_str()); printf("\n"); return f; } else { return 0; } } |
We test it, and it's successful at finding the flow in each graph:
UNIX> make netflow4 g++ -O -c netflow4.cpp g++ -O -o netflow4 netflow4.cpp UNIX> netflow4 < g1.txt Found path with flow of 1.000: A->D D->E E->G Found path with flow of 2.000: A->D D->F F->G Found path with flow of 1.000: A->B B->C C->D D->F F->G Found path with flow of 1.000: A->B B->C C->E E->D D->F F->G Max flow is 5.000 UNIX> netflow4 < g3.txt Found path with flow of 4.923: s->n02 n02->n01 n01->n00 n00->t Found path with flow of 0.006: s->n02 n02->n01 n01->t Found path with flow of 0.741: s->n02 n02->n03 n03->n01 n01->t Found path with flow of 4.417: s->n04 n04->n03 n03->n01 n01->t Max flow is 10.087 UNIX>
double Graph::Find_Max_Flow() { double total; double f; Edge *e; Node *n; int i, j; for (i = 0; i < Edges.size(); i++) { e = Edges[i]; e->flow = 0; e->residual = e->original; } total = 0; while (1) { f = Find_Augmenting_Path(); if (f == 0) { Cut.clear(); for (i = 0; i < Nodes.size(); i++) { n = Nodes[i]; if (n->visited) { for (j = 0; j < n->adj.size(); j++) { e = n->adj[j]; if (e->original > 0 && !e->n2->visited) Cut.push_back(e); } } } return total; } else { total += f; } } } main() { Graph *g; double f; int i; g = new Graph(); f = g->Find_Max_Flow(); printf("Max flow is %.3lf\n", f); printf("Cut:"); for (i = 0; i < g->Cut.size(); i++) printf(" %s", g->Cut[i]->name.c_str()); printf("\n"); } |
It properly finds the cuts in our two examples:
UNIX> make netflow5 g++ -O -c netflow5.cpp g++ -O -o netflow5 netflow5.cpp UNIX> netflow5 < g1.txt Found path with flow of 1.000: A->D D->E E->G Found path with flow of 2.000: A->D D->F F->G Found path with flow of 1.000: A->B B->C C->D D->F F->G Found path with flow of 1.000: A->B B->C C->E E->D D->F F->G Max flow is 5.000 Cut: A->D C->D E->G UNIX> netflow5 < g3.txt Found path with flow of 4.923: s->n02 n02->n01 n01->n00 n00->t Found path with flow of 0.006: s->n02 n02->n01 n01->t Found path with flow of 0.741: s->n02 n02->n03 n03->n01 n01->t Found path with flow of 4.417: s->n04 n04->n03 n03->n01 n01->t Max flow is 10.087 Cut: n02->n01 n02->n03 n04->n03 UNIX>
UNIX> makerandom 50 0 | netflow5 | grep 0\.000 | head -n 1 Found path with flow of 0.000: s->n22 n22->n01 n01->n00 n00->n06 n06->n02 n02->n04 n04->n03 n03->n08 n08->n05 n05->n07 n07->n15 n15->n10 n10->n09 n09->n11 n11->n12 n12->n13 n13->n14 n14->n16 n16->t UNIX>Should this ever happen? No. Why? Because makerandom spits out capacities to three decimal digits. Therefore, no flow should be less than 0.001. What's going on is roundoff error, as many flow values are subtracted from residuals. I'm not going to worry about chasing down the problem and fixing it. However, if you ever wonder how or when roundoff error occurs, this is it. For that reason, in the next lecture, we're going to switch to integers.