← All problems

Revival

Codeforces · Round #1105 (Div. 1) · Problem C

Problem

We would like to recover a permutation PP of the numbers 1..n1..n, given partial information about it. For each position ii we are given a character cic_i and a number xix_i:

  • If ci=pc_i = \texttt{p}, then xix_i is the exact number in position ii of the permutation (1xin)(1 \leq x_i \leq n).
  • If ci=sc_i = \texttt{s}, then xix_i is the number of inversions in the prefix P[1..i]P[1..i] (pairs j<kij < k \leq i with P[j]>P[k]P[j] > P[k]).

Recover any permutation consistent with all the constraints — one is guaranteed to exist. (nn up to 21052 \cdot 10^5.)

Initial Observations
  1. Permutations.
  2. Inversions.
  3. Construction problem.
  4. Dynamic programming?
  5. Work backwards?
  6. Look at the cycle notation?
  7. Data structure for counting.
  8. [Examine Examples], [Write It Out].
  9. [Pursue Extremes] — sweep: lowest number first, highest number first, left-to-right, right-to-left?
  10. Greedy.
IdeaFix the smallest possible item firstNo Solution

All the p\texttt{p} positions are fixed; all the s\texttt{s} positions are unknown and need to be filled. So, sweeping left to right: for each s\texttt{s} position, try to place the smallest available number that doesn't produce a contradiction (make some later s\texttt{s} constraint impossible, drive an inversion count negative, and so on).

Draw from Experience

This has the shape of a classic greedy, and the standard way to prove such a greedy is a swapping argument: assume some valid answer exists, and show you can always swap it toward the greedy's choice without breaking anything.

I couldn't make it work. The trouble is that a choice at one s\texttt{s} position changes the inversion count of every later prefix — each subsequent s\texttt{s} constraint has to be re-checked against every placement made so far, and I couldn't find an invariant (or a working swap argument) that kept the bookkeeping consistent.

Two observations worth salvaging before moving on:

Observation.

The p\texttt{p} positions partition the problem: only the s\texttt{s} positions are free, and whatever we place at one contributes inversions to every s\texttt{s} constraint at or after it.

Observation.

Any left-to-right strategy is forced to keep revising its accounting of all pending constraints on every placement.

IdeaLeft-to-right sweep, maintaining intervals per positionNo Solution

If you look at the first s\texttt{s} position, there is a particular interval of numbers that could legally go there — too large a number creates too many inversions, too small creates too few. So: what if, after every s\texttt{s} position, we maintain for each open position the interval of numbers it could still hold?

As we proceed, each new s\texttt{s} constraint forces us to tighten earlier intervals — later prefix constraints imply things about earlier choices — so we keep intersecting intervals as we go.

I wasn't able to find a clean way to fix up the intervals: handling the overlapping intersections, and the fact that every new s\texttt{s} position can invalidate every earlier interval, got away from me. The same disease as Idea 1: going left-to-right means every new constraint reaches backward over everything already placed.

An aside: I gave myself one hour to solve this problem, and my brain got stuck here making no progress. (See the Learning points.)

IdeaRight-to-left sweep, greedily selecting the item that worksAC

Ideas 1 and 2 kept dying the same death: each s\texttt{s} position contains information about all previous positions, so sweeping left-to-right means every new s\texttt{s} constraint forces an update of everything before it. There is an inherent asymmetry here.

Key Observation

Later s\texttt{s} positions subsume earlier ones: xix_i for an s\texttt{s} position counts all inversions of the whole prefix, so the constraint at an earlier s\texttt{s} position is already folded into every later one. Information flows left-to-right — which means the unconstrained direction to sweep is right-to-left.

Pursue Extremes

It is very common to "sweep" a problem: pick an ordering that the problem structure suggests and attack the subproblems in that order. Our initial observations listed four candidate orderings — left-to-right, right-to-left, smallest-number-first, largest-number-first. The asymmetry above is what makes right-to-left a promising one.

Work Backward

Sometimes it's important to reverse a problem. If we've been looking left-to-right, a common problem-solving move is to start from the end instead; if we were trying the smallest item first, be sure to also try the largest item first. Sometimes — not always — this changes the constraints of the problem just enough to make it tractable.

Key Observation

Look at the last s\texttt{s} position. The number that must go there is uniquely determined.

The proof is two lemmas.

Lemma.

If we place a number vv into the last s\texttt{s} position ii, the total number of inversions in the prefix P[1..i]P[1..i] is completely determined (computable), even though the earlier s\texttt{s} positions are still unfilled.

Proof.

Everything to the right of position ii is a p\texttt{p} position (it's the last s\texttt{s}), so the set of values used to the right of ii is known — and therefore the set of values in P[1..i]P[1..i] is known: it is {1..n}\{1..n\} minus the values placed to the right. Call this set UU.

Let i2i_2 be the previous s\texttt{s} position (to the left of ii), and let the block be the run of p\texttt{p} positions strictly between i2i_2 and ii. Now classify every inversion pair in P[1..i]P[1..i]:

  • Both endpoints in P[1..i2]P[1..i_2]: their count is exactly xi2x_{i_2} — given in the input, regardless of how those positions end up arranged.
  • Right endpoint in the block: each block value bb is known, and the multiset of values to its left is known (it's UU minus vv minus the block values right of bb's position), so the count of values greater than bb to its left is determined — even though their arrangement isn't. Arrangement doesn't matter: an inversion ending at bb only asks how many larger values sit anywhere to the left.
  • Right endpoint is vv at position ii: similarly, the count of values greater than vv in P[1..i1]P[1..i-1] is determined by the set U{v}U \setminus \{v\}.

Sum the three groups: the total is computable from the input and vv alone.

Since the input demands that this total equal xix_i exactly, each candidate vv either hits xix_i or doesn't. Uniqueness comes from:

Lemma.

The total number of inversions in P[1..i]P[1..i] is strictly decreasing in vv: placing a larger available number in the last s\texttt{s} position yields strictly fewer inversions.

Proof.

Take two available candidates v1<v2v_1 < v_2. In the v1v_1-scenario, v1v_1 sits at position ii and v2v_2 sits somewhere in P[1..i2]P[1..i_2]; in the v2v_2-scenario they trade places. Every other value keeps the same set membership, so by the classification in Lemma 1 we only need to track pairs involving v1v_1 or v2v_2:

  • Pairs inside P[1..i2]P[1..i_2]: still exactly xi2x_{i_2} by the input — unchanged. (This is the miracle of the problem: whatever rearrangement happens among the earlier positions, the input pins their total.)
  • The pair (v1,v2)(v_1, v_2) itself: in the v1v_1-scenario, v2v_2 is to the left of v1v_1 and v2>v1v_2 > v_1 — an inversion. In the v2v_2-scenario, v1v_1 is to the left of v2v_2 — not an inversion. Strictly down by one.
  • Pairs with a block value bb: only block values strictly between them matter. If v1<b<v2v_1 < b < v_2: in the v1v_1-scenario, v2v_2 (left of bb) counts against bb, and bb counts against v1v_1 (right of bb) — two inversions; in the v2v_2-scenario, neither pair inverts — zero. If bb is below both or above both, the count is unchanged. Down by two per such bb, never up.

As a tiny example, take U={1,2,3}U = \{1,2,3\} with a single block value b=2b = 2 and the last s\texttt{s} position at the end: choosing v=1v = 1 gives the arrangement (3,2,1)(3, 2, 1)-shaped contributions (3 inversions), v=2v = 2 is impossible (2 is in the block), and v=3v = 3 gives (1,2,3)(1, 2, 3)-shaped contributions (0 inversions) — strictly falling as vv grows.

Every term stays the same or decreases, and the (v1,v2)(v_1, v_2) pair always decreases. QED.

So the choice of vv is unique — and better: since the inversion total is strictly decreasing in vv, we can binary search for it among the available values.

Exploit the Constraints

What saves us is that every s\texttt{s} position's inversion count is given in the input. Standing at the rightmost s\texttt{s} position, we only ever need to look at the run of p\texttt{p} positions immediately to its left, and then the single s\texttt{s} position before that — which already captures all inversions of everything further left. We never have to peek deeper.

Draw from Experience

Binary search is the standard way to exploit a strictly increasing or decreasing function.

But how do we evaluate a candidate vv quickly — how do we count the prefix inversions?

Problem Transformation

The operation we clearly need: maintain a set of numbers under insertion and removal, and for an arbitrary vv, count how many members are greater (or smaller) than vv. If SS is the set of numbers that will end up to the left of vv, then "members of SS greater than vv" is exactly the inversions vv contributes. The problem has become a data-structure problem.

Assume a RankedSet data structure with these operations (all O(logn)O(\log n); see the Aside for the Segment Tree implementation):

insert(v)      -> add v to the set
remove(v)      -> remove v from the set
size()         -> number of items in the set
rank(v)        -> number of items strictly below v
nth(r)         -> the item with rank r (r-th smallest)
inversions(v)  -> number of items greater than v  ( = size() - rank(v) - 1, for v in the set)

The overall solution, sweeping the s\texttt{s} positions from right to left. Conceptually it is very short:

S = RankedSet containing 1..n            # values not yet placed
 
for each 's' position i, from rightmost to leftmost:
    remove from S the values at 'p' positions right of i (not already removed)
 
    # INV(v) := number of inversions in prefix P[1..i] if v goes at position i
    #   - computable for any candidate v   (Lemma 1)
    #   - strictly decreasing in v         (Lemma 2)
    binary search the candidates in S (by rank) for the v with INV(v) == x[i]
 
    P[i] = v ; S.remove(v)

Making the accounting exact. All the real work hides inside evaluating INV(v)\text{INV}(v), and three tricks are needed to get it right and fast:

  1. Evaluate INV(v)\text{INV}(v) through the previous s\texttt{s} position. Following Lemma 1's classification: INV(v)=xi2+(block inversions)+(inversions contributed by v)\text{INV}(v) = x_{i_2} + (\text{block inversions}) + (\text{inversions contributed by } v), where i2i_2 is the previous s\texttt{s} position and the block is the run of p\texttt{p} positions between i2i_2 and ii. The first term is free (it's input), and the last is a single query: values greater than vv among those that will sit to its left.
  2. Charge each block once. The block contribution is (almost) independent of the candidate vv, so compute it once per block — each block value bb contributes the count of still-unplaced values greater than bb — instead of re-summing it every time the binary search tests a candidate. Without this, long blocks blow up the complexity.
  3. Correct for the candidate itself. When the block sums were computed, the candidate vv was still among the unplaced values — so each block value bb counted vv as if it were to the block's left, but vv actually lands to the block's right, where the pair inverts only if b>vb > v. Each time the binary search tests a candidate vv, both directions are fixed in one stroke: subtract the block values below vv, add the block values above vv (keep the block in its own small RankedSet to answer these).

The same pseudocode with the accounting written out:

S = RankedSet containing 1..n            # values not yet placed
remove from S every value at a 'p' position right of the last 's' position
treat position 0 as a virtual 's' with x[0] = 0   # handles the leftmost block
 
for each 's' position i, from rightmost to leftmost:
    let i2    = the previous 's' position (or the virtual position 0)
    let block = the 'p' positions strictly between i2 and i
 
    # trick 2: block contribution, computed ONCE per block
    #   for each block value b (right to left): count values still in S
    #   greater than b, then move b from S into a small RankedSet B
    base = x[i2] + sum of block contributions
 
    # binary search over the remaining candidates in S (by rank):
    #   INV(v) = base + B.inversions-above(v) - B.count-below(v)   # trick 3
    #                 + (items of S other than v that are greater than v)
    binary search the rank r such that v = S.nth(r) gives INV(v) == x[i]
 
    P[i] = v ; S.remove(v)

Each s\texttt{s} position costs O(log2n)O(\log^2 n) (a binary search where each candidate test is a few O(logn)O(\log n) set queries), each p\texttt{p} value is inserted and removed a constant number of times, so the whole thing runs in O(nlog2n)O(n \log^2 n).

Watch the edge cases: a run of p\texttt{p} positions at the very end (strip them before the first iteration), a run at the very beginning (the virtual s\texttt{s} at position 0 absorbs it), and two adjacent s\texttt{s} positions (an empty block).

AsideSegment Trees (the RankedSet)

A Segment Tree handles a fixed universe of items 1..n1..n with point updates + range queries (or the reverse) over any associative operation — sums, mins, maxes. It's also useful for problems that don't look like range queries at all: here, we "add 1" at position vv when vv enters the set and "subtract 1" when it leaves, so the tree maintains counts, and a range query counts how many set members are \leq some value.

Draw from Experience

This problem is probably solvable without a segment tree — there may be a lighter way to build a ranked set — but this is what came to mind, and I've coded segment trees dozens of times, so I trusted it.

The core idea: a binary tree whose leaves are the universe items, where each internal node stores the sum of its children. To keep the arithmetic trivial, every call carries the range (a,b)(a,b) it covers and the node index ii: the root is node 00 covering (1,n)(1,n); node ii's children are 2i+12i+1 (left) and 2i+22i+2 (right); the split point is m=(a+b)/2m = \lfloor (a+b)/2 \rfloor, with the left child covering (a,m)(a,m) and the right (m+1,b)(m+1,b); you're at a leaf when a=ba = b. The splits come out slightly lopsided, but the depth is still O(logn)O(\log n) and the whole thing is very easy to write from memory.

The RankedSet used above, as a segment tree over counts (pseudocode distilled from my accepted C++):

D = [0] * (4 * n)                 # D[i] = number of set members in node i's range
 
def insert(v, a=1, b=n, i=0):     # add 1 at leaf v, fix sums on the way up
    if b < v or a > v: return
    if a == b: D[i] += 1; return
    m = (a + b) // 2
    insert(v, a, m, 2*i+1); insert(v, m+1, b, 2*i+2)
    D[i] = D[2*i+1] + D[2*i+2]
 
def remove(v, a=1, b=n, i=0):     # identical, with D[i] -= 1 at the leaf
    ...
 
def size(): return D[0]
 
def rank(v, a=1, b=n, i=0):       # how many members are strictly below v
    if a == b: return 0
    m = (a + b) // 2
    if v <= m: return rank(v, a, m, 2*i+1)
    return D[2*i+1] + rank(v, m+1, b, 2*i+2)
 
def nth(r, a=1, b=n, i=0):        # the member with rank r
    if a == b: return a
    m = (a + b) // 2
    if D[2*i+1] > r: return nth(r, a, m, 2*i+1)
    return nth(r - D[2*i+1], m+1, b, 2*i+2)
 
def inversions(v): return size() - rank(v) - 1    # members greater than v (v in set)

Review

Reconstruct the permutation right-to-left in O(nlog2n)O(n \log^2 n), filling each s\texttt{s} position by binary search over a segment-tree-backed ranked set. The path there:

  1. This problem is sweepable. Left-to-right, right-to-left, smallest-number-first, or largest-number-first — exploit the structure to pick the extreme that fits.
  2. Right-to-left wins, because of the asymmetry. Later s\texttt{s} positions subsume earlier ones — the prefix definition folds every earlier constraint into every later one — so sweeping backward means never revisiting a decision, where the two forward sweeps (Ideas 1 and 2) drowned in re-accounting. [1]
  3. The last s\texttt{s} position is forced. Given the values to its right, the number vv that must go there is uniquely determined. [2]
  4. Binary search finds vv, because the prefix inversion count is strictly decreasing in vv (Lemma 2) and computable for any candidate (Lemma 1).
  5. A Segment Tree makes every candidate test cheap: maintain the set of unplaced values with O(logn)O(\log n) insert/remove/rank/nth, which is everything the inversion counting needs.
  6. Sweep to the front, block by block: strip each run of p\texttt{p} positions, account its inversions once, place each s\texttt{s} value, and the permutation falls out.

Full pseudocode is in Idea 3; the RankedSet implementation is in the Aside.

References

Problem-solving techniques used:

Pursue Extremes

Picking an ordering to attack the subproblems in is often the core of the whole algorithm. We listed four candidate orderings in the initial observations and worked through them.

Work Backward

Left-to-right and smallest-first are the obvious orderings; reversing them gave the other two candidates, including the one that worked.

Exploit the Constraints

Going back-to-front worked because the input pins every s\texttt{s} prefix's inversion count — the constraint itself is what makes the backward sweep local.

Draw from Experience

A strictly monotone function means binary search; a ranked-set-with-counts means segment tree. Both leaps came from pattern recognition, not derivation.

Problem Transformation

"Count inversions contributed by vv" became "count set members greater than vv" — turning a permutation problem into a data-structure problem.

Learning points:

Topics: