Python implementation of non-determinism
It is possible to use python decorators to implement non-determinism.
First some definitions. In theoretical computer science, non-determinism is used to abstract models of computation which are really interesting to capture stratification of some complexity classes.
A non-deterministic Turing machine is allowed to choose along its execution some transitions. See the wikipedia page for a formal description.
Students often struggle to understand how non-determinism works and
often confuse it with randomised algorithms. It is possible to provide a
Pythonic
variant of a non-deterministic Turing machine by
allowing the Python program to guess some boolean.
For instance, the following program that can’t actually run:
def kclick( V: tuple[int], E: set[tuple[int, int]], k: int) -> bool:
"""
Return True if the input graph contains a k-clique.
See: https://en.wikipedia.org/wiki/Clique_problem
"""
= []
possible_subset for n in V:
if guess():
possible_subset.append(n)for node1 in possible_subset:
for node2 in possible_subset:
if node1 == node2:
continue
if (node1, node2) not in E and (node2, node1) not in E:
return False
return len(possible_subset) => k
Such a function evaluates to True
if and only if there
exists a sequence of guesses such that the final result is
True
. Otherwise the final result is False
.
It is actually possible to rely on high order construction in Python to design a non-deterministic execution of functions.
First we need a class that allows exploring the space of guesses
efficiently. To do that, we just need to choose a way to explore the
binary tree of those guesses, starting with the branch with only
False
and ending with the one with only
True
.
We can implement this through a simple class:
class Guesser:
def __init__(self):
self.current_run = []
self.position = 0
def __call__(self):
if self.position >= len(self.current_run):
self.current_run.append(False)
= self.current_run[self.position]
result self.position += 1
return result
def next(self):
self.current_run = self.current_run[:self.position+1]
if all(self.current_run):
return False
= self.current_run[::-1]
C for i, e in enumerate(C):
if not e:
self.current_run[-i-1] = True
break
else:
self.current_run[-i-1] = False
self.position = 0
return True
With that, we can implement a nice decorator.
from functools import wraps
def choose(fct):
@wraps(fct)
def _fct(*args, **kwargs):
= Guesser()
guess while True:
if fct(guess, *args, **kwargs):
return True
if not guess.next():
return False
return _fct
And now, we can actually run our non-deterministic program:
@choose
def kclique(guess, V: tuple[int], E: set[tuple[int, int]], k: int) -> bool:
"""
Return True if the input graph contains a k-clique.
See: https://en.wikipedia.org/wiki/Clique_problem
"""
= []
possible_subset for n in V:
if guess():
possible_subset.append(n)for node1 in possible_subset:
for node2 in possible_subset:
if node1 == node2:
continue
if (node1, node2) not in E and (node2, node1) not in E:
return False
return len(possible_subset) >= k
print(kclique([0, 1, 2, 3], set(((0, 1), (1, 2), (2, 0), (3, 1))), 3))
True
We can also give a negative example:
print(kclique([0, 1, 2, 3, 4], [(0, 1), (1, 2), (2, 3), (4, 0), (4, 2)], 3))
False
Of course, it is not really efficient but it makes non-determinism a bit more concrete.
We can of course change the setup a little to handle non-boolean
output. By taking the convention that the function is successful if the
result evaluates to False
, we can rewrite the decorator as
follows:
def choose(fct):
@wraps(fct)
def _fct(*args, **kwargs):
= Guesser()
guess while True:
= fct(guess, *args, **kwargs)
res if res:
return res
if not guess.next():
return res
return _fct
Now we can find the actual clique:
@choose
def kclique(guess, V: tuple[int], E: set[tuple[int, int]], k: int) -> list[int] | None:
"""
Return a k-clicque if it exists and None otherwise
See: https://en.wikipedia.org/wiki/Clique_problem
"""
= []
possible_subset for n in V:
if guess():
possible_subset.append(n)for node1 in possible_subset:
for node2 in possible_subset:
if node1 == node2:
continue
if (node1, node2) not in E and (node2, node1) not in E:
return False
if len(possible_subset) >= k:
return possible_subset
print(kclique([0, 1, 2, 3], set(((0, 1), (1, 2), (2, 0), (3, 1))), 3))
[0, 1, 2]
We can also find a 3-coloration:
from typing import Literal
= Literal[0] | Literal[1] | Literal[2]
colors
@choose
def three_col(guess, V: tuple[int], E: set[tuple[int, int]]) -> dict[int, colors] | Literal[False]:
"""
Return a 3 coloration of the input graph if it exists and False otherwise
See: https://en.wikipedia.org/wiki/Graph_coloring#Properties
"""
= dict()
coloring for n in V:
if guess():
= 0
coloring[n] elif guess():
= 1
coloring[n] else:
= 2
coloring[n]
for x,y in E:
if coloring[x] == coloring[y]:
return False
return coloring
print(three_col([0, 1, 2, 3], set(((0, 1), (1, 2), (2, 0), (3, 1)))))
{0: 2, 1: 1, 2: 0, 3: 2}