A minimum spanning arborescence rooted at : every vertex has exactly one incoming edge (except ), and all vertices are reachable from . Kruskal and Prim are wrong here. Chu–Liu/Edmonds: take every vertex’s cheapest incoming edge; if they form no cycle, done; otherwise contract a cycle, reduce the weights of edges entering it by the cycle edge they would replace, and recurse. simply, with mergeable heaps (Tarjan/Gabow).

version

ll directedMST(int n, int root, vector<array<ll, 3>> E) {     // {u, v, w}; -1 if some vertex is unreachable
    const ll INF = LLONG_MAX / 4; ll res = 0;
    while (true) {
        vector<ll> in(n, INF); vector<int> pre(n, -1);
        for (auto [u, v, w] : E) if (u != v && w < in[v]) in[v] = w, pre[v] = u;
        for (int v = 0; v < n; v++) if (v != root && in[v] == INF) return -1;
        int cnt = 0; vector<int> id(n, -1), vis(n, -1); in[root] = 0;
        for (int v = 0; v < n; v++) {
            res += in[v]; int x = v;
            while (vis[x] != v && id[x] == -1 && x != root) vis[x] = v, x = pre[x];
            if (x != root && id[x] == -1) { for (int y = pre[x]; y != x; y = pre[y]) id[y] = cnt; id[x] = cnt++; }
        }
        if (!cnt) return res;
        for (int v = 0; v < n; v++) if (id[v] == -1) id[v] = cnt++;
        vector<array<ll, 3>> NE;
        for (auto [u, v, w] : E) if (id[u] != id[v]) NE.push_back({id[u], id[v], w - in[v]});
        E = NE; n = cnt; root = id[root];
    }
}

with skew heaps (Tarjan / Gabow)

Each (super)vertex keeps a mergeable heap of incoming edges with a lazy weight shift. Walk from each unvisited vertex along cheapest incoming edges; when the walk closes a cycle, merge the heaps of the cycle (each shifted by its chosen edge’s weight) and continue from the merged super-vertex. Contractions are recorded with a rollback DSU so the actual parent array can be reconstructed.

struct DirectedMST {
    struct E { int a, b; ll w; };
    struct H { E key; int l = 0, r = 0; ll lz = 0; };
    vector<H> t{H{}};
    void push(int x) { if (!x) return; t[x].key.w += t[x].lz; if (t[x].l) t[t[x].l].lz += t[x].lz; if (t[x].r) t[t[x].r].lz += t[x].lz; t[x].lz = 0; }
    int merge(int a, int b) {
        if (!a || !b) return a ^ b;
        push(a); push(b); if (t[a].key.w > t[b].key.w) swap(a, b);
        int m = merge(b, t[a].r); t[a].r = t[a].l; t[a].l = m; return a;
    }
    int pop(int x) { push(x); return merge(t[x].l, t[x].r); }
    struct RUF {
        vector<int> e; vector<pair<int, int>> st; RUF(int n) : e(n, -1) {}
        int find(int x) { return e[x] < 0 ? x : find(e[x]); }
        int time() { return sz(st); }
        void rollback(int tm) { for (int i = time(); i-- > tm;) e[st[i].first] = st[i].second; st.resize(tm); }
        bool join(int a, int b) {
            a = find(a), b = find(b); if (a == b) return false;
            if (e[a] > e[b]) swap(a, b);
            st.push_back({a, e[a]}); st.push_back({b, e[b]}); e[a] += e[b]; e[b] = a; return true;
        }
    };
    pair<ll, vector<int>> solve(int n, int r, const vector<E>& g) {   // {cost, parent} or {-1, {}}
        RUF uf(n); vector<int> heap(n);
        for (auto e : g) { t.push_back(H{e}); heap[e.b] = merge(heap[e.b], sz(t) - 1); }
        ll res = 0; vector<int> seen(n, -1), path(n), par(n); seen[r] = r;
        vector<E> Q(n), in(n, E{-1, -1, 0}); deque<tuple<int, int, vector<E>>> cycs;
        for (int s = 0; s < n; s++) {
            int u = s, qi = 0, w;
            while (seen[u] < 0) {
                if (!heap[u]) return {-1, {}};
                push(heap[u]); E e = t[heap[u]].key;
                t[heap[u]].lz -= e.w; heap[u] = pop(heap[u]);
                Q[qi] = e; path[qi++] = u; seen[u] = s; res += e.w; u = uf.find(e.a);
                if (seen[u] == s) {                         // cycle: contract it
                    int cyc = 0, end = qi, tm = uf.time();
                    do cyc = merge(cyc, heap[w = path[--qi]]); while (uf.join(u, w));
                    u = uf.find(u); heap[u] = cyc; seen[u] = -1;
                    cycs.push_front({u, tm, vector<E>(Q.begin() + qi, Q.begin() + end)});
                }
            }
            for (int i = 0; i < qi; i++) in[uf.find(Q[i].b)] = Q[i];
        }
        for (auto& [u, tm, comp] : cycs) {                  // expand cycles to recover parents
            uf.rollback(tm); E inE = in[u];
            for (auto& e : comp) in[uf.find(e.b)] = e;
            in[uf.find(inE.b)] = inE;
        }
        for (int i = 0; i < n; i++) par[i] = in[i].a;
        return {res, par};
    }
};

Variants

  • Maximum arborescence: negate weights.
  • Best root: add a virtual root with an edge of weight (larger than the sum of all weights) to every vertex; the answer minus uses exactly one virtual edge, and its target is the optimal root.
  • Counting arborescences: directed matrix-tree theorem (Matrix-Tree).
  • Minimum arborescence with a degree bound at the root: Aliens trick on the root’s edges.

This models optimal branchings in dependency/precedence graphs generally — task scheduling with directed prerequisites, compiler IR, broadcast trees — anywhere “cheapest way to reach everything from one source, directed” shows up.