ACM ICPC World Finals 2018
Shortest judge solution: 2510 bytes. Shortest team solution (during contest): N/A bytes.
This was one of the two very hard problems in the set. What is not hard to see is that the problem is simply asking for a minimum cost maximum flow between a set of sources and a set of sinks in a flow graph. The flow graph in question has a special structure: most importantly, it is a tree (and in addition, all edge capacities are infinite – we can move as many armies as we like across an edge – only the source and sink vertices have limited capacities). However the tree can have around 250 000 vertices/edges, so running a general min cost max flow algorithm on this would time out rather badly. So to solve the problem we have to (unsurprisingly) exploit the structure of the graph.
Let us without loss of generality make the following two assumptions:
(Applying the transformations may blow up the size of the tree by a factor 4, so from an implementation point of view, one might want to unravel what the transformations mean in the context of the solution below and not actually do the transformations on the tree – the solution works without these assumptions an we have made them only to simplify the presentation.)
Root the tree arbitrarily. A natural idea is to try to solve this by some form of dynamic programming on the tree, moving upwards from the leaves until we reach the root where we get the answer. But even so, it is not at all clear what state to keep in the dynamic program, or how to go up the tree efficiently.
Let us first describe the state to use, and how to use it, without worrying too much about efficiency, and then we will later figure out how to actually make it efficient. For a rooted tree T (which you can think of for now as the subtree of the input below some vertex v), define a function cT : ℤ → ℤ, where cT(a) is the answer to the question “What is the minimum cost of movements within the tree T, assuming that exactly a new armies have been added to the root of T?” If we have this function computed for T the entire input tree, the answer to the problem is then simply cT(0). Note that cT is defined also for negative a, which is interpreted as an additional demand of a armies having been introduced at the root. Let ∆T denote the net demand within T (sum of yi values minus sum of xi values). Then cT(a) is a well-defined non-increasing function for a ≥ ∆T.
We now use this setup to make a solution that is too slow, running in time Θ(nX) where X is the sum of all xi values in the graph. The idea is as mentioned above to build the functions cT for subtrees of the input starting at the leaves and moving upwards. For T being a single leaf node of the input, the function cT is trivial: cT(a) = 0 for all a (there can never be any costs from movement within a tree of a single node). In order to progress up the tree, we need to be able to do the following two operations:
To get the slow Ω(nX) solution, we can simply represent the functions cT as arrays of function values and implement the equations written above.
In order to speed this up, the perhaps key observation is that the functions cT are not only non-increasing, they are also convex (proof omitted, it can be derived from the identies above, or more abstractly from properties of min cost max flows). I.e., there is a diminishing returns type of property – the savings in cost attained by moving in more armies decreases as we move in more armies. For this reason, instead of working with cT directly, it turns out to be more convenient to work with the function fT : ℤ≥0 → ℤ defined by fT(a) = cT(∆T + a) − cT(∆T + a + 1) (i.e., fT(0) says how much the cost decreases when going from ∆T to ∆T + 1 armies moving out of T, and so on). So we will keep this function, and the value bT := cT(∆T), the base cost for the tree T if we only keep the bare minimum of soldiers inside it and move everyone else out of it. The fact that cT is non-increasing and convex then translates into the function fT being non-negative and non-increasing.
How does this change of perspective affect the identites for cT above? The join operation becomes particularly nice in this new perspective: it follows from the convexity property that if we take all the fT_L and fT_R values and merge them into a big list sorted in non-increasing order, then that resulting list are the values of fT (abstractly, this is because the operation we are doing is taking the Minkowski sum of the two functions cT_L and cT_R, and the Minkowski sum of convex sets behaves nicely). This suggest the following way of representing the functions fT: keep the values as a sorted set, and when joining fT_L with fT_R, add the values from the smaller of the two sets to the larger of the two (and be careful not to do anything which takes linear time in the larger of the two sets). By a standard argument, this results in a total running time of O(n + X log2 X) for doing all the joins.
For the extend operation, the identity for cT above now becomes fT₀(a) = max(0, fT(a) − sgn(∆T + a) cost(v, w)) (where we define sgn(a) = +1 if a ≥ 0 and −1 if a < 0). Computing this by updating each individual fT-value would be too costly. Ignoring for a moment the cap at 0 and focusing only on the second argument, we see that the transformation performed on fT is fairly simple: it gets increased by cost(v, w) for all a < −∆T (which are none, if ∆T ≥ 0, i.e., if the subtree does not have more surplus than demand), and decreased by the same amount for all a ≥ −∆T.
We can arrange for this by augmenting the set structure suggested above with a split point and two global shifts – everything below the split point has been shifted by the first shift, and everything above the split point has been shifted by the second split. When we do the extend operation, there may already be an existing split point and shifts, we then have to move the split point, which can be done in time which is O(d log d) where d is the distance between the two split points. This value might be as large as X, but in total over all extend operations done in the tree, the sum of these distances can never be more than 2X, so the total time we get from these moves of the split points is O(n + X log X). Coming back to the ignored max(0, ...) part from above, the easiest way to deal with that is to simply remove any negative values from the end of the set after having performed the operation above.
We also need to revisit the join operation and see that this can still be done with the augmented data structure with shifts. Implementationwise, instead of keeping a single ordered set with a split point, it is probably easier (at least all our implementations seemed to feel so) to keep two separate sets corresponding to the part of values below and the part of values above the split point. Another implementation detail is that using complete ordered sets is not necessary, one can get away with two simple max-heaps.