skip to content

Why can rewriting a compound access guard into conjunctive normal form produce exponentially more clauses than the original?

level: seniorimportance: nice to knowfreq 27%

answer

  1. distribution multiplies rather than adds
  2. one clause per combination of choices
  3. k to the n, not k times n
  4. the blow-up is symmetric both directions
  5. stop at negation normal form

basics

~20 s

Because distribution multiplies. Turning an OR of AND-terms into an AND of OR-clauses forms one clause per way of choosing a single literal from every term, so n terms of k literals become k to the n clauses.

solid answer

~50 s

**Conjunctive normal form (CNF)** is an AND of OR-clauses; **disjunctive normal form (DNF)** is an OR of AND-terms. Converting between them means distributing one connective over the other, and distribution is multiplicative rather than additive. Take a guard already in DNF: `(a AND b) OR (c AND d) OR (e AND f)`. Distributing OR over AND produces one clause for every way of picking one literal from each of the three terms — 2 x 2 x 2 = **8 clauses**, each with 3 literals, so 24 literal occurrences against the original 6. In general n terms of k literals give k to the n clauses. The reverse direction blows up symmetrically. Absorption and dropping subsumed clauses often shrink the result, but no rewriting rule removes the worst case, which is why an automatic normalisation can turn a six-token guard into something no reviewer will read.

code

pseudocode · 9 lines
pseudocode
// dnf_terms: list of AND-terms, each a list of literals
clauses = [ [] ]                       // start with one empty clause
for each term in dnf_terms:
    next = []
    for each clause in clauses:
        for each literal in term:
            next.append(clause + [literal])
    clauses = next
return clauses                         // AND of these OR-clauses

go deeper

for a junior

Know the two shapes by name: an AND of OR-clauses, and an OR of AND-terms. Recognising which one a guard is already in is enough at this stage.

for a middle

Explain the mechanism: distribution creates one clause per combination of choices, so three two-literal terms become eight clauses. Say what n and k are when you quote the formula.

for a senior

Show the operational judgment: normalise for analysis, not for source. Stop at negation normal form when that is all the review needed, and use absorption and subsumption to delete clauses the expansion made redundant.

for a principal

The real decision is what the guard's shape should optimise for — a diagnosable rejection reason per clause, or a named allow-scenario per term. That choice outlives the expression and shapes how the filter is operated.

## Two normal forms, two readings A boolean condition can be put into either of two canonical shapes, and each reads as a different kind of statement about the guard: | form | shape | reads as | fits when | |---|---|---|---| | conjunctive normal form (CNF) | AND of OR-clauses | a list of requirements, all of which must hold | each clause is a separate rejection reason | | disjunctive normal form (DNF) | OR of AND-terms | a list of permitted cases, any one of which suffices | each term is a named scenario that grants access | Both start from **negation normal form**, where every negation already sits on an atom, because distribution cannot proceed through a negated subexpression. Neither form is unique: absorption, reordering and subsumption all produce different but equivalent normal forms of the same condition, so "the CNF" of a guard is a slight abuse of language. ## Why distribution multiplies The single law doing the work is the distribution of OR over AND: `(a AND b) OR X` is equivalent to `(a OR X) AND (b OR X)` Each application replaces one disjunction with two clauses. Apply it across several terms and the counts compose. Take a guard that is already in DNF with three terms of two literals each: `(a AND b) OR (c AND d) OR (e AND f)` Its CNF is the set of all clauses formed by choosing one literal from each term: `(a OR c OR e) AND (a OR c OR f) AND (a OR d OR e) AND (a OR d OR f) AND (b OR c OR e) AND (b OR c OR f) AND (b OR d OR e) AND (b OR d OR f)` That is 2 x 2 x 2 = **8 clauses of 3 literals**, 24 literal occurrences where the original had 6. Add a fourth term of two literals and it becomes 16 clauses of 4 literals, 64 occurrences. The general statement: **n terms of k literals give k to the n clauses, each of n literals.** Growth is exponential in the number of terms, and the same argument in mirror image applies to converting a CNF guard into DNF, because AND distributes over OR in exactly the same multiplicative way. The intuition worth carrying away: the two forms answer different questions, and translating between them means enumerating every combination of the answers. ## What the growth costs in practice - A guard that was six tokens of readable policy becomes two dozen literal occurrences with no structure a reader recognises. Normalising for readability can therefore make readability worse. - The blown-up form still has to be **maintained**. Adding one new condition to the original DNF is a one-line change; adding it to the expanded CNF touches every clause. - Tooling that normalises automatically inherits the blow-up silently, so a condition that compiles instantly at one size can become slow to process after one more term is added. - The forms are not symmetric in usefulness for a filter. CNF clauses map cleanly onto per-clause rejection reasons — fail a clause, report it. DNF terms map onto named allow-scenarios — match a term, log which one granted access. ## Controlling the size Several things genuinely shrink a converted form, and one thing looks like it does but changes the meaning: 1. **Idempotence and absorption.** Repeated literals inside a clause collapse, and any clause that is a superset of another is subsumed and can be deleted. In the worked example, if two terms share a literal the cross-product shrinks sharply. 2. **Complement clauses.** A clause containing both an atom and its negation is a tautology and can be deleted outright, since it is satisfied by every assignment. 3. **Stopping early.** Negation normal form alone — negations pushed to atoms, nothing distributed — is often all a code review needed. It costs nothing in size and delivers most of the readability. 4. **Naming subformulas.** Introducing a fresh name for each subexpression yields a normal form whose size grows linearly rather than exponentially. The catch is that the result is not *equivalent* to the original: it constrains extra names and only preserves whether the condition can be satisfied at all. For a guard that has to compute the same answer for the same request, that is the wrong tool; for a review, the plain named subconditions are better anyway. The practical conclusion is that full normalisation is a tool for analysis, not a target for source code. Push negations inward, delete duplicated and subsumed clauses, name the subconditions that carry meaning, and distribute only when something downstream genuinely requires one of the two canonical shapes.

  • Given the choice, which normal form do you want for an access filter, and why?
    Usually CNF. Each clause is one requirement, so a failing clause is a ready-made rejection reason and the filter can report which requirement was not met. DNF suits the opposite job: enumerating named scenarios that grant access, where you want to log which case matched rather than why access was refused.
  • Does the blow-up happen in the other direction too?
    Yes, symmetrically. Converting an AND of OR-clauses into DNF distributes AND over OR and again forms one term per choice of a literal from each clause, so n clauses of k literals give k to the n terms. Neither form is the safe one; the multiplication is a property of distribution, not of a particular direction.
  • Is the conjunctive normal form of a condition unique?
    No. Clause order, literal order within a clause, redundant clauses and subsumed clauses can all vary while the condition stays equivalent. There are canonical variants that pin this down by insisting every clause mention every atom, but those are larger still, and normal conversation means only that the expression has the AND-of-ORs shape.

saying these in an interview costs you the question

  • Normalising a condition never changes its size
  • Distribution adds one clause per step, so growth is linear
  • DNF is the form that reads as a list of requirements
  • Every condition has exactly one conjunctive normal form
  • The blow-up only happens when converting toward CNF
  • A negated subexpression can be distributed without being pushed inward first