[CodeChef] Starters 250 — Subsequence 2: Counting windows, one threshold at a time
Published on August 06, 2026
SUB2 (Subsequence 2) is the sequel to SUB1 (Subsequence 1), which I wrote up in the previous post. Same \(f\): the largest \(L\) such that \(1, 2, \dots, L\) is a subsequence of the array. But instead of splitting the array, you sum \(f\) over every subarray:
\[\sum_{L=1}^{N} \sum_{R=L}^{N} f(A[L \ldots R])\]Rated ~1895, and it ends up being the SUB1 loop with one operator changed. Getting there took me three wrong turns, which is the interesting part.
Setting up: sum over right endpoints
The standard move for “sum over all subarrays” is to fix the right endpoint. Let
\[dp[i] = \sum_{L=1}^{i} f(A[L \ldots i])\]so the answer is \(\sum_i dp[i]\). The question becomes: how does \(dp[i]\) follow from \(dp[i-1]\)?
Here the SUB1 lesson pays off immediately. Appending one element raises \(f\) by at most 1, and it raises a particular window’s \(f\) only if that window was sitting at value exactly \(a_i - 1\). So
\[dp[i] = dp[i-1] + \#\{L : f(A[L \ldots i-1]) = a_i - 1\}\]In words, and this is the sentence I couldn’t see for far too long: the new answer is the old answer plus the number of left endpoints whose window just became able to reach \(a_i\). Not “at least \(a_i - 1\)” — exactly. A window already at 4 gains nothing when you append a 5 if it’s already past; a window at 2 gains nothing either. Only the ones on the boundary move.
(One bookkeeping detail: at step \(i\) there’s a brand-new window \(L = i\) that didn’t exist at step \(i-1\). If you define \(f\) of the empty array as 0, it obeys the same rule and needs no special case.)
So the whole problem reduces to maintaining that count.
Wrong turn 1: a set of chain starts
My first idea was to keep, for each value \(c\), the set of positions where a chain reaching \(c\)
begins — carried over from SUB1, where I tracked exactly that. On 1 2 3 4 1 2 3 3 4 I wrote the
set for value 3 as \(\{1, 5\}\): the two places a 1 sits that opens a 1,2,3.
That’s the wrong quantity. The DP doesn’t ask where chains begin, it asks which windows contain
one. Check L by L at index 8 on that array:
| \(L\) | window | contains 1,2,3? |
|---|---|---|
| 1 | 1 2 3 4 1 2 3 3 |
yes |
| 2 | 2 3 4 1 2 3 3 |
yes — via the 1 at 5, 2 at 6, 3 at 7 |
| 3 | 3 4 1 2 3 3 |
yes, same |
| 4 | 4 1 2 3 3 |
yes |
| 5 | 1 2 3 3 |
yes |
| 6 | 2 3 3 |
no 1 left |
The answer is \(\{1,2,3,4,5\}\), not \(\{1,5\}\). Storing sets is also \(O(N^2)\) in the worst case, so it was doomed twice over.
The observation: \(f\) is monotone in the left endpoint
Look at that table again. It isn’t an arbitrary set — it’s a prefix. And the reason is one line:
\(A[L{+}1 \ldots i]\) is a subarray of \(A[L \ldots i]\), so every subsequence of the former is a subsequence of the latter. Hence \(v(L) := f(A[L \ldots i])\) is non-increasing in \(L\).
Moving the left endpoint right can only ever take options away. So for every threshold \(c\), the set \(\{L : v(L) \ge c\}\) is a prefix \([1, S_c]\), and the entire function \(v(\cdot)\) — all \(i\) values of it — is pinned down by the single non-increasing sequence
\[S_0 \ge S_1 \ge S_2 \ge \cdots\]where \(S_c\) is just a count: how many windows ending at \(i\) have \(f \ge c\). And \(S_0 = i\) always, since every window contains the empty chain.
That’s the compression. No sets, no \(O(N^2)\).
Wrong turn 2: adding the whole prefix
With \(S\) in hand I wrote dp += S[cur-1] and got 8 on [1,2,1] instead of 7. The bug is the
“exactly” from the recurrence: \(S_c\) counts windows at value \(\ge c\), but the windows already at
\(\ge c\) before the append didn’t move. Only the ones crossing the threshold contribute.
Windows at exactly \(c\) are a difference of adjacent prefixes, so with \(v = a_i\):
\[\#\{L : v(L) = v - 1\} = S_{v-1} - S_v\]read from the pre-append state. Then those windows move up, which is the update \(S_v \leftarrow S_{v-1}\).
Equivalently: the increment is how much \(S_v\) grew. Same number, read two ways.
Wrong turn 3: the update order
Since the appended element is precisely what moves those windows from \(v-1\) to \(v\), reading the
count after applying \(S_v \leftarrow S_{v-1}\) gives 0 every time — the difference you wanted to
measure has already been erased. Read first, update second. Tracing [1,2,1] by hand catches this
in about ten seconds — increments should be \(1, 1, 2\), giving \(dp = 1, 2, 4\) and total 7.
The code
import sys
def main():
data = sys.stdin.buffer.read().split()
p = 0
t = int(data[p]); p += 1
out = []
for _ in range(t):
n = int(data[p]); p += 1
a = data[p:p + n]; p += n
S = [0] * (n + 2) # S[v] = #windows ending at i with f >= v
ans = dp = 0
for i in range(1, n + 1):
v = int(a[i - 1])
S[0] = i # every window contains the empty chain
dp += S[v - 1] - S[v] # windows sitting exactly at v-1 move up to v
S[v] = S[v - 1]
ans += dp
out.append(ans)
sys.stdout.write('\n'.join(map(str, out)) + '\n')
main()
\(O(N)\) time, \(O(N)\) space. Setting S[0] = i inside the loop is what removes the v == 1 special
case — a fresh chain is just an extension of the empty one. Indexing S by value is safe because
the constraints promise \(1 \le A_i \le N\).
Sample 2, [2,1,1,2,1,3,4]: increments \(0,2,1,3,2,3,3\), so \(dp = 0,2,3,6,8,11,14\), total 44. And
[2,3,4,4] is 0 because no window contains a 1.
The invariant that keeps it all honest: \(S_v \le S_{v-1}\) at all times, and each \(S_v\) is non-decreasing across \(i\). So the assignment never shrinks anything, never needs undoing, and a plain array is enough.
The punchline: it’s SUB1 with one operator swapped
Put the two loop bodies side by side. SUB1:
largest_chain_start[0] = i
dp[i] = max(dp[i-1], v + dp[largest_chain_start[v-1] - 1])
largest_chain_start[v] = largest_chain_start[v-1]
SUB2:
S[0] = i
dp += S[v-1] - S[v]
S[v] = S[v-1]
Same structure, same update, same invariant. In SUB1, largest_chain_start[v] is a pointer — the
latest index a chain \(1..v\) can begin at — and you use it to jump the DP. In SUB2 the identical
number is a count — how many left endpoints admit a chain \(1..v\). Those are the same quantity read
two ways, because “the latest start is at position \(S\)” and “exactly \(S\) left endpoints work” are the
same statement once you know the valid \(L\) form a prefix.
Which is really just monotonicity again. Both problems hinge on it; SUB1 uses it to argue the shortest closing chain dominates, SUB2 uses it to compress the whole \(f\)-profile into one array.
What I’d take away
The thing that actually cost me the time was not seeing the recurrence in plain language: \(dp[i] = dp[i-1] +\) the number of new left endpoints \(L\) whose window can now reach \(a_i\). Once that sentence exists, every remaining question is mechanical — how do I count those endpoints, and in what order do I read and update.
Both wrong turns after that were the same mistake in different clothes: I tracked the witness (where a chain physically starts) when the DP needed the count (how many windows admit one). Worth asking directly, before reaching for a data structure — what does my recurrence literally consume? Often it’s a cardinality, and cardinalities of monotone sets collapse to a single integer.
Tags: competitive_programming, codechef, dynamic_programming, counting