Ash Record
Ash Record
Platform: CACTF2026 | Category: Coding | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-07-27 | Status: Solved Techniques: prefix_subsequence_dp, minimal_timestamp_greedy, exchange_argument_optimality, timestamp_sorting, http_code_judge_automation
Summary
Task: narrative-wrapped algorithmic code-judge problem — find the longest prefix of an extraction sequence confirmable as a timestamp-ordered subsequence of residues with a minimum time gap between consecutive matches. Solution: O(N log N + N*P) DP tracking the minimal last-matched timestamp per prefix length (exchange-argument greedy), submitted to a Flask POST /run JSON judge to retrieve the flag.
Ash Record — CACTF2026
Description
Elowen Ashglass recovered residues across a site, each carrying an integer timestamp and a material type. She holds a suspected extraction sequence (list of material types in order). Find the LONGEST PREFIX of that sequence that can be confirmed as a subsequence of the residues in timestamp order, where no two consecutively matched residues are closer together in time than a minimum gap.
Formal I/O
- Line 1:
N P min_gapN= number of recovered residues (1 <= N <= 5000)P= length of the suspected extraction sequence (1 <= P <= 12)min_gap= minimum time difference required between two consecutively matched residues (1 <= min_gap <= 10)
- Line 2:
Pspace-separated material-type strings (the extraction sequence, in order). - Next
Nlines: each residuetimestamp material_type(1 <= timestamp <= 10000; lowercase, length<= 10). Residues are not necessarily in timestamp order. - Output: a single integer — the maximum number of consecutive steps from the beginning of the extraction sequence confirmable by a subsequence of residues satisfying the time-gap constraint.
Sample
5 4 3
ash rope oil ash
1 ash
4 rope
7 oil
10 ash
11 rope
Output: 4 — the full prefix ash rope oil ash matches residues at timestamps 1, 4, 7, 10 (each consecutive gap >= 3).
Recon
The stated target 154.57.164.67:32418 is not a raw interactive TCP service; plain netcat produced silence. It is a Flask/Werkzeug HTTP web app:
GET / HTTP/1.0returnsServer: Werkzeug/3.1.3 Python/3.14.0, page title "Coding".- The page ships a Monaco code editor and loads
/static/js/prompts.jswith per-language starter templates (python/c/cpp/rust). - Submission API:
POST /runwith JSON body{"code": <source>, "language": "python"}. - On a full pass the response is
{"challengeCompleted":true,"flag":"HTB{...}","result":{}}; on failureresultholds the failing test case.
So the challenge is a hidden-test code judge — solve the algorithm locally, then automate an HTTP submission.
Analysis
This is a constrained "longest matching prefix as an ordered subsequence" problem:
- We must match the extraction sequence from its start (
target[0], target[1], ...), stopping at the first step that cannot be confirmed. - Matches must respect timestamp order: since residues arrive unsorted, we first sort by timestamp.
- Between two consecutively matched residues the time difference must be
>= min_gap.
The crucial structural fact is that P <= 12 is tiny, so we can carry a full DP state indexed by prefix length.
Key insight (exchange argument). For a fixed confirmed prefix length k, the only thing the future cares about is the timestamp of the last matched residue, because the next match t' only needs t' - last >= min_gap. A smaller last timestamp is never worse — it can only make future gap constraints easier to satisfy. Therefore, for each k, it is optimal to track the minimal achievable last-matched timestamp.
This collapses the state to a single value per prefix length: best[k] = minimal possible last-matched timestamp that confirms exactly k prefix steps.
Solution
DP over prefix positions, O(N log N + N*P):
- Sort residues by timestamp ascending.
- Init
best[0] = -infinity(zero steps, no timing constraint); allbest[k>0] = +infinity. - Process each residue
(t, type)in timestamp order. IteratekfromP-1down to0:- if
best[k]is finite,type == target[k], andt - best[k] >= min_gap, then relaxbest[k+1] = min(best[k+1], t).
- if
- Answer = the largest
kwith finitebest[k].
Correctness notes
- Optimality: the exchange argument above justifies minimizing
best[k]; any locally better last timestamp dominates. - No self-chaining within one residue: iterating
kin descending order means a residue that relaxesbest[k+1]is not re-read atbest[k]in the same pass. - Equal-timestamp residues cannot chain to each other: their gap would be
0 < min_gap(sincemin_gap >= 1), and thet - best[k] >= min_gaptest rejects it. - Validity: every finite
best[k]is realized by an actual valid subsequence of lengthk.
Solve script
python read-only • copy allowedimport sys def main(): data = sys.stdin.read().split('\n') idx = 0 first = data[idx].split(); idx += 1 N, P, min_gap = int(first[0]), int(first[1]), int(first[2]) target = data[idx].split(); idx += 1 res = [] for _ in range(N): parts = data[idx].split(); idx += 1 t = int(parts[0]); typ = parts[1] res.append((t, typ)) res.sort(key=lambda x: x[0]) INF = float('inf') best = [INF] * (P + 1) best[0] = -INF ans = 0 for t, typ in res: for k in range(P - 1, -1, -1): if best[k] == INF: continue if typ == target[k] and t - best[k] >= min_gap: if t < best[k + 1]: best[k + 1] = t if k + 1 > ans: ans = k + 1 print(ans) main()
Automated submission to the code judge
python read-only • copy allowedimport json, urllib.request code = open('sol.py').read() body = json.dumps({'code': code, 'language': 'python'}).encode() req = urllib.request.Request( 'http://154.57.164.67:32418/run', data=body, headers={'Content-Type': 'application/json'}, ) print(urllib.request.urlopen(req, timeout=60).read().decode())
The submission passed all hidden tests on the first try, and the JSON response returned challengeCompleted: true with the flag.