196 lines
7.7 KiB
JavaScript
196 lines
7.7 KiB
JavaScript
|
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|||
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|||
|
|
|
|||
|
|
// Épreuves lourdes des plafonds (§ 14.4, § 14.12) : l'accord de la
|
|||
|
|
// programmation dynamique et de l'énumération sur une grille plus large que
|
|||
|
|
// celle de la série node, jusqu'à 8 tables et 5 tours ; la forme de la grande
|
|||
|
|
// démonstration et sa variante, arbitrées par l'énumération ; la limite de
|
|||
|
|
// l'énumération à sa frontière exacte.
|
|||
|
|
import assert from 'node:assert/strict';
|
|||
|
|
import fc from 'fast-check';
|
|||
|
|
import { describe, test } from '../../test/lanceur.js';
|
|||
|
|
import { normaliser } from './configuration.js';
|
|||
|
|
import { ErreurConfiguration } from './erreurs.js';
|
|||
|
|
import { plafondsAPriori, plafondsAPrioriParEnumeration } from './plafond.js';
|
|||
|
|
|
|||
|
|
const SANS_CONTRAINTE = {
|
|||
|
|
separerAppartenances: false,
|
|||
|
|
nouveauxVoisins: false,
|
|||
|
|
nouvelleTable: false,
|
|||
|
|
varierAppartenances: false,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Graine de la grille, écrite ici (§ 14.12) ; chaque case de la grille en
|
|||
|
|
// dérive la sienne.
|
|||
|
|
const GRAINE = 314_159;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Instance d'une salle : capacites[t] et ancres[t] décrivent la table
|
|||
|
|
* d'index t ; partiels[j][r] est la table imposée au j-ème partiellement
|
|||
|
|
* fixé au tour r, −1 pour un tour libre. Identifiants 1 à N : les ancrés,
|
|||
|
|
* table par table, puis les partiels, puis les mobiles ; la table d'index t
|
|||
|
|
* a pour identifiant 100 + t. Lève ce que normaliser lève.
|
|||
|
|
*/
|
|||
|
|
function instanceDe({ capacites, ancres, mobiles, tours, partiels = [] }) {
|
|||
|
|
const participants = [];
|
|||
|
|
const reservations = [];
|
|||
|
|
const nouveau = () => {
|
|||
|
|
const id = participants.length + 1;
|
|||
|
|
participants.push({ id, nom: `P${id}`, appartenance: null });
|
|||
|
|
return id;
|
|||
|
|
};
|
|||
|
|
ancres.forEach((a, t) => {
|
|||
|
|
for (let i = 0; i < a; i += 1) {
|
|||
|
|
reservations.push({ participant: nouveau(), table: 100 + t, portee: 'tous' });
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
for (const tables of partiels) {
|
|||
|
|
const id = nouveau();
|
|||
|
|
tables.forEach((t, r) => {
|
|||
|
|
if (t < 0) return;
|
|||
|
|
reservations.push({ participant: id, table: 100 + t, portee: 'tour', tour: r + 1 });
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
for (let i = 0; i < mobiles; i += 1) nouveau();
|
|||
|
|
return normaliser({
|
|||
|
|
participants,
|
|||
|
|
tables: capacites.map((capacite, t) => ({ id: 100 + t, numero: t + 1, capacite })),
|
|||
|
|
tours,
|
|||
|
|
reservations,
|
|||
|
|
contraintes: SANS_CONTRAINTE,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Une case de la grille : T tables et R tours fixés, capacités de 2 à 6.
|
|||
|
|
// Une table est entièrement ancrée une fois sur quatre, et garde sinon de 1 à
|
|||
|
|
// c sièges libres. Chaque tour d'un partiel est libre une fois sur deux, ou
|
|||
|
|
// désigne par son rang l'une des tables qui gardent un siège, rang pris
|
|||
|
|
// modulo leur nombre : seul l'empilement de partiels à une même table, au
|
|||
|
|
// même tour, fait encore refuser une instance.
|
|||
|
|
function arbitraireCase(T, R) {
|
|||
|
|
const table = fc
|
|||
|
|
.integer({ min: 2, max: 6 })
|
|||
|
|
.chain((c) =>
|
|||
|
|
fc.tuple(
|
|||
|
|
fc.constant(c),
|
|||
|
|
fc.oneof(
|
|||
|
|
{ arbitrary: fc.integer({ min: 0, max: c - 1 }), weight: 3 },
|
|||
|
|
{ arbitrary: fc.constant(c), weight: 1 },
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
const tour = fc.oneof(fc.constant(-1), fc.integer({ min: 0, max: T - 1 }));
|
|||
|
|
return fc
|
|||
|
|
.record({
|
|||
|
|
salle: fc.array(table, { minLength: T, maxLength: T }),
|
|||
|
|
mobiles: fc.integer({ min: 0, max: 10 }),
|
|||
|
|
rangs: fc.array(fc.array(tour, { minLength: R, maxLength: R }), { maxLength: 4 }),
|
|||
|
|
})
|
|||
|
|
.map(({ salle, mobiles, rangs }) => {
|
|||
|
|
const ouvertes = [];
|
|||
|
|
salle.forEach(([c, a], t) => {
|
|||
|
|
if (a < c) ouvertes.push(t);
|
|||
|
|
});
|
|||
|
|
const partiels = rangs.map((tours) =>
|
|||
|
|
tours.map((rang) => (rang < 0 || ouvertes.length === 0 ? -1 : ouvertes[rang % ouvertes.length])),
|
|||
|
|
);
|
|||
|
|
return {
|
|||
|
|
capacites: salle.map(([c]) => c),
|
|||
|
|
ancres: salle.map(([, a]) => a),
|
|||
|
|
mobiles,
|
|||
|
|
tours: R,
|
|||
|
|
partiels,
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
describe("plafond a priori : la programmation dynamique contre l'énumération, grille large", () => {
|
|||
|
|
test('de 2 à 8 tables et de 2 à 5 tours, chaque case de la grille : le même plafond pour chacun', () => {
|
|||
|
|
const parCase = [];
|
|||
|
|
const parStatut = [0, 0, 0];
|
|||
|
|
for (let T = 2; T <= 8; T += 1) {
|
|||
|
|
for (let R = 2; R <= 5; R += 1) {
|
|||
|
|
let acceptees = 0;
|
|||
|
|
fc.assert(
|
|||
|
|
fc.property(arbitraireCase(T, R), (description) => {
|
|||
|
|
let instance;
|
|||
|
|
try {
|
|||
|
|
instance = instanceDe(description);
|
|||
|
|
} catch (erreur) {
|
|||
|
|
if (erreur instanceof ErreurConfiguration && erreur.code === 'SURRESERVATION') return;
|
|||
|
|
throw erreur;
|
|||
|
|
}
|
|||
|
|
if (instance.N === 0) return;
|
|||
|
|
acceptees += 1;
|
|||
|
|
for (let p = 0; p < instance.N; p += 1) parStatut[instance.statut[p]] += 1;
|
|||
|
|
assert.deepEqual(plafondsAPriori(instance), plafondsAPrioriParEnumeration(instance));
|
|||
|
|
}),
|
|||
|
|
{ seed: GRAINE + 10 * T + R, numRuns: 300 },
|
|||
|
|
);
|
|||
|
|
parCase.push([T, R, acceptees]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Chaque case compte ses instances acceptées : une case que le
|
|||
|
|
// générateur viderait n'éprouverait rien.
|
|||
|
|
assert.equal(parCase.length, 28);
|
|||
|
|
const maigres = parCase
|
|||
|
|
.filter(([, , acceptees]) => acceptees < 200)
|
|||
|
|
.map(([T, R, n]) => `${T} tables, ${R} tours : ${n} acceptées sur 300, 200 au moins`);
|
|||
|
|
assert.deepEqual(maigres, []);
|
|||
|
|
assert.ok(parStatut[1] >= 5000, `${parStatut[1]} partiellement fixés comparés, 5000 au moins`);
|
|||
|
|
assert.ok(parStatut[2] >= 5000, `${parStatut[2]} ancrés comparés, 5000 au moins`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("forme de la grande démonstration et sa variante : la programmation dynamique égale l'énumération", () => {
|
|||
|
|
// Principale : 29 tables de 8 puis 4 de 7, un ancré par table. Variante :
|
|||
|
|
// 33 tables de 8, 4 places libres. 227 mobiles, 4 tours ; un mobile
|
|||
|
|
// énumère C(36, 4) = 58 905 multiensembles.
|
|||
|
|
const principale = instanceDe({
|
|||
|
|
capacites: [...Array(29).fill(8), ...Array(4).fill(7)],
|
|||
|
|
ancres: Array(33).fill(1),
|
|||
|
|
mobiles: 227,
|
|||
|
|
tours: 4,
|
|||
|
|
});
|
|||
|
|
// Les ancrés portent les identifiants 1 à 33, table par table.
|
|||
|
|
const attendu = new Array(260).fill(28);
|
|||
|
|
for (let t = 29; t < 33; t += 1) attendu[t] = 24;
|
|||
|
|
assert.deepEqual(plafondsAPriori(principale), attendu);
|
|||
|
|
assert.deepEqual(plafondsAPrioriParEnumeration(principale), attendu);
|
|||
|
|
|
|||
|
|
const variante = instanceDe({
|
|||
|
|
capacites: Array(33).fill(8),
|
|||
|
|
ancres: Array(33).fill(1),
|
|||
|
|
mobiles: 227,
|
|||
|
|
tours: 4,
|
|||
|
|
});
|
|||
|
|
const vingtHuit = new Array(260).fill(28);
|
|||
|
|
assert.deepEqual(plafondsAPriori(variante), vingtHuit);
|
|||
|
|
assert.deepEqual(plafondsAPrioriParEnumeration(variante), vingtHuit);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("la limite de l'énumération : 300 000 multiensembles passent, un de plus est refusé", () => {
|
|||
|
|
// Un tour libre : le compte est celui des tables admissibles.
|
|||
|
|
const unTour = (tables) =>
|
|||
|
|
instanceDe({ capacites: Array(tables).fill(2), ancres: [], mobiles: 1, tours: 1 });
|
|||
|
|
assert.deepEqual(plafondsAPrioriParEnumeration(unTour(300_000)), [0]);
|
|||
|
|
assert.throws(
|
|||
|
|
() => plafondsAPrioriParEnumeration(unTour(300_001)),
|
|||
|
|
(erreur) =>
|
|||
|
|
erreur instanceof ErreurConfiguration
|
|||
|
|
&& erreur.code === 'ENUMERATION_TROP_GRANDE'
|
|||
|
|
&& erreur.details.tablesAdmissibles === 300_001
|
|||
|
|
&& erreur.details.toursLibres === 1,
|
|||
|
|
);
|
|||
|
|
// Deux tours libres : C(775, 2) = 299 925 passent, là où 774² le
|
|||
|
|
// refuserait ; C(776, 2) = 300 700 est refusé.
|
|||
|
|
const deuxTours = (tables) =>
|
|||
|
|
instanceDe({ capacites: Array(tables).fill(2), ancres: [], mobiles: 2, tours: 2 });
|
|||
|
|
const passe = deuxTours(774);
|
|||
|
|
assert.deepEqual(plafondsAPrioriParEnumeration(passe), plafondsAPriori(passe));
|
|||
|
|
assert.throws(
|
|||
|
|
() => plafondsAPrioriParEnumeration(deuxTours(775)),
|
|||
|
|
(erreur) => erreur instanceof ErreurConfiguration && erreur.code === 'ENUMERATION_TROP_GRANDE',
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
});
|