We've looked quite a lot at how long it takes for an assortment of algorithms to run. But actually we've picked up a somewhat false picture. In truth, almost nothing is known beyond some trivial results and a tiny handful of deeper results (like the O(N log N) result for comparison-based sorting).
For example, if we want to add two N×N matrices, then the work needed is clearly O(N^2); we can't know what the answer is without looking at all N^2 elements of each matrix, and by adding them up more-or-less as you look, you get the answer with no wasted effort. But if you want to multiply the matrices, what then? The obvious way, working straight from the definition, involves adding up the results of N multiplications for each of the N^2 elements of the answer, so the work done is O(N^3). But that is much more than the O(N^2) needed to look at all the elements, so each element is being used repeatedly. Is there scope for improvement? Yes, certainly. Strassen showed in 1968 that by quartering each matrix [into four matrices each N/2 square], multiplying those, and combining the results, you could get away with 7 matrix multiplications where you would expect 8; applying this idea recursively leads to an O(N ^ lg 7) method. Since lg 7 ~ 2.81 < 3, this is an improvement! Slightly better techniques are now known, but only by tiny amounts, at huge expense in complication, and the traditional technique is certainly still the best known for ordinary matrices with N < 1000000. This leaves a huge gap between practice and theory.
However, the gap between N^2 and N^3 pales into insignificance by comparison with that between N^2 and N! or 2^N. Yet that is the sort of gap we face with many of the backtracking algorithms we looked at in the last lecture. No-one knows whether we absolutely need to backtrack in, for example, the Hamiltonian circuit problem. Perhaps there is a clever way of deciding where to go next that will enable us to determine quickly what route to follow? To see how important this is, let us look at some numbers.
function \ N | 10 | 100 | 1000 | 1000000 |
---|---|---|---|---|
log N | 3us | 7us | 10 us | 20 us |
N | 10us | 0.1ms | 1ms | 1s |
N log N | 30us | 0.7ms | 10ms | 20s |
N^2 | 0.1ms | 10ms | 1s | 2w |
N^3 | 1ms | 1s | 20m | 30000y |
N^5 | 0.1s | 3h | 30y | 3e16 y |
2^N | 1ms | 3e16 y | 3e288 y | large |
N! | 3s | 3e142 y | large | large |
N^N | 3h | 3e186 y | large | large |
Note that even times as `small' as 3e16 years are more than a million times the estimated age of the Universe. Even these times are not entirely hopeless, however. Perhaps the problem is not really N^5 but a tenth or a hundredth of that. Perhaps, in a few years, computers will be a thousand times faster. Perhaps we can, for a project of sufficient importance to mankind, devote a million, or a thousand million, computers to this task. Then the time comes down to, at best, 300 years. Some cathedrals have taken this long to build! In this sort of context, the figure in the table of 30000 years is almost a challenge. Scrounge together all the computers within this University, save a factor of ten or a hundred somewhere, e.g. by waiting for faster computers, and it almost becomes an undergraduate project. Running these algorithms is simply a matter of devoting enough resources to it; if your project is important enough, it can be done.
By contrast, the longer times given, like 3e142 years, are completely beyond any imaginable resources. Numbers like 142 are so homely that non-mathematicians are apt to misunderstand how big they are as exponents. But you could give every man, woman and child on earth a million computers, each a million times faster than a PC, throw in another million for luck, and you're down to 5e114 years. Give every atom in the visible universe this sort of computing power, and you still can't complete this algorithm in a reasonable time. If, perchance, we have a problem which cannot be solved in fewer than 100! steps, then, in plain language, we have to say that this solution cannot feasibly be found. Yet that is, roughly, the situation we find ourselves in with several of our backtracking problems. The annoying feature is that we haven't managed to prove that so many steps are needed; if only we could move these problems up into the polynomial lines.
At the risk of flogging a dead horse, let us also look at a companion table, which shows, for these same functions, how big an N we can handle in a given time.
function \ time | 1 second | 1 hour | 1 week | 1 year | 1000y | 1000000y |
---|---|---|---|---|---|---|
log N | large | large | large | large | large | large |
N | 1e6 | 3e9 | 5e11 | 3e13 | 3e16 | 3e19 |
N log N | 87847 | 2e8 | 2e10 | 1e12 | 1e15 | 8e17 |
N^2 | 1000 | 60000 | 7e5 | 5e6 | 2e8 | 5e9 |
N^3 | 100 | 1532 | 8000 | 31000 | 3e5 | 3e6 |
N^5 | 16 | 81 | 218 | 500 | 2000 | 8000 |
2^N | 20 | 32 | 40 | 45 | 55 | 65 |
N! | 9 | 13 | 15 | 16 | 18 | 21 |
N^N | 7 | 9 | 11 | 12 | 14 | 16 |
The contrast between the polynomials and the last few lines is again stark. If N is more than, say, 100, then running even an N^5 algorithm is straightforward, whereas even a 2^N algorithm is quite out of the question. Bearing in mind, again, that the application of many computers, faster computers, tweaks to the programming, can bring the million-year project to feasibility, we see that even values of N as large as several thousand may be within range for quite high-degree polynomials; but nothing can rescue those last three lines.
This is a pukka mathematical equivalence relation -- it is reflexive [an algorithm is in the same class as itself], symmetric [if A and B are on the same line, so are B and A], and transitive [if A and B are on the same line, and so are B and C, then so also are A and C] -- so it does divide all algorithms into equivalence classes. But we are skating over very thin ice, some of which needs a little more exploration.
We are here begging lots of questions: