“P =? NP” is an archetypical question in computational complexity theory, unanswered since its formulation in the 1970s. The question: Is the computional power of polynomially time-bounded programs increased by adding the ability to “guess” (i.e., nondeterminism) ? This is interesting because “polynomial time” is a plausible candidate for “feasibly solvable”.
Perhaps the second most important question is “L =? P”: whether . Here L is the set of problems solvable by
cursor programs. These also run in polynomial time, but have no rewritable
storage111 One take: a cursor program is a multihead two-way read-only finite automaton. A more classical but equivalent version: a 2-tape Turing machine with
One take: a cursor program is a multihead two-way read-only finite automaton. A more classical but equivalent version: a 2-tape Turing machine with-bit read-only input tape 1, that uses at most bits of storage space on read-write tape 2.. Both questions remain open since Cook and Savitch’s pathbreaking papers in the 1970s [4, 16].
We investigate the question “L =? P” from the viewpoint of functional programming languages: a different viewpoint than Turing machines. The link is earlier characterisations of L and P by “cons-free” programs [7, 8, 9]. The net result: a deeper and finer-grained analysis, illuminated by perspectives both from programming languages and complexity theory.
Some new definitions and theorems give fresh perspectives on the question L =? P. We use programs to define and study complexity classes between the two. By [7, 8, 9] cursor programs exactly capture the problem class L; and cursor programs with recursive function definitions exactly capture the problem class P. A drawback though is that recursive cursor programs can run for exponential time, even though they exactly capture the decision problems that can be solved in polynomial time by Turing machines.
The goal of this paper is to better understand the problems in the interval between classes L and P. Problem class NL is already-studied in this interval, and it is the logspace analog of similar long-standing open problems. Kuroda’s two “LBA problems” posed in 1964 : (1) Is dspace() =? nspace() and (2) Is nspace() closed under complementation? After both stood unresolved for 23 years, (2) was finally answered ”yes” (independently in 1987) by Immerman and by Szelepcsényi [6, 17]: NL and larger nondeterministic space classes (with constructive bounds) are closed under complementation.222Kuroda’s other LBA problem dspace() =? nspace() is still open, as well as the question L =? NL.
We study the problems solvable by an in-between class CFpoly: recursive cursor programs that run in polynomial time. Recursion is in some sense orthogonal to the ability to make nondeterministic choices, i.e., to “guess”. The class CFpoly seems more natural than NL from a programming perspective.
1 Complexity by Turing machines and by programming languages
Let be a set of bit strings. The decision problem for : given , to decide whether or not . We say that is in ptime iff it is decidable by a 1-tape deterministic Turing machine that runs within polynomial time [here is the length of its input , and is a constant independent of ]. Further, is in logspace iff it is decidable by a 2-tape Turing machine that uses at most bits of storage space on tape 2, assuming its -bit input is given on read-only tape 1. Both problem classes are of practical interest as they are decidable by programs with running times bounded by polynomial functions of their input lengths. The essential difference is the amount of allowed storage space. These classes are invariant across a wide range of variations among computation models, and it is easy to see that .
However, the question: is has stood open for many years.
Programs and problem decision. Semantics: a program computes a (partial!) function from bit strings to bits:
Program semantics is call-by-value; and means: p does not terminate on input .
A set is decided by a program p if p terminates on all inputs , and for any ,
Complexity by cons-free programs. We use programming languages (several in this paper) to explore the interesting boundary zone between the problem classes logspace and ptime. Strong links were established in [8, 9]: Each class was characterised by a small general recursive functional programming language. The one language (called CF for “cons-free”) is limited to programs without data constructors.333“Cons-free” is not to be confused with “context-free”. TR stands for “tail recursive”. Data access is read-only in both of our languages, so neither CF nor CFTR is Turing-complete. The other, named CFTR, is identical to CF but has restricted control, allowing only tail recursive calls to defined functions.444These ideas stem from S. A. Cook’s seminal work on complexity classes and pushdown machines . Papers [8, 9] re-express and adapt Cook’s ideas to a cons-free programming context. Paper  extends , characterising decision powers of higher-order cons-free programs. CFTR is (yet another) version of the “cursor programs” mentioned in the Introduction. From  we know:
A compact notation relating programs and problems
L is the set of all problems (i.e., sets ) that are decidable by L-programs:
1.2 The cons-free programming language CF
All programs are first-order in this paper,555The larger language described in  encompasses both first and higher-order programs. and deterministic to begin with (until nondeterminism is expressly added in Section 5). First, a brief overview (details in [7, 8, 9]):
Program syntax: a CF-program is a finite sequence of mutually recursive function definitions. Write CF programs and fragments in teletype, e.g., program p or expression tail(f x y). The first definition, of form , defines the program entry function (with one argument, always named x). A context-free grammar for programs, definitions and expressions e:
p ::= def | p def -- Program = sequence of function definitions
def ::= f x1...xm = e -- (where m >= 0)
e ::= True | False |  | xi | base | if e then e else e | f e1...em
base ::= not e | null e | head e | tail e -- Base function call
Data types: bits and bit lists. Variable and expression values are elements of one of the value sets or list . A string is a bit list, and list can be written [1,0,1,1] as data value.  is the empty list, and b:bs is the result of prepending bit b to list bs. We sometimes identify 0, 1 with False and True, resp. e.g., in the test position e0 of an expression if e0 then e1 else e2.
Expressions: Expression e may be a constant; a variable; a base function call (not, null, head, or tail); a conditional if e0 then e1 else e2; or a call f e1...er to some program-defined function f. A program contains no CONS function or other constructors (hence CF for cons-free). Thus CF does not have successor, or explicit storage allocators such as malloc in , :: in ML, or : in Haskell.
Function definition: A program-defined definition has form f x1 x2...xm = e with . No function may be defined more than once, left-side variables must be distinct, and any variable appearing on the right side (in e) must be one of x1,…,xm.
Semantics: The semantics of CF is given by the inference rules in Figure 1. Given a program p and input , these rules define a (unique) computation tree that we call . The inference rules define statements (at the root of the computation tree) or . The p in is a CF program. The e is a subexpression of the right side of some function definition. Further, is an environment that binds the current program variables to respective argument values ; and is the result value of e. Base or defined function calls are evaluated using call-by-value.
The CF program parity decides membership in the set .
entry x = even x
even z = if (null z) then True else not(even(tail z))
This satisfies if is even, else False. For example, .
1.3 The computation tree and an evaluation order
Evaluation steps: The computation tree can be built systematically, applying the rules of Figure 1 bottom-up and left-to-right. Initially we are given a known program p and , and the computation goal is , where is to be filled in with the appropriate value (if it exists). Intermediate goals are of form , where p, and e are known, and ? is again to be filled in.
Axioms have no premises at all, and base function calls have exactly one premise, making their tree construction straightforward. The inference rules for conditional expressions have two premises, and a call to an -ary function has premises. We choose to evaluate the premises left-to-right. For the case if e then e else e , there are two possibly applicable inference rules. However both possibilities begin by evaluating e: finding such that e . Once is known, only one of the two if rules can be applied.
Claim. the evaluation order above for given p and is general: if is deducible by any finite proof at all, then the evaluation order will terminate with result at the root.
A consequence: tree is unique if it exists, so CF is a deterministic language. (Nondeterministic variants of CF will considered later.)
1.4 The suffix property
CF programs have no constructors. This implies that all values occurring in the tree must be boolean values, or suffixes of the input. Expressed more formally:
For program p and input , define its range of variation and its reachable calls by
The computation tree is finite iff p terminates on input .
If contains then and for every .
Proof: a straightforward induction on the depth of computation trees. If the entry function definition is f x = e, then the root has a parent of the form where the initial environment is . Of course is a suffix of . At any non-root node in the computation tree, any new value must either be a Boolean constant or , or constructed by a base function not, head, tail, or null from a value already proven to be in .
Consequence: a CF computation has at most polynomially many different function call arguments.
For any CF program p there is a polynomial that bounds for all .
Proof: By Lemma 2, any argument of any -ary p function f has at most possible values, so the number of reachable argument tuples for f can at most be .
Remark: this does not necessarily imply that p’s running time is polynomial in . We say more on this in Section 2.
1.5 The tail recursive programming language CFTR
A CFTR program is a CF program p such that every function call occurring on the right side of any defined function is in tail form. This is a syntactic condition; the semantic purpose is to enable a no-stack CFTR implementation, as outlined in Section 2.1. The operational intention is that if a call f e1...em is in tail form then, after the call has been performed, control is immediately returned from the current function: there is “nothing more to do” after the call.
Define function as follows, where the set of descriptions is ordered by . The intention: if expression e contains no function calls then , else if all function calls in e are in tail form then , else if e contains a function call in one or more non-tail-call positions then .
CF program p is in CFTR iff for all function definitions f x1...xm = e.
Some instances in the program parity, to clarify the definition:
. Neither calls any program-defined functions.
Expression even x is a call, and is in tail form: .
The expression even(tail z) is in tail form: .
The expression not(even(tail z)) is not in tail form: .
By point 4, parity is not a CFTR program. However the set it decides can be decided by the following alternative program whose recursive calls are in tail form. Thus is CFTR decidable.
entry x = f x True
f x y = if (null x) then y else f (tail x) (not y)
A very useful result from  is the following: we can assume, without loss of generality, that a CF program contains no nested function calls such as f x1...xm = ...g(h(e))....
For any CF program p there is a CF program p such that p = p, and for each function definition in p, either , or and T for .
An interesting step in the proof of Lemma 4 is to show that a function is CF-computable if and only if its graph is CF-decidable (see  for details). In a CFTR program, no function calls are allowed to occur in the test part of a tail-recursive conditional expression (by the last line of the definition). Thus Lemma 4 does not rule out a call nested in a conditional’s test part, and so does not imply that p is tail-recursive.
2 About time and space measures
Theorems 1 and 2 relate computations by two rather different computation models: cons-free programs and Turing machines. We now focus on time and space relations between the two. We define the time and space used to run given CF program p on input :
is the number of evaluation steps to run program p on (as in Section 1.3).
is the number of bits required to run p on input .
Both and are non-negative integers (or for nontermination). We call the “native time” for CF or CFTR (in contrast to Turing machine time). For the example, . It is reasonably simple to define running time as , i.e., the number of nodes in the computation tree .
A full definition of requires a lower-level execution model than the abstract semantics above. (Section 2.1 will show a bit more detail.) Consider the parity example. The length of the call stack needed to implement the program is linear in the depth of the proof tree, and so . Moreover, each non-root node has an environment that binds x or z to its value. By Lemma 2 any such value is a suffix of the input. Any suffix can be represented in bits, so the total space that parity uses is
2.1 Space usage: a lower-level operational semantics, and the tail call optimisation
To more clearly define space usage and present the tail call optimisation, we use a finer, more operational level of detail based on traditional implementations666Readers may think of machine-code run-time states, e.g., value stacks, stack frames,…; or of tapes and state transition relations. Such implementation paraphernalia are well-known both to compiler writers and programming language theorists.. Programs are executed sequentially as in the proof-tree-building algorithm seen earlier. The expression on the right side of a function definition is evaluated one step at a time as in Section 1.3; but with an extra data structure, a stack used to record some of the environments seen before. The stack’s topmost activation record corresponds closely to the current environment .
Each defined function f x1...xm = e has an activation record format that contains the values of the arguments with which f has been called (plus shorter-lived “temporary results,” i.e., as-yet-unused subexpression values). On any call to f, a new activation record is pushed onto the stack . This activation record will be popped when control returns from the call.
The call history for CF program p and input is a sequence (finite or infinite) of configurations, each of form where f is a defined function name and is an environment. The first configuration always has form .
How the syntactic CFTR condition in Definition 5 allows more efficient implementation:
The tail call optimisation
Claim: every CFTR program can be implemented using at most one stack frame at a time.
Suppose CF program p has a definition f x1...xr = ...(g e1...es)... where the g call is a tail call. If so, then after the call (g e1...es) has been started there can be no future references to the current values of x1,...,xr. Informally, there is “nothing more to do” after a tail call has been completed. (One can review the parity’ and parity examples in this light.)
The tail call optimisation: The new activation record for g overwrites f’s current activation record. In a CFTR program every call will be a tail call. Thus there is never more than one frame at a time in the runtime stack. One stack frame requires bits. For example, this gives parity a major improvement over the space used by parity: bits.
A relaxation of the tail call restriction. Sketch (details in ): It is sufficient that the functions defined in p can be given a partial order such that no call is to another that is earlier in the order, and recursive calls must be tail calls. Under this lighter restriction there may be more than one frame at a time in the runtime stack; but these frames are properly ordered by . Consequently every restricted program can be implemented within a constant depth of stack frames.
2.2 CFTR programs run in polynomial time; but CF can take exponential time
Any terminating CFTR program runs in (native) time polynomial in .
Proof: in the call history of p on input , all runtime configurations must be distinct (else the program is in an infinite loop). There is a constant bound on the number of stack frames. Thus, by Lemma 3 there are only polynomially many distinct runtime configurations.
A terminating run of a CF program can take time exponential in its input length.
First, a trivial example with non-polynomial running time (from ):
f x = if (null x) then True else
if f(tail x) then f(tail x) else False
It is easy to see that due to the repeated calls to tail x. Exponential time is not necessary, though, since the computed function satisfies f(x) = True.
2.3 A more fundamental example: a CF-decidable problem that is ptime-complete
is the Monotone Circuit Value Problem. Technically it is the set of all straight-line Boolean programs whose last computed value is True, e.g.,
is a well-known “hardest” problem for ptime . In complexity theory, completeness means two things: (1) ; and (2) if is any problem in ptime, then is logspace-reducible777Details may be found in , Chapters 25, 26. to . By transitivity of logspace-reduction, if and only if .
Following is the core of a Haskell program to decide membership in , together with a sample run. We chose a compact Boolean program representation, so the program above becomes a list:
program = [ OR 5 4 3, OR 4 3 2, AND 3 2 0, OR 2 1 0 ]
Haskell encoding: Assignment is represented by omitting , and coding as AND in prefix position (and similarly for ). The program is presented in reverse order; and variables X0, X1 always have predefined values False, True resp., so their assignments are omitted in the representation.
type Program = [Instruction]
data Instruction = AND Int Int Int | OR Int Int Int
mcv :: Program -> Bool
vv :: (Int,Program) -> Bool
mcv ((OR lhs x y):s) = if vv(x,s) then True else vv(y,s)
mcv ((AND lhs x y):s) = if vv(x,s) then vv(y,s) else False
vv(0,s) = False -- vv(v,s) = value of variable v at program suffix s
vv(1,s) = True --
vv(v,s) = case s of
((AND lhs x y):s’) -> if v==lhs then mcv(s) else vv(v,s’)
((OR lhs x y):s’) -> if v==lhs then mcv(s) else vv(v,s’)
This works by recursive descent, beginning at the end of the program; and uses no storage at all (beyond the implicit implementation stack). Following is a trace of nontrivial calls to vv, ended by the final result of running p. Note that variable X2 is evaluated repeatedly.
program = [X5:=X4 OR X3, X4:=X3 OR X2, X3:=X2 AND X0, X2:=X1 OR X0]
vv(X4,[ X4 := X3 OR X2, X3 := X2 AND X0, X2 := X1 OR X0])
vv(X3,[ X3 := X2 AND X0, X2 := X1 OR X0])
vv(X2,[ X2 := X1 OR X0])
vv(X2,[ X3 := X2 AND X0, X2 := X1 OR X0])
vv(X2,[ X2 := X1 OR X0])
Re-expressing the Boolean program as a bit string for CF input
Of course CF programs do not have data values of type Int. However, any straight-line Boolean program can be coded by a CF bit string. Example: the Boolean program above can be coded as a 44-bit string888The -- parts delimit comments, and are not part of the Boolean program’s bit-string encoding.:
[1,1,1,0, 1,0,1, 0, 1,0,0, 0,1,1, -- x5 := OR x4 x3
1,0,0, 0, 0,1,1, 0,1,0, -- x4 := OR x3 x2
0,1,1, 1, 0,1,0, 0,0,0, -- x3 := AND x2 x0
0,1,0, 0, 0,0,1, 0,0,0] -- x2 := OR x1 x0
Bag of tricks: The program has 5 variables. The index i of any variable xi can be coded by a -bit binary sequence we denote by , e.g., x3 is coded by = 0,1,1. This has length since .
The beginning 1,1,1,0 of the program code gives (in unary) the code block length, for this program. A Boolean assignment xi := xj op xk is coded as , where the Boolean operators are coded as 0, 1 respectively.
Using this encoding, it is not hard to transform the Haskell-like program to use if-then-else statements rather than case; and then implement it in the true CF language.
A dramatic contrast in running times
The CF program just sketched has repeated (and nested) function calls with the same argument values. For example, Boolean variable X2 in program p is evaluated 3 times in the trace above. More generally, the running time bound is exponential:
On the other hand, one can show that can be decided by a Turing machine in polynomial time: Execute the Boolean instructions from the beginning, store the values of left side variables when computed, and refer to stored variable values as needed.
This major difference is due to data storage: the Turing machine tape can save already-computed values, to be looked up as needed. A CF program has no such storage, and must resort to recomputation999Duplicating CF variables does not suffice, since the number of variables is independent of the length of the input data..
Is this contrast necessary? It seems strange that the cost of strengthening tail recursive programs (in CFTR) by adding recursion (to get CF) is to raise run times from polynomial to exponential. The next sections, abbreviated from , show that the problem is a general one of the relation between logspace and ptime. Thus it is not peculiar to the MCV problem, nor to the way we have programmed its solution.
3 How and are proven
3.1 Proof sketch of Theorem 1, that
For , we simulate a CFTR program p by a logspace Turing machine (informal). Given an input , by Lemma 2 any reachable configuration satisfies for . Each can be coded in bits. Now f and the number of f’s arguments are independent of , so an entire configuration can be coded into bits.
The remaining task is to show that the operational semantics of running p on can be simulated by a logspace Turing machine. The key: because p is tail recursive, there is no nesting of calls to program-defined functions. The construction can be described by cases:
Expressions without calls to any defined function: Suppose p’s current environment is represented on the Turing machine work tape. Simulating this evaluation is straighforward.
Evaluation of an expression f e1…em: Since p is tail recursive, none of e1,...,em contains a call, and there is “nothing more to do” after the call f e1…em has been simulated. Assume inductively that e1…em have all been evaluated. Given the definition f x1…xm = e of f, the remaining steps101010These steps are done using Turing machine representations of the environments as coded on Tape 2. are to collect the values construct a new environment , and replace the current by .
Remark: given an input , the number of times that a call appears in the call history of may be much larger than the number of such calls with distinct argument values, even exponentially larger.
3.2 Proof sketch of Theorem 2, that
For , we simulate a ptime Turing machine by a CF program. (this is the core of Proposition 6.5 from ). Assume Turing machine is given111111As usual is a finite set with initial state , and transition function of type . A total state is a triple where and the tape is . Scan positions are counted from the tape left end. Transition means: if is in state and is the scanned tape symbol, then change the state to , write in place of the scanned symbol, and move the read head from its current position to position . and that it runs in time at most for inputs of length . Consider an input .
Idea: in the space-time diagram of ’s computation on (e.g., Fig. 3 in ), data propagation is local; the information (control state , symbol , whether or not it is scanned) at time and tape position is a mathematical function of the same information at time and tape positions . This connection can be used to compute the contents of any tape cell at any time. Repeating for steps gives the final result of the computation ( is accepted or not accepted).
The simulating CF program computes functions
= the state that is in at time = the scanning position at time = the tape symbol found at time and scanning position
Initially, at time we have total state where for , else . Now suppose and the total state at time is , and that . Then the total state at time will be where and if .
Construction of a CF program z with definitions of state, position and symbol is straightforward. Arguments can be uniquely encoded as tuples of suffixes of . It is not hard to see that such an encoding is possible;  works out details for an imperative version of the Turing machine (cf. Fig. 5).
For , we simulate an arbitrary CF program p by a ptime Turing machine (call it ). We describe the computation informally. Given an input , will systematically build a cache containing . By Lemma 3 its size is polynomially bounded.
The cache (call it ) at any time contains a set of triples where the simulator has completed evaluation of function f on argument values in environment , and this f call has returned value .
Concretely, is built “breadth-first”: when a p call f e1…em is encountered (initially is empty):
First, arguments e1,…,em are evaluated to values ; and these are collected into an environment .
Second, cache is searched. If it contains , then simulation continues using the value as the value of f e1…em.
If contains no such triple, then p must contain a function definition f x1...xm = e. Then expression e is evaluated in environment to yield some value . After finishing, add the triple to , and return the value .
An observation: The CF program of Section 3.2 (simulating a polynomial time Turing machine) has exponential runtime.
Paradoxically, as observed in Section 2.2: even though CF exactly characterises ptime, its programs do not run in polynomial time. The polynomial versus exponential contrast between the running times of CFTR and CF programs is interesting since both program classes are natural; and the decision powers of CFTR and CF are exactly the complexity classes logspace and ptime. Alas, we have found no CF algorithm to simulate polynomial-time Turing machines. We explain how this happens in more detail.
Call overlap occurs if a CF program can call the same function more than once with the same tuple of argument values.
How can this happen? By Lemma 3, only polynomially many argument tuples are distinct. Consequently, superpolynomial running time implies that some simulation functions may be called repeatedly with the same argument value tuples, even though only polynomially many of them are distinct.
This can be seen in the proof that : the value of may depend on the values of , and , and . In turn, the value of may depend on the values of , and , and .
Net effect: a symbol a on the final tape (with ) may depend many distinct function call sequences that “bottom out” at . The number of call sequences may be exponential in .
Call overlap will occur if the length of the call history for is greater than .
Unfortunately, it is is hard to see which calls will overlap (it seems impossible without storage). Furthermore, the “caching” technique used to prove Theorem 2 cannot be used because, in contrast with Turing machines, CF programs do not have a memory that can accumulate previously computed values.
4 Closer to the boundary between CFTR and CF
Viewed extensionally, the difference (if any) between CFTR and CF corresponds to the difference (if any) between logspace and ptime. Viewed intensionally, there seem to be significant differences, e.g., in algorithm expressivity as well as in running time. A relevant early result:
A program transformation by Greibach shows that programs whose calls (to defined functions) are all linear121212A call is linear if it is not contained in a call to any defined function. An example with a linear call that is not a tail call: not(even(tail z)) in Section 1.5. can be made tail recursive . Using this transformation, a CF program p whose calls are tail calls or linear calls may be transformed into one containing only tail calls (and no CONS or other constructors), so it is in CFTR. Thus p decides a problem in logspace.
There is a price to be paid, though: in general, the transformed program may run polynomially slower than the original, due to re-computation. For instance, the Greibach method can transform our first “parity” program into CFTR form. The cost: to raise time complexity from linear to quadratic.
Following is a new tool to investigate the problem. It is analogous to the set of programs for polynomial-time Turing machines, but adapted to the CF world. By Theorem 4, CFpoly CF.
The programming language CFpoly has the same semantics as CF; but CFpoly’s programs are restricted to be those CF programs that terminate in polynomial time.
By Theorem 3, every terminating CFTR program is in CFpoly. One can see Greibach’s result as transforming a subset of CFpoly into CFTR.
If a terminating CF program does not have call overlap, then it is in CFpoly.
The reason: by Lemma 3, such a CF program must run in polynomial time.
Remark: On the other hand, it may have non-tail calls, and so not be in CFTR.
Problem class CFpoly is closed under and complement.
5 Nondeterminism and cons-free programs
Nondeterministic programs allow expression evaluation and program runs to be relations () rather than functions. A nondeterministic version of Figure 1 would use and in place of and . Alas, the algorithm of Section 1.3 cannot be used if p is nondeterministic.
A set is decided by an NCF program p if for all inputs ,
5.1 Nondeterministic versions of the CF program classes
Remark: the reasoning used in Theorem 3 clearly extends to show that . The following is particularly interesting since the question logspace =? nlogspace is still open.
This was proven by Bonfante  by a technique that stems back to in Cook . The implication is that CF is closed under nondeterminism, since both problem classes are equal to ptime. (This does not imply ptime = nptime, since it is not hard to see that any NCF program can be simulated by a deterministic polynomial-time Turing machine: Lemma 3 holds for NCF as well as for CF.)
However it is not known whether CFpoly is closed under nondeterminism, since Bonfante and Cook’s reasoning does not seem to apply. Why? The memoisation used in  yields a Turing machine polynomial time algorithm. Consequently, the problem is decidable by some CF program; but we know no way to reduce its time usage from exponential to polynomial.
A “devil’s advocate” remark: by Theorem 2 the classes CFpoly and nlogspace are both between logspace and ptime:
So why bother with yet another class? One answer: CFpoly seems more natural than NCFTR from a programming viewpoint; and intuitively CFpoly seems to be a larger extension of logspace than nlogspace or the program class NCFTR. However we have no proof that , and no solid reason to think that either class contains the other.
5.2 Simulating CFpoly by a nondeterministic algorithm
Proof: If p CFPoly, there is a polynomial such that for any input one can decide, in time , whether or not . The question: can this be done in significantly less space? We answer “yes”. The proof uses a space-economical nondeterministic algorithm to find at the root of tree . First, observe that any reachable statement can be represented in bits.
To evaluate p on input , we nondeterministically guess a value such that . The remaining task is to confirm that this statement is true. If we cannot do that, the whole algorithm has failed.
To confirm a statement such as , we must confirm the existence of an evaluation tree of the form: … … To do this, we now guess , and now need to confirm two statements. We also guess which of these two statements has the shortest evaluation tree.
For example, suppose we guess that this is the case for . Then we do a recursive call to confirm this statement (which temporarily stores the current state on the algorithm’s stack). Afterwards, we tail-recursively confirm the other statement, . Since this is a tail call, the algorithm’s stack size does not increase.
This extends naturally to multi-argument function calls.
To confirm a statement , we guess whether reduces to True or False; for example, we guess that it reduces to False. Then it suffices to confirm that an evaluation tree of the following form exists: … False … For this, we again have to confirm two statements. As before, we guess which of the two statements has the shorter evaluation tree, evaluate that statement first, and then evaluate the other statement tail-recursively.
If all guessed values are correct, then this algorithm returns the correct result. Furthermore, if we always guessed correctly which statement has the shortest evaluation tree, it does so with an overall stack depth that is never more than log(). The reason is that every time we do a non-tail recursive call, this shortest subtree has size at most half of the previous subtree’s size. Since a statement can be represented in bits we have the desired result.
The idea of applying tail-recursion to the largest subtree to save space was also used to implement Quicksort . However our problem is much more general; and we must use nondeterminism because it cannot be known in advance which subtree will be the largest.
We expect that this construction can be extended to show as well.
5.3 Closure under complementation
The following was shown by a subtle nondeterministic state-enumeration and state-counting algorithm. It was devised independently by Immerman and Szelepcsényi, and solved Kuroda’s second and long-standing open question [12, 6, 17]. It is still open whether .
nlogspace is closed under complement.
6 Conclusions, future work
We have probed the question logspace =? ptime from a programming language viewpoint. A starting point was that the “cons-free” programming language CF exactly captures the Turing complexity class ptime; while its cons-free tail recursive subset CFTR exactly captures logspace. Section 3 recapitulates the reasoning used in .
In more detail: all CFTR programs run in polynomial time; but on the other hand, some CF programs run for exponentially many steps. Further the sets decided by the two program classes have seemingly different closure properties: the questions logspace =? nlogspace and ptime =? nptime and even logspace =? ptime have been open for decades. Given this, it seems almost paradoxical that (from ).
Trying to understand these differences made it natural to consider CFpoly - the class of polynomially time-bounded CF programs since they have feasible running times (even though some CF programs may have superpolynomial behavior=. One test of CFpoly was to see whether it contained any ptime-complete problems. As a case study we wrote (Section 2.3) a CF-program for MCV (the monotone circuit value) problem to clarify where non-tail-recursion was necessary, and where superpolynomial runtimes came into the picture. (One key was nested recursion in function calls, clearly visible in function mcv in the MCV code.) Another test was to see whether CFpoly is perhaps a smaller complexity class than ptime. Theorem 6 leads in this direction, with an interesting proof construction and the surprising upper bound .
Many questions for CFpoly are still to be investigated. One is to see whether the Immerman-Szelepcsenyi algorithm can be adapted to NCFpoly.
-  Siddharth Bhaskar & Jakob G. Simonsen (2020): Implicit Complexity via Controlled Construction and Destruction. Technical Report, Department of Computer Science, University of Copenhagen.
-  Guillaume Bonfante (2006): Some Programming Languages for Logspace and Ptime. In Michael Johnson & Varmo Vene, editors: Algebraic Methodology and Software Technology, 11th International Conference, AMAST 2006, Kuressaare, Estonia, July 5-8, 2006, Proceedings, Lecture Notes in Computer Science 4019, Springer, pp. 66–80, doi:10.1007/11784180_8.
-  Stephen A. Cook (1971): Characterizations of Pushdown Machines in Terms of Time-Bounded Computers. J. ACM 18(1), pp. 4–18, doi:10.1145/321623.321625.
-  Sheila A. Greibach (1975): Theory of Program Structures: Schemes, Semantics, Verification. Lecture Notes in Computer Science 36, Springer, doi:10.1007/BFb0023017.
-  Neil Immerman (1988): Nondeterministic Space is Closed Under Complementation. SIAM J. Comput. 17(5), pp. 935–938, doi:10.1137/0217058.
-  Neil D. Jones (1997): Computability and complexity - from a programming perspective. Foundations of computing series, MIT Press, doi:10.7551/mitpress/2003.001.0001.
-  Neil D. Jones (1999): LOGSPACE and PTIME Characterized by Programming Languages. Theor. Comput. Sci. 228(1-2), pp. 151–174, doi:10.1016/S0304-3975(98)00357-0.
-  Neil D. Jones (2001): The expressive power of higher-order types or, life without CONS. J. Funct. Program. 11(1), pp. 55–94, doi:10.1017/S0956796800003889. Available at http://journals.cambridge.org/action/displayAbstract?aid=68581.
-  Donald E. Knuth (1973): The Art of Computer Programming, Volume III: Sorting and Searching. Addison-Wesley.
-  Cynthia Kop & Jakob Grue Simonsen (2017): The Power of Non-determinism in Higher-Order Implicit Complexity - Characterising Complexity Classes Using Non-deterministic Cons-Free Programming. In Hongseok Yang, editor: Programming Languages and Systems - 26th European Symposium on Programming, ESOP 2017, Held as Part of the European Joint Conferences on Theory and Practice of Software, ETAPS 2017, Uppsala, Sweden, April 22-29, 2017, Proceedings, Lecture Notes in Computer Science 10201, Springer, pp. 668–695, doi:10.1007/978-3-662-54434-1_25.
-  Sige-Yuki Kuroda (1964): Classes of languages and linear-bounded automata. Information and Control 7(2), pp. 207–223, doi:10.1016/S0019-9958(64)90120-2.
-  John McCarthy (1960): Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I. Commun. ACM 3(4), pp. 184–195, doi:10.1145/367177.367199.
-  Yiannis Moschovakis (2019): Abstract Recursion and Intrinsic Complexity. Cambridge University Press (Lecture Notes in Logic), doi:10.1017/9781108234238.
-  Christos H. Papadimitriou (1994): Computational complexity. Addison-Wesley.
-  Walter J. Savitch (1970): Relationships Between Nondeterministic and Deterministic Tape Complexities. J. Comput. Syst. Sci. 4(2), pp. 177–192, doi:10.1016/S0022-0000(70)80006-X.
-  Róbert Szelepcsényi (1988): The Method of Forced Enumeration for Nondeterministic Automata. Acta Inf. 26(3), pp. 279–284, doi:10.1007/BF00299636.
-  Alan M. Turing (1936-7): On Computable Numbers with an Application to the Entscheidungsproblem. Proceedings of the London Mathematical Society 42(2), pp. 230–265, doi:10.1112/plms/s2-42.1.230.