Skip to content
academia.sh

Lesson 08 / 10

co-NP and Relationships Between Classes

The status of complement problems and the measured asymmetry between yes and no answers: on 20 examples, a yes certificate closes in 101 steps, while a no answer by exhaustive search needs 81,920. On structured no examples, a short 260-step proof works on 20 of 20; on unstructured examples, the same proof can say nothing on 20 of 20. At input size 24, the yes certificate takes 25 steps, the no proof 16,777,216. The threshold problem closes in 260 steps in both directions.

Contents

Everything measured up to this point pointed in the yes direction: a certificate verified a “yes”; a reduction carried a “yes.” When an answer is “no,” what can be shown? This lesson measures the missing direction and counts that the two directions are not symmetric.

The source of the problem is the definition of NP itself. The definition demands a witness only for a “yes” answer; it promises nothing for a “no” answer. The Advanced Algorithms course had already observed this asymmetry in the Hamiltonian path lesson: on every graph with no path, the decision method traversed the entire tree the counting method traversed. That observation turns into a class distinction here.

  • CC29. A decision problem’s complement is the problem with the answer flipped on the same input: the complement of “does some subset sum to the target” is “does no subset sum to the target.”
  • CC30. co-NP is the class of decision problems whose complement is in class NP. An equivalent reading: problems for which the “no” answer has a short, quickly testable witness.
  • CC31. Three example sets are built. Yes set: the shared definition’s 20 examples. Structured no set: all numbers are doubled, the target is made odd. Unstructured no set: the same numbers, with an unreachable value chosen as the target.
  • CC32. Choosing the unstructured set’s target requires working out reachable sums exactly. This is a setup step, not a verifier, and its steps are not included in the measurement.
  • CC33. The short no proof tests only a necessary condition: if all numbers are even, their sum is even too, so an odd target cannot be reached. This proof can say “no”; it cannot say “yes.” When it cannot decide, it returns unknown.
  • CC34. For the short proof, a step is reading one number; for exhaustive search, it is one subset.
  • CC35. The budget sweep is done at four values: 13, 100, 1000, 10,000 steps.
  • CC36. Examples come from the shared definition’s generator, seed 20260218. There is no second seed.
  • CC37. Whether NP equals co-NP is an open question and is not answered in this lesson. Measurement only counts whether specific proofs work on specific examples.

Why the Complement Is a Separate Question

A method that solves a decision problem also solves its complement: flipping the answer does not even cost a step. This is why, in terms of solvability, there is no difference between a problem and its complement, and class P is closed under complement — the complement of a problem in P is also in P.

The situation is different in terms of verifiability. A witness saying “this subset sums to the target” does not support the claim “no subset sums to it” — on the contrary, it refutes that claim. What should be shown to say “none does” is a separate design question, and it is not known whether every problem has an answer to it.

This is why NP and co-NP carry separate names. Subset sum being in class NP was shown in 02; whether it is in class co-NP was not shown, and this lesson does not show it either.

Measuring the Two Directions

The block below runs all three example sets through the same verifier, then tries two separate short proofs on each set: a certificate for “yes,” an even/odd necessary condition for “no.”

SEED = 20260218


def examples(seed=SEED, n=12, count=20):
    d = seed
    result = []
    for _ in range(count):
        numbers = []
        for _ in range(n):
            d = (d * 1103515245 + 12345) % 2147483648
            numbers.append(d % 97 + 3)
        d = (d * 1103515245 + 12345) % 2147483648
        result.append({"numbers": numbers, "target": sum(numbers) // 3 + d % 7})
    return result


def verifier(numbers, target):
    """Exhaustive search. Returns: (exists, steps, certificate)."""
    steps, n = 0, len(numbers)
    for mask in range(1 << n):
        steps += 1
        if sum(numbers[i] for i in range(n) if mask >> i & 1) == target:
            return True, steps, [i for i in range(n) if mask >> i & 1]
    return False, steps, None


def yes_verify(numbers, target, certificate):
    """One step = one index read."""
    steps, total = 0, 0
    for i in certificate:
        steps += 1
        total += numbers[i]
    return total == target, steps + 1


def no_verify(numbers, target):
    """One-directional short proof: if all numbers are even, their sum
    is even too, so an odd target can never be reached."""
    steps = 0
    for s in numbers:
        steps += 1
        if s % 2:
            return "unknown", steps
    steps += 1
    return ("no", steps) if target % 2 else ("unknown", steps)


def unreachable(numbers):
    """Setup step (not a verifier): reachable sums are worked out and the
    unreached value closest to a third of the total is chosen."""
    reached = {0}
    for x in numbers:
        reached |= {u + x for u in reached}
    return min((t for t in range(1, sum(numbers)) if t not in reached),
               key=lambda t: abs(t - sum(numbers) // 3))


EX = examples()
YES = [(o["numbers"], o["target"]) for o in EX]
STRUCTURED = [([2 * x for x in o["numbers"]], 2 * o["target"] + 1) for o in EX]
UNSTRUCTURED = [(o["numbers"], unreachable(o["numbers"])) for o in EX]

print("set        verifier answer  exhaustive search steps")
for name, batch, expect in (("yes        ", YES, True), ("structured ", STRUCTURED, False),
                        ("unstructured", UNSTRUCTURED, False)):
    total = sum(verifier(s, h)[1] for s, h in batch)
    matched = sum(1 for s, h in batch if verifier(s, h)[0] == expect)
    print(f"{name}   {matched:2d}/20 expected  {total:16d}")
print()
es = sum(yes_verify(s, h, verifier(s, h)[2])[1] for s, h in YES)
print("yes certificate (20 examples) verification steps:", es)
for name, batch in (("structured ", STRUCTURED), ("unstructured", UNSTRUCTURED)):
    hy = sum(1 for s, h in batch if no_verify(s, h)[0] == "no")
    ha = sum(no_verify(s, h)[1] for s, h in batch)
    print(f"short no proof {name} | said 'no': {hy:2d}/20 | steps: {ha}")
print()
print("budget  yes certificate  structured no proof  unstructured exhaustive search")
for b in (13, 100, 1000, 10000):
    a1 = sum(1 for s, h in YES if yes_verify(s, h, verifier(s, h)[2])[1] <= b)
    a2 = sum(1 for s, h in STRUCTURED if no_verify(s, h)[0] == "no"
             and no_verify(s, h)[1] <= b)
    a3 = sum(1 for s, h in UNSTRUCTURED if verifier(s, h)[1] <= b)
    print(f"{b:5d}  {a1:16d}  {a2:19d}  {a3:19d}")
set        verifier answer  exhaustive search steps
yes           20/20 expected              4321
structured    20/20 expected             81920
unstructured   20/20 expected             81920

yes certificate (20 examples) verification steps: 101
short no proof structured  | said 'no': 20/20 | steps: 260
short no proof unstructured | said 'no':  0/20 | steps: 39

budget  yes certificate  structured no proof  unstructured exhaustive search
   13                20                   20                    0
  100                20                   20                    0
 1000                20                   20                    0
10000                20                   20                   20

Reading the Asymmetry

The first table establishes the asymmetry all by itself. On the yes set, exhaustive search spends 4321 steps, because it stops once a matching subset is found. On the two no sets it spends 81,920 steps — exactly 4096 per example, no discount at all. The number is identical across the two sets because the reason is not procedural but logical: to say “none does,” not a single unseen subset can remain.

The second block brings three numbers together. Yes certificate: 101 steps. Structured no proof: 260 steps, and it can say “no” on 20 of 20 examples. Unstructured no proof: 39 steps, and it can say nothing on none of the 20 examples — it backs off the moment it sees the first odd number.

This trio shows why the definition of co-NP is an existence claim. A short “no” witness exists for some examples, and it was measured: 13 steps instead of 4096. But its existing for every example is a separate claim, and the unstructured set shows this claim does not hold automatically. What the measurement says is: this proof did not work on these examples. What it does not say is that no other short proof exists; no such proof was searched for — only this one was tried.

The budget sweep gives the same distinction once more. The yes certificate and the structured no proof are full at budget 13; raising the budget a thousandfold changes nothing in either. The unstructured set, meanwhile, is still zero at 1000 and only reaches 20 at 10,000.

A Problem Where Both Directions Are Cheap

Not every problem has this asymmetry. The threshold problem — does the sum of the numbers exceed the target — closes with the same method in both directions: the total is computed once and compared.

SEED = 20260218


def examples(seed=SEED, n=12, count=20):
    d = seed
    result = []
    for _ in range(count):
        numbers = []
        for _ in range(n):
            d = (d * 1103515245 + 12345) % 2147483648
            numbers.append(d % 97 + 3)
        d = (d * 1103515245 + 12345) % 2147483648
        result.append({"numbers": numbers, "target": sum(numbers) // 3 + d % 7})
    return result


def threshold(numbers, target):
    """Does the sum exceed the target. Both directions close with the same method."""
    total, steps = 0, 0
    for s in numbers:
        total += s
        steps += 1
    return total > target, steps + 1


yes = no = ya = na = 0
for o in examples():
    y1, a1 = threshold(o["numbers"], o["target"])
    y2, a2 = threshold(o["numbers"], sum(o["numbers"]) + 1)
    yes, no = yes + y1, no + (not y2)
    ya, na = ya + a1, na + a2
print("threshold problem | yes:", yes, "/20 ,", ya, "steps | no:", no, "/20 ,",
      na, "steps")
print()
print(" n  yes certificate  no proof (exhaustive search)")
for n in (8, 12, 16, 20, 24):
    print(f"{n:2d}  {n + 1:16d}  {1 << n:25d}")
threshold problem | yes: 20 /20 , 260 steps | no: 20 /20 , 260 steps

 n  yes certificate  no proof (exhaustive search)
 8                 9                        256
12                13                       4096
16                17                      65536
20                21                    1048576
24                25                   16777216

In the threshold problem, both directions take 260 steps: identical, and the equality is not a coincidence but comes from the method’s structure. This problem is in both class NP and class co-NP, because it is in class P, and P sits inside both classes. The direction of the answer does not change the cost.

The table below it is the opposite extreme. As input size rises from 8 to 24, the yes certificate goes from 9 to 25 steps, the no proof from 256 to 16,777,216. The two columns are two faces of the same problem, and they do not grow at the same speed.

The Same No, Fewer Steps

Exhaustive search spending 81,920 steps on the unstructured set is not the cost of saying “no” for those examples; it is only the cost of exhaustive search. The way to show this is to try another method that establishes the same answer in fewer steps. The block below tries two: a pruned search that cuts a branch once the partial sum exceeds the target, and the reachable-sum table introduced in 01.

SEED = 20260218


def examples(seed=SEED, n=12, count=20):
    d = seed
    result = []
    for _ in range(count):
        numbers = []
        for _ in range(n):
            d = (d * 1103515245 + 12345) % 2147483648
            numbers.append(d % 97 + 3)
        d = (d * 1103515245 + 12345) % 2147483648
        result.append({"numbers": numbers, "target": sum(numbers) // 3 + d % 7})
    return result


def unreachable(numbers):
    reached = {0}
    for x in numbers:
        reached |= {u + x for u in reached}
    return min((t for t in range(1, sum(numbers)) if t not in reached),
               key=lambda t: abs(t - sum(numbers) // 3))


def brute_force(numbers, target):
    steps, n = 0, len(numbers)
    for mask in range(1 << n):
        steps += 1
        if sum(numbers[i] for i in range(n) if mask >> i & 1) == target:
            return True, steps
    return False, steps


def pruned(numbers, target):
    """A branch is cut once the partial sum exceeds the target. One step = one node."""
    s = sorted(numbers)
    n, steps = len(s), 0

    def visit(i, total):
        nonlocal steps
        steps += 1
        if total == target:
            return True
        if total > target or i == n:
            return False
        return visit(i + 1, total + s[i]) or visit(i + 1, total)

    return visit(0, 0), steps


def dp(numbers, target):
    """Reachable-sum table. One step = one table cell."""
    reached = [False] * (target + 1)
    reached[0] = True
    steps = 0
    for x in numbers:
        for t in range(target, x - 1, -1):
            steps += 1
            if reached[t - x]:
                reached[t] = True
    return reached[target], steps


UNSTRUCTURED = [(o["numbers"], unreachable(o["numbers"])) for o in examples()]
print("method          says no  total steps  budget 1000  budget 10000")
for name, f in (("exhaustive search", brute_force), ("pruned search  ", pruned),
              ("dp table       ", dp)):
    hy = top = b3 = b4 = 0
    for s, h in UNSTRUCTURED:
        y, a = f(s, h)
        hy += not y
        top += a
        b3 += a <= 1000
        b4 += a <= 10000
    print(f"{name:15s}  {hy:11d}  {top:11d}  {b3:10d}  {b4:11d}")
method          says no  total steps  budget 1000  budget 10000
exhaustive search           20        81920           0           20
pruned search             20        21594          12           20
dp table                  20        15736          16           20

All three methods say “no” on 20 of 20 examples, so all three establish the same answer. Their step counts, though, are 81,920, 21,594, and 15,736: pruned search spends less than a quarter of exhaustive search, the table method less than a fifth. At budget 1000, exhaustive search stays at zero, pruned search decides 12 examples, and the table method 16.

One section ago it was said “this proof did not work on these examples”; now the same examples close four to five times cheaper. Even so, closing did not fall to n+1 steps, and the table method’s cheapness, as seen in 01, depends on the value of the target. The conclusion fits two sentences: the best number measured is not the best number that could be measured; and finding a better number is not the same thing as proving a bound.

What Is Known and What Is Open

What is known is short. Class P sits inside both NP and co-NP, and is closed under complement. It is not known whether an NP-complete problem’s complement is in class NP. If a problem is in both NP and co-NP, it means it has a short witness in both directions; this is a strong property, and not every problem has it.

What is open is also short: it is not known whether NP equals co-NP. This lesson does not answer that question and does not try to. What it measures is whether a single short proof works on 20 examples or does not. The proof giving 0/20 on the unstructured set does not show that no short proof exists for those examples; it only shows that this proof does not fit them. This distinction is a direct application of the course’s ban on overclaiming.

The engineering counterpart is concrete. In a validator, justifying a “valid” answer and justifying an “invalid” one are two separate jobs: the first can show a witness, the second often has to say “I have seen every possibility.” Unless the two directions’ step counts are written down separately, the validator’s cost does not count as known.

Summary

  • A problem’s complement is indistinguishable from it in terms of solvability; in terms of verifiability it is a separate question, because a certificate cannot be flipped and reused.
  • On the yes set, exhaustive search spends 4321 steps; on both no sets, 81,920 — 4096 per example, no discount at all.
  • The short no proof works on 20 of 20 structured examples and takes 260 steps; it says nothing on any of the unstructured examples and backs off in 39 steps.
  • In the threshold problem both directions close in 260 steps; because this problem is in class P, it is in both NP and co-NP.
  • As input size rises from 8 to 24, the yes certificate goes from 9 to 25 steps, the no proof from 256 to 16,777,216.
  • Whether NP equals co-NP is an open question; a proof not working on these examples does not show that no short proof exists.

Next Step

With this lesson, four class names and the known relationships between them are established. What remains is the most talked-about, least answered question: is everything verifiable also solvable. The next lesson states this question and does not try to measure what it cannot measure. The only thing it can measure is the gap between the best known method and the best known lower bound; whether that gap can be narrowed, whether narrowing it closes it, and whether the narrowing itself can be counted as a sign of direction.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close