← Back to the problem

Kazakhstan FAIO 2024 · Correction by IOAI Training

G. Into Space

This page gives the full approach. Read it once you have searched.

Metric
exact answer on hidden tests
Evaluated on
self-generated tests (official tests are hidden)
Our score
0 errors on 924 multisets
Expected level
correct yes/no on every hidden test

Expected level reached

We solved the problem ourselves: downloaded the data, ran the experiments and measured the score below. The statement and data belong to their organisers.

The problem in brief

Four magnets must be placed in one plane so that all six pairwise distances are whole numbers. You receive six natural numbers and must answer "yes" if four points in a plane can have exactly these six distances, "no" otherwise. The statement does not say which number is the distance between which pair of magnets, so we treat the assignment as free: the answer is "yes" if some way of attaching the six numbers to the six pairs works. Behind this little puzzle lies a central tool of machine learning: recovering coordinates from distances.

How you are scored

Hidden tests, one line of output, "yes" or "no" (time limit 1 s). We validate our program against an independent geometric construction on all 924 multisets of six numbers between 1 and 7, and on hand-made examples.

First look: what can go wrong?

Three things can prevent a placement:

  1. No placement at all, even in space: the distances violate a triangle inequality, like a "triangle" with sides 1, 1, 3.
  2. A placement in space only: six equal distances 1 describe a regular tetrahedron, which cannot be flattened.
  3. A wrong pairing: the numbers 3, 3, 4, 4, 5, 5 work as a 3 by 4 rectangle with diagonals 5, but if you attach both 5s to opposite sides, they do not.

A natural first idea is to use the Cayley-Menger determinant, which equals 288 V2288\,V^2 where VV is the volume of the tetrahedron spanned by the four points: flat means V=0V = 0. But it is not enough. The numbers 1,1,1,3,3,81, 1, 1, 3, 3, 8 admit a pairing with zero determinant, yet no placement at all: that pairing contains the impossible triangle 1, 1, 3. A "volume squared" of 0 computed from impossible distances means nothing. We need a test that checks existence and flatness together.

The key idea

Turn distances into dot products. Put magnet 0 at the origin and call p1,p2,p3p_1, p_2, p_3 the positions of the others. Since ∥pi−pj∥2=∥pi∥2+∥pj∥2−2 pi⋅pj\lVert p_i - p_j\rVert^2 = \lVert p_i\rVert^2 + \lVert p_j\rVert^2 - 2\,p_i \cdot p_j,

Gij=pi⋅pj=d0i2+d0j2−dij22,i,j∈{1,2,3}.G_{ij} = p_i \cdot p_j = \frac{d_{0i}^2 + d_{0j}^2 - d_{ij}^2}{2}, \qquad i, j \in \{1, 2, 3\}.

GG is the Gram matrix of the three vectors, computed from the distances alone. Two facts from linear algebra settle everything:

  • Real points with these distances exist if and only if GG is positive semidefinite (PSD), because a Gram matrix is always G=P⊤PG = P^\top P (so x⊤Gx=∥Px∥2≥0x^\top G x = \lVert P x\rVert^2 \ge 0), and conversely a PSD matrix can be factored as P⊤PP^\top P to recover the points.
  • The points fit in a plane if and only if, in addition, rank⁡G≤2\operatorname{rank} G \le 2, that is det⁡G=0\det G = 0 (three vectors in a plane are linearly dependent).

For a symmetric 3×33 \times 3 matrix with det⁡G=0\det G = 0, being PSD is equivalent to all its principal minors being ≥0\ge 0: the three diagonal entries and the three 2×22\times 2 minors GiiGjj−Gij2G_{ii}G_{jj} - G_{ij}^2. Each 2×22 \times 2 minor is, up to a factor, Heron's formula for the squared area of a triangle containing magnet 0, so this check is precisely what catches the impossible triangle in 1,1,1,3,3,81, 1, 1, 3, 3, 8: there G11G22−G122=1−49/4<0G_{11}G_{22} - G_{12}^2 = 1 - 49/4 < 0.

Everything is an integer if we work with 2G2G, so the test is exact: no floating-point tolerance to tune, which matters because a flat configuration sits exactly on the boundary det⁡G=0\det G = 0.

Three planar quadruples with whole-number distances found by the program: the 3 by 4 rectangle, 2 2 3 4 4 4 and 3 5 7 7 7 8.

This "distances to Gram matrix" step is exactly classical multidimensional scaling (MDS): double-centre the squared distances, take the eigenvectors of the resulting Gram matrix, and you get coordinates, with the number of non-zero eigenvalues giving the dimension. It is used to draw maps from distance tables and to visualise embeddings; kernel methods rely on the same PSD condition.

The full solution

PAIRS = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]

def planar(D):                       # D: pair -> distance
    G = gram2(D)                     # 2 x Gram matrix, integers
    if det3(G) != 0:
        return False                 # needs a third dimension (or nothing works)
    if any(G[k][k] < 0 for k in range(3)):
        return False
    return all(G[i][i] * G[j][j] - G[i][j] ** 2 >= 0
               for i, j in [(0, 1), (0, 2), (1, 2)])

def solve(nums):
    return any(planar(dict(zip(PAIRS, p))) for p in set(itertools.permutations(nums)))

There are at most 6!=7206! = 720 pairings, so trying them all is instant. If your reading of the statement is that the input order is fixed (first number for pair 01, second for 02 and so on), drop the loop and call planar once.

Validation. solution.py --check builds the points with ruler-and-compass geometry in floating point (place 0 and 1 on an axis, intersect circles for 2 and 3, compare the last distance) and compares with the exact test on all 924 multisets of numbers 1 to 7, with zero disagreements. Hand cases: rectangle (yes), rhombus with sides 5 and diagonals 6, 8 (yes), collinear points at 0, 1, 3, 6 (yes: degenerate but planar), regular tetrahedron (no), 1,1,1,3,3,81, 1, 1, 3, 3, 8 (no).

Results

approachverdict on tricky casescorrect?
Gram matrix PSD + det⁡=0\det = 0, exact integers, all pairingsall hand cases and 924 multisets rightyes
Cayley-Menger (or det⁡G\det G) =0= 0 onlysays "yes" to 1,1,1,3,3,81, 1, 1, 3, 3, 8no
same with floats and abs(det) < 1e-9fragile for large inputs (values up to 101810^{18})risky
fixed pairing in input order only"no" on 3 3 4 4 5 5 if the 5s are not lastdepends on the reading
ruler-and-compass construction in floatsright with a toleranceyes, but needs care

What to remember

  • Distances determine dot products: pi⋅pj=12(d0i2+d0j2−dij2)p_i \cdot p_j = \frac{1}{2}(d_{0i}^2 + d_{0j}^2 - d_{ij}^2). This is the heart of classical MDS.
  • A matrix of dot products must be positive semidefinite, and its rank is the dimension the points need.
  • "Determinant zero" is necessary, not sufficient: always check that the configuration exists at all (PSD, triangle inequalities).
  • Prefer exact integer arithmetic when the answer sits on a boundary (det⁡=0\det = 0): floating-point tolerances turn yes/no questions into guesses.
  • When the statement leaves a mapping unspecified, search over it: 720 permutations cost nothing.
Full code (solution.py)
"""FAIO 2024 (Kazakhstan) qualification, problem G "Into Space".

Six natural numbers: can they be the six pairwise distances of 4 points
lying in one plane? The statement does not say which number belongs to
which pair, so we try every assignment of the numbers to the 6 pairs.

Test for one assignment (exact integer arithmetic, no floating point):
put point 0 at the origin; the Gram matrix of the vectors p1, p2, p3 is
    G_ij = p_i . p_j = (d0i^2 + d0j^2 - dij^2) / 2 .
Distances come from real points  <=>  G is positive semidefinite;
the points fit in a plane        <=>  in addition rank G <= 2, i.e. det G = 0.

    echo "3 4 5 4 5 3" | python solution.py   -> yes
    python solution.py --check                -> cross-checks + figure
"""
import sys
import itertools
import math

PAIRS = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]


def gram2(D):
    """2 * Gram matrix of p1, p2, p3 relative to p0 (integers)."""
    d = lambda i, j: 0 if i == j else D[(min(i, j), max(i, j))]
    return [[d(0, i) ** 2 + d(0, j) ** 2 - d(i, j) ** 2 for j in (1, 2, 3)] for i in (1, 2, 3)]


def det3(M):
    (a, b, c), (d, e, f), (g, h, i) = M
    return a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g)


def planar(D):
    G = gram2(D)
    if det3(G) != 0:                                   # rank 3: needs 3 dimensions
        return False
    # PSD test for a singular symmetric 3x3: every principal minor >= 0
    if any(G[k][k] < 0 for k in range(3)):
        return False
    return all(G[i][i] * G[j][j] - G[i][j] ** 2 >= 0 for i, j in [(0, 1), (0, 2), (1, 2)])


def solve(nums):
    return any(planar(dict(zip(PAIRS, perm))) for perm in set(itertools.permutations(nums)))


# ------------------------------------------------------------------ checks
def numeric_planar(D, eps=1e-7):
    """Independent check: actually construct the points with ruler and compass."""
    d = lambda i, j: D[(min(i, j), max(i, j))]
    a, b, c = d(0, 1), d(0, 2), d(1, 2)
    if a + b < c or a + c < b or b + c < a:
        return False
    x = (a * a + b * b - c * c) / (2 * a)
    p2 = (x, math.sqrt(max(b * b - x * x, 0)))
    r0, r1 = d(0, 3), d(1, 3)
    if r0 + r1 < a - eps or abs(r0 - r1) > a + eps:    # the two circles do not meet
        return False
    x3 = (r0 * r0 - r1 * r1 + a * a) / (2 * a)
    y3 = math.sqrt(max(r0 * r0 - x3 * x3, 0))
    return any(abs(math.dist((x3, s * y3), p2) - d(2, 3)) < eps for s in (1, -1))


def check():
    import matplotlib
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt

    cases = {(3, 4, 5, 4, 5, 3): True,      # 3 x 4 rectangle, diagonals 5
             (1, 3, 6, 2, 5, 3): True,      # collinear points 0, 1, 3, 6
             (5, 5, 5, 5, 6, 8): True,      # rhombus with diagonals 6 and 8
             (1, 1, 1, 1, 1, 1): False,     # regular tetrahedron: needs 3-D
             (1, 1, 1, 3, 3, 8): False}     # det G = 0 for some assignment, but not realisable
    for c, want in cases.items():
        assert solve(c) == want, c
    cm_only = lambda ms: any(det3(gram2(dict(zip(PAIRS, p)))) == 0
                             for p in set(itertools.permutations(ms)))
    yes = mism = fake = 0
    for ms in itertools.combinations_with_replacement(range(1, 8), 6):
        exact = solve(ms)
        num = any(numeric_planar(dict(zip(PAIRS, p))) for p in set(itertools.permutations(ms)))
        mism += exact != num; yes += exact; fake += cm_only(ms) and not exact
    print(f"multisets of 1..7: {yes} 'yes', {mism} disagreements with the construction, "
          f"{fake} where det = 0 alone would wrongly say yes")
    assert mism == 0

    # figure: three integral planar quadruples found by the search
    plt.rcParams.update({"font.size": 12})
    fig, axs = plt.subplots(1, 3, figsize=(13, 4.6), dpi=150)
    for ax, ms, title in zip(axs, [(3, 3, 4, 4, 5, 5), (2, 2, 3, 4, 4, 4), (3, 5, 7, 7, 7, 8)],
                             ["rectangle 3 x 4: 3 3 4 4 5 5", "2 2 3 4 4 4", "3 5 7 7 7 8"]):
        D = next(dict(zip(PAIRS, p)) for p in itertools.permutations(ms) if planar(dict(zip(PAIRS, p))))
        a, b, c = D[(0, 1)], D[(0, 2)], D[(1, 2)]
        x = (a * a + b * b - c * c) / (2 * a); p = [(0, 0), (a, 0), (x, math.sqrt(max(b * b - x * x, 0)))]
        r0, r1 = D[(0, 3)], D[(1, 3)]
        x3 = (r0 * r0 - r1 * r1 + a * a) / (2 * a); y3 = math.sqrt(max(r0 * r0 - x3 * x3, 0))
        p.append(next((x3, s * y3) for s in (1, -1) if abs(math.dist((x3, s * y3), p[2]) - D[(2, 3)]) < 1e-7))
        for (i, j), dist in D.items():
            (x1, y1), (x2, y2) = p[i], p[j]
            ax.plot([x1, x2], [y1, y2], color="#2e86ab", lw=2)
            ax.text((x1 + x2) / 2, (y1 + y2) / 2, str(dist), color="#d1495b",
                    fontsize=12, ha="center", va="center",
                    bbox=dict(boxstyle="round,pad=0.15", fc="white", ec="none"))
        ax.scatter(*zip(*p), s=90, color="#1d3557", zorder=5)
        ax.set_aspect("equal"); ax.axis("off"); ax.set_title(title)
    fig.suptitle("Four points in a plane with all six distances whole numbers (red labels)")
    fig.tight_layout(); fig.savefig("figures/examples.png", facecolor="white")
    print("figure written")


if __name__ == "__main__":
    if "--check" in sys.argv:
        check()
    else:
        nums = list(map(int, sys.stdin.read().split()[:6]))
        print("yes" if solve(nums) else "no")