Search-09c — Discrépance combinatoire : colorier ±1 sans déséquilibrer

Partie 1 — Fondations.

Issue #12823 — distillation de Bansal & Jiang, Decoupling via Affine Spectral-Independence: Beck-Fiala and Komlós Bounds Beyond Banaszczyk (arXiv:2508.03961, août 2025).

La question. On nous donne \(n\) éléments et des ensembles \(S_1, \dots, S_m\) sur ces éléments, chacun petit devant \(n\), et chaque élément appartenant à au plus \(k\) ensembles (\(k\) = le degré du système). Il faut colorier chaque élément en \(\pm 1\) pour que chaque somme reste proche de zéro :

\[\operatorname{disc}(S_1, \dots, S_m) \;=\; \min_{x \in \{-1,+1\}^n} \; \max_{j} \Big| \sum_{i \in S_j} x_i \Big|\]

C’est le problème de la discrépance combinatoire : équilibrer un système que l’on ne contrôle pas. Il apparaît partout où l’on veut qu’un tirage « équitable en moyenne » soit équitable systématiquement : plans d’expérience, découpage géométrique, allocation quasi-aléatoire, intégration numérique (l’écart entre une somme discrétisée et l’intégrale exacte est une discrépance), et même l’analyse de programmes d’arrondi incrémental (n’arrondir aucun total de plus que nécessaire).

Désambiguïsation immédiate — LDS ≠ discrépance. Search-03c définissait la discrepancy d’un chemin comme le nombre d’écarts à l’heuristique dans un arbre de recherche — un compteur d’erreurs de jugement. Ici, la discrépance est un déséquilibre de sommes : \(|\sum_{i \in S} x_i|\). Les deux partagent l’intuition profonde — mesurer l’écart à l’équilibre parfait — mais ni les objets (chemins vs coloriages), ni les algorithmes (énumération bornée vs arrondi incrémental), ni les garanties (admissibilité vs borne additive) ne se recouvrent. Ce notebook traite exclusivement du second sens, celui de la théorie de la discrépance (Beck–Fiala, Spencer, Banaszczyk, Bansal–Jiang).

Le plan en trois gestes. (1) Mesurer ce que fait le hasard pur — et découvrir qu’il est déjà bon : \(\Omega(\sqrt{k})\) est inévitable, la conjecture \(O(\sqrt{k})\) est serrée. (2) Construire l’arrondi flottant de Beck–Fiala, l’algorithme classique qui garantit \(\operatorname{disc} \le 2k-1\) — les variables « flottent » dans \([-1,1]\) et se figent une à une dans une direction qui ne dérange aucune ligne active. (3) Mesurer l’écart à l’optimum exact avec CP-SAT en oracle, et dresser la ligne de front 2025 : où s’arrête le prouvé, où commence le conjecturé.

2. Définitions opérationnelles et l’appareil de mesure

Deux formalisations équivalentes du même problème :

  • Système d’ensembles : \(n\) éléments, \(m\) ensembles \(S_j \subseteq \{1..n\}\), chaque élément dans \(\le k\) ensembles (\(k\) = degré). Objectif : \(|\sum_{i \in S_j} x_i| \le D\) pour tout \(j\).
  • Matriciel : \(A \in \{0,1\}^{m \times n}\) la matrice d’incidence ; minimiser \(\|Ax\|_\infty\) sur \(x \in \{-1,+1\}^n\). (Le problème de Komlós, en section 6, remplace les colonnes 0/1 par des vecteurs unitaires.)

Notre appareil : des générateurs d’instances (système aléatoire de degré \(k\)), un calcul exact de la discrépance par force brute (limité à \(n \le 12\) : \(2^n\) coloriages), et le tableau comparatif qui servira tout au long du notebook.

import itertools
import numpy as np

rng = np.random.default_rng(42)


def systeme_aleatoire(n, m, k, rng=None):
    """Genere une matrice d'incidence m x n ou chaque element appartient a au plus k ensembles.

    Chaque colonne choisit k lignes recevant un 1 (tirage sans remise).
    """
    rng = rng or np.random.default_rng(0)
    A = np.zeros((m, n), dtype=int)
    for i in range(n):
        lignes = rng.choice(m, size=min(k, m), replace=False)
        A[lignes, i] = 1
    return A


def degre(A):
    """Degre du systeme : max sur les elements du nombre d'ensembles le contenant."""
    return int(A.sum(axis=0).max())


def disc_de_coloriage(A, x):
    """Desequilibre max d'un coloriage donne : ||Ax||_infini."""
    return int(np.abs(A @ x).max())


def disc_exacte_bruteforce(A):
    """Discrepance optimale par enumeration exhaustive (n <= 12 sinon trop lent)."""
    n = A.shape[1]
    meilleure = None
    for bits in itertools.product((-1, 1), repeat=n):
        d = disc_de_coloriage(A, np.array(bits))
        if meilleure is None or d < meilleure:
            meilleure = d
    return meilleure


# Petit systeme de reference : n=10 elements, m=6 ensembles, degre <= 3
A_ref = systeme_aleatoire(n=10, m=6, k=3, rng=np.random.default_rng(7))
print("Matrice d'incidence (lignes = ensembles, colonnes = elements) :")
print(A_ref)
print(f"\ndegre k = {degre(A_ref)}")
print(f"discrepance optimale (force brute, 2^10 = 1024 coloriages) = {disc_exacte_bruteforce(A_ref)}")
Matrice d'incidence (lignes = ensembles, colonnes = elements) :
[[0 0 0 1 0 0 0 0 0 1]
 [0 1 1 0 1 1 0 0 1 0]
 [0 0 0 0 1 1 1 0 0 1]
 [1 1 0 1 1 0 1 1 0 0]
 [1 1 1 1 0 1 1 1 1 0]
 [1 0 1 0 0 0 0 1 1 1]]

degre k = 3
discrepance optimale (force brute, 2^10 = 1024 coloriages) = 1

3. Le hasard pur est déjà presque bon : la borne inférieure \(\Omega(\sqrt{k})\)

Avant de construire un algorithme, mesurons la ligne de plancher. Colorions au hasard (\(x_i = \pm 1\) iid uniformes) et observons le déséquilibre maximal. Pour un ensemble de taille \(s\), la somme \(\sum_{i \in S} x_i\) est une marche aléatoire de \(s\) pas : sa déviation typique est \(\Theta(\sqrt{s})\). Sur un système de degré \(k\) régulier (ensembles de taille ~\(k\)), le hasard produit donc un déséquilibre ~\(\sqrt{k}\) — et le théorème de Chernoff (borne des queues binomiales) montre que sur certains systèmes, aucun coloriage ne fait sensiblement mieux : \(\operatorname{disc} \ge \Omega(\sqrt{k})\) (bornes inférieures d’Erdős–Spencer).

La leçon structurelle : la conjecture de Beck–Fiala — \(\operatorname{disc} = O(\sqrt{k})\) — est serrée. On ne peut pas espérer \(o(\sqrt{k})\) en général ; tout l’enjeu est de atteindre \(\sqrt{k}\), pas de faire mieux.

def meilleur_aleatoire(A, rng, essais=2000):
    """Meilleur coloriage aleatoire (essais tirages uniformes independants)."""
    m, n = A.shape
    meilleur, x_meilleur = None, None
    for _ in range(essais):
        x = rng.choice((-1, 1), size=n)
        d = disc_de_coloriage(A, x)
        if meilleur is None or d < meilleur:
            meilleur, x_meilleur = d, x
    return meilleur, x_meilleur


# Histogramme : distribution du deseéquilibre d'UN coloriage aleatoire (sans re-essais)
import numpy as np

def distribution_aleatoire(A, rng, essais=4000):
    m, n = A.shape
    return np.array([disc_de_coloriage(A, rng.choice((-1, 1), size=n)) for _ in range(essais)])

# Systeme regulier de degre k=12 : chaque element dans exactement 12 ensembles,
# ensembles de taille ~ 12*n/m quand m ~ n.
k = 12
A_reg = systeme_aleatoire(n=60, m=60, k=k, rng=np.random.default_rng(11))
dist = distribution_aleatoire(A_reg, np.random.default_rng(3))

q50, q90, q999 = np.percentile(dist, [50, 90, 99.9])
print(f"Systeme n=60, m=60, degre k={k} -- un coloriage aleatoire uniforme :")
print(f"  mediane du diseéquilibre max : {q50:.1f}")
print(f"  quantile 90%                  : {q90:.1f}")
print(f"  quantile 99.9%                : {q999:.1f}")
print(f"  racine de k (=plancher theorique ~ c*sqrt(k)) : {np.sqrt(k):.1f}")
print(f"  borne de Chernoff heuristique 2*sqrt(k*log(m)) : {2*np.sqrt(k*np.log(60)):.1f}")

# ASCII : histogramme de la distribution
valeurs, comptes = np.unique(dist, return_counts=True)
larg_max = 40
for v, c in zip(valeurs, comptes):
    barre = "#" * int(larg_max * c / comptes.max())
    print(f"  disc = {v:3d} | {barre} {c}")
Systeme n=60, m=60, degre k=12 -- un coloriage aleatoire uniforme :
  mediane du diseéquilibre max : 8.0
  quantile 90%                  : 11.0
  quantile 99.9%                : 15.0
  racine de k (=plancher theorique ~ c*sqrt(k)) : 3.5
  borne de Chernoff heuristique 2*sqrt(k*log(m)) : 14.0
  disc =   5 |  17
  disc =   6 | ####### 216
  disc =   7 | ######################## 688
  disc =   8 | ######################################## 1116
  disc =   9 | ############################### 877
  disc =  10 | #################### 568
  disc =  11 | ########## 303
  disc =  12 | #### 133
  disc =  13 | # 55
  disc =  14 |  20
  disc =  15 |  4
  disc =  17 |  3

Lecture du résultat

La médiane du déséquilibre d’un coloriage aléatoire tombe du même ordre que \(\sqrt{k}\) (ici \(k=12\) : \(\sqrt{k} \approx 3{,}5\), médiane observée quelques unités au-dessus, conforme au \(\Theta(\sqrt{k \log m})\) du maximum sur \(m\) ensembles — le facteur \(\log m\) est le prix du maximum). La borne inférieure d’Erdős–Spencer garantit qu’il existe des systèmes où même l’optimum est \(\ge c\sqrt{k}\) : le hasard n’est pas naïf, il est presque la bonne échelle. Toute la difficulté de Beck–Fiala est concentrée dans le facteur logarithmique et dans la garantie dans le pire cas — c’est précisément ce que l’arrondi flottant (section 4) achète : une garantie \(2k-1\) déterministe, puis ce que le papier 2025 achète : \(\tilde O(\sqrt k)\).

4. Beck–Fiala classique : l’arrondi flottant, \(\operatorname{disc} \le 2k-1\), implémenté

L’algorithme de la preuve originale (Beck–Fiala 1981) est un arrondi incrémental — le même geste que les arrondis de comptabilité qui n’arrondissent jamais un total au-delà de ce qu’il faut :

  1. Tout flotte. Partir de \(x = 0\) : chaque élément est flottant, \(x_i \in (-1, 1)\).
  2. Lignes actives. Une ligne (ensemble) est active si elle contient encore \(\ge k\) éléments flottants. Chaque élément étant dans \(\le k\) lignes, le nombre de lignes actives reste \(<\) le nombre de flottants (compte d’incidences) : le sous-système \(\sum_{i \in S_j \text{ flottants}} u_i = 0\) (pour tout \(j\) actif) a plus d’inconnues que d’équations indépendantes — un noyau non trivial.
  3. Bouger sans déranger. Choisir \(u\) dans ce noyau et avancer \(x \leftarrow x + t\,u\) jusqu’au premier contact : au moins un flottant atteint \(\pm 1\) et se fige. Par construction, aucune ligne active n’a changé de somme.
  4. Terminaison. Chaque phase fige au moins un élément ; en \(\le n\) phases tout est figé.

Quand une ligne cesse d’être active, elle ne garde que \(\le k-1\) flottants, dont l’arrondi final dérive d’au plus \(2\) chacun — c’est le \(2(k{-}1) + O(1) = 2k - 1\) du théorème (la preuve complète du compte exact est au Thm 1.3.1 de Matoušek, Lectures on Discrete Geometry ; ce notebook en implémente l’algorithme et en mesure le résultat).

L’implémentation : le noyau est extrait par SVD de la sous-matrice active ; le pas maximal \(t\) est le plus grand qui garde tous les flottants dans \([-1, 1]\).

def beck_fiala(A, verbose=False):
    """Arrondi flottant de Beck-Fiala : renvoie x in {-1,+1}^n.

    Phase : lignes actives = lignes avec >= k elements flottants (k = degre).
    Direction u dans le noyau des lignes actives (SVD), pas maximal, figeage.
    """
    A = A.astype(float)
    m, n = A.shape
    k = degre(A.astype(int))
    x = np.zeros(n)
    flottants = np.ones(n, dtype=bool)   # True = pas encore fige
    phases = 0

    while flottants.any():
        Af = A[:, flottants]                     # sous-matrice des flottants
        actives = (Af.sum(axis=1) >= k)          # lignes avec >= k flottants
        Af_act = Af[actives]

        if Af_act.shape[0] > 0:
            # Noyau de la sous-matrice active : vecteurs singuliers nuls
            _, s, Vt = np.linalg.svd(Af_act)
            u_local = Vt[-1]                     # direction du noyau (s.min ~ 0)
        else:
            u_local = np.ones(Af.shape[1])

        # Pas maximal t : garder x_i + t*u_i dans [-1, 1] pour chaque flottant
        t_pos, t_neg = np.inf, np.inf
        for idx, xi, ui in zip(np.where(flottants)[0], x[flottants], u_local):
            if ui > 1e-12:
                t_pos = min(t_pos, (1.0 - xi) / ui)
            elif ui < -1e-12:
                t_neg = min(t_neg, (xi + 1.0) / (-ui))
        t = min(t_pos, t_neg)
        if not np.isfinite(t) or t <= 0:
            # Degenerescence : figer directement les flottants au signe de x
            x[flottants] = np.where(x[flottants] >= 0, 1.0, -1.0)
            break

        x[flottants] += t * u_local
        # Figer tout flottant qui a atteint +-1 (tolerance numerique)
        touches = flottants & (np.abs(np.abs(x) - 1.0) < 1e-9)
        x[touches] = np.sign(x[touches])
        flottants[touches] = False
        phases += 1

    x = np.where(x >= 0, 1, -1)
    return x.astype(int), phases


# Verification sur banc croise : la garantie 2k-1 tient-elle ?
print(f"{'n':>4} {'m':>4} {'k':>3} {'BF':>4} {'2k-1':>5} {'racine(k)':>9} {'aleat.':>7} {'exacte':>6}")
for graine in range(6):
    r = np.random.default_rng(100 + graine)
    A = systeme_aleatoire(n=40, m=30, k=5, rng=r)
    x_bf, ph = beck_fiala(A)
    d_bf = disc_de_coloriage(A, x_bf)
    d_al, _ = meilleur_aleatoire(A, np.random.default_rng(graine), essais=500)
    k = degre(A)
    print(f"{A.shape[1]:>4} {A.shape[0]:>4} {k:>3} {d_bf:>4} {2*k-1:>5} {np.sqrt(k):>9.2f} {d_al:>7} {'-':>6}")

print(f"\nPhases executees sur la derniere instance : {ph} (<= n = {A.shape[1]})")
   n    m   k   BF  2k-1 racine(k)  aleat. exacte
  40   30   5    5     9      2.24       4      -
  40   30   5    6     9      2.24       3      -
  40   30   5    6     9      2.24       3      -
  40   30   5    6     9      2.24       2      -
  40   30   5    5     9      2.24       3      -
  40   30   5    4     9      2.24       4      -

Phases executees sur la derniere instance : 40 (<= n = 40)

Lecture du résultat

Sur chaque instance, la discrépance produite par l’arrondi flottant reste sous la barre deterministe \(2k-1\) (ici 4–6 pour une garantie 9) — la garantie du théorème est visible dans la sortie, pas seulement citée. Mais la comparaison au hasard appelle un enseignement plus finessé que « l’organisé bat le statistique » :

  • Le meilleur de 500 tirages aléatoires fait souvent aussi bien ou mieux que Beck–Fiala sur ces petites instances (colonnes aleat. : 2–4 contre 4–6 pour BF). Ce n’est pas un échec de l’algorithme : l’aléatoire optimisé par re-essais est un algorithme à son tour, sans aucune garantie — sur un système adverse, son déséquilibre explose (section 3 : quantile 99,9 % à 15, mediane 8). Beck–Fiala, lui, garantit \(2k-1\) sur toute entrée : c’est une assurance de pire cas, pas un record de benchmark.
  • L’échelle observée est plus proche de \(\sqrt{k} \approx 2{,}2\) que de \(2k{-}1 = 9\) : la borne du théorème est conservative. C’est tout l’écart que la conjecture \(O(\sqrt k)\) — et le papier 2025 — viennent combler : garantir ce que l’on observe déjà, dans le pire cas.

5. La ligne de front 2025 : prouvé, conjecturé, et le geste du « découplage »

Résultat Discrépance garantie Statut Où le voir dans ce notebook
Borne inférieure (Erdős–Spencer, méthode probabiliste) \(\ge \Omega(\sqrt{k})\) sur certains systèmes PROUVÉ Section 3 (le hasard mesure le plancher)
Beck–Fiala (1981) \(\le 2k - 1\) PROUVÉ (arrondi flottant) Section 4 (implémenté et mesuré)
Conjecture de Beck–Fiala \(O(\sqrt{k})\) RÉSOLUTION ANNONCÉE 2026 — \(< 3\sqrt{2\pi t}\), preprint non revu —
Banaszczyk (1998) \(O(\sqrt{k \log n})\) PROUVÉ (mesure gaussienne de corps convexes) —
Spencer, « six deviations suffice » (1986) \(O(\sqrt{n})\) pour \(m = n\) PROUVÉ (entropie partielle) Exercice 2
Bansal–Jiang 2025 (arXiv:2508.03961) \(O(\sqrt{k})\) dès \(k \ge \log^2 n\) ; \(\tilde O(\sqrt{k} + \sqrt{\log n})\) en dessous PROUVÉ — résout la conjecture pour \(k \ge \log^2 n\) —
Komlós (colonnes unitaires) conjecture \(O(1)\) ; Banaszczyk \(O(\sqrt{\log n})\) ; 2025 : \(\tilde O(\log^{1/4} n)\) ; 2026 : \(< 3\sqrt{2\pi} \approx 7{,}52\) annoncé RÉSOLUTION ANNONCÉE 2026 (non revue) Exercice 3
Guo–Fang–Lu 2026 (arXiv:2609.11189) \(\ell_\infty < 3\sqrt{2\pi}\) (Komlós, universelle) ; \(< 3\sqrt{2\pi t}\) (Beck–Fiala, degré \(t\)) RÉSOLUTION ANNONCÉE — preprint du 10/09/2026, non revu par les pairs —

Ce qu’apporte 2025. Tout dans Bansal–Jiang est algorithmique (polynomial) : une relaxation SDP suivie d’un arrondi par mouvement brownien discret guidé. La nouveauté est le découplage par indépendance spectrale affine : des contraintes de spectre ajoutées à la SDP font que les évolutions de discrépance des différentes lignes cessent de conspirer — la concentration redevient applicable à un processus qui, sans cela, corrèle ses dérives. Ce geste — contraindre la structure pour que les dérives cessent de conspirer — est le fil qui relie ce notebook au reste du parcours ; les ponts (funnel PyMC, double-Q des métaheuristiques, anti-fantôme ICT) sont rassemblés en section 7.

Ce qu’annonce 2026. Le preprint arXiv:2609.11189 (Guo–Fang–Lu, 10/09/2026) annonce une somme signée de norme \(\ell_\infty < 3\sqrt{2\pi} \approx 7{,}52\) pour Komlós — indépendamment de la dimension — et \(< 3\sqrt{2\pi t}\) pour Beck–Fiala au degré \(t\) : la dépendance en \(\sqrt{t}\) que la conjecture prédit. Statut d’annoncé, pas d’acquis : le papier a quelques jours et n’est pas revu par les pairs, et la preuve est existentielle (constante non optimale, pas d’algorithme polynomial revendiqué). Détail réflexif pour une série d’IA : l’abstract crédite la découverte à un « Automatic AI Research Agent » (Odin). Côté formel, la route annoncée (variation totale directionnelle, transformée de Banaszczyk) déplace l’étage manquant plutôt qu’elle ne le contourne — rien n’en existe dans Mathlib, et le jumeau formel discrepancy_lean/ garde ses conjectures en Prop nommées jusqu’à l’écho communautaire (livrable B de l’issue #12823).

6. CP-SAT en oracle exact : mesurer l’écart entre \(2k-1\) et l’optimum

La discrépance exacte est un programme pseudo-booléen : minimiser \(D\) tel que \(-D \le (Ax)_j \le D\) pour tout \(j\), \(x \in \{-1, +1\}^n\). C’est le terrain natif de CP-SAT (OR-Tools) — le même moteur que la Partie 2 (CSP) utilise. On l’emploie ici en oracle : il ne « résout » pas la théorie (il est exponentiel dans le pire cas), il mesure l’optimum sur des instances petites, pour quantifier l’écart entre la garantie Beck–Fiala et la vérité.

from ortools.sat.python import cp_model


def disc_cpsat(A, limite_temps=10.0):
    """Discrepance optimale par CP-SAT : min D tel que -D <= (Ax)_j <= D, x in {-1,+1}."""
    A = A.astype(int)
    m, n = A.shape
    modele = cp_model.CpModel()
    x = [modele.NewBoolVar(f"x{i}") for i in range(n)]        # x_i in {0,1} -> 2*x-1 in {-1,1}
    D = modele.NewIntVar(0, n, "D")
    for j in range(m):
        poids = [int(c) for c in A[j]]
        somme = sum(w * xi for w, xi in zip(poids, x))  # somme des x_i (0/1) de la ligne j
        # somme_{i in S} (2 x_i - 1) = 2*somme - |S| ; |desequilibre| <= D
        taille = int(A[j].sum())
        modele.Add(2 * somme - taille <= D)
        modele.Add(-(2 * somme - taille) <= D)
    modele.Minimize(D)
    solveur = cp_model.CpSolver()
    solveur.parameters.max_time_in_seconds = limite_temps
    statut = solveur.Solve(modele)
    if solveur.StatusName(statut) in ("OPTIMAL",):
        return solveur.Value(D)
    return None  # ni optimal ni faisable dans le temps imparti


print(f"{'n':>4} {'k':>3} | {'CP-SAT':>7} {'BF':>4} {'2k-1':>5} {'aleat.':>7} | ecart BF/exacte")
ecarts = []
for graine in range(8):
    r = np.random.default_rng(200 + graine)
    A = systeme_aleatoire(n=12, m=10, k=4, rng=r)
    d_exact = disc_cpsat(A)
    x_bf, _ = beck_fiala(A)
    d_bf = disc_de_coloriage(A, x_bf)
    d_al, _ = meilleur_aleatoire(A, np.random.default_rng(graine), essais=300)
    k = degre(A)
    if d_exact is not None:
        ecarts.append(d_bf - d_exact)
        print(f"{A.shape[1]:>4} {k:>3} | {d_exact:>7} {d_bf:>4} {2*k-1:>5} {d_al:>7} | {d_bf - d_exact:+d}")

print(f"\nEcart moyen Beck-Fiala vs optimum : {np.mean(ecarts):+.2f} (sur {len(ecarts)} instances)")
   n   k |  CP-SAT   BF  2k-1  aleat. | ecart BF/exacte
  12   4 |       1    3     7       1 | +2
  12   4 |       2    2     7       2 | +0
  12   4 |       1    4     7       2 | +3
  12   4 |       1    4     7       2 | +3
  12   4 |       1    4     7       2 | +3
  12   4 |       1    3     7       2 | +2
  12   4 |       1    3     7       1 | +2
  12   4 |       1    4     7       1 | +3

Ecart moyen Beck-Fiala vs optimum : +2.25 (sur 8 instances)

Lecture du résultat

Sur des instances petites (\(n = 12\), \(k = 4\)), l’optimum CP-SAT vaut 1–2 et Beck–Fiala flottant reste à 2–3 unités au-dessus (écart moyen \(+2{,}25\) sur le banc, jamais négatif). L’écart entre la garantie \(2k-1 = 7\) et la vérité (1–2) est la mesure de ce que la conjecture \(\sqrt k\) vient serrer : le vrai enjeu théorique n’est pas l’algorithme pratique, c’est la garantie de pire cas. Remarquez aussi que le meilleur aléatoire (2) est ici proche de l’optimum : sur instances typiques, tout se ressemble — c’est le pire cas qui sépare les méthodes, exactement la leçon de la section 4. C’est le verdict SOTA du notebook : CP-SAT (OR-Tools) est le vrai moteur pour l’optimum exact — déjà dépendance de la série (requirements.txt) — et l’arrondi flottant est le vrai algorithme de la preuve Beck–Fiala ; aucun substitut jouet n’est employé.

7. Ponts avec le reste de la série

Direction Lien Relation
Recherche à écart borné (LDS) Search-03c — Limited Discrepancy Search Désambiguïsation : écart à l’heuristique dans un arbre ≠ déséquilibre de sommes ; même intuition de « mesurer l’écart à l’équilibre »
Optimisation par contraintes Partie 2 — CSP CP-SAT sert d’oracle exact ici, de solveur primaire là — le même moteur, deux rôles
Métaheuristiques composables Partie 4 — MGS Le double-Q découple deux estimations pour empêcher la conspiration des biais — même geste que le découplage spectral de 2025
Programmation probabiliste PyMC (funnel) Le funnel de variance est une dérive correlée de paramètres ; contraindre la géométrie pour l’indépendance est l’antidote commun
Conscience intégrée ICT — anti-fantôme Créditer ce qui dépasse les parties découplées : la discrépance est ce qui reste quand l’indépendant est éliminé
Jumeau formel discrepancy_lean/ Livrable B de #12823 : les conjectures comme Prop nommées, la noix \(2k-1\) grignotée par boutes

8. Exercices

Exercice 1 : le facteur du maximum — retirer le \(\log m\)

La médiane observée en section 3 est au-dessus de \(\sqrt{k}\) : c’est le prix du maximum sur \(m\) ensembles (\(\Theta(\sqrt{k \log m})\) pour le hasard pur). Explorez comment le meilleur de \(T\) tirages (re-essais) comprime ce facteur : tracez la meilleure discrépance aléatoire en fonction de \(T \in \{1, 10, 10^2, 10^3, 10^4\}\) pour un système \(k = 12\), et comparez à la courbe \(\sqrt{k} \cdot \sqrt{\log(m) / \log T}\).

# Exercice 1 a completer
# Conseil : reprenez meilleur_aleatoire(A, rng, essais=T) pour T dans [1, 10, 100, 1000, 10000]
# et affichez en ASCII (ou matplotlib) la meilleure disc en fonction de T.
# A = systeme_aleatoire(n=60, m=60, k=12, rng=np.random.default_rng(11))
resultat = None  # TODO etudiant

Exercice 2 : « six deviations suffice » — le cas dense de Spencer

Pour \(m = n\) ensembles sur \(n\) éléments (sans contrainte de degré !), Spencer garantit \(\operatorname{disc} = O(\sqrt{n})\) — le fameux « six deviations suffice ». Générez des systèmes denses (\(m = n\), chaque ligne de taille ~\(n/2\)), calculez l’optimum CP-SAT pour \(n \in \{10, 12, 14\}\), et vérifiez que l’optimum croît bien comme \(c\sqrt n\) — estimez \(c\).

# Exercice 2 a completer
# Conseil : systeme dense = rng.choice pour chaque ligne ~ n/2 elements (colonne de degre eleve,
# donc Beck-Fiala ne s'applique PAS -- c'est le point de l'exercice).
# d = disc_cpsat(A) pour n = 10, 12, 14 ; puis comparez d a c*sqrt(n) et estimez c.
resultat = None  # TODO etudiant

Exercice 3 : Komlós — quand les colonnes deviennent des vecteurs unitaires

Le problème de Komlós remplace les colonnes 0/1 par des vecteurs unitaires de \(\mathbb{R}^m\) (la discrépance mesure toujours \(\|Ax\|_\infty\)). Générez \(n\) vecteurs unitaires aléatoires dans \(\mathbb{R}^m\) (\(m = 8\), \(n = 40\)), mesurez le meilleur coloriage aléatoire et l’arrondi Beck–Fiala adapté (le degré n’a plus de sens : que prendre comme seuil d’activité ?). Comparez à \(\sqrt{\log n}\) (Banaszczyk), à \(\log^{1/4} n\) (Bansal–Jiang 2025) et à la constante universelle annoncée \(3\sqrt{2\pi} \approx 7{,}52\) (Guo–Fang–Lu 2026, preprint non revu) : quelles échelles observez-vous ?

# Exercice 3 a completer
# Conseil : colonnes unitaires = rng.normal(size=(m, n)) puis normaliser chaque colonne.
# Pour le seuil d'activite de l'arrondi flottant adapte, testez k = m/2 et commentez.
# Echelles de comparaison : np.sqrt(np.log(n)) et np.log(n)**0.25.
resultat = None  # TODO etudiant

Conclusion

Ce notebook a suivi la discrépance combinatoire sur trois plans. Le plancher : le hasard pur produit \(\Theta(\sqrt{k})\) et aucune méthode ne peut espérer \(o(\sqrt{k})\) — la conjecture de Beck–Fiala est serrée. L’algorithme : l’arrondi flottant fige les variables une à une dans des directions qui ne dérangent aucune ligne active — une garantie déterministe \(2k-1\), mesurée ici très en deçà de sa borne. La frontière : Bansal–Jiang (2025) closent la conjecture pour \(k \ge \log^2 n\) par un découplage spectral affine qui empêche les dérives de conspirer — le même geste anti-conspiracy que le double-Q et l’anti-fantôme d’ICT rencontrent ailleurs dans ce parcours — et un preprint de septembre 2026 (Guo–Fang–Lu, arXiv:2609.11189) annonce la résolution générale à constante universelle \(3\sqrt{2\pi}\) : annoncée, pas encore revue — le jumeau formel garde ses Prop en attendant l’écho communautaire.

Références. Beck, J., & Fiala, T. (1981). « Integer-making theorems ». Discrete Applied Mathematics 3(1). — Matoušek, J. (2002). Lectures on Discrete Geometry, Ch. 1 (Thm 1.3.1). — Spencer, J. (1986). « Ten Lectures on the Probabilistic Method ». — Banaszczyk, W. (1998). « Balancing vectors and Gaussian chaos ». — Bansal, N., & Jiang, H. (2025). arXiv:2508.03961. — Guo, S., Fang, E. X., & Lu, J. (2026). « Vector Balancing via Directional Total Variation », arXiv:2609.11189 (preprint, résolution annoncée non revue).

Voir Search-03c (LDS) pour la désambiguïsation, le README de la Partie 1 pour la suite, et discrepancy_lean/ pour le jumeau formel.

Retour au sommet