Skip to content

directed_mst.hpp

SECTIONGraph INCLUDEnoya/graph/directed_mst.hpp

平行边归一化、批量左偏堆与缩环求最小树形图,返回总权值和原始边编号。

Complexity: Time: \(O(n + m + M \log (n + 1))\), \(M\) distinct eligible edge pairs. Space: \(O(n + m)\).

AC 记录:directedmst。

跳到代码 · GitHub ↗

Implementation

当前头文件,省略 include guard;依赖见 #include。

/// @complexity Time: O(n + m + M log(n + 1)), M distinct eligible edge pairs.
/// Space: O(n + m).

#include <cassert>
#include <cstddef>
#include <limits>
#include <numeric>
#include <optional>
#include <tuple>
#include <utility>
#include <vector>

namespace noya {

namespace directed_mst_internal {

template <class T> struct solver {
  struct edge {
    T cst{};
    int u = -1;
    int id = -1;
  };

  struct heap_node {
    heap_node *l = nullptr;
    heap_node *r = nullptr;
    edge val;
    T lz{};
    int rk = 1;
    bool dirty = false;
  };

  int n;
  int rt;
  const std::vector<std::tuple<T, int, int>> &es;
  std::vector<heap_node> buf;
  int ptr = 0;

  solver(int n_, int rt_, const std::vector<std::tuple<T, int, int>> &es_)
      : n(n_), rt(rt_), es(es_) {}

  static int heap_rank(heap_node *nd) { return nd == nullptr ? 0 : nd->rk; }

  static heap_node *add(heap_node *nd, const T &dlt) {
    if (nd != nullptr) {
      nd->val.cst += dlt;
      nd->lz += dlt;
      nd->dirty = true;
    }
    return nd;
  }

  static void push(heap_node *nd) {
    if (nd == nullptr || !nd->dirty) {
      return;
    }
    add(nd->l, nd->lz);
    add(nd->r, nd->lz);
    nd->lz = T{};
    nd->dirty = false;
  }

  static heap_node *meld(heap_node *a, heap_node *b) {
    if (a == nullptr) {
      return b;
    }
    if (b == nullptr) {
      return a;
    }
    if (std::pair(b->val.cst, b->val.id) < std::pair(a->val.cst, a->val.id)) {
      std::swap(a, b);
    }
    push(a);
    a->r = meld(a->r, b);
    if (heap_rank(a->l) < heap_rank(a->r)) {
      std::swap(a->l, a->r);
    }
    a->rk = heap_rank(a->r) + 1;
    return a;
  }

  heap_node *make_node(T cst, int u, int id) {
    heap_node *nd = &buf[ptr++];
    nd->val = {cst, u, id};
    return nd;
  }

  static edge pop(heap_node *&nd) {
    push(nd);
    edge res = nd->val;
    nd = meld(nd->l, nd->r);
    return res;
  }

  std::optional<std::pair<T, std::vector<int>>> run() {
    assert(0 < n && n <= std::numeric_limits<int>::max() / 2);
    assert(0 <= rt && rt < n);
    assert(es.size() <= std::size_t(std::numeric_limits<int>::max()));
    const int cap = 2 * n;
    std::vector<std::size_t> start(std::size_t(n) + 1);
    for (const auto &[cst, u, to] : es) {
      assert(0 <= u && u < n);
      assert(0 <= to && to < n);
      if (u != to && to != rt) {
        ++start[to];
      }
    }
    for (int v = 0; v < n; ++v) {
      if (v != rt && start[v] == 0) {
        return std::nullopt;
      }
    }
    if (n == 1) {
      return std::pair<T, std::vector<int>>{T{}, {}};
    }
    for (int v = 0; v < n; ++v) {
      start[v + 1] += start[v];
    }
    std::vector<int> ids(start.back());
    // Stable incoming-edge rows, without a second array of write cursors.
    for (int id = int(es.size()); id-- > 0;) {
      const auto &[cst, u, to] = es[id];
      if (u != to && to != rt) {
        ids[--start[to]] = id;
      }
    }
    // Parallel edges receive identical reductions in every contracted heap.
    // Keep the cheapest one, breaking ties by the original input index.
    // If there are n-1 eligible edges, the precheck guarantees unique rows.
    if (ids.size() > std::size_t(n - 1)) {
      std::vector<int> seen(n, -1);
      std::vector<std::size_t> best(n);
      std::size_t kept = 0;
      for (int v = 0; v < n; ++v) {
        auto l = start[v], r = start[v + 1];
        start[v] = kept;
        for (auto i = l; i < r; ++i) {
          int id = ids[i], u = std::get<1>(es[id]);
          if (seen[u] != v) {
            seen[u] = v;
            best[u] = kept;
            ids[kept++] = id;
          } else if (std::get<0>(es[id]) < std::get<0>(es[ids[best[u]]])) {
            ids[best[u]] = id;
          }
        }
      }
      start[n] = kept;
      ids.resize(kept);
    }
    buf.resize(ids.size());
    std::vector<heap_node *> in(cap, nullptr), roots;
    for (int v = 0; v < n; ++v) {
      roots.clear();
      for (auto i = start[v]; i < start[v + 1]; ++i) {
        int id = ids[i];
        const auto &[cst, u, to] = es[id];
        roots.push_back(make_node(cst, u, id));
      }
      // Pair adjacent heaps in rounds: linear total construction work.
      for (auto r = roots.size(); r > 1;) {
        std::size_t next = 0;
        for (std::size_t i = 0; i < r; i += 2) {
          roots[next++] = i + 1 < r ? meld(roots[i], roots[i + 1]) : roots[i];
        }
        r = next;
      }
      if (!roots.empty()) {
        in[v] = roots[0];
      }
    }

    std::vector<int> dsu(cap);
    std::iota(dsu.begin(), dsu.end(), 0);
    auto fd = [&](int v) {
      int uf = v;
      while (dsu[uf] != uf) {
        uf = dsu[uf];
      }
      while (dsu[v] != v) {
        int nxt = dsu[v];
        dsu[v] = uf;
        v = nxt;
      }
      return uf;
    };

    std::vector<char> st(cap);
    std::vector<edge> sel(cap);
    std::vector<int> cp(cap, -1);
    st[rt] = 2;
    int cnt = n;

    std::vector<int> pth;
    for (int s = 0; s < n; s++) {
      if (st[s] != 0) {
        continue;
      }
      pth.clear();
      pth.push_back(s);
      while (true) {
        int com = pth.back();
        st[com] = 1;

        edge bst;
        int pre = -1;
        while (in[com] != nullptr) {
          edge can = pop(in[com]);
          int from = fd(can.u);
          // Active path entries are already component representatives.
          if (from != com) {
            bst = can;
            pre = from;
            break;
          }
        }
        if (pre == -1) {
          return std::nullopt;
        }
        sel[com] = bst;
        if (st[pre] == 0) {
          pth.push_back(pre);
          continue;
        }
        if (st[pre] == 2) {
          break;
        }

        int ctr = cnt++;
        assert(ctr < cap);
        while (true) {
          int mem = pth.back();
          pth.pop_back();
          in[ctr] = meld(in[ctr], add(in[mem], -sel[mem].cst));
          dsu[mem] = ctr;
          cp[mem] = ctr;
          st[mem] = 2;
          if (mem == pre) {
            break;
          }
        }
        pth.push_back(ctr);
      }
      for (int com : pth) {
        st[com] = 2;
      }
    }

    std::vector<char> ext(cnt);
    ext[rt] = true;
    std::vector<int> eid;
    eid.reserve(n - 1);
    for (int com = cnt - 1; com >= 0; com--) {
      if (ext[com]) {
        continue;
      }
      int id = sel[com].id;
      if (id == -1) {
        return std::nullopt;
      }
      eid.push_back(id);
      int v = std::get<2>(es[id]);
      while (v != -1 && !ext[v]) {
        ext[v] = true;
        v = cp[v];
      }
    }
    if (int(eid.size()) != n - 1) {
      return std::nullopt;
    }
    T cst{};
    for (int id : eid) {
      cst += std::get<0>(es[id]);
    }
    return std::pair<T, std::vector<int>>{cst, std::move(eid)};
  }
};

} // namespace directed_mst_internal

/// @brief Minimum spanning arborescence rooted at rt.
/// Edges are (cost, from, to); loops and edges entering rt are ignored.
/// Parallel edges keep the minimum (cost, original index).
/// Requires 0 < n <= INT_MAX/2, valid vertices, and at most INT_MAX edges.
/// T needs zero/default construction, copying, <, unary -, and += with
/// ordered additive semantics; all intermediate values must be representable.
/// @return (cost, edge indices), or nullopt if some vertex is unreachable.
template <class T>
std::optional<std::pair<T, std::vector<int>>>
directed_mst(int n, int rt, const std::vector<std::tuple<T, int, int>> &es) {
  return directed_mst_internal::solver<T>(n, rt, es).run();
}

} // namespace noya

 // NOYA_DIRECTED_MST_HPP
#ifndef NOYA_DIRECTED_MST_HPP
#define NOYA_DIRECTED_MST_HPP 1

/// @complexity Time: O(n + m + M log(n + 1)), M distinct eligible edge pairs.
/// Space: O(n + m).

#include <cassert>
#include <cstddef>
#include <limits>
#include <numeric>
#include <optional>
#include <tuple>
#include <utility>
#include <vector>

namespace noya {

namespace directed_mst_internal {

template <class T> struct solver {
  struct edge {
    T cst{};
    int u = -1;
    int id = -1;
  };

  struct heap_node {
    heap_node *l = nullptr;
    heap_node *r = nullptr;
    edge val;
    T lz{};
    int rk = 1;
    bool dirty = false;
  };

  int n;
  int rt;
  const std::vector<std::tuple<T, int, int>> &es;
  std::vector<heap_node> buf;
  int ptr = 0;

  solver(int n_, int rt_, const std::vector<std::tuple<T, int, int>> &es_)
      : n(n_), rt(rt_), es(es_) {}

  static int heap_rank(heap_node *nd) { return nd == nullptr ? 0 : nd->rk; }

  static heap_node *add(heap_node *nd, const T &dlt) {
    if (nd != nullptr) {
      nd->val.cst += dlt;
      nd->lz += dlt;
      nd->dirty = true;
    }
    return nd;
  }

  static void push(heap_node *nd) {
    if (nd == nullptr || !nd->dirty) {
      return;
    }
    add(nd->l, nd->lz);
    add(nd->r, nd->lz);
    nd->lz = T{};
    nd->dirty = false;
  }

  static heap_node *meld(heap_node *a, heap_node *b) {
    if (a == nullptr) {
      return b;
    }
    if (b == nullptr) {
      return a;
    }
    if (std::pair(b->val.cst, b->val.id) < std::pair(a->val.cst, a->val.id)) {
      std::swap(a, b);
    }
    push(a);
    a->r = meld(a->r, b);
    if (heap_rank(a->l) < heap_rank(a->r)) {
      std::swap(a->l, a->r);
    }
    a->rk = heap_rank(a->r) + 1;
    return a;
  }

  heap_node *make_node(T cst, int u, int id) {
    heap_node *nd = &buf[ptr++];
    nd->val = {cst, u, id};
    return nd;
  }

  static edge pop(heap_node *&nd) {
    push(nd);
    edge res = nd->val;
    nd = meld(nd->l, nd->r);
    return res;
  }

  std::optional<std::pair<T, std::vector<int>>> run() {
    assert(0 < n && n <= std::numeric_limits<int>::max() / 2);
    assert(0 <= rt && rt < n);
    assert(es.size() <= std::size_t(std::numeric_limits<int>::max()));
    const int cap = 2 * n;
    std::vector<std::size_t> start(std::size_t(n) + 1);
    for (const auto &[cst, u, to] : es) {
      assert(0 <= u && u < n);
      assert(0 <= to && to < n);
      if (u != to && to != rt) {
        ++start[to];
      }
    }
    for (int v = 0; v < n; ++v) {
      if (v != rt && start[v] == 0) {
        return std::nullopt;
      }
    }
    if (n == 1) {
      return std::pair<T, std::vector<int>>{T{}, {}};
    }
    for (int v = 0; v < n; ++v) {
      start[v + 1] += start[v];
    }
    std::vector<int> ids(start.back());
    // Stable incoming-edge rows, without a second array of write cursors.
    for (int id = int(es.size()); id-- > 0;) {
      const auto &[cst, u, to] = es[id];
      if (u != to && to != rt) {
        ids[--start[to]] = id;
      }
    }
    // Parallel edges receive identical reductions in every contracted heap.
    // Keep the cheapest one, breaking ties by the original input index.
    // If there are n-1 eligible edges, the precheck guarantees unique rows.
    if (ids.size() > std::size_t(n - 1)) {
      std::vector<int> seen(n, -1);
      std::vector<std::size_t> best(n);
      std::size_t kept = 0;
      for (int v = 0; v < n; ++v) {
        auto l = start[v], r = start[v + 1];
        start[v] = kept;
        for (auto i = l; i < r; ++i) {
          int id = ids[i], u = std::get<1>(es[id]);
          if (seen[u] != v) {
            seen[u] = v;
            best[u] = kept;
            ids[kept++] = id;
          } else if (std::get<0>(es[id]) < std::get<0>(es[ids[best[u]]])) {
            ids[best[u]] = id;
          }
        }
      }
      start[n] = kept;
      ids.resize(kept);
    }
    buf.resize(ids.size());
    std::vector<heap_node *> in(cap, nullptr), roots;
    for (int v = 0; v < n; ++v) {
      roots.clear();
      for (auto i = start[v]; i < start[v + 1]; ++i) {
        int id = ids[i];
        const auto &[cst, u, to] = es[id];
        roots.push_back(make_node(cst, u, id));
      }
      // Pair adjacent heaps in rounds: linear total construction work.
      for (auto r = roots.size(); r > 1;) {
        std::size_t next = 0;
        for (std::size_t i = 0; i < r; i += 2) {
          roots[next++] = i + 1 < r ? meld(roots[i], roots[i + 1]) : roots[i];
        }
        r = next;
      }
      if (!roots.empty()) {
        in[v] = roots[0];
      }
    }

    std::vector<int> dsu(cap);
    std::iota(dsu.begin(), dsu.end(), 0);
    auto fd = [&](int v) {
      int uf = v;
      while (dsu[uf] != uf) {
        uf = dsu[uf];
      }
      while (dsu[v] != v) {
        int nxt = dsu[v];
        dsu[v] = uf;
        v = nxt;
      }
      return uf;
    };

    std::vector<char> st(cap);
    std::vector<edge> sel(cap);
    std::vector<int> cp(cap, -1);
    st[rt] = 2;
    int cnt = n;

    std::vector<int> pth;
    for (int s = 0; s < n; s++) {
      if (st[s] != 0) {
        continue;
      }
      pth.clear();
      pth.push_back(s);
      while (true) {
        int com = pth.back();
        st[com] = 1;

        edge bst;
        int pre = -1;
        while (in[com] != nullptr) {
          edge can = pop(in[com]);
          int from = fd(can.u);
          // Active path entries are already component representatives.
          if (from != com) {
            bst = can;
            pre = from;
            break;
          }
        }
        if (pre == -1) {
          return std::nullopt;
        }
        sel[com] = bst;
        if (st[pre] == 0) {
          pth.push_back(pre);
          continue;
        }
        if (st[pre] == 2) {
          break;
        }

        int ctr = cnt++;
        assert(ctr < cap);
        while (true) {
          int mem = pth.back();
          pth.pop_back();
          in[ctr] = meld(in[ctr], add(in[mem], -sel[mem].cst));
          dsu[mem] = ctr;
          cp[mem] = ctr;
          st[mem] = 2;
          if (mem == pre) {
            break;
          }
        }
        pth.push_back(ctr);
      }
      for (int com : pth) {
        st[com] = 2;
      }
    }

    std::vector<char> ext(cnt);
    ext[rt] = true;
    std::vector<int> eid;
    eid.reserve(n - 1);
    for (int com = cnt - 1; com >= 0; com--) {
      if (ext[com]) {
        continue;
      }
      int id = sel[com].id;
      if (id == -1) {
        return std::nullopt;
      }
      eid.push_back(id);
      int v = std::get<2>(es[id]);
      while (v != -1 && !ext[v]) {
        ext[v] = true;
        v = cp[v];
      }
    }
    if (int(eid.size()) != n - 1) {
      return std::nullopt;
    }
    T cst{};
    for (int id : eid) {
      cst += std::get<0>(es[id]);
    }
    return std::pair<T, std::vector<int>>{cst, std::move(eid)};
  }
};

} // namespace directed_mst_internal

/// @brief Minimum spanning arborescence rooted at rt.
/// Edges are (cost, from, to); loops and edges entering rt are ignored.
/// Parallel edges keep the minimum (cost, original index).
/// Requires 0 < n <= INT_MAX/2, valid vertices, and at most INT_MAX edges.
/// T needs zero/default construction, copying, <, unary -, and += with
/// ordered additive semantics; all intermediate values must be representable.
/// @return (cost, edge indices), or nullopt if some vertex is unreachable.
template <class T>
std::optional<std::pair<T, std::vector<int>>>
directed_mst(int n, int rt, const std::vector<std::tuple<T, int, int>> &es) {
  return directed_mst_internal::solver<T>(n, rt, es).run();
}

} // namespace noya

#endif // NOYA_DIRECTED_MST_HPP
/// @complexity Time: O(n + m + M log(n + 1)), M distinct eligible edge pairs.
/// Space: O(n + m).

#include <cassert>
#include <cstddef>
#include <limits>
#include <numeric>
#include <optional>
#include <tuple>
#include <utility>
#include <vector>

namespace noya {

namespace directed_mst_internal {

template <class T> struct solver {
  struct edge {
    T cst{};
    int u = -1;
    int id = -1;
  };

  struct heap_node {
    heap_node *l = nullptr;
    heap_node *r = nullptr;
    edge val;
    T lz{};
    int rk = 1;
    bool dirty = false;
  };

  int n;
  int rt;
  const std::vector<std::tuple<T, int, int>> &es;
  std::vector<heap_node> buf;
  int ptr = 0;

  solver(int n_, int rt_, const std::vector<std::tuple<T, int, int>> &es_)
      : n(n_), rt(rt_), es(es_) {}

  static int heap_rank(heap_node *nd) { return nd == nullptr ? 0 : nd->rk; }

  static heap_node *add(heap_node *nd, const T &dlt) {
    if (nd != nullptr) {
      nd->val.cst += dlt;
      nd->lz += dlt;
      nd->dirty = true;
    }
    return nd;
  }

  static void push(heap_node *nd) {
    if (nd == nullptr || !nd->dirty) {
      return;
    }
    add(nd->l, nd->lz);
    add(nd->r, nd->lz);
    nd->lz = T{};
    nd->dirty = false;
  }

  static heap_node *meld(heap_node *a, heap_node *b) {
    if (a == nullptr) {
      return b;
    }
    if (b == nullptr) {
      return a;
    }
    if (std::pair(b->val.cst, b->val.id) < std::pair(a->val.cst, a->val.id)) {
      std::swap(a, b);
    }
    push(a);
    a->r = meld(a->r, b);
    if (heap_rank(a->l) < heap_rank(a->r)) {
      std::swap(a->l, a->r);
    }
    a->rk = heap_rank(a->r) + 1;
    return a;
  }

  heap_node *make_node(T cst, int u, int id) {
    heap_node *nd = &buf[ptr++];
    nd->val = {cst, u, id};
    return nd;
  }

  static edge pop(heap_node *&nd) {
    push(nd);
    edge res = nd->val;
    nd = meld(nd->l, nd->r);
    return res;
  }

  std::optional<std::pair<T, std::vector<int>>> run() {
    assert(0 < n && n <= std::numeric_limits<int>::max() / 2);
    assert(0 <= rt && rt < n);
    assert(es.size() <= std::size_t(std::numeric_limits<int>::max()));
    const int cap = 2 * n;
    std::vector<std::size_t> start(std::size_t(n) + 1);
    for (const auto &[cst, u, to] : es) {
      assert(0 <= u && u < n);
      assert(0 <= to && to < n);
      if (u != to && to != rt) {
        ++start[to];
      }
    }
    for (int v = 0; v < n; ++v) {
      if (v != rt && start[v] == 0) {
        return std::nullopt;
      }
    }
    if (n == 1) {
      return std::pair<T, std::vector<int>>{T{}, {}};
    }
    for (int v = 0; v < n; ++v) {
      start[v + 1] += start[v];
    }
    std::vector<int> ids(start.back());
    // Stable incoming-edge rows, without a second array of write cursors.
    for (int id = int(es.size()); id-- > 0;) {
      const auto &[cst, u, to] = es[id];
      if (u != to && to != rt) {
        ids[--start[to]] = id;
      }
    }
    // Parallel edges receive identical reductions in every contracted heap.
    // Keep the cheapest one, breaking ties by the original input index.
    // If there are n-1 eligible edges, the precheck guarantees unique rows.
    if (ids.size() > std::size_t(n - 1)) {
      std::vector<int> seen(n, -1);
      std::vector<std::size_t> best(n);
      std::size_t kept = 0;
      for (int v = 0; v < n; ++v) {
        auto l = start[v], r = start[v + 1];
        start[v] = kept;
        for (auto i = l; i < r; ++i) {
          int id = ids[i], u = std::get<1>(es[id]);
          if (seen[u] != v) {
            seen[u] = v;
            best[u] = kept;
            ids[kept++] = id;
          } else if (std::get<0>(es[id]) < std::get<0>(es[ids[best[u]]])) {
            ids[best[u]] = id;
          }
        }
      }
      start[n] = kept;
      ids.resize(kept);
    }
    buf.resize(ids.size());
    std::vector<heap_node *> in(cap, nullptr), roots;
    for (int v = 0; v < n; ++v) {
      roots.clear();
      for (auto i = start[v]; i < start[v + 1]; ++i) {
        int id = ids[i];
        const auto &[cst, u, to] = es[id];
        roots.push_back(make_node(cst, u, id));
      }
      // Pair adjacent heaps in rounds: linear total construction work.
      for (auto r = roots.size(); r > 1;) {
        std::size_t next = 0;
        for (std::size_t i = 0; i < r; i += 2) {
          roots[next++] = i + 1 < r ? meld(roots[i], roots[i + 1]) : roots[i];
        }
        r = next;
      }
      if (!roots.empty()) {
        in[v] = roots[0];
      }
    }

    std::vector<int> dsu(cap);
    std::iota(dsu.begin(), dsu.end(), 0);
    auto fd = [&](int v) {
      int uf = v;
      while (dsu[uf] != uf) {
        uf = dsu[uf];
      }
      while (dsu[v] != v) {
        int nxt = dsu[v];
        dsu[v] = uf;
        v = nxt;
      }
      return uf;
    };

    std::vector<char> st(cap);
    std::vector<edge> sel(cap);
    std::vector<int> cp(cap, -1);
    st[rt] = 2;
    int cnt = n;

    std::vector<int> pth;
    for (int s = 0; s < n; s++) {
      if (st[s] != 0) {
        continue;
      }
      pth.clear();
      pth.push_back(s);
      while (true) {
        int com = pth.back();
        st[com] = 1;

        edge bst;
        int pre = -1;
        while (in[com] != nullptr) {
          edge can = pop(in[com]);
          int from = fd(can.u);
          // Active path entries are already component representatives.
          if (from != com) {
            bst = can;
            pre = from;
            break;
          }
        }
        if (pre == -1) {
          return std::nullopt;
        }
        sel[com] = bst;
        if (st[pre] == 0) {
          pth.push_back(pre);
          continue;
        }
        if (st[pre] == 2) {
          break;
        }

        int ctr = cnt++;
        assert(ctr < cap);
        while (true) {
          int mem = pth.back();
          pth.pop_back();
          in[ctr] = meld(in[ctr], add(in[mem], -sel[mem].cst));
          dsu[mem] = ctr;
          cp[mem] = ctr;
          st[mem] = 2;
          if (mem == pre) {
            break;
          }
        }
        pth.push_back(ctr);
      }
      for (int com : pth) {
        st[com] = 2;
      }
    }

    std::vector<char> ext(cnt);
    ext[rt] = true;
    std::vector<int> eid;
    eid.reserve(n - 1);
    for (int com = cnt - 1; com >= 0; com--) {
      if (ext[com]) {
        continue;
      }
      int id = sel[com].id;
      if (id == -1) {
        return std::nullopt;
      }
      eid.push_back(id);
      int v = std::get<2>(es[id]);
      while (v != -1 && !ext[v]) {
        ext[v] = true;
        v = cp[v];
      }
    }
    if (int(eid.size()) != n - 1) {
      return std::nullopt;
    }
    T cst{};
    for (int id : eid) {
      cst += std::get<0>(es[id]);
    }
    return std::pair<T, std::vector<int>>{cst, std::move(eid)};
  }
};

} // namespace directed_mst_internal

/// @brief Minimum spanning arborescence rooted at rt.
/// Edges are (cost, from, to); loops and edges entering rt are ignored.
/// Parallel edges keep the minimum (cost, original index).
/// Requires 0 < n <= INT_MAX/2, valid vertices, and at most INT_MAX edges.
/// T needs zero/default construction, copying, <, unary -, and += with
/// ordered additive semantics; all intermediate values must be representable.
/// @return (cost, edge indices), or nullopt if some vertex is unreachable.
template <class T>
std::optional<std::pair<T, std::vector<int>>>
directed_mst(int n, int rt, const std::vector<std::tuple<T, int, int>> &es) {
  return directed_mst_internal::solver<T>(n, rt, es).run();
}

} // namespace noya

 // NOYA_DIRECTED_MST_HPP