Problem (Kirkman, 1850). Fifteen schoolgirls walk out three abreast for seven days. Arrange them so that no two walk together twice.

Answer. Possible. In design language: a Steiner triple system STS() is a set of triples of points covering every pair exactly once; it is resolvable (a Kirkman triple system, KTS) if the triples split into parallel classes (days), each partitioning the points. Kirkman’s problem asks for a KTS(15): triples, 5 per day, 7 days.

ObjectExists iffConstruction
STS() (Kirkman, 1847)Bose (), Skolem () below
KTS() (Ray-Chaudhuri and Wilson, 1971)KTS(15): resolve the lines of the projective space PG(3, 2)
KTS(15) up to isomorphism7 solutions (from 4 of the 80 STS(15))computer classification
number of triples of STS(); each point in triples

The 15-girl schedule. Label the girls by the nonzero vectors of , i.e. as 4-bit masks. The 35 triples are the lines of PG(3, 2): every pair lies in exactly the one line . A short search splits them into 7 days of 5 disjoint lines; with girl written as the letter of A..O, kirkman15() returns:

DayRows of three
1ABC, DHL, EJO, FKM, GIN
2ADE, BHJ, CMN, FIO, GKL
3AFG, BIK, CLO, DJN, EHM
4AHI, BLN, CEF, DKO, GJM
5AJK, BMO, CDG, EIL, FHN
6ALM, BDF, CIJ, EKN, GHO
7ANO, BEG, CHK, DIM, FJL
using Triple = array<int, 3>;
 
vector<Triple> boseSTS(int v) {                     // v = 3n, n odd; points x + n * level
    int n = v / 3, h = (n + 1) / 2; vector<Triple> T;
    auto op = [&](int x, int y) { return (x + y) * h % n; };   // idempotent commutative quasigroup: x o x = x
    for (int x = 0; x < n; x++) T.push_back({x, x + n, x + 2 * n});
    for (int i = 0; i < 3; i++) for (int x = 0; x < n; x++) for (int y = x + 1; y < n; y++)
        T.push_back({x + i * n, y + i * n, op(x, y) + (i + 1) % 3 * n});
    return T;
}
 
vector<Triple> skolemSTS(int v) {                   // v = 6n + 1; points x + 2n * level, infinity = 6n
    int n = (v - 1) / 6, m = 2 * n, inf = 6 * n; vector<Triple> T;
    auto op = [&](int x, int y) { int k = (x + y) % m; return k % 2 == 0 ? k / 2 : n + (k - 1) / 2; };   // x o x = x mod n
    for (int x = 0; x < n; x++) T.push_back({x, x + m, x + 2 * m});
    for (int i = 0; i < 3; i++) for (int x = 0; x < n; x++) T.push_back({inf, n + x + i * m, x + (i + 1) % 3 * m});
    for (int i = 0; i < 3; i++) for (int x = 0; x < m; x++) for (int y = x + 1; y < m; y++)
        T.push_back({x + i * m, y + i * m, op(x, y) + (i + 1) % 3 * m});
    return T;
}
 
// Split a Steiner triple system on points 0..v-1 into parallel classes (days); empty if it is not resolvable.
vector<vector<Triple>> resolve(int v, const vector<Triple>& T) {
    int days = (v - 1) / 2; vector<vector<int>> at(v);
    for (int t = 0; t < sz(T); t++) for (int p : T[t]) at[p].push_back(t);
    vector<char> used(sz(T), 0); vector<vector<Triple>> res(days);
    auto go = [&](auto&& self, int d, ll covered) -> bool {     // covered: points already placed on day d
        if (d == days) return true;
        if (covered == (1LL << v) - 1) return self(self, d + 1, 0);
        int p = __builtin_ctzll(~covered);                       // smallest point still walking alone
        for (int t : at[p]) if (!used[t] && !(covered >> T[t][0] & 1) && !(covered >> T[t][1] & 1) && !(covered >> T[t][2] & 1)) {
            used[t] = 1; res[d].push_back(T[t]);
            if (self(self, d, covered | 1LL << T[t][0] | 1LL << T[t][1] | 1LL << T[t][2])) return true;
            used[t] = 0; res[d].pop_back();
        }
        return false;
    };
    if (!go(go, 0, 0)) return {};
    return res;
}
 
vector<vector<Triple>> kirkman15() {                // girls 0..14 = nonzero 4-bit vectors minus 1
    vector<Triple> lines;
    for (int a = 1; a < 16; a++) for (int b = a + 1; b < 16; b++) if ((a ^ b) > b) lines.push_back({a - 1, b - 1, (a ^ b) - 1});
    return resolve(15, lines);
}