for
CSCI 1301: Great Ideas in Computer Science
(c) 1993 - 2007 John E. Howland, All Rights Reserved
Department of Computer Science
Trinity University
One Trinity Place
San Antonio, Texas 78212-7200
Internet: jhowland@ariel.cs.trinity.edu
We say a computer program is order f n, written as O(f n), if there is a function f and a constant k such that the execution time of the program is not more than k * f n. We say that n is the size of the problem which the program solves and, of course, we are often interested in what happens to the execution time when the size of the problem is large.
As an example of the order notation consider the following function which computes the largest element of a non-empty list of numbers.
maximum =: monad def 'max_helper (rest y.) ; head y.'
max_helper =: monad define
('ls' ; 'max') =. y.
if. nullp ls
do. max
elseif. max < head ls
do. max_helper (rest ls) ; head ls
elseif. 1
do. max_helper (rest ls) ; max
end.
)
The
traced version, traced_maximum, gives the following output:
traced_maximum =: monad def 'traced_max_helper (rest y.) ; head y.'
traced_max_helper =: monad define
entering y.
('ls' ; 'max') =. y.
if. nullp ls
do. max
elseif. max < head ls
do. leaving traced_max_helper (rest ls) ; head ls
elseif. 1
do. leaving traced_max_helper (rest ls) ; max
end.
)
traced_maximum 1 9 2 4 10 5 ==>
Entering, input =
+----------+-+
|9 2 4 10 5|1|
+----------+-+
Entering, input =
+--------+-+
|2 4 10 5|9|
+--------+-+
Entering, input =
+------+-+
|4 10 5|9|
+------+-+
Entering, input =
+----+-+
|10 5|9|
+----+-+
Entering, input =
+-+--+
|5|10|
+-+--+
Entering, input =
++--+
||10|
++--+
Leaving, result = 10
Leaving, result = 10
Leaving, result = 10
Leaving, result = 10
Leaving, result = 10
10
One
can easily verify that the number of times max_helper is used, i.e.,
the execution time of maximum, is n, the number of elements
in ls. Hence, the execution time of maximum is of order n.
sort_pass =: monad define
NB. stop when we have a list of two items
if. nullp rest rest y.
do. if. (head y.) < head rest y.
do. y.
else. reverse y.
end.
else. one =. head y.
two =. head rest y.
if. one < two
do. one append sort_pass rest y.
else. two append sort_pass one append rest rest y.
end.
end.
)
sort_pass 9 1 4 2 ==>
1 4 2 9
Next
we use a traced version of sort_pass to study the execution of this function.
traced_sort_pass =: monad define
NB. stop when we have a list of two items
entering y.
if. nullp rest rest y.
do. if. (head y.) < head rest y.
do. leaving y.
else. leaving reverse y.
end.
else. one =. head y.
two =. head rest y.
if. one < two
do. leaving one append traced_sort_pass rest y.
else. leaving two append traced_sort_pass one append rest rest y.
end.
end.
)
traced_sort_pass 9 1 4 2 ==>
Entering, input = 9 1 4 2
Entering, input = 9 4 2
Entering, input = 9 2
Leaving, result = 2 9
Leaving, result = 4 2 9
Leaving, result = 1 4 2 9
1 4 2 9
In
the above trace of sort_pass with n = 4, it is clear that 3 comparisons were
made. From this it follows that when a list has n elements,
sort-pass makes n - 1 comparisons.To complete the sort of a list, we notice that we could use sort_pass on the list which remains after removing the last (which is now known to be the largest) element. This process should be repeated on successively shorter lists until a list of two numbers is obtained.
sort =: monad define
NB. stop when we have a list of two items
if. nullp rest rest y.
do. sort_pass y.
else. result =. sort_pass y.
(sort drop_last result) append last result
end.
)
sort 9 1 4 2 ==>
1 2 4 9
If
we use the traced_sort, we have:
traced_sort =: monad define
NB. stop when we have a list of two items
entering y.
if. nullp rest rest y.
do. leaving sort_pass y.
else. result =. sort_pass y.
leaving (traced_sort drop_last result) append last result
end.
)
traced_sort 9 1 4 2 ==>
Entering, input = 9 1 4 2
Entering, input = 1 4 2
Entering, input = 1 2
Leaving, result = 1 2
Leaving, result = 1 2 4
Leaving, result = 1 2 4 9
1 2 4 9
This
sort algorithm is referred to as bubble sort. The name is derived from the
fact that as each pass is made, the largest element in a sub-list sinks all the
way to the bottom of the list, displacing each smaller element one position, at
most, upward. As the sort progresses, the larger elements sink quickly to
their lower positions while the smaller elements slowly "bubble" up to their
final resting place, one position per sort_pass.We see that when n = 4, sort-pass is used 3 times, that is, sort_pass is used on the entire list 9 1 4 2 to find the largest element, 9, and put it at the end. Then sort_pass is used again on the list 1 4 2 to find its largest element, 4. Finally, sort_pass is used on 1 2 and that two element list is returned since the elements are in the right order. Next, we count the number of comparisons required to sort this 4 element list. The first sort_pass uses 3 comparisons, the second uses 2 comparisons and the last uses 1 comparison for a total of 6 comparisons.
For a list of n elements, the comparison total is:
c = (n - 1) + (n - 2) +...+ 2 + 1We wish to simplify the formula for c. We can also write c as:
c = 1 + 2 +...+ (n - 2) + (n + 1)Adding these two formulas we have
2 * c = n + n +...+ n + n = n * (n - 1)so that
c = n * (n - 1) % 2) = (n * n % 2) - n % 2Since c involves only terms of n squared and n, we say that the bubble sort algorithm is of order n2. To sort a list of 1000 elements will require almost 500,000 comparisons while sorting a list 1,000,000 elements will require about 1012 comparisons. This number is so large that bubble sorting lists of this size is not practical. There are faster sorting algorithms. Hoare's QuickSort algorithm runs in a time of order n * log2 n.
The execution times for the maximum function, bubble sort and QuickSort are compared in the following chart.

The numeric data for this graph shows that the bubble sort is probably not practical for n values much larger than 1000 and it also shows that even QuickSort will have difficulty sorting large lists.
1000 10000 100000 1000000 n
499500 49995000 4999950000 5E+11 n * (n - 1) % 2
9965.78428 132877.124 1660964.05 19931568.6 n * log2 n
a0 + a1*n1 + a2*n2 +...+ ak*nkwhere n is the size of the problem and the ai 's are constants.
A computation is said to be tractable if its execution time is less than or equal to some polynomial in n. It follows that a computation is intractable if its execution time is greater than any polynomial.
fibonacci =. monad define if. y. < 2 do. y. else. (fibonacci y. - 1) + fibonacci y. - 2 end. )had an execution time of
e*qn + f*rnfor suitably chosen e, q,f and r.
We can verify the exponential behavior of the execution time of fibonacci experimentally. The rather complex J expression:
(11 10 $ 'fibonacci ') ,. format 11 1 $ 15 + i. 11 fibonacci 15 fibonacci 16 fibonacci 17 fibonacci 18 fibonacci 19 fibonacci 20 fibonacci 21 fibonacci 22 fibonacci 23 fibonacci 24 fibonacci 25can be used to create a list of the times of executing fibonacci on problems of size 15 16 17 18 19 20 21 22 23 24 25.
times =: time "1 (11 10 $ 'fibonacci ') ,. format 11 1 $ 15 + i. 11 times 0.030072 0.048091 0.077764 0.125323 0.203099 0.328096 0.526294 0.851216 1.37865 2.23347 3.6165If we divide the list of all but the first times by the list of all but the last times we have:
(1 drop times) % _1 drop times 1.5992 1.61702 1.61158 1.6206 1.61545 1.60409 1.61738 1.61963 1.62004 1.61923This tells us that the ratio of the time it takes to calculate one term in the fibonacci series is about 1.61442 (the average of the above ratios, the actual explicit solution is 0.5 * 1 + 5 ^ 0.5 which equals 1.61803) larger than the time it takes to calculate the previous term in the fibonacci series.
All this means that the execution time of fibonacci is exponential (approximately 1.61442n) which is clearly greater than any polynomial in n. Hence, the fibonacci function above is an example of an intractable computation.
Suppose we wish to move one disk from peg a to peg c. This is an easy task; just move the disk from a -> c.

Next, consider the problem of moving two disks from peg a to peg c.

The moves would be a -> b

a -> c

and b -> c.

The problem of moving three disks from a -> c is a bit more complex. However, it can be decomposed into three smaller problems, namely, moving the top two disks from a -> b, moving one disk from a ->c and moving two disks from b -> c.

The moves for this problem are:
a -> c
a -> b, c -> b a -> c, b -> a b -> c, a -> c
show_move =: monad def 'display (0 from y.) , '' -> '' , 1 from y.'Next we define the function, move, which calculates the moves required for a solution ot the tower of hanoi problem of size n.
move =: monad define
('n' ; 'src' ; 'dst' ; 'hlp') =. y.
if. n = 1
do. show_move src , dst
else. move (n - 1) ; src ; hlp ; dst
show_move src , dst
move (n - 1) ; hlp ; dst ; src
end.
)
Finally,
we define display_tower_hanoi which computes a solution to the tower
of hanoi problem.
display_tower_hanoi =. monad def '0$move y. ; ''a'' ; ''c'' ; ''b'''We now try to analyze display_tower_hanoi to discover the order of execution time for this function. Notice that:
display_tower_hanoi 1
a -> c
display_tower_hanoi 2
a -> b
a -> c
b -> c
display_tower_hanoi 3
a -> c
a -> b
c -> b
a -> c
b -> a
b -> c
a -> c
display_tower_hanoi 4
a -> b
a -> c
b -> c
a -> b
c -> a
c -> b
a -> b
a -> c
b -> c
b -> a
c -> a
b -> c
a -> b
a -> c
b -> c
display_tower_hanoi 5
a -> c
a -> b
c -> b
a -> c
b -> a
b -> c
a -> c
a -> b
c -> b
c -> a
b -> a
c -> b
a -> c
a -> b
c -> b
a -> c
b -> a
b -> c
a -> c
b -> a
c -> b
c -> a
b -> a
b -> c
a -> c
a -> b
c -> b
a -> c
b -> a
b -> c
a -> c
A
little additional analysis shows that display_tower_hanoi n
produces _1+2^n moves. The display-tower-of-hanoi program is
intractable since its order is _1+2^n which is exponential
(exponential times are always greater that polynomial time).
_1+2^64x ==> 18446744073709551615xIf a workstation can generate 1000 moves per second, it will take that workstation approximately 5,849,424 centuries, 17 years, 129 days, 14 hours, 25 minutes and 52 seconds to generate all of the moves. A workstation which is 1000 times faster (generating 1,000,000 moves per second) would still take more than 5,849 centuries to generate all of the moves!

A brute-force algorithm exists which computes each possible path. There are 5 ways to choose the first city in a path, 4 ways to choose the second city, etc. Since the path from city a to city b is the same length as that from city b to city a, there are (5 * 4 * 3 * 2 * 1) % 2 paths to consider. For n cities, the brute-force program requires computing the lengths of (fact n) % 2 paths where:
fact =: monad define if. y. < 2 do. 1 else. y. * fact y. - 1 end. )If n = 20, this algorithm would require computing the lengths of 1216451004088320000 paths between cities. If a workstation can compute the lengths of 10000 paths per second, this algorithm requires approximately 38,573 centuries, 40 years, 302 days, 21 hours, 7 minutes and 12 seconds to finish.
This problem has been studied for more than 50 years and some researchers have found better than brute-force solutions, but no one has found a tractable solution yet! Research continues on this problem.

Here, the brute-force solution (consider all possible positions of the shapes) is a bit harder to describe, since the object orientation must be considered as well. This problem, as of yet, has no tractable solution.
For example, in the game of Checkers, each piece has a small number of moves. At the start of a game, black has 7 possible moves. White has 7 possble responses to some of these moves, but some starting move which black makes have only 2 reasonable responses. It is left as an exercise to the reader to consider some of these possibilities. We can organize the complexity of this decision process by drawing tree diagrams which represent the decision choices.

Samuels, at IBM Research, studied this problem and developed a program which learned to make checkers decisions by playing against a copy of the program which made random choices for moves. The program saved information about decisions which led to wins and learned to avoid decisions which lead to losses. After a learning period of time, the program was able to easily beat Samuels in Checkers games! Brute force attempts to consider all possible choices are clearly intractable.
More complex games, such as Chess, have also been studied. Heuristic algorithms (algorithms which cannot be proven to terminate in a finite amount of time or which do not necessarily lead to an optimal solution) have been used in Chess playing computer programs. Programs exist now which play almost at the Master's level using such methods.
Many problems remain for which only intractable solutions have been found. Such problems provide exciting and challanging research experiences for computer scientists.