Title

ECS 120 Theory of Computation
Problems in P
Julian Panetta
University of California, Davis

Recall: Time Complexity Class \(\P\)

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
  • Counting the “steps” (innermost line) run: \[ \small \begin{aligned} \text{Total steps} &= \fragment{\sum_{i = 1}^{n} \sum_{j = i}^{n} \sum_{k = j}^{n} 1} \fragment{= \cdots} \fragment{= \frac{n (n + 1) (n + 2)}{6}} \fragment{= O(n^3)} \end{aligned} \]
  • This is polynomial in the “size” of the input \(n\), so \(3\text{Sum} \in \P\).
  • Remember, we’re not concerned about the encoding details!

Other Problems in \(\P\)

We’ll next show that the following problems are in \(\P\) by exhibiting and analyzing algorithms:

  • \(\probRelprime\): Given two integers \(a\) and \(b\), check if they are relatively prime.
  • \(\probPath\): Given a graph \(G\) and two vertices \(u\) and \(v\), check if there is a path from \(u\) to \(v\).
  • \(\probConnected\): Given an undirected graph \(G\), check if it is connected.
  • \(\probEulerianCycle\): Given an undirected graph \(G\), check if an Eulerian cycle exists.

\(\probRelprime\) and the Euclidean Algorithm

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
  • Based on the identity: \(\gcd(a, b) = \gcd(a \mod b, b)\).
  • How many “steps” does this algorithm take?
    Count loop iterations (each does constant work).
  • Each iteration halves the value of the larger number (\(a\))
    • Suppose \(b \le \frac{a}{2}\). Then \(a \mod b < b \le \frac{a}{2}\).
    • Alternatively, if \(b > \frac{a}{2}\), then \(a \mod b = a - b < \frac{a}{2}\).
  • So the number of iterations run is at most: \(2 \log_2 a \leq |\encoding{a, b}|\)
  • This is linear in the size of the input (the number of bits needed to represent \(a\) and \(b\)),
    so \(\probRelprime \in \P\).

Simply checking each integer up to \(b\) would take exponential time!

\(\probPath\)

Directed reachability problem:

s s a a s->a d d s->d b b d->b t t d->t b->a b->t c c b->c c->s c->t e e c->e e->c

\[ \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
  • What is the asymptotic complexity of this algorithm in terms of \(n = |V|\) and \(m = |E|\)?
  • How many times does the while loop run? At most \(n\) (each node enters q at most once).
  • How long does building neighbors take?
    \(m\) steps (each edge is checked once).
  • How many total times does the for loop run?
    At most \(m\) (traverse each edge at most once).

This is \(O(n \cdot m + m)\).
Could we do better?

\(\probConnected\)

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

\(\probEulerianCycle\)

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)