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.
With this in mind, the solution can be laid out in a few steps:
- Connect the graph, taking note of nodes with no prerequisite (roots) and nodes with no successors (leaves).
- Determine the root-to-node path cost and depth of each node with a DFS.
- 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_73using ll = long long;_73using namespace std;_73_73int 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!