Abridged Reading

This problem stumped me in the ECNA 2021. I revisited the problem today with fresh eyes to see how my approach has changed over the last year. Pays to practice!

The problem describes a graph of dependencies between chapters in a book. Each chapter has a number of pages (its cost to read) and 0 or more chapters that must be read after it. If a chapter has no successors, then it is a "culminating concept" chapter. The task is to find the two "culminating concept" chapters that require the least total pages to read. The problem states that a chapter has at most one prerequisite. This means the chapters form a tree. The task can then be restated as "find the lowest-cost branching path in the tree that passes through the root(s) and two leaf nodes".

The main concept behind solving this problem is the lowest common ancestor (LCA). The lowest common ancestor of two nodes is the first "lowest" node in the tree that appears in both nodes' paths. Being as we don't need to reread a shared prerequisite between two chapters, if we count the root-to-node cost of two leaves, we can subtract the root-to-node cost of their LCA.

An illustration of our solution for the first test case on Kattis

With this in mind, the solution can be laid out in a few steps:

  1. Connect the graph, taking note of nodes with no prerequisite (roots) and nodes with no successors (leaves).
  2. Determine the root-to-node path cost and depth of each node with a DFS.
  3. For each pair of leaves, see if their total cost minus the cost of their LCA, if any, is less than the current best.

_73
#include <iostream>
_73
#include <functional>
_73
#include <vector>
_73
_73
using ll = long long;
_73
using namespace std;
_73
_73
int main () {
_73
int n, m;
_73
cin >> n >> m;
_73
vector<vector<int>> adj (n);
_73
vector<ll> cost (n, 0),
_73
parent (n, -1),
_73
outdegree (n, 0),
_73
depth (n, 0),
_73
roots,
_73
leaves;
_73
_73
// read costs
_73
for (auto& c : cost) cin >> c;
_73
_73
// connect tree
_73
for (int i = 0; i < m; ++i) {
_73
int from, to;
_73
cin >> from >> to;
_73
from--; to--;
_73
adj[from].push_back(to);
_73
parent[to] = from;
_73
outdegree[from]++;
_73
}
_73
_73
// determine parents and leaves
_73
for (int i = 0; i < n; ++i) {
_73
if (parent[i]==-1) roots.push_back(i);
_73
if (outdegree[i]==0) leaves.push_back(i);
_73
}
_73
_73
// pass down cost and depth through the tree
_73
// a "treefix sum", if you will...
_73
function<void(int)> pass_down_from = [&] (int parent) {
_73
for (auto child : adj[parent]) {
_73
cost[child] += cost[parent];
_73
depth[child] = depth[parent]+1;
_73
pass_down_from(child);
_73
}
_73
};
_73
_73
for (auto root : roots) { pass_down_from(root); }
_73
_73
// determine the cost of the lowest common ancestor of nodes a and b
_73
function<ll(int,int)> lca_cost = [&] (int a, int b) {
_73
while (depth[a] < depth[b]) { b = parent[b]; }
_73
while (depth[b] < depth[a]) { a = parent[a]; }
_73
while (a != b && a != -1 && b != -1) {
_73
a = parent[a];
_73
b = parent[b];
_73
}
_73
if (a == -1) return 0LL;
_73
return cost[a];
_73
};
_73
_73
ll minCost = cost[leaves[0]] + cost[leaves[1]];
_73
_73
for (int i = 0; i < leaves.size(); ++i) {
_73
for (int j = i+1; j < leaves.size(); ++j) {
_73
int a = leaves[i], b = leaves[j];
_73
ll costAB = cost[a] + cost[b] - lca_cost(a,b);
_73
minCost = min(minCost, costAB);
_73
}
_73
}
_73
_73
cout << minCost << '\n';
_73
}

If you found this solution helpful, consider leaving a star!