← Back to the problem

Indonesia IOAI selection 2025 · Correction by IOAI Training

Online Selection Test

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

Metric
points (1000, exact answers)
Evaluated on
written answers
Our score
all 40 items answered, verified
Expected level
exact answers (key hidden on TLX)

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

This is the online selection round of the 2025 exhibition AI event attached to the Indonesian national science olympiad (OSN), set on the TLX TOKI platform in Indonesian. It contains 40 answer boxes in seven themed blocks worth 1000 points in total: problem solving (a game and a path-counting puzzle), statistics, conditional probability, a least-squares line, word vectors (including decoding an alien vocabulary), output sizes of convolution layers, and a toy "language model" that recognises species from DNA strings. Most answers are a number, a short string or one option.

Every answer below was checked by computation (exhaustive search, dynamic programming, exact fractions or least squares): see the solution code.

How you are scored

Each box has the points printed in the statement, from 10 to 80 (for example 40 for the game, 80 for the path count, 13 for each word or DNA item), with no penalty for wrong answers. Short answers must match a fixed format (A x B, X.Y; A.B, lower-case words...), so the format is part of the answer. The platform does not show the answer key, so there is no official target score beyond getting each item exactly right.

Answers at a glance

itemsanswers
1 (game)b: Nesy always wins
2 (IOAI paths)174
3 (variance of duplicated sample)c: X>YX > Y
42N
5, 6, 7 (cinema)48, 28, 21
8, 9 (linear model)1192.7; 112.4; 7.4
10, 11 (vectors)25; 18 18
12 to 21 (alien words)Gohok wanita, Subi kalah, Matot ayah, Befar ibu, Dursit ratu, Gien menang, Ibakuz senang, Gelon raja, Wazuke pria, Nusgen sedih
22 to 25 (conv sizes)32 x 32, 14 x 14, 16 x 16, 15 x 7
26 to 40 (DNA)Ikan, Kucing, Kucing, Bebek, Bebek, Ikan, Ikan, Bebek, Kucing, Bebek, Ikan, Kucing, Ikan, Kucing, Bebek

Problem solving (items 1 and 2)

Item 1: the number-line game. Nesy always wins. This is a symmetry strategy. Nesy first plays the centre n/2n/2 (a rational number). After that, whenever Nyoo plays xx, Nesy answers with its mirror image n−xn - x. Why is the mirror move always legal?

  • Before Nyoo's move, the set of chosen numbers is symmetric around n/2n/2. Nyoo's xx is more than 1 away from every chosen number, so by symmetry n−xn - x is more than 1 away from every chosen number too.
  • n−xn - x must also be far from xx itself: ∣x−(n−x)∣=2∣x−n/2∣>2>1|x - (n-x)| = 2|x - n/2| > 2 > 1, because xx had to be more than 1 away from the centre n/2n/2.

So Nesy can always reply and is never the one stuck; the game is finite because the chosen points are more than 1 apart. A random-play simulation of this strategy (for n=4n = 4 to 9) never fails.

Item 2: counting IOAI on the board. 174. A path is a sequence of 4 cells, each one a king's move (8 directions) from the previous one, and revisiting a cell is allowed. Count with dynamic programming: for each cell, keep the number of ways to arrive there having spelled the prefix so far.

  1. Start: every I cell has 1 way.
  2. For O, A, I in turn: the count of a cell holding the next letter is the sum of the counts of its 8 neighbours.
  3. The answer is the sum of the final counts.

This makes O(cells×8×word length)O(\text{cells} \times 8 \times \text{word length}) operations instead of listing every path. Revisits come for free: a path may step back onto a cell it has already used, and the counts handle this automatically.

Statistics (items 3 and 4)

Item 3: c, X>YX > Y. Doubling the sample (every value appears twice) leaves the mean xˉ\bar x and the sum of squared deviations doubles: S→2SS \to 2S. With the sample variance, which divides by N−1N - 1,

X=SN−1,Y=2S2N−1,YX=2N−22N−1<1.X = \frac{S}{N-1}, \qquad Y = \frac{2S}{2N-1}, \qquad \frac{Y}{X} = \frac{2N-2}{2N-1} < 1 .

Duplicating data makes the sample look larger than it is, and the N−1N-1 correction shrinks. (With the population formula, dividing by NN, the two would be equal. Since the statement says "variansi sampel", the N−1N-1 convention applies.)

Item 4: 2N. Let μ\mu be the mean of the first 2N2N people. The combined mean is

2Nμ+Mμ/22N+M=34μ  ⟺  2N+M2=32N+34M  ⟺  M=2N.\frac{2N\mu + M\mu/2}{2N + M} = \frac34\mu \;\Longleftrightarrow\; 2N + \frac M2 = \frac32 N + \frac34 M \;\Longleftrightarrow\; M = 2N .

Probability (items 5 to 7)

Each probability is a sum over the three film types, weighted by P(film)P(\text{film}) (law of total probability).

  • Item 5: 48. P(popcorn)=0.40(0.20+0.40)+0.35(0.20+0.20)+0.25(0.10+0.30)=0.24+0.14+0.10=0.48P(\text{popcorn}) = 0.40(0.20 + 0.40) + 0.35(0.20 + 0.20) + 0.25(0.10 + 0.30) = 0.24 + 0.14 + 0.10 = 0.48, counting "popcorn only" and "both".
  • Item 6: 28. P(action∧some snack)=0.40×(1−0.30)=0.28P(\text{action} \wedge \text{some snack}) = 0.40 \times (1 - 0.30) = 0.28.
  • Item 7: 21. By Bayes, with "only a soft drink":
P(horror∣drink only)=0.25×0.100.40×0.10+0.35×0.15+0.25×0.10=0.0250.1175≈21.3%.P(\text{horror} \mid \text{drink only}) = \frac{0.25 \times 0.10}{0.40 \times 0.10 + 0.35 \times 0.15 + 0.25 \times 0.10} = \frac{0.025}{0.1175} \approx 21.3\% .

(Reading the condition as "bought a soft drink, possibly with popcorn" would give 23.7 %, so 23. The text says "only a soft drink", hence 21.)

Linear model (items 8 and 9)

Item 8: 1192.7. With α=227\alpha = 227, β=6\beta = 6 the predictions are 797, 743, 635. The residuals are −12,47,−35-12, 47, -35, so L=(144+2209+1225)/3=1192.67L = (144 + 2209 + 1225)/3 = 1192.67.

Item 9: 112.4; 7.4. Setting the two partial derivatives of LL to zero gives the usual least-squares line:

β=∑(Mi−Mˉ)(yi−yˉ)∑(Mi−Mˉ)2=2790378=7.381,α=yˉ−βMˉ=725−7.381×83=112.38.\beta = \frac{\sum (M_i - \bar M)(y_i - \bar y)}{\sum (M_i - \bar M)^2} = \frac{2790}{378} = 7.381, \qquad \alpha = \bar y - \beta \bar M = 725 - 7.381 \times 83 = 112.38 .

The statement rightly insists on rounding after optimising, and the minimal loss is L=952.4L = 952.4.

Word vectors (items 10 to 21)

Item 10: 25. raja−kalah=(8−23,36−16)=(−15,20)\text{raja} - \text{kalah} = (8 - 23, 36 - 16) = (-15, 20), and 225+400=25\sqrt{225 + 400} = 25, a 3-4-5 triangle.

Item 11: 18 18. senang+kalah−menang=(21+23−26,10+16−8)=(18,18)\text{senang} + \text{kalah} - \text{menang} = (21 + 23 - 26, 10 + 16 - 8) = (18, 18). That is almost exactly sedih (18,19)(18, 19): "happy + lose - win ≈\approx sad", the same analogy trick as "king + woman - man ≈\approx queen".

Items 12 to 21: decoding the alien words. "The structure between the words is the same" means the relations (difference vectors) are preserved. Look for one transformation that maps the 10 Indonesian points onto the 10 alien points.

  • A quick human approach: compare difference vectors. wanita−pria=(24,−5)\text{wanita} - \text{pria} = (24, -5), and among the alien words Gohok−Wazuke=(24,−5)\text{Gohok} - \text{Wazuke} = (24, -5) exactly. Likewise, the extreme points correspond: the highest Indonesian word raja maps to the highest alien word Gelon, the lowest menang to Gien, and so on.
  • A systematic check: we tried every affine map fixed by three anchor pairs, refined it by optimal assignment (Hungarian algorithm) plus least squares, and kept the best one. It is essentially a translation by about (3.7,11.3)(3.7, 11.3) (the linear part is close to the identity) with small noise, and its residual is less than a third of that of the next candidate. No exact isometry or affine map exists, so the "same structure" is only approximate, as with real embeddings.

Ten Indonesian word vectors (blue dots) each joined by an arrow to its alien translation (red squares); all arrows point roughly the same way, a shift of about 3.7 right and 11.3 up.

alienGohokSubiMatotBefarDursitGienIbakuzGelonWazukeNusgen
Indonesianwanitakalahayahiburatumenangsenangrajapriasedih

Convolution output sizes (items 22 to 25)

For each axis, out=⌊n+2p−ks⌋+1\text{out} = \left\lfloor \frac{n + 2p - k}{s} \right\rfloor + 1, with input size nn, kernel size kk, padding pp on each side and stride ss. "Same" padding means p=(k−1)/2p = (k-1)/2. Height and width are computed separately.

  • Item 22: 32 x 32, since "same" with stride 1 keeps the size.
  • Item 23: 14 x 14: 32→32→⌊27/2⌋+1=1432 \to 32 \to \lfloor 27/2 \rfloor + 1 = 14.
  • Item 24: 16 x 16: 64→⌊63/2⌋+1=3264 \to \lfloor 63/2 \rfloor + 1 = 32, then "same" (p=1p = 1) with stride 2 gives ⌊31/2⌋+1=16\lfloor 31/2 \rfloor + 1 = 16, then the 1×11\times1 convolution keeps 16.
  • Item 25: 15 x 7, tracking (height, width): (128,64)→(64,64)→(32,32)→(32,32)→(32,16)→(15,7)(128, 64) \to (64, 64) \to (32, 32) \to (32, 32) \to (32, 16) \to (15, 7). Layer 4's 1×71\times 7 kernel with stride (1,2)(1,2) halves only the width; the last layer gives ⌊29/2⌋+1=15\lfloor 29/2 \rfloor + 1 = 15 and ⌊13/2⌋+1=7\lfloor 13/2 \rfloor + 1 = 7.

DNA "language model" (items 26 to 40)

Look at the 12 training sequences before reaching for a model. Two numbers separate the species perfectly:

  • Kucing (cat) sequences are A/T-rich: 75 to 83 % of their bases are A or T, against at most 50 % for the others.
  • Bebek (duck) sequences never repeat a base twice in a row: they alternate, like CGCGAGAGAGCG.
  • Ikan (fish) sequences are G/C-rich with runs such as GGGG and CCCC (4 to 6 adjacent repeats).

The rule "A/T fraction at least 0.7: Kucing; otherwise no adjacent repeat: Bebek; otherwise Ikan" fits all 12 training sequences. A character bigram language model per species gives the same answers on all 15 test sequences: it learns P(next base∣current base)P(\text{next base} \mid \text{current base}) with add-one smoothing and picks the species whose model gives the sequence the highest likelihood. The bigram model is the natural tool here, because "repeats" and "alternation" are exactly properties of consecutive pairs, which a single-base (unigram) model cannot see: a unigram model confuses Bebek and Ikan, which share the same G/C content.

Scatter plot of the A/T fraction against the number of adjacent repeated bases: Kucing sequences sit on the right (A/T above 0.7), Bebek sequences on the bottom line with zero repeats, Ikan sequences top-left; the 15 test sequences (rings) fall clearly into these three regions.

What to remember

  • Symmetry strategies solve many two-player games: take the centre, then mirror. Always check that the mirrored move is legal.
  • Count paths with dynamic programming over (cell, prefix), not by listing them; revisits are then free.
  • Duplicating data does not add information: it shrinks the N−1N-1 correction and makes the sample look larger than it is.
  • Word-vector analogies are vector arithmetic: compare difference vectors, or fit one transformation plus an assignment, to align two embedding spaces.
  • Convolution sizes: ⌊(n+2p−k)/s⌋+1\lfloor (n + 2p - k)/s \rfloor + 1, axis by axis, layer by layer.
  • Look at the data before modelling: two hand-made features, or a bigram model that captures pairs of neighbours, beat any heavy machinery on 12 examples.
Full code (solution.py)
"""OSN AI 2025 exhibition, online selection (TLX TOKI bundle 'Seleksi Ekshibisi OSN AI 2025').
Brute-force / exact checks of every computational item, and the two figures."""
import itertools, math
from collections import Counter
from fractions import Fraction
import numpy as np
from scipy.optimize import linear_sum_assignment
import matplotlib; matplotlib.use("Agg"); import matplotlib.pyplot as plt

# ---- Q1: number-line game. Nesy plays n/2, then mirrors x -> n - x. Check the mirror is always legal
# on a fine rational grid by brute force over random games.
def mirror_ok(n, trials=2000, rng=np.random.default_rng(0)):
    for _ in range(trials):
        chosen = [n / 2]
        while True:
            grid = np.linspace(0, n, 400 * n + 1)
            legal = [x for x in grid if all(abs(x - c) > 1 for c in chosen)]
            if not legal: return False            # Nyoo cannot move: Nesy has won this game
            x = legal[rng.integers(len(legal))]; chosen.append(x)
            m = n - x
            if not all(abs(m - c) > 1 for c in chosen): return "mirror failed"
            chosen.append(m)
print("Q1 mirror strategy never fails:", all(mirror_ok(n, 30) is False for n in range(4, 10)))

# ---- Q2: IOAI paths on the 7x7 board (8-neighbour moves, revisits allowed)
G = ["IIIIIII", "IOAOOAI", "IOAAAAI", "IOOIAOI", "IOOOOOI", "IOAAAOI", "IIIIIII"]
D8 = [(a, b) for a in (-1, 0, 1) for b in (-1, 0, 1) if (a, b) != (0, 0)]
def count_paths(word):
    ways = {(r, c): 1 for r in range(7) for c in range(7) if G[r][c] == word[0]}   # DP over positions
    for ch in word[1:]:
        nxt = Counter()
        for (r, c), w in ways.items():
            for a, b in D8:
                rr, cc = r + a, c + b
                if 0 <= rr < 7 and 0 <= cc < 7 and G[rr][cc] == ch: nxt[(rr, cc)] += w
        ways = nxt
    return sum(ways.values())
print("Q2 IOAI paths:", count_paths("IOAI"))

# ---- Q3-4
x = np.array([60, 72, 55, 81, 64.]); xx = np.r_[x, x]
print("Q3 sample variance (N-1):", x.var(ddof=1), "->", xx.var(ddof=1), "| population (N):", x.var(), "->", xx.var())
N = Fraction(1); M = (Fraction(2) * N * (1 - Fraction(3, 4))) / (Fraction(3, 4) - Fraction(1, 2)); print("Q4 M =", M, "N")

# ---- Q5-7 (cinema snacks)
P = {"action": .40, "family": .35, "horror": .25}
T = {"action": (.30, .20, .10, .40), "family": (.45, .20, .15, .20), "horror": (.50, .10, .10, .30)}  # none, pop only, drink only, both
pop = sum(P[f] * (T[f][1] + T[f][3]) for f in P)
only_drink = sum(P[f] * T[f][2] for f in P)
print("Q5", math.floor(100 * pop + 1e-9), "Q6", math.floor(100 * P["action"] * (1 - T["action"][0]) + 1e-9),
      "Q7", math.floor(100 * P["horror"] * T["horror"][2] / only_drink), f"({P['horror'] * T['horror'][2] / only_drink:.4f})")

# ---- Q8-9 (linear model)
Mx = np.array([95, 86, 68.]); y = np.array([785, 790, 600.])
print("Q8", round(np.mean((y - (227 + 6 * Mx)) ** 2), 1))
beta = np.cov(Mx, y, bias=True)[0, 1] / Mx.var(); alpha = y.mean() - beta * Mx.mean()
print(f"Q9 alpha={alpha:.4f} beta={beta:.4f} -> {alpha:.1f}; {beta:.1f}")

# ---- Q10-21 (word vectors)
V = {"senang": (21, 10), "pria": (6, 29), "wanita": (30, 24), "ayah": (11, 26), "ibu": (36, 19), "raja": (8, 36),
     "ratu": (32, 29), "sedih": (18, 19), "menang": (26, 8), "kalah": (23, 16)}
A = {"Gohok": (34, 37), "Subi": (28, 29), "Matot": (14, 35), "Befar": (38, 31), "Dursit": (38, 40), "Gien": (29, 19),
     "Ibakuz": (24, 20), "Gelon": (11, 46), "Wazuke": (10, 42), "Nusgen": (22, 30)}
print("Q10", math.dist(V["raja"], V["kalah"]), " Q11", tuple(np.add(V["senang"], V["kalah"]) - V["menang"]))
ind, al = list(V), list(A); Vi = np.array([V[k] for k in ind], float); Al = np.array([A[k] for k in al], float)
# search: every affine map defined by 3 anchor pairs, refined by assignment + least squares; keep the best
best = (np.inf, None)
for tri in itertools.permutations(range(10), 3):
    X3 = np.c_[Vi[list(tri)], np.ones(3)]
    if abs(np.linalg.det(X3)) < 1e-9: continue
    Tm = np.linalg.solve(X3, Al[[0, 2, 5]])
    for _ in range(5):
        C = (((np.c_[Vi, np.ones(10)] @ Tm)[:, None] - Al[None]) ** 2).sum(2); _, col = linear_sum_assignment(C)
        Tm = np.linalg.lstsq(np.c_[Vi, np.ones(10)], Al[col], rcond=None)[0]
    res = (((np.c_[Vi, np.ones(10)] @ Tm) - Al[col]) ** 2).sum()
    if res < best[0] - 1e-9: best = (res, tuple(col), Tm)
res, col, Tm = best
pairs = {al[j]: ind[i] for i, j in enumerate(col)}
shift = (Al[list(col)] - Vi).mean(0)
print("Q12-21", pairs, f"best affine residual {res:.1f}; mean shift {shift.round(1)}")
fig, ax = plt.subplots(figsize=(8.5, 6.5))
for i, j in enumerate(col):
    ax.annotate("", Al[j], Vi[i], arrowprops=dict(arrowstyle="->", color="#bbb"))
    ax.plot(*Vi[i], "o", color="#2471a3"); ax.text(Vi[i][0] + .4, Vi[i][1] - 1.2, ind[i], color="#2471a3", fontsize=9)
    ax.plot(*Al[j], "s", color="#c0392b"); ax.text(Al[j][0] + .4, Al[j][1] + .5, al[j], color="#c0392b", fontsize=9)
ax.set_title(f"Alien words = Indonesian words shifted by about ({shift[0]:.1f}, {shift[1]:.1f})")
ax.set_xlabel("dimension 1"); ax.set_ylabel("dimension 2")
plt.tight_layout(); plt.savefig("figures/word_vectors.png", dpi=150); plt.close()

# ---- Q22-25 (convolution output sizes): out = floor((n + 2p - k) / s) + 1 ; 'same' = p = (k-1)/2
o = lambda n, k, p, s: (n + 2 * p - k) // s + 1
print("Q22 32 x 32  Q23", o(32, 5, 0, 2), "Q24", o(o(o(64, 7, 3, 2), 3, 1, 2), 1, 0, 1))
h, w = o(128, 7, 3, 2), o(64, 3, 1, 1); h, w = o(h, 5, 2, 2), o(w, 5, 2, 2); h, w = o(h, 3, 1, 1), o(w, 3, 1, 1)
h, w = o(h, 1, 0, 1), o(w, 7, 3, 2); h, w = o(h, 3, 0, 2), o(w, 3, 0, 2); print("Q25", h, "x", w)

# ---- Q26-40 (DNA species): two hand features + a bigram language model per species
train = {"Kucing": ["GATTATAAAGCA", "AATCTAATTATG", "ACAAAAAATTTC", "CATAAAGAAATA"],
         "Bebek": ["ACGACGCGATCG", "CGCGAGAGAGCG", "GCTGCGTGTCAG", "CGAGCTAGCAGC"],
         "Ikan": ["CCAGGGGAGCCC", "TTAGGTCAAGGC", "CGGTCCCCAAAG", "AGCCGGGGCAAG"]}
test = ["CCGGCTCGTAAG", "CATGAAAATATA", "TAATAACACATA", "TCGCTCGCACGT", "GCACGCGCAGAT", "TTTCCCGCTGGG", "CGGGCTTCCCGG",
        "GTCACTATCACG", "CGAAATTTATTA", "GCGCTATATGCG", "GGCAGGTAGCCC", "ACAAATAAACCA", "ATGAGGGCCCCG", "AAAAACTATTAT", "CGCTCTAGCGCA"]
at = lambda s: sum(c in "AT" for c in s) / len(s)
rep = lambda s: sum(s[i] == s[i + 1] for i in range(len(s) - 1))
rule = lambda s: "Kucing" if at(s) >= 0.7 else ("Bebek" if rep(s) == 0 else "Ikan")
def bigram_lm(seqs):
    c, ctx = Counter(), Counter()
    for s in seqs:
        for i in range(1, len(s)): c[s[i-1:i+1]] += 1; ctx[s[i-1]] += 1
    return lambda s: sum(math.log((c[s[i-1:i+1]] + 1) / (ctx[s[i-1]] + 4)) for i in range(1, len(s)))
lms = {k: bigram_lm(v) for k, v in train.items()}
assert all(rule(s) == k for k, v in train.items() for s in v)        # the rule fits the training data
for q, s in enumerate(test, 26):
    lm_pred = max(lms, key=lambda k: lms[k](s))
    print(f"Q{q} {s} rule={rule(s):6s} bigram-LM={lm_pred}")
fig, ax = plt.subplots(figsize=(8, 5.2)); colors = {"Kucing": "#e67e22", "Bebek": "#2471a3", "Ikan": "#27ae60"}
jit = np.random.default_rng(1)
for k, v in train.items():
    ax.scatter([at(s) for s in v], [rep(s) + jit.uniform(-.12, .12) for s in v], s=90, color=colors[k], label=f"{k} (train)")
for s in test:
    ax.scatter(at(s), rep(s) + jit.uniform(-.12, .12), s=60, facecolors="none", edgecolors=colors[rule(s)], linewidths=1.8)
ax.axvline(0.7, color="#888", ls="--"); ax.axhline(0.5, xmax=0.62, color="#888", ls=":")
ax.text(0.72, 6.6, "A/T-rich: Kucing", fontsize=9); ax.text(0.52, 0.15, "no repeated base: Bebek", fontsize=9); ax.text(0.38, 6.4, "G/C runs: Ikan", fontsize=9)
ax.set_xlabel("fraction of A and T"); ax.set_ylabel("adjacent repeated bases (AA, GG, ...)")
ax.set_title("Training sequences (filled) and test sequences (rings, coloured by prediction)"); ax.legend(frameon=False, fontsize=9)
plt.tight_layout(); plt.savefig("figures/dna_features.png", dpi=150); plt.close()