Cook's Theorem: CNFSAT is NP-complete


Part I. CNFSAT is in NP.

Non-determinisitc Algorithm:

Step 1: Go through the instance of CNFSAT and list each Boolean variable used in the expression. (O(n2) steps if n is the total size of the CNFSAT instance.)
Step 2: Loop through the list of variables, and for each one ask the "CHOICE" function whether it should be true or false. Write down whether it should be true or false along with the variable. (O(n2) steps again.)
Step 3: Replace each instance of a variable in the CNFSAT instance with the true or false value you have created. (O(n2) again.)
Step 4: Go through the CNFSAT expression and make sure every sum contains at least one true. (O(n2) again.)

If Step 4 verifies that every sum contains at least one true in each sum then the instance is satisfiable. If not, it is not since the "CHOICE" would have given a correct value to satisfy the expression if the expression could be satisfied.

Part II: Every problem in NP can be translated into an instance of CNFSAT in polynomial time.

Let P be a problem in NP. Then there is a non-determinisitic, polynomial-time algorithm to solve P. Suppose we have a program to solve P at the machine-language level. In fact, to simplify things, we are going to assume that we have a one-bit machine containing 1-bit addressable memory. We are going to show how we can translate such a program into an instance of CNFSAT.

From CSCI 150, we know that all operations on a computer can be reduced to multiple AND, OR, and NOT operations together with LOAD/STORE operations, conditional jumps, and a clock to move from one instruction to the next. We are going to show how to implement AND, OR, NOT, "CHOICE" calls, conditional jumps, and a clock and conclude that since the algorithm can be implemented in this "low-level" way that the program can be translated into an instance of CNFSAT.

Here is how we do it:

We translate a given nondeterministic program into a logical expression describing the complete execution of the program.

First, our transformed program will need a concept of time (corresponding to the system clock), memory (in our case 1-bit addressable memory), and, of course, the transformed program.

Let us suppose to start that we have L instructions in the nondeterministic program. Let p(n) be a polynomial and let n be the length of an instance of the problem. Suppose p(n) is a polynomial that is "big enough" that the nondeterministic program will solve an instance of this problem of size n in time <=p(n) if the instance should return true. (Remember, we are just doing decision problems - problems that return true or false.)

Our "clock", then, is just a variable t representing time that "runs" for as long as the problem needs to run to get an answer.

Since the program finishes in less than p(n) time, we will only let the variable t run from 0 to p(n). If t becomes larger than p(n) we will assume the instance could not return true so it will return false. In addition, there is the interesting fact that if the program runs in less than p(n) time, it cannot possibly access more than p(n) memory locations. We will picture our memory as pairs of addresses and bits and a load or store of a memory bit will, then, simply mean going through that list until you find the matching address and, then, modifying that bit. Thus each memory access could take p(n) time, there could be p(n) times, and there could be at most p(n) memory locations. We have, then, modified our program so that it could take p(n)2 time but since this is still a polynomial we are ok with it. In fact, let us call the new polynomial q(n). Now let time t run from 0 to q(n)+1.

Let E(i,t) be a series of logical variables meaning that instruction i is executed at time t if E(i,t) is true. Let M(j,t) be a series of Boolean variables representing the contents of memory bit j at time t. Notice that we have a constant number of instructions (L) and time running from 0 to q(n)+1 as above, so we have at most L*(q(n)+2) of these E variables. Since both j and t could be q(n) different values, we have at most q(n)2 of the M variables.

We will deal with the following instructions:

if(M) goto k and M=NT and M=N+T and CHOICE() and M=N and M=N'

where the if is obvious, M=NT means M receives the AND of N and T,  M=N+T means M receivers the OR of N and T, CHOICE() sets a memory location called G to true or false and M=N does a simple assignment.

Here are the logical steps in the solution:

  1. At time 0 the first instruction will be executed. This is represented by E(1,0)E'(2,0)E'(3,0)...E'(L,0) where this is the and of these variables and where E'(i,j) means the negation of E(i,j). Note this is simple CNF.
  2. At time 0 the memory contains the instance of the problem to be run. Whatever the bits required - say, for example, 101111001....010 - we would then have M(0,0)M'(1,0)M(2,0)M(3,0)M(4,0)M(5,0)M'(6,0)M'(7,0)M(8,0)...M'(n-3,0)M(n-2,0)M'(n,0) as a simple AND to represent that condition. (Here M' represents 0, M represents 1.)
  3. At any time t, only one instruction can be executed. This is represented at a particular time t by E(1,t) => E'(2,t)E'(3,t)...E'(L-1,t)E'(L,t), E(2,t) => E'(1,t)E'(3,t)...E'(L-1,t)E'(L,t), ...  E(L,t) => E'(1,t)E'(2,t)E'(3,t)...E'(L-1,t). Note that since A => B is equivalent to A'+B, we can rewrite this as (E'(1,t)+E'(2,t)E'(3,t)...E'(L,t))(E'(2,t)+E'(1,t)E'(3,t)...E'(L,t))...(E'(L,t)+E'(1,t)E'(2,t)E'(3,t)...E'(L-1,t)) and this can be fixed by distributing the + over the products to get (E'(1,t)+E'(2,t))(E'(1,t)+E'(3,t))...(E'(1,t)+E'(L,t))(E'(2,t)+E'(1,t))(E'(2,t)+E'(3,t)) ...(E'(2,t)+E'(L,t))...(E'(L,t)+E'(1,t))(E'(L,t)+E'(2,t))(E'(L,t)+E'(3,t))...(E'(L,t)+E'(L-1,t)). If we and this for all t from 1 to q(n)+1 and for all j from 1 to L we will have what we want and it is in CNF.
  4. For any instruction, we can write the effect of that instruction in CNF. The effect always consists of determining which instruction executes next AND what affect the instruction has. (We will demonstrate the CNF for the instructions above.)
  5. The run of the program returns true if and only if at time q(n)+1 the return true instruction is executing. This can be written E(rt,q(n)+1) where rt is the index of the return true instruction.
Clearly, an and of these 4 parts will represent a complete correct run of the program - modulo some hand-waving. Also, if we can rewrite each part of 1-4 in CNF then the and of those parts will be in CNF.

Note 1 2, and 3 are done. (I.e., they are in CNF and obviously correct.) Number 5 can be handled in the following way. Whenever we find ourselves executing a return true instruction, we simply replace it with a return true followed by a statement that  "infinite loops" in the same spot. Hence, E(rt,t) => E(rt,t+1) and since an A => B can be rewritten as A'+B we see that we need only add the and of all statements of the form E'(rt,t)+E(rt,t+1) for each t and, then, the instance comes out true if and only if E(rt,q(t)+1) is true and this and of these or's is in CNF.

Suppose instruction j is if(M(i,t)) then goto k. This is E(j,t) means if(M(i,t)) then k is the instruction at t+1 else j+1 is the instruction at t+1. Thus E(j,t) => ( M(i,t) => E(k,t+1) && M'(i,t) => E(j+1,t+1)). Using the rule for implication gives us E'(j,t)+(M'(i,t)+E(k,t+1))(M(i,t)+E(j+1,t+1)) which is logically equivalent to (E'(j,t)+M'(i,t)+E(k,t+1))(E'(j,t)+M(i,t)+E(j+1,t+1)) which is in CNF. In addition, for every memory location M(i,t+1)=M(i,t) is equivalent to (M'(i,t+1)+M(i,t))(M(i,t+1)+M'(i,t)) should be added as a product which the E'(j,t) should be distributed over but we will leave that off for simplicity. Note that it should be there.

Suppose that instruction j is M(i)=M(k). Then what we have is E(j,t) => [(M(i,t+1)=M(k,t+1)) and E(j+1,t+1)]. Using our distributive law again, we write this as (E'(j,t)+(M(i,t+1)=M(k,t+1)))(E'(j,t)+E(j+1,t+1)). We need to show that E'(j,t)+(M(i,t+1)=M(k,t+1)) can be put in CNF. If we use product of sum formulation from a truth table we see that A=B <=> (A+B')(A'+B)  so that E'(j,t)+(M(i,t+1)=M(k,t+1)) is equivalent to E'(j,t)+(M(i,t+1)+M'(k,t+1))(M'(i,t+1)+M(k,t+1)) and distributing + gives us the expression (E'(j,t)+M(i,t+1)+M'(k,t+1))(E'(j,t)+M'(i,t+1)+M(k,t+1)) and when we and this with (E'(j,t)+E(j+1,t+1)) the result is clearly in CNF. Again, we should add an and of M(m,t+1)=M(m,t) for every m not equal to i as above. Again, we keep it simple by not doing this.

Suppose that instruction j is CHOICE(). What should it do? Well, it should set M(G,t+1) to true if that will work or to false if that will work. Thus, all we can say is that the choice could be either one but if it can be done it will be chosen so as to make the program work and, therefore, the corresponding CNF expression to work. This means that CHOICE() should advance and allow M(G,T+1) to be either value in the CNF so that the expression can give the value to it that will make the expression come true. In other words, E(j,t)=>(M(G,t+1)+M'(G,t+1)) && E(j+1,t+1). This becomes E'(j,t)+(M(G,t+1)+M'(G,t+1)E(j+1,t+1) which by distribution is equivallent to (E'(j,t)+M(G,t+1)+M'(G,t+1))(E'(j,t)+E(j+1,t+1)) which is in CNF. Again, all other memory is preserved.

Suppose instruction j is M(i)=M(k)M(h). Recall that A=B can be written as (A+B')(A'+B) we get that instruction j at time t is E(j,t) => (M(i)+(M(k)M(h))')(M'(i)+M(k)M(h)). (M(k)M(h))' is (M'(k)+M'(h)) so this becomes (M(i)+M'(k)+M'(h))(M'(i)+M(k))(M'(i)+M(h)) so using the rule for => and distributing E'(j,t) we represent this instruction by (E'(j,t)+M(i,t+1)+M'(k,t)+M'(h,t))(E'(j,t)+M'(i,t+1)+M(k,t))(E'(j,t)+M'(i,t+1)+M(h,t))(E'(j,t)+E(j+1,t+1)) in CNF form.

M(i)=M(k)+M(h) and M(i)=M'(k) are left as exercises.

Note that we have part 1 taking at most O(q2(n)), part 2 takes at most O(q2(n)), part 3 takes O(L2q2(n)) which is O(q2(n)), part 4 takes O(q(n)4) steps and part 5 takes only a slight rewrite of the return instruction. All that gets added is the one ending statement E(rt,q(n+1)+1) anded with everything else.

QEAD (Quid Errat almost demonstratum).