We would like to recover a permutation of the numbers , given partial information about it. For each position we are given a character and a number :
- If , then is the exact number in position of the permutation .
- If , then is the number of inversions in the prefix (pairs with ).
Recover any permutation consistent with all the constraints — one is guaranteed to exist. ( up to .)
- Permutations.
- Inversions.
- Construction problem.
- Dynamic programming?
- Work backwards?
- Look at the cycle notation?
- Data structure for counting.
- [Examine Examples], [Write It Out].
- [Pursue Extremes] — sweep: lowest number first, highest number first, left-to-right, right-to-left?
- Greedy.
IdeaFix the smallest possible item firstNo Solution
All the positions are fixed; all the positions are unknown and need to be filled. So, sweeping left to right: for each position, try to place the smallest available number that doesn't produce a contradiction (make some later constraint impossible, drive an inversion count negative, and so on).
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 position changes the inversion count of every later prefix — each subsequent 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:
The positions partition the problem: only the positions are free, and whatever we place at one contributes inversions to every constraint at or after it.
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 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 position, we maintain for each open position the interval of numbers it could still hold?
As we proceed, each new 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 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 position contains information about all previous positions, so sweeping left-to-right means every new constraint forces an update of everything before it. There is an inherent asymmetry here.
Later positions subsume earlier ones: for an position counts all inversions of the whole prefix, so the constraint at an earlier position is already folded into every later one. Information flows left-to-right — which means the unconstrained direction to sweep is right-to-left.
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.
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.
Look at the last position. The number that must go there is uniquely determined.
The proof is two lemmas.
If we place a number into the last position , the total number of inversions in the prefix is completely determined (computable), even though the earlier positions are still unfilled.
Everything to the right of position is a position (it's the last ), so the set of values used to the right of is known — and therefore the set of values in is known: it is minus the values placed to the right. Call this set .
Let be the previous position (to the left of ), and let the block be the run of positions strictly between and . Now classify every inversion pair in :
- Both endpoints in : their count is exactly — given in the input, regardless of how those positions end up arranged.
- Right endpoint in the block: each block value is known, and the multiset of values to its left is known (it's minus minus the block values right of 's position), so the count of values greater than to its left is determined — even though their arrangement isn't. Arrangement doesn't matter: an inversion ending at only asks how many larger values sit anywhere to the left.
- Right endpoint is at position : similarly, the count of values greater than in is determined by the set .
Sum the three groups: the total is computable from the input and alone.
Since the input demands that this total equal exactly, each candidate either hits or doesn't. Uniqueness comes from:
The total number of inversions in is strictly decreasing in : placing a larger available number in the last position yields strictly fewer inversions.
Take two available candidates . In the -scenario, sits at position and sits somewhere in ; in the -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 or :
- Pairs inside : still exactly 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 itself: in the -scenario, is to the left of and — an inversion. In the -scenario, is to the left of — not an inversion. Strictly down by one.
- Pairs with a block value : only block values strictly between them matter. If : in the -scenario, (left of ) counts against , and counts against (right of ) — two inversions; in the -scenario, neither pair inverts — zero. If is below both or above both, the count is unchanged. Down by two per such , never up.
As a tiny example, take with a single block value and the last position at the end: choosing gives the arrangement -shaped contributions (3 inversions), is impossible (2 is in the block), and gives -shaped contributions (0 inversions) — strictly falling as grows.
Every term stays the same or decreases, and the pair always decreases. QED.
So the choice of is unique — and better: since the inversion total is strictly decreasing in , we can binary search for it among the available values.
What saves us is that every position's inversion count is given in the input. Standing at the rightmost position, we only ever need to look at the run of positions immediately to its left, and then the single position before that — which already captures all inversions of everything further left. We never have to peek deeper.
Binary search is the standard way to exploit a strictly increasing or decreasing function.
But how do we evaluate a candidate quickly — how do we count the prefix inversions?
The operation we clearly need: maintain a set of numbers under insertion and removal, and for an arbitrary , count how many members are greater (or smaller) than . If is the set of numbers that will end up to the left of , then "members of greater than " is exactly the inversions contributes. The problem has become a data-structure problem.
Assume a RankedSet data structure with these operations (all ; 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 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 , and three tricks are needed to get it right and fast:
- Evaluate through the previous position. Following Lemma 1's classification: , where is the previous position and the block is the run of positions between and . The first term is free (it's input), and the last is a single query: values greater than among those that will sit to its left.
- Charge each block once. The block contribution is (almost) independent of the candidate , so compute it once per block — each block value contributes the count of still-unplaced values greater than — instead of re-summing it every time the binary search tests a candidate. Without this, long blocks blow up the complexity.
- Correct for the candidate itself. When the block sums were computed, the candidate was still among the unplaced values — so each block value counted as if it were to the block's left, but actually lands to the block's right, where the pair inverts only if . Each time the binary search tests a candidate , both directions are fixed in one stroke: subtract the block values below , add the block values above (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 position costs (a binary search where each candidate test is a few set queries), each value is inserted and removed a constant number of times, so the whole thing runs in .
Watch the edge cases: a run of positions at the very end (strip them before the first iteration), a run at the very beginning (the virtual at position 0 absorbs it), and two adjacent positions (an empty block).
AsideSegment Trees (the RankedSet)
A Segment Tree handles a fixed universe of items 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 when enters the set and "subtract 1" when it leaves, so the tree maintains counts, and a range query counts how many set members are some value.
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 it covers and the node index : the root is node covering ; node 's children are (left) and (right); the split point is , with the left child covering and the right ; you're at a leaf when . The splits come out slightly lopsided, but the depth is still 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 , filling each position by binary search over a segment-tree-backed ranked set. The path there:
- 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.
- Right-to-left wins, because of the asymmetry. Later 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]
- The last position is forced. Given the values to its right, the number that must go there is uniquely determined. [2]
- Binary search finds , because the prefix inversion count is strictly decreasing in (Lemma 2) and computable for any candidate (Lemma 1).
- A Segment Tree makes every candidate test cheap: maintain the set of unplaced values with insert/remove/rank/nth, which is everything the inversion counting needs.
- Sweep to the front, block by block: strip each run of positions, account its inversions once, place each value, and the permutation falls out.
Full pseudocode is in Idea 3; the RankedSet implementation is in the Aside.
References
Problem-solving techniques used:
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.
Left-to-right and smallest-first are the obvious orderings; reversing them gave the other two candidates, including the one that worked.
Going back-to-front worked because the input pins every prefix's inversion count — the constraint itself is what makes the backward sweep local.
A strictly monotone function means binary search; a ranked-set-with-counts means segment tree. Both leaps came from pattern recognition, not derivation.
"Count inversions contributed by " became "count set members greater than " — turning a permutation problem into a data-structure problem.
Learning points:
- I got really stuck on a wrong approach for a long time, and should have moved on sooner. I was aware of the right approach but kept trying to make the wrong one work.
- I wanted to solve this in under an hour, and made essentially no progress by the end of it — then made steady progress after coming back to the problem later.
- I should have tried the reverse direction earlier — the asymmetry was visible in my very first observations.
- A nice use of Segment Trees to maintain a count.
- A nice use of permutations and inversions.
Topics:
- Permutations, inversions
- Greedy
- Binary search
- Data structures / Segment Trees
- Construction problems
- Sweeps / orderings