โ† Back to the problem

India INAIO 2026 ยท Correction by IOAI Training

Stage 1 Alternate Test (28 February 2026)

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

Metric
points (exact answers)
Evaluated on
written answers
Our score
all items solved, checked in Python

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 make-up sitting (28 February 2026) of the first-round online test of the Indian National AI Olympiad 2026. It has the same structure as the main test, with new numbers and twists. Section A1 is puzzle-style reasoning: a toy language, choosing which LLM-extracted features to trust, designing a similarity function, a recursive epidemic, a knight's path. Section A2 contains short modelling puzzles: facility location under two distances, a chess-team match, reading a price model off a graph, constrained optimisation, Bayes. Section B has four case studies: a three-class hospital triage classifier, ranking metrics, 1-D convolution kernels, and pruning a linear classifier. Answers are an option or an integer, usually a floor โŒŠโ‹…โŒ‹\lfloor\cdot\rfloor. Every computable answer was checked by exact or brute-force computation in solution.py.

How you are scored

Each item is worth the points printed next to it (3 points per A1/A2 item, 1 to 4 per case-study question), with no partial credit inside a question. No answer key or qualifying score is published, so the answers below are our own derivations.

Answers at a glance

itemanswer
A1 Imperial glyphsE: ๅœŸ้€ ่˜‹ๆžœ็”œ
A1 Faulty oracleF: {F1,F3,F6}\{F_1, F_3, F_6\}
A1 Professor searchC
A1 V-67 outbreakD: 102
A1 PikachuE: 15
A2 Godown15
A2 Blind board gambit31
A2 Crude curve49
A2 Catalyst45
A2 Pit-stop15
B1 Triage Q1 to Q743, 90, option 5 (point E), option 2, option 3, claims 3 and 5, 13
B2 WonkaRank Q1 to Q510, option 4, option 3, option 4 (ScoreD only), 30
B3 Convolution Q1 to Q5A; D, F; E, F; E, F; B, D, F
B4 Pruning Q1 to Q336, 13, 65

The key idea

Most items reward the same habits: rebuild the full object (confusion matrix, ranking, parameter set) from the partial information given, look for invariants and closed forms (the minimiser of a sum of Itakura-Saito terms is the mean, antisymmetric kernels vanish on palindromes), and brute-force anything small enough (knight paths, team assignments, pruning pairs) instead of reasoning by hand.

The full solution

Section A1

Imperial glyphs: E. Aligning the sentences gives ่˜‹ๆžœ = apple, ็”œ = sweet ("This is a sweet apple" = ้€™็”œ่˜‹ๆžœ) and ้€  = make. "Does the tree make the apple beautiful?" (ๆจน้€ ่˜‹ๆžœ็พŽ๏ผŸ) and "The mountain paints the palace black" (ๅฑฑๅก—ๅฎฎ้ป‘) show the order subject, verb, object, resulting adjective. "Soil" never appears in the examples, but every option except one uses either ๅฑฑ (mountain), a wrong order or an extra ๅพˆ ("very"). Only E ๅœŸ้€ ่˜‹ๆžœ็”œ has a new subject glyph, ้€ , ่˜‹ๆžœ, ็”œ in the right order. The traps: A uses ๅฑฑ (that is "mountain makes"), B adds ๅพˆ, C and F put the apple first, D uses ๅก— and ๅฎฎ (paint the palace).

Faulty oracle: F {F1,F3,F6}\{F_1, F_3, F_6\}. Ask whether a small text-only model with web retrieval can produce each feature reliably.

  • F1F_1 (character count) is broken by tokenisation: the model sees sub-word tokens, not characters (a single len() would do it exactly).
  • F6F_6 (which outlet published this headline) and F3F_3 (that outlet's political leaning) require guessing the source from a headline. That is speculative attribution: errors in F6F_6 propagate into F3F_3, the leaning label is itself subjective, and a source guess is a proxy for the label rather than evidence about the content.
  • With retrieval, F2F_2 (publication year) becomes checkable, F4F_4 (headline/body consistency) is a reading-comprehension task on the given text, and F5F_5, F7F_7, F8F_8 are standard.

The only option made of the three structurally unreliable features is F. (In the main test there was no retrieval, so the year F2F_2 was the unreliable one.)

Professor search: C. Check each rule. A, E and F can exceed 1 (rule 5). B multiplies by NED(d,dโ€ฒ)\mathrm{NED}(d, d'), which is not 0 when departments differ (rule 2). D penalises titles more than names (rule 4). C, 1d=dโ€ฒexpโก(โˆ’2(1โˆ’NED(n,nโ€ฒ))โˆ’(1โˆ’NED(t,tโ€ฒ)))\mathbf 1_{d = d'}\exp\big(-2(1 - \mathrm{NED}(n, n')) - (1 - \mathrm{NED}(t, t'))\big), is 0 across departments, symmetric (edit distance is), in (0,1](0, 1], equal to 1 exactly when both NED are 1 (identical strings), and the name mismatch has twice the weight.

V-67 outbreak: D 102. Iterating day by day, with Xt=2Nt+(2+4t)MtX_t = 2N_t + (2 + 4t)M_t, Nt+1=Nt/2+3Xt/4N_{t+1} = N_t/2 + 3X_t/4, Mt+1=Mt/2+Xt/4M_{t+1} = M_t/2 + X_t/4 and RR collecting half of Nt+MtN_t + M_t each day: R1=8R_1 = 8, R2=28R_2 = 28, R3=102R_3 = 102.

Pikachu: E 15. An exhaustive depth-first search over knight paths on the 4ร—44\times 4 board finds a longest path from (0,0)(0,0) to (3,3)(3,3) visiting 15 pads. All 16 is impossible: a knight alternates colours, (0,0)(0,0) and (3,3)(3,3) have the same colour, so a path between them has an odd number of pads.

Section A2

Godown: 15. For Manhattan distance, xx and yy separate, and each minimiser is any median. The xx values sorted are 40,80,120,22040, 80, 120, 220, so the medians form [80,120][80, 120], midpoint 100; for yy also 100. So GS=(100,100)G_S = (100, 100). For Itakura-Saito,

โˆ‚โˆ‚uโˆ‘i(piuโˆ’lnโกpiuโˆ’1)=โˆ‘i(1uโˆ’piu2)=0โ€…โ€ŠโŸบโ€…โ€Šu=pห‰,\frac{\partial}{\partial u}\sum_i \Big(\frac{p_i}{u} - \ln\frac{p_i}{u} - 1\Big) = \sum_i\Big(\frac{1}{u} - \frac{p_i}{u^2}\Big) = 0 \iff u = \bar p,

so GPG_P is the mean, (115,100)(115, 100). Then D=15D = 15 exactly. (A numerical optimiser returns 114.993 and a careless floor of 14: use the closed form.)

Blind board gambit: 31. Enumerating Anand's 6 assignments against the three equally likely Gukesh assignments, the best on average is (Y,X,Z)(Y, X, Z). Against the actual (C,A,B)(C, A, B) the board probabilities for Anand are P(Yย beatsย C)=0.640P(Y \text{ beats } C) = 0.640, P(Xย beatsย A)=0.360P(X \text{ beats } A) = 0.360 and P(Zย beatsย B)=0.151P(Z \text{ beats } B) = 0.151. Winning at least 2 of 3 boards gives p=0.3118p = 0.3118, so โŒŠ100pโŒ‹=31\lfloor 100p\rfloor = 31.

Crude curve: 49. Read the terms off their fingerprints. Before month 18 only A+Bt+Ecosโก(2ฯ€t/ฯ„E)A + Bt + E\cos(2\pi t/\tau_E) acts. With ฯ„E=12\tau_E = 12 (extremes every 6 months), the points (0,58)(0, 58), (6,45)(6, 45), (12,64)(12, 64) give A=50A = 50, B=0.5B = 0.5, E=8E = 8, and (18โˆ’,51)(18^-, 51) checks. The vertical jump at 18 gives D=26โˆ’51=โˆ’25D = 26 - 51 = -25; (24,45)(24, 45) and (30,32)(30, 32) check. The recovery starts at ฯ„C=30\tau_C = 30, and (36,71)(36, 71) gives C=20/(1โˆ’eโˆ’6)=20.05C = 20/(1 - e^{-6}) = 20.05. The downward trend starts at ฯ„F=42\tau_F = 42, and (48,71)(48, 71) gives F=โˆ’1.008F = -1.008; (54,52)(54, 52) and (60,65)(60, 65) check (51.95 and 64.90). At t=75t = 75 the cosine is cosโก(12.5ฯ€)=0\cos(12.5\pi) = 0:

P(75)=50+37.5โˆ’25+20.05โˆ’1.008โ‹…33=49.28โ‡’49.P(75) = 50 + 37.5 - 25 + 20.05 - 1.008\cdot 33 = 49.28 \Rightarrow 49 .

Catalyst: 45. Maximise 10cAโˆ’2cA2+20cBโˆ’3cB210c_A - 2c_A^2 + 20c_B - 3c_B^2 with cA+cBโ‰ค5c_A + c_B \le 5. The unconstrained optima (cA=2.5c_A = 2.5, cB=3.33c_B = 3.33) need more than 5 moles, so the constraint binds. Equal marginal yields, 10โˆ’4cA=20โˆ’6cB10 - 4c_A = 20 - 6c_B with cA+cB=5c_A + c_B = 5, give cA=2c_A = 2, cB=3c_B = 3, and a yield of 12+33=4512 + 33 = 45.

Pit-stop: 15. By Bayes over the 8 (safety car, strategy) cells, with weights P(SC)=0.4P(\mathrm{SC}) = 0.4:

P(neitherโˆฃH)=0.6โ‹…0.5โ‹…0.2+0.4โ‹…0.05โ‹…0.4โˆ‘sc,sP(sc)โ€‰P(sโˆฃsc)โ€‰P(Hโˆฃs,sc)=0.0680.4465=0.152โ‡’15.P(\text{neither} \mid H) = \frac{0.6\cdot0.5\cdot0.2 + 0.4\cdot0.05\cdot0.4}{\sum_{\mathrm{sc},s} P(\mathrm{sc})\,P(s\mid \mathrm{sc})\,P(H\mid s,\mathrm{sc})} = \frac{0.068}{0.4465} = 0.152 \Rightarrow 15 .

Section B1: triage

Rebuild the confusion matrix (rows predicted, columns actual; class sizes 500, 350, 150). The diagonal is sensitivity times class size: 420, 245, 120. Specificity gives the false positives of each predicted class: (1โˆ’speci)(1000โˆ’ni)=50(1 - \text{spec}_i)(1000 - n_i) = 50, 7878, 93.593.5. With the two given entries (10 ICU predicted Outpatient, 25 Outpatient predicted ICU) the remaining cells follow, but the published numbers are slightly inconsistent: the column of actual class 2 adds up to 353.5 instead of 350, and the false positives of predicted class 2 come out at 75 instead of 78. We therefore use, for each question, the most direct relation.

  • Q1: 43. Among predicted ICU, the false alarms are FP3=0.11ร—850=93.5\mathrm{FP}_3 = 0.11\times850 = 93.5, so x=93.5/(120+93.5)=0.438x = 93.5/(120 + 93.5) = 0.438. (Rebuilding the cell from the class-2 column instead gives 42.)
  • Q2: 90. Hospital patients sent home are exactly the false positives of predicted class 1, 0.10ร—500=500.10 \times 500 = 50, so x=450/500=0.90x = 450/500 = 0.90.
  • Q3: point E (option 5). The expected cost per patient is 0.15โ‹…8โ€‰(1โˆ’TPR)+0.85โ€‰FPR0.15\cdot 8\,(1 - \mathrm{TPR}) + 0.85\,\mathrm{FPR}: A 0.593, B 0.427, C 0.356, D 0.334, E 0.287, F 0.295. The optimum is where the ROC slope equals 0.85โ‹…10.15โ‹…8=0.71\frac{0.85\cdot 1}{0.15\cdot 8} = 0.71.
  • Q4: option 2, the curve improves. The biomarker carries information independent of solds_{\text{old}} given the class, so adding it with a positive weight separates the classes better. In a simulation (Gaussian scores, AUC 0.87) the new curve lies above the old one at every FPR and the AUC rises to 0.95. Strictly, a fixed ฮด\delta is not the optimal (likelihood-ratio) weight, and with an already near-perfect score the curves can touch; but among the options "improves consistently" is the intended answer.
  • Q5: option 3. A ROC curve is invariant to prevalence (1), to the threshold (2: moving along the curve), to monotone transforms (4) and to removing a random subset of a class (6), and sampling noise (5) does not produce a consistent shift. Only a better score, from new predictive features, moves the whole curve up and left.
  • Q6: claims 3 and 5. Claim 1 is false: the cascade Y always outputs one label (only the pairwise scheme Z can be inconsistent and need voting). Claim 2 is false: "strictly greater on any dataset" never holds. Claim 3 is true: a single scorer with thresholds is ordinal regression, which squared loss on ordered targets enforces naturally, and squared loss is outlier-sensitive. Claim 4 is false: it is the opposite. Claim 5 is true: the log loss is a proper scoring rule, while the hinge loss only produces margins.
  • Q7: 13. The cost is (55โ‹…10+25โ‹…100+40โ‹…50+68.5โ‹…20+10โ‹…500+20โ‹…100)/1000=13.42\big(55\cdot10 + 25\cdot100 + 40\cdot50 + 68.5\cdot20 + 10\cdot500 + 20\cdot100\big)/1000 = 13.42. The answer is 13 whichever consistent completion of the matrix is used (13.25 to 13.42).

Section B2: WonkaRank

  • Q1: 10. DCG@5=3logโก22+0+10logโก24+2logโก25+5logโก26=3+5+0.861+1.934=10.80\mathrm{DCG}@5 = \frac{3}{\log_2 2} + 0 + \frac{10}{\log_2 4} + \frac{2}{\log_2 5} + \frac{5}{\log_2 6} = 3 + 5 + 0.861 + 1.934 = 10.80.
  • Q2: option 4. The top 8 of Strategy A contains 3 of the 4 relevant chocolates (precision 0.375, recall 0.75). Strategy B gives score 1 to D9 (it has hazelnut), which enters the top 8 instead of D6: precision 0.5, recall 1. Both improve.
  • Q3: option 3. DCG is maximised by sorting by relevance, [D1,D8,D5,D9][D_1, D_8, D_5, D_9] (relevances 10, 5, 3, 2): 15.52, against 15.45 for option 5 and 15.25 for option 1.
  • Q4: option 4, ScoreD only. The top 3 are: ScoreA [D2,D5,D1][D_2, D_5, D_1] (2/3), ScoreB [D2,D3,D4][D_2, D_3, D_4] (0), ScoreC [D1,D2,D5][D_1, D_2, D_5] (2/3), ScoreD [D1,D8,D5][D_1, D_8, D_5] (3/3). Dividing by 1+โˆ‘di1 + \sum d_i penalises chocolates that contain everything (the ticketless D2).
  • Q5: 30. The ScoreA ranking is [D2,D5,D1,D8,D3][D_2, D_5, D_1, D_8, D_3]; relevant items sit at positions 2, 3, 4, so PI-Recall@5=(0.6+0.4+0.2)/4=0.30\text{PI-Recall}@5 = (0.6 + 0.4 + 0.2)/4 = 0.30.

Section B3: convolution

With yi=โˆ‘jhjxi+kโˆ’jy_i = \sum_j h_j x_{i+k-j} (kernel reversed):

  • Q1: A. The padded series is [2,1,3,2,4,2][2, 1, 3, 2, 4, 2]; with stride 2 the windows (2,1)(2, 1), (3,2)(3, 2), (4,2)(4, 2) give [4,7,8][4, 7, 8].
  • Q2: D and F. A second-difference kernel (โˆ‘h=0\sum h = 0 and zero on linear ramps) gives 0 on constants and on the smooth ramp, and large values on zig-zags: [1,โˆ’2,1][1, -2, 1] gives S=โˆ’10S = -10, and [โˆ’2,4,โˆ’2][-2, 4, -2] gives 2020 on zz. A gives 12 on the smooth signal, B fails on constants, C and E are first-difference-like.
  • Q3: E and F. SS must be 0 for constant velocity, negative when decelerating and positive when accelerating, i.e. a positive multiple of [1,โˆ’2,1][1, -2, 1]: EE (โˆ’3-3 and +3+3) and FF (โˆ’9-9 and +9+9). B has the opposite sign.
  • Q4: E and F. A kernel that vanishes on every linear signal (the second difference, up to sign) gives Sabs=0S_{\text{abs}} = 0 on constants and on the ramp zz, and 24 on the spike. All others are non-zero on zz.
  • Q5: B, D and F. The single output is y=h1x4+h2x3+h3x2+h4x1y = h_1x_4 + h_2x_3 + h_3x_2 + h_4x_1. It vanishes on every palindrome (x1=x4x_1 = x_4, x2=x3x_2 = x_3) exactly when hh is antisymmetric (h4=โˆ’h1h_4 = -h_1, h3=โˆ’h2h_3 = -h_2): [1,โˆ’1,1,โˆ’1][1,-1,1,-1], [2,1,โˆ’1,โˆ’2][2,1,-1,-2] and [1,0,0,โˆ’1][1,0,0,-1]. (No single linear output can be non-zero on all asymmetric signals, since its null space has dimension 3, but these three are the kernels that pass the stated test.)

Section B4: pruning

  • Q1: 36. Option A forces w=(2,โˆ’3,โˆ’1,โˆ’1)w = (2, -3, -1, -1), so s6=w1+w3+w4=0s_6 = w_1 + w_3 + w_4 = 0: predicted 0 for a positive example, accuracy 5/6. Option B forces w=(3,โˆ’5,โˆ’1,โˆ’1)w = (3, -5, -1, -1) with all six signs correct, accuracy 1. So โˆฅwโˆ—โˆฅ2=9+25+1+1=36\lVert w^\ast\rVert^2 = 9 + 25 + 1 + 1 = 36.
  • Q2: 13. Trying all 21 pairs, the unique best (accuracy 6/7) is {6,7}\{6, 7\}, so jโˆ—+kโˆ—=13j^\ast + k^\ast = 13.
  • Q3: 65. After pruning, the scores are (1,1,1,1,1,0,โˆ’5)(1, 1, 1, 1, 1, 0, -5). Every bโˆˆ[1,5]b \in [1, 5] gives accuracy 7/7, and the tie-break picks the largest, bโˆ—=5b^\ast = 5, so 13ร—5=6513\times5 = 65.

Results

All answers are in the table at the top. Every numeric item (V-67, Pikachu, Godown, gambit, crude curve, catalyst, pit-stop, all of B1 Q1 to Q3 and Q7, B2, B3, B4) was recomputed in solution.py. Two items depend on how a slightly inconsistent statement is read: B1 Q1 (43 with the specificity of class 3, 42 if the cell is rebuilt from the class-2 column) and B1 Q4 (option 2 intended; option 6 is the pedantic alternative).

What to remember

  • Closed forms beat optimisers for floors. The Itakura-Saito minimiser is exactly the mean; a numerical optimiser stopping at 114.993 would have turned 15 into 14.
  • Rebuild the confusion matrix and check it. Sensitivities, specificities and class sizes overdetermine it; checking the redundant equations reveals inconsistencies in the data before they silently change your answer.
  • ROC curves depend only on the ranking within each class. Prevalence, thresholds, monotone transforms and random subsampling never move them; better features do.
  • Kernels are finite differences. First difference: velocity; second difference: acceleration and spikes; antisymmetric kernels vanish on palindromes. Check the null space before checking the options.
  • Brute force small search spaces. Knight paths, team assignments and pruning pairs are enumerable in milliseconds, and parity arguments (the knight's colour) explain the answer afterwards.
Full code (solution.py)
"""INAIO 2026 Stage 1 Alternate Test (28 Feb 2026): brute-force / exact checks of every computable item."""
import itertools, math
import numpy as np
from scipy.optimize import minimize

# ---------- A1 V-67 outbreak ----------
N, M, R = 8.0, 8.0, 0.0
for t in range(3):
    X = 2 * N + (2 + 4 * t) * M
    R += 0.5 * N + 0.5 * M
    N, M = 0.5 * N + 0.75 * X, 0.5 * M + 0.25 * X
print("A1 V-67   R3 =", R)

# ---------- A1 Pikachu: longest knight path (0,0) -> (3,3) on 4x4 ----------
moves = [(1, 2), (2, 1), (-1, 2), (-2, 1), (1, -2), (2, -1), (-1, -2), (-2, -1)]
best = 0
def dfs(p, seen):
    global best
    if p == (3, 3):
        best = max(best, len(seen)); return
    for dx, dy in moves:
        q = (p[0] + dx, p[1] + dy)
        if 0 <= q[0] < 4 and 0 <= q[1] < 4 and q not in seen:
            seen.add(q); dfs(q, seen); seen.remove(q)
dfs((0, 0), {(0, 0)})
print("A1 Pikachu max pads =", best)

# ---------- A2 Godown ----------
xs, ys = np.array([40, 120, 220, 80.]), np.array([180, 80, 120, 20.])
GS = np.array([np.mean(np.sort(xs)[1:3]), np.mean(np.sort(ys)[1:3])])     # L1: median interval midpoint
IS = lambda p, u: p / u - np.log(p / u) - 1
GPnum = minimize(lambda z: IS(xs, z[0]).sum() + IS(ys, z[1]).sum(), [100, 100]).x
GP = np.array([xs.mean(), ys.mean()])     # d/du sum IS(p_i, u) = sum(1/u - p_i/u^2) = 0  ->  u = mean(p)
print("A2 Godown GS", GS, "GP exact", GP, "(numerical", GPnum.round(3), ") floor D =", math.floor(np.hypot(*(GS - GP)) + 1e-9))

# ---------- A2 Blind board gambit ----------
G = dict(A=2100, B=1800, C=1600); An = dict(X=2000, Y=1700, Z=1500)
pw = lambda r, s: 1 / (1 + 10 ** ((s - r) / 400))
def p_anand(gk, an):
    ps = [pw(An[a], G[g]) for g, a in zip(gk, an)]
    return sum(np.prod([ps[i] if w else 1 - ps[i] for i, w in enumerate(res)])
               for res in itertools.product([0, 1], repeat=3) if sum(res) >= 2)
cands = [("B", "C", "A"), ("C", "A", "B"), ("C", "B", "A")]
choice = max(itertools.permutations("XYZ"), key=lambda an: np.mean([p_anand(g, an) for g in cands]))
p = p_anand(("C", "A", "B"), choice)
print("A2 Gambit Anand picks", choice, "p =", round(p, 4), "floor 100p =", math.floor(100 * p))

# ---------- A2 Crude curve (parameters read off the graph) ----------
A_, B_, E_, tauE = 50, 0.5, 8, 12
D_ = 26 - 51
C_ = (71 - (A_ + B_ * 36 + E_ * math.cos(2 * math.pi * 36 / tauE) + D_)) / (1 - math.exp(-6))
F_ = (71 - (A_ + B_ * 48 + E_ * math.cos(2 * math.pi * 48 / tauE) + D_ + C_ * (1 - math.exp(-18)))) / 6
P = lambda t: (A_ + B_ * t + C_ * (1 - math.exp(-(t - 30))) * (t >= 30) + D_ * (t >= 18)
               + F_ * (t - 42) * (t >= 42) + E_ * math.cos(2 * math.pi * t / tauE))
print("A2 Crude  C=%.3f F=%.3f  check P(54)=%.2f P(60)=%.2f  P(75)=%.3f" % (C_, F_, P(54), P(60), P(75)))

# ---------- A2 Catalyst ----------
cA = np.linspace(0, 5, 500001); y = 10 * cA - 2 * cA**2 + 20 * (5 - cA) - 3 * (5 - cA) ** 2
print("A2 Catalyst max yield =", round(y.max(), 4), "at cA =", round(cA[y.argmax()], 3), "(unconstrained optimum needs 2.5 + 3.33 > 5 moles)")

# ---------- A2 Pit stop ----------
pS = {False: [0.50, 0.25, 0.10, 0.15], True: [0.05, 0.10, 0.05, 0.80]}
pH = {False: [0.20, 0.85, 0.10, 0.50], True: [0.40, 0.15, 0.90, 0.55]}
joint = {(sc, k): (0.4 if sc else 0.6) * pS[sc][k] * pH[sc][k] for sc in (False, True) for k in range(4)}
pn = (joint[(False, 0)] + joint[(True, 0)]) / sum(joint.values())
print("A2 Pit-stop P(neither | H wins) = %.4f -> %d" % (pn, math.floor(100 * pn)))

# ---------- B1 Triage ----------
n = np.array([500, 350, 150]); sens = np.array([.84, .70, .80]); spec = np.array([.90, .88, .89])
tp = sens * n; fp = (1 - spec) * (1000 - n)            # predicted i but actual not i
# unknown confusion entries C[pred][actual]; C[0][2]=10, C[2][0]=25 given
C = np.zeros((3, 3)); np.fill_diagonal(C, tp); C[0, 2] = 10; C[2, 0] = 25
C[1, 0] = n[0] - tp[0] - C[2, 0]; C[1, 2] = n[2] - tp[2] - C[0, 2]
C[0, 1] = fp[0] - C[0, 2]; C[2, 1] = fp[2] - C[2, 0]
print("B1 confusion (rows=predicted, cols=actual):\n", C, "\n   column sums", C.sum(0), "row-check fp", C[1, 0] + C[1, 2], "vs", fp[1])
print("B1 Q1  false alarms among predicted ICU: %.4f -> %d" % ((C[2].sum() - C[2, 2]) / C[2].sum(), math.floor(100 * (C[2].sum() - C[2, 2]) / C[2].sum())))
hosp = (n[1:].sum() - fp[0]) / n[1:].sum()      # predicted C1 among actual non-C1 = FP of class 1; print("B1 Q2  hospitalised correctly flagged: %.4f -> %d" % (hosp, math.floor(100 * hosp)))
pts = dict(A=(.02, .52), B=(.05, .68), C=(.08, .76), D=(.11, .80), E=(.14, .86), F=(.22, .91))
cost = {k: 0.15 * 8 * (1 - t) + 0.85 * 1 * f for k, (f, t) in pts.items()}
print("B1 Q3  expected cost per point", {k: round(v, 4) for k, v in cost.items()}, "best", min(cost, key=cost.get))
Cost = np.array([[0, 10, 100], [50, 0, 20], [500, 100, 0]])   # [actual][pred]
x = sum(Cost[a, p_] * C[p_, a] for a in range(3) for p_ in range(3)) / 1000
print("B1 Q7  expected cost per patient = %.3f -> %d" % (x, math.floor(x)))
rng = np.random.default_rng(0)                          # Q4: simulate the RDT augmentation
yb = rng.random(200000) < .15; s_old = rng.normal(0, 1, yb.size) + 1.6 * yb
b = np.where(yb, rng.random(yb.size) < .85, rng.random(yb.size) > .88)
from sklearn.metrics import roc_auc_score
from sklearn.metrics import roc_curve
print("B1 Q4  AUC old %.3f  new %.3f" % (roc_auc_score(yb, s_old), roc_auc_score(yb, s_old + 1.5 * (2 * b - 1))))
f0, t0, _ = roc_curve(yb, s_old); f1, t1, _ = roc_curve(yb, s_old + 1.5 * (2 * b - 1)); g = np.linspace(.005, .995, 199)
for sep in (0.5, 1.6, 3.0):
    so = rng.normal(0, 1, yb.size) + sep * yb; f0, t0, _ = roc_curve(yb, so); f1, t1, _ = roc_curve(yb, so + 1.5 * (2 * b - 1))
    print(f"B1 Q4  class separation {sep}: new ROC above old at {np.mean(np.interp(g, f1, t1) >= np.interp(g, f0, t0) - 1e-3):.0%} of FPR grid")

# ---------- B2 WonkaRank ----------
D = {1: (1, 1, 0, 0, 10), 2: (1, 1, 1, 1, 0), 3: (1, 0, 1, 1, 0), 4: (0, 1, 1, 1, 0), 5: (1, 1, 0, 1, 3),
     6: (0, 0, 1, 1, 0), 7: (1, 0, 0, 0, 0), 8: (1, 1, 0, 0, 5), 9: (0, 1, 1, 0, 2), 10: (0, 1, 0, 0, 0)}
rel = {k: v[4] for k, v in D.items()}; Rtot = sum(r > 0 for r in rel.values())
dcg = lambda order: sum(rel[d] / math.log2(i + 2) for i, d in enumerate(order))
print("B2 Q1  DCG@5 =", round(dcg([5, 3, 1, 9, 8]), 4))
rank = lambda f: sorted(D, key=lambda k: (-f(D[k]), k))
sA = lambda v: 2 * (v[0] and v[1]); sB = lambda v: 2 if (v[0] and v[1]) else (1 if (v[0] or v[1]) else 0)
for name, f in (("A", sA), ("B", sB)):
    top = rank(f)[:8]; hit = sum(rel[d] > 0 for d in top)
    print(f"B2 Q2  strategy {name} top8 {top} precision {hit / 8:.3f} recall {hit / Rtot:.3f}")
print("B2 Q3  ", {str(o): round(dcg(o), 3) for o in ([1, 5, 8, 9], [8, 1, 5, 9], [1, 8, 5, 9], [5, 9, 1, 8], [1, 8, 9, 5], [9, 5, 8, 1])})
W = (3, 3, 1, 1)
scores = {"A": lambda v: sum(d * w for d, w in zip(v[:4], W)), "B": lambda v: sum(min(1, d) * min(1, w) for d, w in zip(v[:4], W)),
          "C": lambda v: sum(d * w for d, w in zip(v[:4], W)) - sum(v[:4]),
          "D": lambda v: sum(d * w for d, w in zip(v[:4], W)) / (1 + sum(v[:4]))}
for k, f in scores.items():
    top = rank(f)[:3]; print(f"B2 Q4  score {k}: top3 {top} precision@3 {sum(rel[d] > 0 for d in top) / 3:.3f}")
pi = [.8, .6, .4, .2, .1]; top5 = rank(scores["A"])[:5]
pir = sum(p_ * (rel[d] > 0) for p_, d in zip(pi, top5)) / Rtot
print("B2 Q5  ranking", top5, "PI-recall@5 = %.4f -> %d" % (pir, math.floor(100 * pir + 1e-9)))

# ---------- B3 Convolution ----------
def conv(x, h, stride=1):
    k = len(h); return [sum(h[j] * x[i + k - 1 - j] for j in range(k)) for i in range(0, len(x) - k + 1, stride)]
print("B3 Q1  ", conv([2, 1, 3, 2, 4, 2], [2, 1], 2))
x2, z2 = [2, 4, 6, 8, 10], [2, 6, 3, 10, 4]
for name, h in zip("ABCDEF", ([1, 0, -1], [2, -3, 2], [1, 1, -2], [1, -2, 1], [0, -1, 1], [-2, 4, -2])):
    print(f"B3 Q2  {name} {h}: const S={sum(conv([5] * 5, h))} smooth S={sum(conv(x2, h))} fluct S={sum(conv(z2, h))}")
x3, w3, lin = [1, 5, 8, 10, 11], [1, 2, 4, 7, 11], [3, 5, 7, 9, 11]
for name, h in zip("ABCDEF", ([-1, 0, 1], [-1, 2, -1], [1, -1, 0], [0, 1, -2], [1, -2, 1], [3, -6, 3])):
    print(f"B3 Q3  {name} {h}: const-vel S={sum(conv(lin, h))} decel S={sum(conv(x3, h))} accel S={sum(conv(w3, h))}")
x4, z4 = [1, 2, 8, 2, 1, 2], [1, 3, 5, 7, 9, 11]
for name, h in zip("ABCDEF", ([1, 1, -2], [-1, 0, 1], [2, -3, 1], [1, -1, 0], [-1, 2, -1], [1, -2, 1])):
    sab = lambda s: sum(abs(v) for v in conv(s, h))
    print(f"B3 Q4  {name} {h}: const {sab([4] * 6)} smooth {sab(z4)} spike {sab(x4)}")
rs = np.random.default_rng(1)
for name, h in zip("ABCDEF", ([1, 1, 1, -1], [1, -1, 1, -1], [-1, 2, 1, 1], [2, 1, -1, -2], [1, -1, -1, 0], [1, 0, 0, -1])):
    sym = all(abs(conv([a, b_, b_, a], h)[0]) < 1e-12 for a, b_ in rs.integers(-9, 9, (200, 2)))
    asym = np.mean([abs(conv(list(v), h)[0]) > 0 for v in rs.integers(-9, 9, (2000, 4)) if v[0] != v[3] or v[1] != v[2]])
    print(f"B3 Q5  {name} {h}: zero on all symmetric: {sym}; non-zero on {asym:.0%} of asymmetric")

# ---------- B4 Pruning ----------
X1 = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], [1, 1, 0, 0], [1, 0, 1, 1]]); y1 = np.array([1, 0, 0, 0, 0, 1])
wA = np.array([2, -3, -1, -1])            # s1=w1=2, s3=w3=-1, s4=w4=-1, s5=w1+w2=-1 -> w2=-3
wB = np.array([3, -5, -1, -1])            # s1=3, s2=-5, s4=-1, s6=w1+w3+w4=1 -> w3=-1
for name, w in (("A", wA), ("B", wB)):
    s = X1 @ w; acc = np.mean((s > 0) == y1)
    print(f"B4 Q1  option {name}: w={w} s={s} accuracy {acc:.3f} |w|^2={np.sum(w**2)}")
X2 = np.array([[-1, 0, 0, 0, 0, 0, 3], [0, -1, 0, 0, 0, 0, 0], [0, 0, -1, 0, 0, 0, -2], [0, 0, 0, -1, 0, 0, 6],
               [0, 0, 0, 0, -1, 0, 7], [0, 0, 0, 0, 0, 1, -6], [1, 1, 1, 1, 1, 5, -9]])
w2 = np.array([-1, -1, -1, -1, -1, 1, 0]); y2 = np.array([1, 1, 1, 1, 1, 1, 0])
accs = {}
for j, k in itertools.combinations(range(7), 2):
    w = w2.copy(); w[[j, k]] = 0; accs[(j + 1, k + 1)] = np.mean(((X2 @ w) > 0) == y2)
bestacc = max(accs.values()); bests = [p_ for p_, a in accs.items() if a == bestacc]
print("B4 Q2  best accuracy %.3f with pairs %s (unpruned %.3f)" % (bestacc, bests, np.mean(((X2 @ w2) > 0) == y2)))
for (j, k) in bests:
    w = w2.copy(); w[[j - 1, k - 1]] = 0; s = X2 @ w
    accb = {b_: np.mean(((s + b_) > 0) == y2) for b_ in range(-5, 6)}
    m = max(accb.values()); bstar = max(b_ for b_, a in accb.items() if a == m)
    print(f"B4 Q3  pair {(j, k)}: scores {s}, best acc {m:.3f}, b* = {bstar}, answer {(j + k) * abs(bstar)}")