This is an interactive problem: your program submits a series of queries to a judge, and gets a result back after each query.
There is a hidden array of items. Each item carries a value from to : every value appears exactly twice, except for one special value that appears exactly three times.
On your turn, you can query any set of indices between and , and the judge answers with the number of values that appear exactly once among the indices you chose (the "singletons" of your set). The indices you choose do not need to be consecutive.
Your goal is to determine the three positions of the special value.
The problem comes in two versions. In the easy version (B1) you may ask at most queries. In the hard version (B2) you may ask at most . In both versions, .
- Construction.
- Subsets.
- Positions.
- Pairs / parity.
- Bit-by-bit.
- [Generate and Test].
- Strategy.
IdeaQuery a set and its complement
To get a feel for the problem, let's set up a small example to play with, say :
array: 1 2 3 2 1 4 3 1 4 special value 1, at positions 1, 5, 8
index: 1 2 3 4 5 6 7 8 9The first thing to look at: what happens if you query a set and then query its complement, as in everything that's not in ?
For any of the paired values, one of three things is true:
- Both copies are in . Then they're a pair inside , so they contribute to that query, and they don't appear in the complement at all, so they contribute there too.
- Both copies are outside . Then they don't appear in at all, so they contribute there, and they form a pair in the complement, which also contributes .
- The pair is split, one copy on each side. Then it shows up as a singleton in , contributing , and it also shows up as a singleton in the complement, contributing over there.
So in every case, a paired value contributes exactly the same amount to both queries. The special value is where it gets interesting:
- One copy in , two out: a singleton inside (), and a pair outside ().
- Two copies in, one out: a pair inside (), and a singleton outside ().
- All three in, or all three out: all three copies sit on one side, where they count for nothing, and the other side has none of them. to both.
Compare the two answers. If they differ, they differ by exactly , and whichever side got the bigger answer holds exactly one of the three special positions (the other side holds two). If they're equal, then either all three are in or none are.
So how do we tell three apart from zero? Let's write out some queries on the example and look.
Querying some sets that contain all three special copies, next to some sets that don't:
S (indices) values in S answer |S| parities?
{1,5,8} 1 1 1 0 3 differ (even vs odd)
{2,1,5,8} 2 1 1 1 1 4 differ
{2,4,1,5,8} 2 2 1 1 1 0 5 differ
{2,3} 2 3 2 2 match
{1,2} 1 2 2 2 match
{1,5,2} 1 1 2 1 3 matchInteresting. In the rows where all three special copies are inside , the parity of the answer is different from the parity of , and in every other row they are the same. Let me convince myself. Every paired value contributes to and to the answer with the same parity: both copies in adds to the size and to the answer (both even), a split adds and (both odd), both out adds and . And the special value with copies in adds to the size, but adds to the answer only when . So for the two parities track each other, and is the one case that pushes them apart: three to the size, nothing to the answer.
So when the answer for both queries is the same, we can use the parity to tell whether it's all three or none. Putting it all together:
For any set , querying and then querying its complement tells you exactly whether , , , or of the special positions are in . If the answers differ by one, the side with the bigger answer holds exactly one of them (and the other side holds two). If the answers are equal, it's all three or none, and comparing the parity of the answer with the parity of settles which.
Written out as pseudo-code:
count(S): # how many special positions are in S
a = query(S)
b = query(everything not in S)
if a == b + 1: return 1
if b == a + 1: return 2
# a == b: zero or three -- decide by parity
return 3 if (a mod 2) != (|S| mod 2) else 0This felt really powerful, but I didn't really know what to do with it yet.
IdeaBinary search on prefixesAC
Since counting is now easy (Key Observation 1: two queries tell us exactly how many special positions are in any set), here's an idea: find the smallest region that contains at least one special position, and shrink it to pinpoint one exactly.
Think prefixes. Let be the number of special positions in , which we can compute with two queries. is monotone in , so binary search: the smallest with is the first special position, the smallest with is the second, and the smallest with is the third.
There are positions, so each binary search takes about steps, and each step costs two queries. That's queries per special position, in total, which fits in the budget for B1.
# count(S) is the two-query counter from Idea 1
for t = 1, 2, 3:
binary search the smallest i in [1, 2n+1] with count({1..i}) >= t
the t-th special position is that iAccepted for B1 (my submission). But queries is double the budget for B2, so we need something better.
IdeaBit by bit?No Solution
I kept going back to a bit-by-bit idea. The positions are -bit numbers, so what if, for each bit , I figure out which of the three special positions have bit on?
The query for that: take the set of all indices with bit on, plus its complement, and use the counter from Idea 1. If the count is , all three positions have the bit on. If it's , all three have it off. Otherwise the bit splits them, and now you're into case analysis about which item has which bit.
Working from the most significant bit down, you can treat the three items as sorted while you discover them, filling in a little grid of bits with three columns and rows. As long as every bit comes out -or-, the three items are still indistinguishable from each other, so on the first split there is nothing to figure out: assign the bits in sorted order, for free.
count() per bit, filling the grid from the most significant bit down:
item1 item2 item3 count what happens
bit 10: 0 0 0 0 all off; still indistinguishable
bit 9: 1 1 1 3 all on; still indistinguishable
bit 8: 0 0 1 1 first split: assign in sorted order
for free; item 3 is now distinct
bit 7: ? ? ? 1 ambiguous: the lone 1 could be on
item 3, or on one of items 1, 2That last row is the problem. Once some item is distinguished, a split bit no longer assigns itself: we have to find out which group the lone belongs to, and that takes one more count(). The trick for the extra query is to use the known bit-prefixes: in the grid above, item 3 is the only item whose bits so far are , so a query restricted to indices starting with is guaranteed not to contain items 1 and 2.
extra count() on { indices with bits 10..8 = 011, bit 7 = 1 }:
1 -> item 3 has bit 7 = 1; items 1 and 2 get 0 0
(those two are still indistinguishable from each other)
0 -> item 3 has bit 7 = 0; one of items 1, 2 has the 1, and those
two are still interchangeable, so assign in sorted order for
free: they get 0 1 -- and now all three items are distinctSo it's a little state machine: at each bit, the three items are either all still indistinguishable, or one is distinguished from the other two, or all three are distinct, and the state decides whether a split bit assigns itself for free or costs an extra count(). I convinced myself that some variant of this works out to at most four queries per bit: in total. I'll admit I never wrote it out fully.
But is still not . And it was clear that any solution leaning on count() automatically pays two queries for each thing it learns, which really eats up the budget. I got stuck here.
IdeaLeverage the all-three caseAC
What can I figure out in ONE query, instead of two? As long as every piece of information costs two queries through count(), we are not going to fit in .
A learning point from previous analyses is to vary the examples slowly, one small change at a time, and watch how the answer moves. So let's take a single query and grow the set by one index at a time, on the same array as before:
array: 1 2 3 2 1 4 3 1 4 special value 1, at positions 1, 5, 8
index: 1 2 3 4 5 6 7 8 9
S (indices) values in S answer
{2} 2 1
{2,4} 2 2 0 down by 1 (completed a pair)
{2,4,1} 2 2 1 1 up by 1 (new singleton)
{2,4,1,5} 2 2 1 1 0 down by 1 (completed a pair)
{2,4,1,5,8} 2 2 1 1 1 0 unchanged!Adding an index either brings a net-new value into , and the answer goes up by one, or it completes a pair that's already in there, and the answer goes down by one. The last row is the odd one out: adding the third copy of the special value turned a pair into a triple, and neither of those counts for anything, so the answer didn't move.
Adding one item to changes the answer by or , except in one case: adding the third copy of the special value changes it by .
I'll note that at this point I didn't know how to use this, but it felt very worthwhile.
The other thing to try is going back to the case analysis we already have from Idea 1.
Is there any sub-case of count() that we could have learned with one query instead of two?
Go through the branches of count(). The first two, where the answers differ by one, genuinely compare the two answers, so they need both queries. But look at the last branch: to separate three from zero, it checks the parity of the answer against the parity of , and is something we already know without asking the judge.
The all-three-versus-zero branch never actually uses the complement's answer at all. So maybe the all-three case can be detected with a single query?
Let's check that against the behavior. Start from the empty set: the answer is and the size is , so the parities match. Now add items one at a time. Each addition grows the size by exactly and moves the answer by or , so both parities flip together and stay in sync. The only way they can fall out of sync is an addition that leaves the answer alone, and we just saw there is exactly one of those: the third copy of the special value. And once that happens, the tracking picks right back up, so the mismatch never repairs itself.
For any set , a single query tests whether all three special positions are in :
In all the other cases (, , or of them in ) the parities match, so this test can't tell those apart. But it's partial information from a single query, and that's new for us.
I could have gotten here much sooner. The key question was already written down, and the answer was already sitting in the two-query case analysis; if I had gone back over what I'd learned and checked which cases were deducible with one query instead of two, I would have made much quicker progress. I kept getting distracted by the bit-by-bit idea (Idea 3) instead. When a key question is written down, work it directly.
So we have a new tool: a single query that checks whether all three special positions are in a set. Let's call it the "all-three test". The question is whether this test alone can pin down the positions. And it can, with the same prefix trick as B1:
First, binary search the smallest such that passes the all-three test; that is the largest special position. Now pin it: search prefixes of , but always toss the found position into the queried set. The all-three test then fires exactly when the remaining two special positions are in the prefix, so the smallest passing prefix ends at the second-largest special position. Pin both and search once more for the first. Monotone every time, one query per step.
Three binary searches, steps each, one query per step: queries.
all_three(S): # single query
return (query(S) mod 2) != (|S| mod 2)
found = {} # special positions discovered so far
mx = 2n + 1
repeat 3 times:
binary search the smallest k in [1, mx] with
all_three({1..k} ∪ found)
found.insert(k)
mx = k - 1
output "!" found # the three positionsAccepted for B2 (my submission).
Review
The solution is all about counting: we can use judge queries to find out how many of the three special positions are inside a chosen set . An exact count costs two queries, but just checking whether all three are in the set costs only one. And we can binary search on prefixes, using these counts, to pick out where the special positions are. The path:
- Query a set and its complement. Every paired value contributes the same amount to both answers; the special value adds one to whichever side holds exactly one of its copies, and nothing when all three copies are on the same side. So answers differing by one locate a side with exactly one special position; equal answers mean all-three-or-none, and comparing the answer's parity with 's settles which. Two queries count exactly how many of the three special positions are in . [1]
- Binary search on prefixes (solves B1). The count over is monotone in : find the smallest where it reaches , then , then ; those are the three positions. Three searches, steps each, two queries per step: . [2]
- Checking for all three costs only one query. Adding an index to moves the answer by , except the third copy of the special value, which moves it by . That's the only way the parities of the answer and can fall out of sync, so iff all three special positions are in . [KQ] [Lemma 1]
- Pin and search again (solves B2). Binary search the smallest prefix that contains all three special positions, one query per check; that prefix ends at the largest special position. Add it to every subsequent query set and search for the second; pin both and find the first. Three searches, one query per step: . [3]
(My accepted B1 and B2 submissions.)
References
Problem-solving techniques used:
Vary the examples slowly; this has been a constant learning point from the last few contests. Both of the key facts here came from watching one query's answer as the set changes by a single item: the case analysis for a set versus its complement, and the behavior that became the one-query test.
Going back to what I already knew about the problem. The one-query test wasn't new information: it was sitting inside the two-query case analysis the whole time, in the all-three branch, which never actually used the complement's answer. When the key question "what can I learn in one query?" showed up, the right move was to walk back through the cases I'd already worked out and check which of them survive on a single query.
The parity arguments are the kind of thing you have to write out to see. The tally of contributions per value type is three lines once it's on paper, and the B2 solution comes right out of it.
Learning points:
- I asked the right key question ("what can I figure out in one query?") and then kept getting distracted by the bit-by-bit idea (Idea 3) instead of answering it. When a key question is written down, work it directly: go back over what's already known and check what transfers (the lesson from Idea 4).
- It took me a long time to notice the all-three case was special, even though my own case analysis had already treated it specially (it's the one that needed the parity argument). The signal was on the page before I saw it.
- This problem was hard. I noticed tourist got B1 in contest but not B2, and a few other grandmasters missed it as well, so it's probably possible even for very good coders to miss that last step. Still, I think the single-query question was findable.
Topics:
- Interactive problems
- Construction problems
- Binary search
- Parity / invariants
- Counting