Title

ECS 120 Theory of Computation
The class NP
David Doty (adapted from slides authored by Julian Panetta)
University of California, Davis

The complexity class \(\NP\): Problems with efficiently verifiable solutions

  • 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!

\(\probHamPath\)

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
  • \(G\) has a Hamiltonian path if and only if there is some list of nodes \(p\) such that ham_path_verify accepts. (proof: see comments)
  • Since 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\)

\[ \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}\)?

    • \(O(k)\) for the comparison
    • \(O(k^2)\) for the division using the standard grade-school algorithm

Deciding \(\probComposites\) using the verifier

\[ \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
  • Can we decide \(\probComposites\) using the verifier?
    • Yes, but in exponential time!
    def composites_decide_slow(n: int) -> bool:
        for d in range(2, n):
            if composite_verify(n, d):
                return True
        return False
    • Running time: \(\Omega(n) = \Omega(2^{|\encoding{n}|})\) since \(|\encoding{n}| = O(\log n)\)

The class \(\NP\)

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}\).

  • We can measure the running time of \(V\) either with respect to \(|x|\) or \(|\encoding{x, w}|\).
    • Since \(|w| = O(|x|^k)\), these only differ in length by a polynomial.
    • Therefore \(V\) runs in polynomial time with respect to \(|x|\)
      if and only if it runs in polynomial time with respect to \(|\encoding{x, w}|\).

Decision vs optimization problems

  • Our focus on decision problems in this course might seem limiting and unrealistic.
  • Many problems are not posed as a yes/no question.
  • Another very common type of problem is an optimization problem.
    • For example, find the minimum-cost path between two nodes in a weighted graph.
      (“What is the cheapest way to fly from Sacramento to Rome”)
    • Find the maximum clique in a graph.
  • However, we can convert any optimization problem into a decision problem.
    • Pick a threshold \(k\) and ask if there is a solution at least as good as \(k\).
    • For example, “Is there a flight from Sacramento to Rome that fits within my budget of \(k\)?”
      This decision problem is clearly in \(\NP\)!
  • We can then solve the original optimization problem by solving a sequence of decision problems. How? Binary search!
  • If the decision problem is in \(\P\) or \(\NP\), then so is the optimization problem! (“morally”; technically only decision problems are in \(\P\) or \(\NP\))

Decision vs search problems

  • All problems in \(\NP\) can also be thought of as a search problem.
  • Every problem \(A\) in \(\NP\) has a verifier \(V\).
  • Deciding if \(x \in A\) is equivalent to searching for a witness \(w\) such that \(V\) accepts \(\encoding{x, w}\)
    or reporting that none exists.
    • For example, for \(\probHamPath\), we search for a Hamiltonian path \(p\).
    • While the search problem sounds harder (it needs to exhibit \(w\) rather than answer yes/no),
      it is not actually harder.
    • If \(\P = \NP\), then every search problem corresponding to a decision problem in \(\NP\) can be solved in polynomial time.

More examples of problems in \(\NP\): \(\probClique\)

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?

More examples of problems in \(\NP\): \(\probSubsetSum\)

“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
  • Let \(n = \log t + \sum_{x \in C} \log x + \sum_{y \in S} \log y\) be the input length.
  • We must do \(|S| - 1\) additions to compute sum(S), with \(|S| = O(n)\).
  • The each partial sum is at most \(n\) bits, so each addition takes \(O(n)\) time, so \(O(n^2)\) time total.
  • Subset check takes time \(O(|S| \cdot |C|) = O(n^2)\).

Recall \(\NP\): Problems with efficiently verifiable solutions

A polynomial-time verifier for a language \(A\) is a Turing machine \(V\) such that: \[ A = \setbuild{x \in \binary^*}{\left(\exists w \in \binary^{\le |x|^k}\right) \; V \text{ accepts } \encoding{x, w}} \] for some constant \(k\). Furthermore, \(V\) must run in time \(O(|x|^c)\) for some constant \(c\).

Here, \(w\) is called a witness or certificate or proof for \(x\).

\(\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}\).

Recall: Example problems in \(\NP\)

\[\probHamPath = \setbuild{\encoding{G}}{ G \text{ is a directed graph with a Hamiltonian path }}\]

\[ \probClique = \setbuild{\encoding{G, k}}{ G \text{ is an undirected graph with a } k\text{-clique}} \]

\[ \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} \]

The class \(\coNP\)

  • The complements of \(\probHamPath, \probClique, \probSubsetSum\) are not obviously in \(\NP\)
    (and are believed not to be)
    • Why not?
    • Suppose \(G\) has no Hamiltonian path: what’s the short, easy-to-check proof?
    • Only obvious way is the same obvious way to decide \(\probHamPath\): enumerate all \(n!\) permutations of nodes and check that each is not a Hamiltonian path.
  • The class of languages whose complements are in \(\NP\) is called \(\coNP\).
  • We don’t know whether \(\NP = \coNP\) (we doubt it), but note:
    • \(\P = \mathsf{coP}\quad\) (why?)
    • So proving \(\NP \ne \coNP\) would imply \(\P \ne \NP\).

    “Proving \(\coNP \ne \NP\) is at least as hard as proving \(\P \ne \NP\).”

\(\P \subseteq \NP\)

Theorem: \(\P \subseteq \NP\)

Proof:

  • Let \(L \in \P\), then there is a polynomial-time algorithm A(x) deciding whether \(x \in L\).
  • The following is a verifier for \(L\):
def V(x: str, w: str) -> bool:
    return A(x)
  • Why does this work?
    • \(x \in L \iff A \text{ accepts } x \fragment{\iff (\exists w \in \binary^{|x|})\ V \text{ accepts } \encoding{x,w}.}\)
    • In other words the verifier just ignores the witness and directly “verifies” by calling the decider.
  • Somewhat more obvious that \(\P \subseteq \NP\) using the “nondeterministic polynomial-time Turing machine” characterization of \(\NP\):
    • A deterministic algorithm is a special case of a nondeterministic algorithm that never makes guesses.

The (literal) million-dollar question: \(\P = \NP\)?

NP P P = NP or
  • We showed that \(\P \subseteq \NP\), so either
    • \(\P \subsetneq \NP\); or
    • \(\P = \NP\).
  • We don’t know which is true, but most conjecture that \(\P \subsetneq \NP\).
  • The \(\P = \NP\) question is one of the most important open problems in computer science.
  • Informally, it’s asking: “Is finding solutions as easy as verifying that they work?”
    • Intuitively it does seem like it should be harder to find a solution than to verify one…
    • Many important problems are not known to be in \(\P\), but for some reason, most important problems we encounter in practice have the “\(\NP\) property”: You know a good solution when you see one.

\(\NP\) problems are decidable in exponential time

  • For many problems in \(\NP\) we don’t know how to decide them in polynomial time.
  • But at worst, we can use their efficient verifiers to decide them by brute-force search: try all possible witness strings \(w\) and check if any work. How long does this take?

Theorem: \(\NP \subseteq \EXP.\)

Proof:

k = 7 # change this constant depending on L
def exhaustive(x: str) -> bool:
    for i in range(k+1):
        for w in binary_strings_of_length(i):
            if V(x,w):
                return True
    return False
  • Given problem \(L \in \NP\), let \(V\) be its verifier.
  • Thus \(x \in L \iff (\exists w \in \binary^{\le |x|^k}) \; V \text{ accepts } \encoding{x, w}\).
  • Here \(k\) is some constant, and \(V\) runs in time \(O(|x|^c)\) for some other constant \(c\).
  • We can thus decide \(L\) by:
    • running \(V\) on all possible strings \(w\) of length \(\le |x|^k\)
    • accepting \(x\) if any of the \(\encoding{x, w}\) are accepted by \(V\).
  • The number of strings we must try is \(2^0 + 2^1 + \ldots + 2^{|x|^k} \fragment{= 2^{|x|^k + 1} - 1} \fragment{= O(2^{|x|^k}).}\)
  • The total runtime is therefore \(O(|x|^c \cdot 2^{|x|^k}) \fragment{= O(2^{2 |x|^k})\text{; thus } L \in \EXP.}\)