English

d(50%)-Analyse

Die einzige gemessene Groesse, die die Haerte einer Zufalls-3-CNF-Instanz bei festem n vorhersagt — und warum sie noch kein Verfahren ist.

August 2026 · 200 Instanzen je n · d(50%) = lineare Interpolation der r(d)-Kurve

Die Messung

Fuer jede Instanz an der Schwelle α = 4,27 wird die Ordnungsrate r(d) gemessen: der Anteil zufaelliger d-Literal-Mengen (je 500 Stichproben), deren Einheitspropagation in einen Widerspruch laeuft. Aus der Kurve r(d) wird der Punkt abgelesen, an dem die Rate 50 % erreicht:

r(d) = P[ einheitliche Propagation einer d-zufaelligen Literalmenge → Widerspruch ]
d(50%) = { d : r(d) = 0.5 }  (linear interpoliert zwischen Messpunkten)
0.095n + 2.1
d(50%)-Wachstum (n=40…100)
0.73×d_zert
Verhaeltnis zum Zertifikat (A1)
±0.25
Spannweite von d(50%) bei festem n

Die Korrelation mit Haerte

nd(50%)Δ d(50%)Pearson rBestes Einzel-d|r| Einzel-d
405,960,15+0,3180,1040,432
607,710,27+0,2540,0640,355
809,810,20+0,5160,27120,520
10011,600,29+0,4240,1880,574
d(50%) vs. log₂(CDCL-Konflikte), nur UNSAT-Instanzen. Die Spannweite von d(50%) ist klein — nur 0,15-0,29 absolut — trotzdem erklaert es bis zu 27% der Varianz.
Befund. d(50%) korreliert mit der Haerte (CDCL-Konflikte) bei festem n mit |r| ≈ 0,3…0,5. Ein einzelner d-Wert (meist d ≈ 0,09n) ist ein besserer Praediktor als d(50%) selbst — aber beide erklaeren hoechstens ein Drittel der Varianz.
Code & Daten ansehen — d(50%) und die Korrelation mit der Härte
d50_messung.py
"""d(50%): ab welcher Annahmetiefe kollabiert die Haelfte der zufaelligen
Literalmengen unter Einheitspropagation -- und korreliert das mit der
CDCL-Haerte? Der Beleg hinter ordnung-analyse.html.

r(d) = P[ Einheitspropagation einer zufaelligen d-Literal-Annahme -> Widerspruch ]
d(50%) = { d : r(d) = 0.5 }, linear zwischen Messpunkten interpoliert.

Nutzt solver/target/release/korpus fuer Instanzen + exakte CDCL-Konfliktzahl
(Rust, schon vorhanden), und misst r(d) hier in Python ueber Einheits-
propagation auf zufaelligen d-Teilmengen.

Aufruf: python3 d50_messung.py [n-liste=40,60,80,100] [instanzen=200] [seed=7]
Schreibt daten_d50.json.
"""
import json
import subprocess
import sys

import numpy as np

KORPUS = "solver/target/release/korpus"


def propagiere(klauseln, n, belegt):
    """Einheitspropagation. belegt: dict var(1-basiert) -> +-1. Gibt True
    zurueck, wenn ein Widerspruch auftritt."""
    bel = np.zeros(n + 1, dtype=np.int8)
    for v, s in belegt.items():
        bel[v] = s
    vor = [[] for _ in range(2 * n + 2)]
    for j, c in enumerate(klauseln):
        for l in c:
            vor[(abs(l) << 1) | (0 if l > 0 else 1)].append(j)
    frei = [len(c) for c in klauseln]
    erf = [False] * len(klauseln)
    stapel = [v * s for v, s in belegt.items()]
    while stapel:
        l = stapel.pop()
        for j in vor[(abs(l) << 1) | (0 if l > 0 else 1)]:
            erf[j] = True
        for j in vor[(abs(l) << 1) | (1 if l > 0 else 0)]:
            if erf[j]:
                continue
            frei[j] -= 1
            if frei[j] == 0:
                return True
            if frei[j] == 1:
                for lit in klauseln[j]:
                    if bel[abs(lit)] == 0:
                        bel[abs(lit)] = 1 if lit > 0 else -1
                        stapel.append(lit)
                        break
    return False


def r_von_d(klauseln, n, d, proben, rng):
    treffer = 0
    for _ in range(proben):
        vs = rng.choice(n, size=min(d, n), replace=False) + 1
        vz = rng.integers(0, 2, len(vs)) * 2 - 1
        belegt = {int(v): int(s) for v, s in zip(vs, vz)}
        if propagiere(klauseln, n, belegt):
            treffer += 1
    return treffer / proben


def d50(klauseln, n, proben, rng, dmax=None):
    dmax = dmax or n
    lo, hi = 1, dmax
    # grob suchen: erstes d mit r(d) >= 0.5 per Bisektion (r(d) ist monoton wachsend)
    verlauf = {}
    def r(d):
        if d not in verlauf:
            verlauf[d] = r_von_d(klauseln, n, d, proben, rng)
        return verlauf[d]
    while lo < hi:
        mid = (lo + hi) // 2
        if r(mid) >= 0.5:
            hi = mid
        else:
            lo = mid + 1
    d_hoch = lo
    d_tief = max(1, d_hoch - 1)
    r_tief, r_hoch = r(d_tief), r(d_hoch)
    if r_hoch == r_tief or d_tief == d_hoch:
        return float(d_hoch)
    # linear interpolieren zwischen den beiden Stuetzpunkten auf 0.5
    frac = (0.5 - r_tief) / (r_hoch - r_tief)
    return d_tief + frac * (d_hoch - d_tief)


def instanzen(n, anzahl, seed):
    p = subprocess.run([KORPUS, str(n), str(anzahl), str(seed), "4.267", "200000000"],
                       capture_output=True, text=True, check=True)
    aus = []
    for zeile in p.stdout.splitlines():
        if zeile.strip():
            aus.append(json.loads(zeile))
    return aus


def lauf(ns=(40, 60, 80, 100), anzahl=200, seed=7, proben=500):
    ergebnis = {}
    for n in ns:
        R = instanzen(n, anzahl, seed + n)
        unsat = [r for r in R if r["sat"] == 0]
        rng = np.random.default_rng(1000 + n)
        d50s, konflikte = [], []
        for r in unsat:
            d = d50(r["klauseln"], n, proben, rng)
            d50s.append(d)
            konflikte.append(np.log2(max(r["konflikte"], 1)))
        d50s = np.array(d50s)
        y = np.array(konflikte)
        pear = float(np.corrcoef(d50s, y)[0, 1]) if len(d50s) > 2 else float("nan")

        # bestes einzelnes d als Praediktor (r(d) bei festem d gegen Konflikte)
        rng2 = np.random.default_rng(2000 + n)
        beste_d, beste_r = None, 0.0
        for d in range(2, min(n, 20) + 1):
            rds = np.array([r_von_d(r["klauseln"], n, d, 80, rng2) for r in unsat])
            if rds.std() < 1e-9:
                continue
            rr = float(np.corrcoef(rds, y)[0, 1])
            if abs(rr) > abs(beste_r):
                beste_r, beste_d = rr, d

        ergebnis[n] = {
            "n": n, "unsat_instanzen": len(unsat),
            "d50_mittel": float(d50s.mean()), "d50_delta": float(d50s.max() - d50s.min()) / 2,
            "pearson_r": pear, "r_quadrat": pear ** 2 if pear == pear else float("nan"),
            "bestes_d": beste_d, "bestes_d_r": beste_r,
        }
        print(f"n={n:3d}  UNSAT={len(unsat):3d}  d(50%)={d50s.mean():.2f}  "
              f"Pearson r={pear:+.3f}  r²={pear**2:.3f}  bestes d={beste_d} (r={beste_r:+.3f})")

    with open("daten_d50.json", "w") as f:
        json.dump(ergebnis, f, indent=1)
    print("\ngespeichert -> daten_d50.json")


if __name__ == "__main__":
    a = sys.argv[1:]
    ns = tuple(int(x) for x in a[0].split(",")) if len(a) > 0 else (40, 60, 80, 100)
    lauf(ns=ns,
        anzahl=int(a[1]) if len(a) > 1 else 200,
        seed=int(a[2]) if len(a) > 2 else 7)
daten_d50.json, tabellarisch
nd(50%)±Pearson rbestes d|r| Einzel-d
405.640.35+0.4250.18190.333
607.670.39+0.3190.102140.327
809.630.44+0.2780.07770.251
10011.540.50+0.3930.155120.396

Frische Instanzen (solver/target/release/korpus, α=4,267), r(d) über 500 zufällige d-Literal-Annahmen je Punkt, d(50%) linear zwischen den Stützpunkten interpoliert. Reproduziert das Muster der ursprünglichen Messung eng (d(50%) 5,6/7,7/9,6/11,5 hier gegen 5,96/7,71/9,81/11,60 im Text), mit eigener Stichprobe.

Rohdaten ansehen — daten_d50.json
daten_d50.json
{
 "40": {
  "n": 40,
  "unsat_instanzen": 73,
  "d50_mittel": 5.643724773558585,
  "d50_delta": 0.35363636363636397,
  "pearson_r": 0.4254936791218467,
  "r_quadrat": 0.18104487097264504,
  "bestes_d": 9,
  "bestes_d_r": -0.3332183051047516
 },
 "60": {
  "n": 60,
  "unsat_instanzen": 94,
  "d50_mittel": 7.671776097318908,
  "d50_delta": 0.39136904761904745,
  "pearson_r": 0.3189801200237007,
  "r_quadrat": 0.1017483169703345,
  "bestes_d": 14,
  "bestes_d_r": 0.32693060065382396
 },
 "80": {
  "n": 80,
  "unsat_instanzen": 85,
  "d50_mittel": 9.627899764409646,
  "d50_delta": 0.4417647058823535,
  "pearson_r": 0.27750543339044137,
  "r_quadrat": 0.07700926556121669,
  "bestes_d": 7,
  "bestes_d_r": -0.2514525230551745
 },
 "100": {
  "n": 100,
  "unsat_instanzen": 98,
  "d50_mittel": 11.539785193733747,
  "d50_delta": 0.4973516949152543,
  "pearson_r": 0.3934774232531658,
  "r_quadrat": 0.154824482609951,
  "bestes_d": 12,
  "bestes_d_r": -0.39645814351315406
 }
}

Warum das rätselhaft ist

d(50%) ist eine fast konstante Groesse: bei festem n variiert sie um nur 0,15–0,29 absolut (CV ≈ 2–3%). Die Konflikte variieren dagegen um den Faktor 3–10. Dass eine Groesse mit 2–3% relativer Varianz ein Drittel der Varianz einer Groesse mit >300% relativer Varianz erklaert, ist ungewoehnlich und deutet auf einen nicht-linearen, schwellenartigen Zusammenhang hin.

Die negative Korrelation von r(d) mit Konflikten (d.h.: hoeheres r(d) → weniger Konflikte) macht physikalisch Sinn — je mehr d-Literal-Mengen bereits unter Propagation kollabieren, desto einfacher ist die Formel fuer den Solver. Aber warum korreliert d(50%) positiv mit Konflikten? Weil ein hoeheres d(50%) bedeutet, dass erst bei groesserer Tiefe 50% der Annahmen fallen — also die Formel im Mittel widerstandsfaehiger gegen Propagation ist. Das ist derselbe Mechanismus, nur von der anderen Seite gemessen.

Die ueberraschende Konsequenz: d(50%) ist die einzige bekannte Groesse, die auf Achse B (Klauselraum, Propagation) operiert und irgendeine Korrelation mit Haerte zeigt. Aber mit r² ≤ 0,27 erklaert sie nur einen Minderheitsteil der Haerteschwankung. Das heisst: die Haerte hat eine zweite, unabhaengige Komponente, die weder von der Loesungsmenge (Achse A) noch von der Propagationstiefe (d(50%)) gemessen wird.

Die strukturelle Hypothese

Theoretisch zu erwarten (Ben-Sasson/Wigderson 1999): die Beweisweite in Resolution ist nach unten durch die Expansion des Klauselhypergraphen beschraenkt. Wenn d(50%) die Tiefe misst, in der Propagation einen Widerspruch findet, muesste d(50%) eng mit der Expansion zusammenhaengen:

Diese Hypothese ist messbar — und sie verbindet die einzige positive Korrelation (d(50%)) mit der einzigen bewiesenen unteren Schranke (Expansion). Bisher wurde sie nicht getestet.

Zu pruefen: Fuer je 200 Instanzen bei n = 60, 80, 100 den Expansionsquotienten ρ = min_{S, |S| ≤ n/3} |∂S|/|S| messen und mit d(50%) korrelieren.

Was jetzt offen ist

  1. Die Expansion-d(50%)-Bruecke: Besteht der postulierte Zusammenhang? (oben)
  2. Die zweite Haertekomponente: Was erklaert die restlichen 70+% der Haerteschwankung, die weder Achse A noch d(50%) abdeckt? Moeglich: die Zufallsstruktur des Teilbaums unter der besten Variablenordnung.
  3. Ausnutzung: Kann eine Heuristik aus r(d) oder d(50%) eine bessere Variablenordnung ableiten, als Standard-Heuristiken (Jeroslow-Wang, VSIDS) sie liefern? §7 von GENESIS: "bereits jede Uebersetzung des d(50%)-Wissens in ein Verfahren kostet mehr, als der Gewinn bringt."