We denote by \(\P\) the class of languages decidable in polynomial time by a (deterministic) Turing machine: \[ \P = \bigcup_{k = 1}^\infty \time{n^k} \]
Example problem in \(\P\):
3Sum Problem:
Given a set \(A\) of \(n\) integers (\(A \subset \Z\), \(|A| = n\)), check if there are three elements that sum to zero (i.e., \(a + b + c = 0\) for \(a, b, c \in A\)).
def three_sum_1(A):
for i in range(len(A)):
for j in range(i, len(A)):
for k in range(j, len(A)):
if A[i] + A[j] + A[k] == 0:
return True
return False
We’ll next show that the following problems are in \(\P\) by exhibiting and analyzing algorithms:
Integers \(a, b \in \N^+\) are called relatively prime if their greatest common divisor \(\gcd(a, b) = 1\).
\[ \probRelprime = \setbuild{\encoding{a, b}}{ a,b \in \N^+ \text{ and } \gcd(a, b) = 1 } \]
Theorem: \(\probRelprime \in \P\)
Proof: We can compute \(\gcd(a, b)\) efficiently using the Euclidean algorithm: (c. 300 BC!)
def gcd(a: int, b: int) -> int:
# Order so a ≥ b (simplifies analysis)
if (a < b): a, b = b, a
while b > 0:
a = a % b
a, b = b, a
return a
def rel_prime(a: int, b: int) -> bool:
return gcd(a, b) == 1
Simply checking each integer up to \(b\) would take exponential time!
Directed reachability problem:
\[ \probPath = \setbuild{\encoding{G, s, t}}{ G \text{ is a directed graph containing a path from node } s \text{ to } t } \]
Theorem: \(\probPath \in \P \hspace{1em}\) (Use a breadth-first search)
def path(G, s, t):
V, E = G
seen = {s}
q = collections.deque([s])
while len(q) > 0:
node = q.popleft()
neighbors = [v for (u,v) in E if u==node]
for v in neighbors:
if v == t:
return True
if v not in seen:
seen.add(v)
q.append(v)
return False
while
loop run? At most \(n\) (each node enters q
at most once).neighbors
take?for
loop run?This is \(O(n \cdot m + m)\).
Could we do better?
Given an undirected graph \(G\), check if it is connected.
\[\probConnected = \setbuild{\encoding{G}}{ G \text{ is connected} }\]
Theorem: \(\probConnected \in \P\)
Simple approach for handling undirected graphs:
convert them to an equivalent directed graph (of proportional size) in polynomial time.
def add_reverse_edges(G):
V,E = G
reverse_edges = []
for (u,v) in E:
if (v,u) not in E and (v,u) not in reverse_edges:
reverse_edges.append((v,u))
return (V, E+reverse_edges)
Now just check if all pairs of nodes are connected by a path:
def connected(G):
V,E = G
for (s,t) in itertools.combinations(V,2):
if not path(G,s,t):
return False
return True
Given an undirected graph \(G\), check if an Eulerian cycle exists.
\[\probEulerianCycle = \setbuild{\encoding{G}}{ G \text{ is an undirected graph withan Eulerian cycle} }\]
Theorem: \(\probEulerianCycle \in \P\) Proof:
Euler: an undirected graph \(G\) has an Eulerian cycle if every node has an even degree, and all edges belong to a single connected component.
We can verify this in polynomial time:
def degree(node, E):
return sum(1 for (u,v) in E if u==node)
def eulerian_cycle(G):
V,E = G
V_pos = [] # nodes with positive degree
for u in V:
deg = degree(u, E)
if deg % 2 == 1: return False
if deg > 0: V_pos.append(u)
G_pos = (V_pos,E)
return connected(G_pos)