Given undirected graph \(G\): does it have an Eulerian cycle? (visiting each edge exactly once)
\[\probEulerianCycle = \setbuild{\encoding{G}}{ G \text{ is an undirected graph with an Eulerian cycle} }\]
Theorem: \(\probEulerianCycle \in \P\)
Proof:
Euler: an undirected graph \(G\) has an Eulerian cycle if and only if every node has an even degree, and all edges belong to a single connected component.
from typing import TypeVar
Node = TypeVar('Node')
type Graph[Node] = tuple[list[Node], list[tuple[Node, Node]]]
def eulerian_cycle(G: Graph[Node]) -> bool:
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)
def degree(node: Node, E: list[tuple[Node, Node]]) -> int:
return sum(1 for (u, v) in E if u==node)
degree takes time \(O(m)\).for loop executes \(n\) iterations, calling degree once each time (other two lines are \(O(1)\)): \(O(nm)\) total.connected takes polynomial time (previous slides).So far we’ve seen problems that can be solved (decided) efficiently (in polynomial time).
Next we’ll look at problems that we do not know how to solve efficiently, but we can efficiently verify a potential solution.
These belong to the complexity class NP (“nondeterministic polynomial time”).
\(\NP\): solvable in polynomial time by a nondeterministic Turing machine
We’ll use a more natural characterization using polynomial-time verifiers.
Let’s see some examples!
Given directed graph \(G\): does it have a Hamiltonian path? (visiting each node exactly once)
\[\probHamPath = \setbuild{\encoding{G}}{ G \text{ is a directed graph with a Hamiltonian path }}\]
Is \(\probHamPath \in \P\)? We don’t know of any polynomial-time algorithm for it!
However, we can efficiently verify a “potential solution” that is given to us: \[ \verifier{\probHamPath} = \setbuild{\encoding{G, p}}{ G \text{ is a directed graph with the Hamiltonian path } p } \]
from typing import TypeVar
from collections.abc import Hashable
Node = TypeVar('Node', bound=Hashable) # needed to put nodes in set
type Graph[Node] = tuple[list[Node], list[tuple[Node, Node]]]
def ham_path_verify(G: Graph[Node], p: list[Node]) -> bool:
V, E = G
# verify each pair of adjacent nodes in p is connected by an edge in `E`
for i in range(len(p) - 1): # O(|p|) = O(n) iterations
if (p[i], p[i+1]) not in E: # O(m) time to search E each iteration
return False
# verify p and V have same number of nodes
if len(p) != len(V): # O(1) time
return False
# verify each node appears at most once in p
if len(set(p)) != len(p): # O(n log n) to build set, removing duplicates
return False
return True
ham_path_verify accepts. (proof: see comments)ham_path_verify is polynomial-time (see source comments), i.e., since the language \(\verifier{\probHamPath} \in \P\), this means that \(\probHamPath \in \NP\).\[ \probComposites = \setbuild{\encoding{n}}{ n \in \N^+ \text{ and } n = p q \text{ for some integers } p, q \ge 2} \]
Is \(\probComposites \in \P\)?
Surprisingly, yes! But this is not obvious (AKS primality test discovered in 2002)
But it’s easy to prove that \(\probComposites \in \NP\).
What is the verification language? \[ \verifier{\probComposites} = \left\{\fragment{\encoding{n, d} \; \big| \; n, d \in \N^+,} \; \fragment{d \text{ divides } n,} \; \fragment{\text{ and } \; 1 < d < n}\right\} \]
What’s an algorithm for deciding \(\verifier{\probComposites}\)?
def composite_verify(n: int, d: int) -> bool:
return n % d == 0 and 1 < d < n
What’s the time complexity in terms of the number of bits \(k = \encoding{n,d}\)?
\[ \probComposites = \setbuild{\encoding{n}}{ n \in \N^+ \text{ and } n = p q \text{ for some integers } p, q \ge 2} \]
Is \(\probComposites \in \P\)?
Surprisingly, yes! But this is not obvious (AKS primality test discovered in 2002)
But it’s easy to prove that \(\probComposites \in \NP\).
What is the verification language? \[ \verifier{\probComposites} = \left\{\encoding{n, d} \; \big| \; n, d \in \N^+, \; d \text{ divides } n, \; \text{ and } \; 1 < d < n\right\} \]
What’s an algorithm for deciding \(\verifier{\probComposites}\)?
def composite_verify(n: int, d: int) -> bool:
return n % d == 0 and 1 < d < n
def composites_decide_slow(n: int) -> bool:
for d in range(2, n):
if composite_verify(n, d):
return True
return False
Now let’s formally define the class \(\NP\).
A polynomial-time verifier for a language \(A\) is a polynomial-time Turing machine \(V\) such that, for some constant \(k\): \[ \fragment{A = \setbuild{x \in \binary^*}{\fragment{\left(\exists w \in \binary^{\le |x|^k}\right)} \; \fragment{V \text{ accepts } \encoding{x, w}}}} \]
\(\NP\) is the class of languages that have polynomial-time verifiers.
We call the language decided by the verifier for \(A \in \NP\) the verification language of \(A\), denoted \(\verifier{A}\).
A clique in a graph \(G\) is a subset of vertices such that every two are connected by an edge.
A \(k\)-clique is a clique with \(k\) vertices.
\[ \probClique = \setbuild{\encoding{G, k}}{ G \text{ is an undirected graph with a } k\text{-clique}} \]
Theorem: \(\probClique \in \NP\).
Proof: the following is a polynomial-time verifier, deciding \[ \verifier{\probClique} = \setbuild{\encoding{G, k, \fragment{C}}}{ G \text{ is an undirected graph } \fragment{\text{with the } k\text{-clique } C}} \]
from itertools import combinations as subsets
def clique_verifier(G: Graph[Node], k: int, C: list[Node]) -> bool:
V, E = G
# verify C is the correct size, and k is not too large
if len(C) != k or k > len(V): # O(1) time
return False
# verify each pair of nodes in C shares an edge
for (u, v) in subsets(C, 2): # O(n^2) time
if (u, v) not in E: return False
return True
Why is this algorithm polynomial-time?
Is the witness short enough?
“Given a collection of integers, can we select some of them that sum to target integer \(t\)?”
\[ \probSubsetSum = \setbuild{\encoding{C, t}}{ C \text{ is a } \textbf{multiset} \text{of integers, and } (\exists S \subseteq C) \; t = \sum_{y \in S} y} \]
Example: \(\encoding{\{4, 4, 11, 16, 21, 27\}, 29} \in \probSubsetSum\),
but \(\encoding{\{4, 11, 16\}, 13} \notin \probSubsetSum\).
Theorem: \(\probSubsetSum \in \NP\).
Proof: the following is a polynomial-time verifier
def subset_sum_verify(C: list[int], t: int, S: list[int]) -> bool:
if sum(S) != t: # verify sum
return False
for x in S: # verify S is a subset of C
if x not in C:
return False
return True
sum(S), with \(|S| = O(n)\).