Editorial for WC '16 Finals S4 - Bug Infestation


Remember to use this editorial only when stuck, and not to copy-paste code from it. Please be respectful to the problem author and editorialist.
Submitting an official solution before solving the problem yourself is a bannable offence.

The monkeys' program can be modeled as a directed graph with N nodes. The i-th node corresponds to the i-th line of code, and for each line i whose debugging would result in another line gaining a bug (L_i > 0), there's an edge leading from node i to node L_i. Note that every node in this graph has an out-degree of at most 1. A graph of this form has some useful properties regarding the structure of its connected components – in particular, each component can have one of two types. A type-1 component is just a tree rooted at some node (with an edge leading up from each node to its parent). Meanwhile, a type-2 component contains a single cycle (consisting of 2 or more nodes), with each of the cycle's nodes possibly being the root of a tree of other, non-cycle nodes.

The graph's components are totally independent of one another, so our overall approach will be to find each component, traverse it to determine its type, and then compute the 2 required answer values for it (the minimum number of bugs which it can be reduced to having, and the minimum amount of time required to do so). These answers can then be tallied up over all of the components.

To find all of the graph's components, we can iterate over all N nodes in any order, while keeping track of which nodes belong to components which have already been processed (initially none). Each time we come across a node which hasn't yet been marked in this way, we'll know that we've found a new component to process. The first step will be to find the "base" of the component – in particular, its root node if it's a type-1 component, or any node belonging to its cycle if it's a type-2 component. This can be accomplished by repeatedly following the single outgoing edge from the current node until we either reach a node with no outgoing edge, or re-visit a node for the second time. In the former case, the final node must be the root of a type-1 component, while in the latter case, the re-visited node must lie on a type-2 component's cycle (given that the sequence of traversed edges has led back to it).

A type-1 component can always be reduced to having 0 bugs, and the optimal way to do so involves essentially "propagating" all of its bugs gradually up to the root of the tree, and then debugging the root node to remove the final bug entirely. In total, each node in the tree whose subtree contains at least one bug will need to be debugged once, while any other nodes won't need to be debugged at all. Counting the number of such nodes can be done using a simple instance of dynamic programming, by recursively determining whether or not each node's subtree contains any bugs based on its children's subtrees.

A type-2 component is trickier to deal with. Each tree branching off from the component's cycle should first be handled as if it were a type-1 component itself (reusing the same recursive procedure), resulting in it getting reduced to its root node (which is part of the cycle) containing a bug if the tree initially contained at least one bug.

We're then left with just a cycle with some number of nodes c, which can be numbered from 1 to c by iterating around the cycle once. Each of the c nodes either contains a bug, or doesn't. Assuming there are k such bugged nodes, we can assemble a list P_{1 \dots k} of their positions in the cycle. Assuming that k is positive, not all of the bugs can ever be removed, so the best we can do is "sweep" around the cycle, essentially collecting all of the bugs as we go until only a single node is left containing a bug. However, it's unclear where along the cycle this sweep should start. We should certainly start by debugging one of the bugged nodes, and then we'll end up going around until reaching the last bugged node before the initial chosen one. In particular, if we start the sweep at cycle position P_i (such that 2 \le i \le k), then it will take c-(P_i-P_{i-1}) minutes to complete the sweep. Starting at P_1 and ending at P_k is similar. All k possible sweeps of this sort should be considered, with the fastest one being chosen for the component.

In every individual portion of the above algorithm (scanning through all of the nodes to find components, finding the base of each component, recursively solving each tree, traversing each cycle, and considering all possible sweep points around each cycle), each node in the graph is processed at most once in total. Therefore, the overall time complexity is \mathcal O(N).


Comments

There are no comments at the moment.