There are people at a round table, numbered clockwise, and person has a weight . You may give a gift to any subset of them. Each person's field of view is the people clockwise and the people counter-clockwise from them ( people in total).
- If person receives a gift and people in their field of view did not, they gain happiness.
- If person does not receive a gift and people in their field of view did, they lose happiness.
Choose who to gift so that the total happiness is maximized.
- Graph.
- Flow?
- Dynamic programming.
- Double to handle wrap-around.
- Greedy construction?
- Use long longs (really big numbers).
- Sweep... (starting from where?)
- Consider intervals / windows .
- The answer is always (gift nobody).
- Sort by ?
- 2-SAT?
IdeaCuts, flows, and augmenting paths?
I originally thought of this as potentially a dynamic programming problem — but DP usually wants an ordering, and there is no natural ordering here, because of the cyclical nature of the problem (the candidates literally sit in a circle around a table).
One possible "natural ordering" would be to start with the smallest or largest element.
I started playing around with the score formula (the total happiness — I'll just call it the score from here on) and found some interesting observations.
Suppose person is gifted, person is ungifted, and they are neighbors (within distance around the circle). Then this pair contributes exactly to the total score — and the whole score is the sum of over all such gifted–ungifted neighbor pairs.
Rewrite the formula. A gifted person contributes where is their number of ungifted neighbors — that is, once per ungifted neighbor . Symmetrically, an ungifted contributes once per gifted neighbor. Grouping the two contributions by pair: each gifted–ungifted neighbor pair contributes , and nothing else contributes at all.
(We will be using this observation almost as a given from here on.)
From here, the problem reminded me of the min-cut / max-cut problem.
Max Cut: given a graph with costs on the edges (possibly negative), select a subset of nodes; for each edge with and , add its cost to the total; maximize.
In a way, our problem is a max-cut problem on the circle-window graph. However, Max Cut is NP-hard in general (I believe), so this can't work as-is for a linear-time target — but specialized graphs often admit cleaner solutions, and the min-cut problem (non-negative weights, minimize) is polynomial via the max-flow min-cut theorem. So I started wondering: can I solve this with a max-flow algorithm on this graph, or some modified variant? The graph is also highly symmetric, since each node's neighbors are just the interval around it.
The core of max-flow (and of relatives like bipartite matching) is the augmenting path: given a current selection with some score, incrementally improve it — typically via a chain of additions and removals that individually might not help but collectively do. Given the structure here, I figured this might lead to a greedy or incremental approach.
Three candidate algorithms came out of this:
- Greedy. Start with nobody gifted. Repeatedly pick a person and gift them if that increases the total; otherwise leave them ungifted.
- Augmenting path. Start with nobody gifted. When gifting person hurts because some ungifted neighbor contributes a large negative, try gifting too; or if some gifted neighbor's contribution just dropped to zero, try un-gifting them — a BFS-like search for a chain of flips that collectively improves the score, like the augmenting step in max-flow.
- Greedy sweep, largest first. Gift people in decreasing order of , as long as each addition doesn't decrease the score.
The first is just a greedy simplification of the second, and given the symmetry of the problem it felt like a simpler variant should work here; the third exploits the symmetry a bit more.
At this point I played with the sample test cases, and , to get a feel for what an optimal assignment looks like and where an augmenting path might hide. For instance, on with (each person sees only their immediate neighbors):
1
/ \
6 4 gifted: {4, 5, 6}
| | ungifted: {1, 2}
2 --- 5
pairs: 4-1 = 3, 5-2 = 3, 6-2 = 4, 6-1 = 5 total = 15(For contrast: gift everyone and the total is — no gifted–ungifted pairs exist at all.)
There's a Key Question hiding in here that I should have asked myself — "under what conditions does gifting a person actually increase the score versus decrease it?" — but I did not, and kept poking at examples instead.
I spent most of my time exploring variants of the three algorithms above. Ironically (spoiler alert), it turns out that the greedy algorithm actually does work here, but I wasn't able to prove it by now. I kept flip-flopping between the greedy and the augmenting-path framing, trying to understand what could work. More or less got stuck.
What kept getting me: the relationship between gifting some person and un-gifting some person seemed complicated, because the neighborhoods all overlap with no obvious starting point. I kept playing with the versus relationships and it was stumping me.
IdeaWrite out the greedy / augmentation stepAC
In Idea 1, through a variety of framings, we arrived at some kind of greedy algorithm (gift a person if it increases the score) or an augmenting-path variant (gift some, un-gift others) — with not much luck making either into a working solution. Here are some observations made along the way:
Any person whose weight is all of their neighbors' weights should always be gifted. Why: leaving them ungifted produces a negative (or zero) term against every gifted neighbor, while gifting them produces only zero-or-positive terms against every neighbor. The symmetric argument says a person whose weight is all of their neighbors' should always be ungifted.
This was good — it started to imply even more of a greedy structure.
Looking at , an optimal solution is to gift and leave ungifted, for a score of . Oddly enough, we gifted all of the highest elements and skipped all of the smallest. There's definitely a there-there. But why?
Let's try a more extreme example: with . Clearly we want to gift the and skip the two 's around it, collecting per pair — total. I then wanted to also gift the two 's and skip the between them for a couple more points — but check it carefully: each gains against the and loses against its neighboring , so those flips are exactly break-even. The optimum is , and the 's can go either way. So the dumb "gift the largest, skip the smallest" picture is already subtler than it looks.
Overall though, intuitively, it felt like there was symmetry in the constraints waiting to be exploited. It all hinged on when we should greedily gift an element versus not, based on the configuration of its neighbors — and I didn't have a good answer for that.
After struggling with this long enough, I finally asked myself the Key Question...
Under what conditions does gifting person improve the score, compared to leaving them ungifted?
To answer that, let's just actually write it down.
Consider person : say they have gifted neighbors with weights , and ungifted neighbors with weights . Using Key Observation 1, the total contribution of all pairs involving :
If is ungifted, each gifted neighbor forms a pair with :
If is gifted, each ungifted neighbor forms a pair with :
It's better to gift iff , so we literally just write it out:
Miraculously, this simplified with alone on one side. And the really cool part: is just the sum over all neighbors of — gifted or not — and is the total number of neighbors. The gifted/ungifted split canceled out entirely.
Person should be gifted iff — iff their weight is at least the average of all their neighbors' weights. It doesn't even matter what configuration the others are in: each person's decision is independent.
This all follows from writing out the formula explicitly and simplifying. It yields a pretty nice algorithm:
Given n, A[1..n], and D
# pick the optimal set using the greedy rule
for each person i:
sum_of_neighbors = sum of A[j] over the 2D neighbors j of i
(wrap around the circle; exclude i itself)
if A[i] >= sum_of_neighbors / (2D): gift person i
# compute the answer with the original formula
answer = 0
for each person i:
if i gifted: answer += A[i] * (ungifted neighbors of i)
else: answer -= A[i] * (gifted neighbors of i)At this point we're effectively done, and it's a matter of implementing this in , using two relatively standard techniques:
In a regular array, a cumulative-sum array (with ) gives any range sum as (being careful with the edge cases).
To handle a circular array, "triple" it: set (with suitable shifts to keep indices non-negative), then build the cumulative sums on the result. Any window of neighbors, forwards or backwards across the wrap, becomes an ordinary range.
Counting the gifted neighbors of each person works the same way — a cumulative count over the tripled gift-indicator array.
So here is the final algorithm with all the details:
Given n, A[1..n], and D
X[i] := 1 iff person i is gifted; X[1..n] = 0
triple A for wrap-around: A[1..3n] = A + A + A
prefix sums: S[i] = S[i-1] + A[i] for i = 1..3n
for each person i (using tripled index i+n):
# the window [i+n-D, i+n+D] covers 2D+1 people INCLUDING i — subtract i
sum_of_neighbors = S[i+n+D] - S[i+n-D-1] - A[i+n]
if A[i] >= sum_of_neighbors / (2D): X[i] = 1
triple X for wrap-around: X[1..3n] = X + X + X
prefix counts: cnt[i] = cnt[i-1] + X[i] for i = 1..3n
answer = 0
for each person i (using tripled index i+n):
gifted_neighbors = cnt[i+n+D] - cnt[i+n-D-1] - X[i+n] # exclude i again
ungifted_neighbors = 2D - gifted_neighbors
if X[i]: answer += A[i] * ungifted_neighbors
else: answer -= A[i] * gifted_neighbors
print answer(Implementation notes: compare instead of dividing, and use 64-bit integers throughout — the totals get big.)
Tada!
At this point we have a correct solution, but Key Observation 2 did not sit well with me. Why? Because it didn't seem intuitive — and still doesn't quite — that a person must be gifted iff their weight exceeds the average of all their neighbors. It seemed to just miraculously pop out of the algebra, which I didn't fully understand beyond "it just works." But... why? Can we think this through to a more intuitive understanding — one I could have reached faster?
Is there a more intuitive reason why a person is gifted iff their weight is at least the average of their neighbors'?
Yes — it still requires writing it out, but it's more satisfying. Suppose person is ungifted, and we switch them to gifted. What happens to the contribution with each individual neighbor ? If is gifted, the old pair contributed and the new pair contributes : the change is . If is ungifted, the old pair contributed and the new one contributes : the change is again. The change is for every neighbor, regardless of whether is gifted.
Therefore the total change from gifting person is:
which is positive iff
— exactly the condition we derived earlier. It's effectively the same formula, but now we can see the core of why it works. It feels more like an "aha!" to me: gifting always adds across all neighbors , so if beats the neighbor average, the sum of those terms is positive — independent of everyone else's gifts.
And here we are. I think we're done.
Review
Each person is gifted iff their weight is at least the average of their neighbors' weights — an independent, per-person rule — and prefix sums over a tripled array make the whole thing . The path:
- Rewrite the score as pairs. Every gifted–ungifted neighbor pair contributes exactly , and that's the whole formula. [1]
- The problem smells greedy / augmenting-path-like, from its similarity to cut problems and from the symmetry of the circle and the -windows.
- The Key Question: what happens when you switch one person from ungifted to gifted? (Equivalently: when is it better for a person to be gifted?)
- Writing out the formulas, the split cancels: person should be gifted iff is at least the average of all their neighbors' weights, independent of everyone else's status. This is the most important observation in the problem. [2]
- Cumulative sums + a triple-concatenated array handle the circular windows in per person. [3][4]
- (Bonus) The change-in-contribution view derives the same rule more intuitively: gifting changes the score by exactly over neighbors, regardless of their status. [5]
References
Problem-solving techniques used:
As per usual, a good place to start — working small examples by hand developed the intuition that the size of is critical, though with some nuances.
Arguably the most important technique for this problem. Once we had some greedy/augmentation approach in hand, we would have saved a LOT of time by immediately writing out the algebra for "what exactly happens to the contribution of when goes from ungifted to gifted." The math and the intuition simplified beautifully.
Somewhat helpful for looking at the largest and smallest items, though not a breakthrough here.
A lot of the direction (augmenting / greedy) and the final details (prefix sums, wrap-around tricks) came from experience with many problems before.
Rewriting the formula in terms of pairs and cuts made the problem more tractable. Going all the way to max-cut was probably a distraction — I spent too long on a far more complicated framing — but the small transformations really did simplify things.
Not really noted above, but "what happens when a single person is flipped from ungifted to gifted" is itself a simplification — one local question instead of a global one.
The symmetry of the fixed-size windows really simplifies the problem. We should have exploited it sooner (and in the end, somewhat did).
Learning points:
- I went way too complicated with the problem transformation. Thinking in terms of cuts was helpful for intuition, but exploring a full augmenting-path or min-cut/max-flow formulation was not. The problem ended up being FAR more greedy than I initially thought.
- Writing out the algebra and simplifying was key. I should have done it much earlier.
- There was a second, more intuitive framing: what is the change in contribution when you gift person ? It comes out to minus the neighbor average, and that framing would have gotten me to the solution immensely faster as well.
- Maybe the biggest lesson: "KEEP IT SIMPLE, STUPID." Pursue the dumbest approach first — greedy is simplest, the augmenting-path variant second. I should have gone further down the rabbit hole on the easier solution before reaching for heavier machinery.
Topics:
- Greedy
- Optimization problems
- Construction
- Math / algebra
- Prefix sums / cumulative arrays
- Circular arrays
- Graph theory / cuts