Polynomial-time Computability

Definitions ▸
`Bits` (= `BitString`) is the type of bit strings: `List Bool`. `bitStringToNat` and `natToBitString` convert between bit strings and natural numbers (LSB-first). `natKthRoot k n` computes $\lfloor n^{1/k} \rfloor$.
/-- The free monoid {0,1}* (Kleene star over the alphabet {0,1}). -/
abbrev Bits := List Bool

/-- Alias for `Bits`; used when emphasising the role of a value as a bit string
    rather than a list. -/
abbrev BitString := Bits

/-- Convert a bit string (LSB-first) to a natural number. -/
def bitStringToNat : Bits → ℕ
  | []      => 0
  | b :: bs => (if b then 1 else 0) + 2 * bitStringToNat bs

/-- Convert a natural number to its LSB-first bit string (using `Nat.bits`). -/
def natToBitString : ℕ → Bits := Nat.bits

/-- The integer k-th root: `⌊n^(1/k)⌋`. Poly-time computable for any fixed `k`. -/
noncomputable def natKthRoot (k n : ℕ) : ℕ :=
  Nat.floor ((n : ℝ) ^ ((1 : ℝ) / (k : ℝ)))
# Definition 1: Polynomial-time arithmetic operations `PolyTimeArith` is an inductive predicate on functions $\mathbb{N} \to \mathbb{N}$. A proof of `PolyTimeArith f` is a derivation that `f` is computable in time polynomial in the **bit-length** of its input. The constructors are: - `id`, `const k`: identity and constants - `addConst`, `subConst`: $O(n)$ - `mulConst`, `divConst`, `modConst`: $O(n^2)$ - `comp`, `mul`: closure under composition and pointwise product - `kthRoot`: $\lfloor n^{1/k} \rfloor$ via Newton's method, $O(n^2)$
/-- Basic arithmetic operations on `ℕ` that are poly-time in the bit-length of
    their input. -/
inductive PolyTimeArith : (ℕ → ℕ) → Prop where
  | id                             : PolyTimeArith id
  | const (k : ℕ)                  : PolyTimeArith (fun _ => k)
  | addConst (k : ℕ)               : PolyTimeArith (· + k)
  | subConst (k : ℕ)               : PolyTimeArith (· - k)
  | mulConst (k : ℕ)               : PolyTimeArith (· * k)
  | divConst (k : ℕ) (hk : 0 < k) : PolyTimeArith (· / k)
  | modConst (k : ℕ) (hk : 0 < k) : PolyTimeArith (· % k)
  | comp {f g} : PolyTimeArith f → PolyTimeArith g → PolyTimeArith (f ∘ g)
  | mul  {f g} : PolyTimeArith f → PolyTimeArith g →
      PolyTimeArith (fun n => f n * g n)
  | kthRoot (k : ℕ) (hk : 0 < k)  : PolyTimeArith (natKthRoot k)
# Definition 2: Polynomial-time computable functions on bit strings `PolyTimeComputable` is an inductive predicate on `BitString → BitString`. **Constructing a term of this type is the proof of poly-time computability** — there is no separate complexity argument to supply. The constructors are: - `id`, `const c`: identity and constants - `comp`: closure under composition - `mapBool op`: apply a bitwise operation pointwise, $O(n)$ - `arithLift`: lift a `PolyTimeArith` operation via `bitStringToNat`/`natToBitString` - `take k`, `drop k`: prefix/suffix, $O(n)$ for fixed $k$ - `arithTake`, `arithDrop`: prefix/suffix of length $\mathrm{op}(n)$ for a poly-time arithmetic function $\mathrm{op}$, $O(n)$ - `append`: concatenate outputs of two poly-time functions, $O(n)$ - `polyIter k`: iterate a poly-time step function $n^k$ times, $O(n^k \cdot T_f(n))$
/-- Poly-time computable functions on bit strings, defined inductively over
    natural combinators. Constructing a term of this type *is* the proof of
    poly-time computability. -/
inductive PolyTimeComputable : (Bits → Bits) → Prop where
  | id      : PolyTimeComputable id
  | const   (c : Bits) : PolyTimeComputable (fun _ => c)
  | comp    {f g} : PolyTimeComputable f → PolyTimeComputable g →
      PolyTimeComputable (f ∘ g)
  | mapBool (op : Bool → Bool) : PolyTimeComputable (List.map op)
  | arithLift {op} : PolyTimeArith op →
      PolyTimeComputable (fun x => natToBitString (op (bitStringToNat x)))
  /-- Take the first `k` bits — fixed prefix length. -/
  | take (k : ℕ) : PolyTimeComputable (List.take k)
  /-- Drop the first `k` bits — fixed prefix length. -/
  | drop (k : ℕ) : PolyTimeComputable (List.drop k)
  /-- Take the first `op(|x|)` bits, where `op` is a poly-time arithmetic function
      of the input length. -/
  | arithTake {op} : PolyTimeArith op →
      PolyTimeComputable (fun x => x.take (op x.length))
  /-- Drop the first `op(|x|)` bits, where `op` is a poly-time arithmetic function
      of the input length. -/
  | arithDrop {op} : PolyTimeArith op →
      PolyTimeComputable (fun x => x.drop (op x.length))
  /-- Take the first `⌊√|x|⌋` bits: $O(\sqrt{n})$ to compute the length, $O(n)$
      to scan. Special case of `arithTake` for the square-root function. -/
  | sqrtTake : PolyTimeComputable (fun x => x.take (Nat.sqrt x.length))
  /-- Drop the first `⌊√|x|⌋` bits. Special case of `arithDrop`. -/
  | sqrtDrop : PolyTimeComputable (fun x => x.drop (Nat.sqrt x.length))
  | append {f g} : PolyTimeComputable f → PolyTimeComputable g →
      PolyTimeComputable (fun x => f x ++ g x)
  /-- Iterate a poly-time step function `f` exactly `|x|^k` times.
      Total cost $O(n^k \cdot T_f(n))$, which is polynomial for fixed `k`. -/
  | polyIter (k : ℕ) {f} : PolyTimeComputable f →
      PolyTimeComputable (fun x => Nat.iterate f (x.length ^ k) x)
# Theorem 1: `natToBitString` is a right inverse of `bitStringToNat` i.e. `bitStringToNat (natToBitString n) = n` for all `n : ℕ` **proof sketch:** induction on the binary representation of `n` using `Nat.binaryRec'`, with the base case $n = 0$ and the inductive step appending a bit via `Nat.bits_append_bit`.
/-- Round-trip: converting a natural number to its bit representation and back
    is the identity. -/
lemma bitStringToNat_natToBitString (n : ℕ) :
    bitStringToNat (natToBitString n) = n := by
unfold `natToBitString` to `Nat.bits`
  simp only [natToBitString]
induct on the binary representation of `n`
  induction n using Nat.binaryRec' with
base case: `Nat.bits 0 = []` and `bitStringToNat [] = 0`
  | zero => simp [Nat.bits, bitStringToNat]
inductive step: `n = bit b m`; use `Nat.bits_append_bit` to unfold, then the inductive hypothesis `ih : bitStringToNat (Nat.bits m) = m`, and case-split on `b` to close both branches by `omega`
  | bit b m h ih =>
    rw [Nat.bits_append_bit m b h]
    simp only [bitStringToNat, ih]
    cases b <;> simp [Nat.bit_true_apply, Nat.bit_false_apply]; omega
# Theorem 2: `n^k` is poly-time for any fixed `k` **proof:** by induction on `k`: - $k = 0$: `fun n => 1` is `PolyTimeArith.const 1` - $k + 1$: `fun n => n^{k+1} = n * n^k` is `PolyTimeArith.mul id (ofPow k)`
/-- `n^k` is poly-time for any fixed `k`, derived from repeated pointwise
    multiplication. -/
def PolyTimeArith.ofPow : ∀ k : ℕ, PolyTimeArith (fun n => n ^ k)
  | 0     => by simpa using PolyTimeArith.const 1
  | k + 1 =>
rewrite `n^(k+1)` as `n * n^k` so `.mul .id (.ofPow k)` applies
      have h : (fun n : ℕ => n ^ (k + 1)) = (fun n : ℕ => n * n ^ k) :=
        funext fun n => by rw [pow_succ, mul_comm]
      h ▸ .mul .id (.ofPow k)

Referenced by

Raw source
import Mathlib.Data.Fintype.Vector
import Mathlib.Data.Nat.Bits
import Mathlib.Data.Nat.BinaryRec
import Mathlib.Analysis.SpecialFunctions.Pow.Real

-- leandown
-- [meta]
-- title = "Polynomial-time computability"
-- group = "Cryptography"
-- [content]

-- [hidden: Definitions]
-- `Bits` (= `BitString`) is the type of bit strings: `List Bool`.
--
-- `bitStringToNat` and `natToBitString` convert between bit strings and natural
-- numbers (LSB-first). `natKthRoot k n` computes $\lfloor n^{1/k} \rfloor$.
/-- The free monoid {0,1}* (Kleene star over the alphabet {0,1}). -/
abbrev Bits := List Bool

/-- Alias for `Bits`; used when emphasising the role of a value as a bit string
    rather than a list. -/
abbrev BitString := Bits

/-- Convert a bit string (LSB-first) to a natural number. -/
def bitStringToNat : Bits → ℕ
  | []      => 0
  | b :: bs => (if b then 1 else 0) + 2 * bitStringToNat bs

/-- Convert a natural number to its LSB-first bit string (using `Nat.bits`). -/
def natToBitString : ℕ → Bits := Nat.bits

/-- The integer k-th root: `⌊n^(1/k)⌋`. Poly-time computable for any fixed `k`. -/
noncomputable def natKthRoot (k n : ℕ) : ℕ :=
  Nat.floor ((n : ℝ) ^ ((1 : ℝ) / (k : ℝ)))
-- [/hidden]

-- # {{definition}}: Polynomial-time arithmetic operations
--
-- `PolyTimeArith` is an inductive predicate on functions $\mathbb{N} \to \mathbb{N}$.
-- A proof of `PolyTimeArith f` is a derivation that `f` is computable in time
-- polynomial in the **bit-length** of its input.
--
-- The constructors are:
-- - `id`, `const k`: identity and constants
-- - `addConst`, `subConst`: $O(n)$
-- - `mulConst`, `divConst`, `modConst`: $O(n^2)$
-- - `comp`, `mul`: closure under composition and pointwise product
-- - `kthRoot`: $\lfloor n^{1/k} \rfloor$ via Newton's method, $O(n^2)$
--
/-- Basic arithmetic operations on `ℕ` that are poly-time in the bit-length of
    their input. -/
inductive PolyTimeArith : (ℕ → ℕ) → Prop where
  | id                             : PolyTimeArith id
  | const (k : ℕ)                  : PolyTimeArith (fun _ => k)
  | addConst (k : ℕ)               : PolyTimeArith (· + k)
  | subConst (k : ℕ)               : PolyTimeArith (· - k)
  | mulConst (k : ℕ)               : PolyTimeArith (· * k)
  | divConst (k : ℕ) (hk : 0 < k) : PolyTimeArith (· / k)
  | modConst (k : ℕ) (hk : 0 < k) : PolyTimeArith (· % k)
  | comp {f g} : PolyTimeArith f → PolyTimeArith g → PolyTimeArith (f ∘ g)
  | mul  {f g} : PolyTimeArith f → PolyTimeArith g →
      PolyTimeArith (fun n => f n * g n)
  | kthRoot (k : ℕ) (hk : 0 < k)  : PolyTimeArith (natKthRoot k)

-- # {{definition}}: Polynomial-time computable functions on bit strings
--
-- `PolyTimeComputable` is an inductive predicate on `BitString → BitString`.
-- **Constructing a term of this type is the proof of poly-time computability** —
-- there is no separate complexity argument to supply.
--
-- The constructors are:
-- - `id`, `const c`: identity and constants
-- - `comp`: closure under composition
-- - `mapBool op`: apply a bitwise operation pointwise, $O(n)$
-- - `arithLift`: lift a `PolyTimeArith` operation via `bitStringToNat`/`natToBitString`
-- - `take k`, `drop k`: prefix/suffix, $O(n)$ for fixed $k$
-- - `arithTake`, `arithDrop`: prefix/suffix of length $\mathrm{op}(n)$
--   for a poly-time arithmetic function $\mathrm{op}$, $O(n)$
-- - `append`: concatenate outputs of two poly-time functions, $O(n)$
-- - `polyIter k`: iterate a poly-time step function $n^k$ times, $O(n^k \cdot T_f(n))$
--
/-- Poly-time computable functions on bit strings, defined inductively over
    natural combinators. Constructing a term of this type *is* the proof of
    poly-time computability. -/
inductive PolyTimeComputable : (Bits → Bits) → Prop where
  | id      : PolyTimeComputable id
  | const   (c : Bits) : PolyTimeComputable (fun _ => c)
  | comp    {f g} : PolyTimeComputable f → PolyTimeComputable g →
      PolyTimeComputable (f ∘ g)
  | mapBool (op : Bool → Bool) : PolyTimeComputable (List.map op)
  | arithLift {op} : PolyTimeArith op →
      PolyTimeComputable (fun x => natToBitString (op (bitStringToNat x)))
  /-- Take the first `k` bits — fixed prefix length. -/
  | take (k : ℕ) : PolyTimeComputable (List.take k)
  /-- Drop the first `k` bits — fixed prefix length. -/
  | drop (k : ℕ) : PolyTimeComputable (List.drop k)
  /-- Take the first `op(|x|)` bits, where `op` is a poly-time arithmetic function
      of the input length. -/
  | arithTake {op} : PolyTimeArith op →
      PolyTimeComputable (fun x => x.take (op x.length))
  /-- Drop the first `op(|x|)` bits, where `op` is a poly-time arithmetic function
      of the input length. -/
  | arithDrop {op} : PolyTimeArith op →
      PolyTimeComputable (fun x => x.drop (op x.length))
  /-- Take the first `⌊√|x|⌋` bits: $O(\sqrt{n})$ to compute the length, $O(n)$
      to scan. Special case of `arithTake` for the square-root function. -/
  | sqrtTake : PolyTimeComputable (fun x => x.take (Nat.sqrt x.length))
  /-- Drop the first `⌊√|x|⌋` bits. Special case of `arithDrop`. -/
  | sqrtDrop : PolyTimeComputable (fun x => x.drop (Nat.sqrt x.length))
  | append {f g} : PolyTimeComputable f → PolyTimeComputable g →
      PolyTimeComputable (fun x => f x ++ g x)
  /-- Iterate a poly-time step function `f` exactly `|x|^k` times.
      Total cost $O(n^k \cdot T_f(n))$, which is polynomial for fixed `k`. -/
  | polyIter (k : ℕ) {f} : PolyTimeComputable f →
      PolyTimeComputable (fun x => Nat.iterate f (x.length ^ k) x)

-- # {{theorem}}: `natToBitString` is a right inverse of `bitStringToNat`
--
-- i.e. `bitStringToNat (natToBitString n) = n` for all `n : ℕ`
--
-- **proof sketch:** induction on the binary representation of `n`
-- using `Nat.binaryRec'`, with the base case $n = 0$ and the inductive step
-- appending a bit via `Nat.bits_append_bit`.
--
/-- Round-trip: converting a natural number to its bit representation and back
    is the identity. -/
lemma bitStringToNat_natToBitString (n : ℕ) :
    bitStringToNat (natToBitString n) = n := by
  -- unfold `natToBitString` to `Nat.bits`
  simp only [natToBitString]
  -- induct on the binary representation of `n`
  induction n using Nat.binaryRec' with
  -- base case: `Nat.bits 0 = []` and `bitStringToNat [] = 0`
  | zero => simp [Nat.bits, bitStringToNat]
  -- inductive step: `n = bit b m`; use `Nat.bits_append_bit` to unfold,
  -- then the inductive hypothesis `ih : bitStringToNat (Nat.bits m) = m`,
  -- and case-split on `b` to close both branches by `omega`
  | bit b m h ih =>
    rw [Nat.bits_append_bit m b h]
    simp only [bitStringToNat, ih]
    cases b <;> simp [Nat.bit_true_apply, Nat.bit_false_apply]; omega

-- # {{theorem}}: `n^k` is poly-time for any fixed `k`
--
-- **proof:** by induction on `k`:
-- - $k = 0$: `fun n => 1` is `PolyTimeArith.const 1`
-- - $k + 1$: `fun n => n^{k+1} = n * n^k` is `PolyTimeArith.mul id (ofPow k)`
--
/-- `n^k` is poly-time for any fixed `k`, derived from repeated pointwise
    multiplication. -/
def PolyTimeArith.ofPow : ∀ k : ℕ, PolyTimeArith (fun n => n ^ k)
  | 0     => by simpa using PolyTimeArith.const 1
  | k + 1 =>
      -- rewrite `n^(k+1)` as `n * n^k` so `.mul .id (.ofPow k)` applies
      have h : (fun n : ℕ => n ^ (k + 1)) = (fun n : ℕ => n * n ^ k) :=
        funext fun n => by rw [pow_succ, mul_comm]
      h ▸ .mul .id (.ofPow k)