Exponential and NP Algorithms
Steven J. Zeil
We’ve spent a lot of time and effort this semester worrying about finding efficient algorithms, and learning how to tell if they are efficient in the first place. It’s fitting that we end up by noting that there are some very practical problems for which no known efficient solution exists, and for which there is considerable reason to doubt that an efficient solution is possible.
1 A Hard Problem - Graph Coloring
An optimizing compiler may try to save storage by storing variables in the same memory locations if their values are never needed simultaneously:
{
int w,x,y,z;
cin >> w >> x;
y = x + w;
cout << y;
z = x - 1;
cout << z << x;
}
z
could share a location with w
or y
. No other sharing is possible.
The compiler has enough information to construct an “interference graph” in which
-
vertices represent variables
-
an edge connects two vertices if the corresponding variables cannot share storage
We then try to color the graph, using as few colors as possible, so that no two adjacent vertices have the same color.
Colors represent storage locations. Two vertices with the same color represent variables that can be stored at the same location without interfering with each other.
Graph coloring problems arise in a number of scheduling and resource allocation situations. Similar problems include scheduling a series of meetings so that people attending the meetings are never scheduled to be in two places at once, assigning classrooms to courses, etc.
1.1 Graph Coloring: A Backtracking Solution
The most obvious solution to this problem is arrived at through a design referred to as backtracking.
Recall that the essence of backtracking is:
-
Number the solution variables $\left[ v_{0}, v_{1}, \ldots , v_{n-1}\right]$.
-
Number the possible values for each variable $\left[c_{0}, c_{1}, \ldots , c_{k-1}\right]$.
-
Start by assigning $c_{0}$ to each $v_{i}$.
-
If we have an acceptable solution, stop.
-
If the current solution is not acceptable, let
i = n-1
. -
If
i < 0
, stop and signal that no solution is possible. -
Let j be the index such that $v_{i} = c_{j}$. If
j < k-1
, assign $c_{j+1}$ to $v_{i}$ and go back to step 4. -
But if $j \geq k-1$, assign $c_{0}$ to $v_{i}$, decrement i, and go back to step 6.
Although this approach will find a solution eventually (if one exists), it isn’t speedy. Backtracking over n variables, each of which can take on k possible values, is $O(k^{n})$.
For graph coloring, we will have one variable for each node in the graph. Each variable will take on any of the available colors.
To do a backtracking solution to the graph coloring problem, we start with the plausibility test. The function shown here assumes that the vertices are sequentially numbered and that the colors are represented by integers stored in a backtracking state generator (from our earlier lesson on backtracking).
int clashingColors (const Graph& g,
const BackTrack& colors)
{
// Test to see if the current coloring is OK. If not, return the
// lowest number of a vertex that we want to change for the next
// potential solution.
int vertexToChange = vertexNumbers.size();
for (auto v = vertices(g).first; v != vertices(g).second; ++v)
{
int vNum = *v;
int vColor = colors[vNum];
// Check to see if *v is adjacent to a
// vertex of the same color.
// If so, one of them has to change.
int clashingVertex = -1;
auto outgoing = out_edges(*v, g);
for (auto e = outgoing.first;
(clashingVertex < 0) && (e != outgoing.second); ++e)
{
Vertex w = target(*e, g);
int wColor = colors[w];
if (vColor == wColor)
clashingVertex = max(vNum, w);
}
if (clashingVertex >= 0)
vertexToChange = min(vertexToChange, clashingVertex);
}
return vertexToChange;
}
This function checks all the vertices in the graph to see if any are adjacent to another of the same color. To facilitate pruning of the backtrack search, when it finds clashing assignments, it returns the smallest vertex number that we can safely assume must be changed to yield a solution.
The main routine is a fairly straightforward use of the backtracking generator with pruning.
void colorGraph_backtracking (Graph& g, unsigned numColors,
ColorMap& colors)
{
// Search for acceptable solution via backtracking
BackTrack problem (vertexNumbers.size(), numColors);
bool solved = false;
while (!solved && problem.more())
{
int pruneAt = clashingColors(g, problem);
if (pruneAt >= vertexNumbers.size())
solved = true;
else
problem.prune(pruneAt+1);
}
colors.clear();
if (solved)
{
// Construct the solution (map of vertices to colors)
for (auto v = vertices(g).first; v != vertices(g).second; ++v)
colors[*v] = problem[*v];
}
}
Try out the running the backtracking form in an animation. Try it at least once where you use fewer colors than are actually possible for that graph, to give yourself a feel for how backtracking (mis)behaves when searching for a subtle or non-existent solution.
Before moving on, it’s worth remembering that backtracking is often (usually?) written in recursive form.
bool colorGraph_backtracking (Graph& g, unsigned numColors,
ColorMap& colors)
{
for (auto v = vertices(g).first; v != vertices(g).second; ++v)
{
colors[*v] = -1;
}
colorGraph_rec (g, 0, numColors, colors);
}
bool colorGraph_rec (Graph& g,
Vertex v,
unsigned numColors,
ColorMap& colors)
{
if (v != *(vertices(g).second))
{
// Try to color vertex v, then recursively color the rest.
c = chooseColor(g, v, numColors, colors, -1);
while (c >= 0 && c < numColors)
{
colors[v] = c;
Vertex w = v;
++w;
if (colorGraph_rec(g,w,numColors,colors))
return true; // A solution has been found
else
// We have backtracked to here - try a different color for v
c = chooseColor(g, v, numColors, colors, c);
}
// If we exit the above loop, no solution is possible given the
// colors that had already been assigned to g.vbegin()..v
colors[v] = -1;
return false;
}
else
return true;
}
If the solution variables for a problem form a graph or tree, working with adjacent variables may cause pruning to take place earlier, since the plausibility tests are often framed in terms of properties of adjacent variables.
The solutions considered by this recursive routine are the same as in the iterative case, but now this tree could also be considered to represent the recursive calls made, with each vertex representing an activation (call) to the recursive routine from its parent.
The algorithms I have shown you for coloring are exponential time.
Can we do better?
-
Yes, but only in the constant multipliers.
-
There is no known algorithm for graph coloring that is not worst-case exponential time.
-
There is considerable reason to believe that no polynomial-time algorithm is possible for this problem.
2 NP Problems
We call the set of programs whose worst case is a polynomial order the class P.
-
Suppose that we had an infinite number of computers at our disposal,
-
and could spawn off problems to any number of them in constant time.
-
These computers could then run in parallel with each other.
2.1 A Parallel Approach to Graph Coloring
This procedure works by asking different computers to separately consider the different colors for v
.
bool colorGraph_rec (Graph& g,
Vertex v,
unsigned numColors,
ColorMap& colors)
{
if (v != *(vertices(g).second))
{
// Try to color vertex v, then recursively color the rest.
c = chooseColor(g, v, numColors, colors, -1);
while (c >= 0 && c < numColors)
{
colors[v] = c;
Vertex w = v;
++w;
spawn colorGraph_rec(g,w,numColors,colors);
c = chooseColor(g, v, numColors, colors, c);
}
if (any spawned process succeeds)
return true; // A solution has been found
else
return false;
}
}
The solutions being considered would still look like a (pruned) tree:
But now, all nodes at the same level of the call trees are being executed in parallel.
The running time is therefore proportional the depth of the call tree times the cost per call: $O(|V|)$. And that’s a polynomial!
2.2 Nondeterministic Machines
We call this mythical machine that allows us to spawn off parallel computations at no cost a nondeterministic machine.
The set of programs that can be run in Polynomial time on a Nondeterministic machine is called the NP algorithms, and the set of problems solvable by an algorithm from that set are NP problems.
Note that any problem that can be solved in polynomial time on a single, conventional processor, can certainly be solved in polynomial time on an infinite number of processors, so $P \subseteq NP$.
There are some exponential time algorithms that can’t be solved in polynomial time even on a nondeterministic machine. So the NP problems clearly occupy an intermediate niche between these really nasty exponential problems and the polynomial time P problems.
This leads to one of the more famous unresolved speculations in computer science:
Is P = NP ?
In other words, is an NP problem one for which a polynomial-time solution on a conventional processor exists, but we just haven’t discovered it yet? Or are at least some NP problems truly exponential-time, with no polynomial-time solution possible?
There’s another interesting thing about the NP problems. They all have the curious property that it’s “easier” to tell if we have a solution than it is to compute such a solution in the first place. In fact, this is how the NP problems are usually defined: the set of problems for which we can determine, in polynomial time, whether a proposed solution is correct or not.
You can see how that idea relates to graph coloring. It’s hard to find a coloring for graph. But if you show me a colored graph, I can traverse the graph in polynomial time, comparing each vertex to the ones adjacent to it, and determine whether any two adjacent vertices have the same color. So I can tell, in polynomial time, whether a proposed coloring is correct, but I need exponential time to find such a coloring.
2.3 NP-Complete Problems
The speculation about P = NP is particularly interesting in view of the discovery of a special subset of the NP problems.
Among the NP problems, there is a collection of problems called NP-complete problems that have the property
-
for any NP-complete problem
A
, and -
for any other NP-complete problem
B
B
can be reduced (converted) to A
in polynomial time.
Consequently, if we could find a polynomial-time solution to even one NP-complete problem, we would have a polynomial-time solution to all the NP-complete problems.
Graph coloring is NP-complete.
2.3.1 The Traveling Salesman Problem
The best known NP-complete problem is the Traveling Salesman Problem: Given a complete graph with edge costs, find the cheapest simple cycle that visits each node exactly once.
Distances | ||||||
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
1 | 0 | 6 | 14 | 13 | 20 | 21 |
2 | 6 | 0 | 18 | 13 | 11 | 20 |
3 | 14 | 18 | 0 | 5 | 24 | 8 |
4 | 13 | 13 | 5 | 0 | 7 | 12 |
5 | 20 | 11 | 24 | 7 | 0 | 15 |
6 | 21 | 20 | 8 | 12 | 15 | 0 |
A lot of practical resource allocation and scheduling problems can be shown to be equivalent to the traveling salesman problem, which is somewhat unfortunate because, like all NP-complete problems, we only know exponential-time algorithms for it.
2.3.2 Hamiltonian Cycles
By way of illustration of this idea of reducing one problem to another, consider the Hamiltonian Cycle problem:
Given a connected graph, is there a simple cycle visiting each node?
This can be converted into the Traveling Salesman problem in $O(|V|^2)$ time.
For example, given this graph (This has Hamiltonian cycle [3,1,4,2,5,6,3].)
We assign each edge a cost 1:
and then fill in the remaining edges, giving each new edge the cost 2:
The original graph has a Hamiltonian cycle if and only if this graph has a Traveling Salesman solution with cost = $|V|$.
For example, if we solve the traveling salesman problem for this graph, we see that the cheapest cycle involves only edges from the original graph.
There are $|V|$ of them (since the cycle must visit every vertex), so the total cost is $|V|$.
If the cost had come out higher, we would know that there had to be at least one cost=2 edge, meaning that no Hamiltonian cycle existed.
So, if we could solve the Traveling Salesman problem in polynomial time, we would be able to solve the Hamiltonian Cycle problem in polynomial time as well.
This is typical of the kind of reduction that relates NP-complete problems to the rest of the NP problems.
2.4 So, is P = NP?
-
To date, no one has found a polynomial time algorithm for any NP-complete problem.
-
No one has been able to prove that no such polynomial-time algorithm exists.
-
The answer to the question “is P = NP?” is generally believed to be, “No”.
3 If you can’t get across, go around.
In practice, when faced with an NP or exponential problem, we often resort to approximate solutions or to algorithms that do not always give the best solution.
- For example, polynomial time algorithms are known that can solve the traveling salesman problem within some error factor.
A heuristic is an approach to a solution that often, but not always, succeeds. Heuristic algorithms often work by exploring a small number of the most common or most likely possibilities in any given situation. For example, a common heuristic approach for many algorithms is the “greedy” approach: always try to take the largest possible step towards a likely overall solution.
A heuristic algorithm for coloring a graph using k colors:
-
Discard all vertices of degree less than k, keeping them on a stack.
-
If the rest is successful, we can come back to these and trivially color them.
-
-
Keep remaining vertices sorted by the number of colors already assigned to adjacent vertices. (We call this the “number of constraints” on the vertex.)
-
Repeatedly pick the most constrained vertex and assign it any legal color. (This is the “greedy” part, as we are hoping that by guessing early at the “hardest” parts of the overall problem, the rest will all work out.)
-
If all the remaining vertices are colored, start popping discarded vertices from the stack, assigning them any legal color as you do.
Try out the the heuristic form of graph coloring in an animation. Note how much better it behaves when faced with problems where you have allowed too few colors to actually color the graph.
Now, this algorithm is not guaranteed to always find a solution, even if one is possible. But it’s not all that easy to come up with a graph on which this algorithm will fail but the much more expensive backtracking approach would succeed. Try it!