Skip to content

k_shortest_walk.hpp

SECTIONGraph INCLUDEnoya/graph/k_shortest_walk.hpp

按长度顺序求源点到终点的前 \(k\) 条游走,允许重复经过顶点和边。

Complexity: Time: \(O((V + E) \log (E + 2) + k \log (k + 2))\). Space: \(O(V + E + V \log (E + 2) + k)\).

AC 记录:k_shortest_walk。

跳到代码 · GitHub ↗

Implementation

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

/// @complexity Time: O((V + E) log(E + 2) + k log(k + 2)).
/// Space: O(V + E + V log(E + 2) + k).

#include <algorithm>
#include <cassert>
#include <functional>
#include <limits>
#include <queue>
#include <tuple>
#include <utility>
#include <vector>

namespace noya {
namespace k_shortest_walk_internal {

template <class Weight> struct persistent_leftist_heap {
  struct node {
    Weight val{};
    int to1 = -1, l = -1, r = -1, rk = 1;
  };
  std::vector<node> nd;
  int node_rank(int id) const { return id == -1 ? 0 : nd[id].rk; }
  int append(node x) {
    assert(nd.size() < std::size_t(std::numeric_limits<int>::max()));
    nd.push_back(x);
    return int(nd.size()) - 1;
  }
  int meld(int a, int b) {
    if (a == -1 || b == -1) return a == -1 ? b : a;
    if (nd[b].val < nd[a].val) std::swap(a, b);
    node x = nd[a];
    x.r = meld(x.r, b);
    if (node_rank(x.l) < node_rank(x.r)) std::swap(x.l, x.r);
    x.rk = node_rank(x.r) + 1;
    return append(x);
  }
  // Newly appended nodes have no external references. Heapify them in O(d);
  // a complete binary heap is leftist, so it can join the persistent heap.
  int build(int first) {
    std::size_t count = nd.size() - first;
    if (count == 0) return -1;
    std::make_heap(nd.begin() + first, nd.end(),
                   [](const node &a, const node &b) { return b.val < a.val; });
    for (std::size_t i = count; i-- > 0;) {
      auto &x = nd[first + i];
      std::size_t l = 2 * i + 1, r = l + 1;
      x.l = l < count ? int(first + l) : -1;
      x.r = r < count ? int(first + r) : -1;
      x.rk = node_rank(x.r) + 1;
    }
    return first;
  }
};

} // namespace k_shortest_walk_internal

/// @brief First cnt representable lengths of directed walks from s to t,
/// in nondecreasing order. Edges and vertices may repeat; parallel edges
/// define distinct walks. The empty walk is included when s == t.
/// Weights are nonnegative and arithmetic must be exact, as for integers.
/// Lengths beyond numeric_limits<Weight>::max() are omitted. If fewer than
/// cnt representable walks exist, return all of them. Input is unchanged.
/// Edge count and the persistent heap's total node count must fit int.
template <class Weight>
std::vector<Weight>
k_shortest_walk_lengths(int n,
                        const std::vector<std::tuple<int, int, Weight>> &es,
                        int s, int t, int cnt) {
  assert(n >= 0 && 0 <= s && s < n && 0 <= t && t < n && cnt >= 0);
  if (cnt == 0) return {};
  assert(es.size() <= std::size_t(std::numeric_limits<int>::max()));
  int m = int(es.size());
  std::vector<int> head(std::size_t(n) + 1), ids(m);
  for (auto [u, v, w] : es) {
    assert(0 <= u && u < n && 0 <= v && v < n && w >= Weight{});
    ++head[v + 1];
  }
  for (int u = 0; u < n; ++u) head[u + 1] += head[u];
  auto cursor = head;
  for (int id = 0; id < m; ++id) ids[cursor[std::get<1>(es[id])]++] = id;

  const Weight maximum = std::numeric_limits<Weight>::max();
  std::vector<Weight> dis(n, maximum);
  std::vector<char> reached(n);
  std::vector<int> pe(n, -1), parent(n, -1), order;
  using state = std::pair<Weight, int>;
  std::priority_queue<state, std::vector<state>, std::greater<state>> queue;
  reached[t] = 1;
  dis[t] = Weight{};
  queue.emplace(Weight{}, t);
  while (!queue.empty()) {
    auto [du, u] = queue.top();
    queue.pop();
    if (du != dis[u]) continue;
    if (cnt == 1 && u == s) return {du};
    // A chosen tree parent was settled before its child, even on zero edges.
    order.push_back(u);
    for (int j = head[u]; j < head[u + 1]; ++j) {
      int id = ids[j];
      auto [v, to, w] = es[id];
      if (w > maximum - du) continue;
      Weight candidate = du + w;
      if (!reached[v] || candidate < dis[v]) {
        reached[v] = 1;
        dis[v] = candidate;
        parent[v] = u;
        pe[v] = id;
        queue.emplace(candidate, v);
      }
    }
  }
  if (!reached[s]) return {};

  // Reverse adjacency is no longer needed; reuse its buffers for outgoing IDs.
  std::fill(head.begin(), head.end(), 0);
  for (auto [u, v, w] : es) ++head[u + 1];
  for (int u = 0; u < n; ++u) head[u + 1] += head[u];
  cursor = head;
  for (int id = 0; id < m; ++id) ids[cursor[std::get<0>(es[id])]++] = id;

  k_shortest_walk_internal::persistent_leftist_heap<Weight> heap;
  std::vector<int> roots(n, -1);
  for (int u : order) {
    int first = int(heap.nd.size());
    for (int j = head[u]; j < head[u + 1]; ++j) {
      int id = ids[j];
      auto [from, v, w] = es[id];
      if (id == pe[u] || !reached[v]) continue;
      Weight sidetrack;
      // Evaluate w + dis[v] - dis[u] without overflowing a cancelling sum.
      if (dis[v] >= dis[u]) {
        Weight gap = dis[v] - dis[u];
        if (w > maximum - gap) continue;
        sidetrack = w + gap;
      } else {
        sidetrack = w - (dis[u] - dis[v]);
      }
      heap.append({sidetrack, v});
    }
    int local = heap.build(first);
    int inherited = parent[u] == -1 ? -1 : roots[parent[u]];
    roots[u] = heap.meld(inherited, local);
  }

  // A walk is its shortest base path plus an ordered sequence of sidetracks.
  std::priority_queue<state, std::vector<state>, std::greater<state>> candidates;
  candidates.emplace(dis[s], -1);
  std::vector<Weight> result;
  auto push = [&](Weight total, Weight delta, int node) {
    if (delta <= maximum - total) candidates.emplace(total + delta, node);
  };
  while (!candidates.empty()) {
    auto [total, id] = candidates.top();
    candidates.pop();
    result.push_back(total);
    if (int(result.size()) == cnt) break;
    int next;
    if (id == -1) {
      next = roots[s];
    } else {
      const auto &node = heap.nd[id];
      for (int child : {node.l, node.r}) if (child != -1)
        push(total, heap.nd[child].val - node.val, child);
      next = roots[node.to1];
    }
    if (next != -1) push(total, heap.nd[next].val, next);
  }
  return result;
}

} // namespace noya

 // NOYA_K_SHORTEST_WALK_HPP
#ifndef NOYA_K_SHORTEST_WALK_HPP
#define NOYA_K_SHORTEST_WALK_HPP 1

/// @complexity Time: O((V + E) log(E + 2) + k log(k + 2)).
/// Space: O(V + E + V log(E + 2) + k).

#include <algorithm>
#include <cassert>
#include <functional>
#include <limits>
#include <queue>
#include <tuple>
#include <utility>
#include <vector>

namespace noya {
namespace k_shortest_walk_internal {

template <class Weight> struct persistent_leftist_heap {
  struct node {
    Weight val{};
    int to1 = -1, l = -1, r = -1, rk = 1;
  };
  std::vector<node> nd;
  int node_rank(int id) const { return id == -1 ? 0 : nd[id].rk; }
  int append(node x) {
    assert(nd.size() < std::size_t(std::numeric_limits<int>::max()));
    nd.push_back(x);
    return int(nd.size()) - 1;
  }
  int meld(int a, int b) {
    if (a == -1 || b == -1) return a == -1 ? b : a;
    if (nd[b].val < nd[a].val) std::swap(a, b);
    node x = nd[a];
    x.r = meld(x.r, b);
    if (node_rank(x.l) < node_rank(x.r)) std::swap(x.l, x.r);
    x.rk = node_rank(x.r) + 1;
    return append(x);
  }
  // Newly appended nodes have no external references. Heapify them in O(d);
  // a complete binary heap is leftist, so it can join the persistent heap.
  int build(int first) {
    std::size_t count = nd.size() - first;
    if (count == 0) return -1;
    std::make_heap(nd.begin() + first, nd.end(),
                   [](const node &a, const node &b) { return b.val < a.val; });
    for (std::size_t i = count; i-- > 0;) {
      auto &x = nd[first + i];
      std::size_t l = 2 * i + 1, r = l + 1;
      x.l = l < count ? int(first + l) : -1;
      x.r = r < count ? int(first + r) : -1;
      x.rk = node_rank(x.r) + 1;
    }
    return first;
  }
};

} // namespace k_shortest_walk_internal

/// @brief First cnt representable lengths of directed walks from s to t,
/// in nondecreasing order. Edges and vertices may repeat; parallel edges
/// define distinct walks. The empty walk is included when s == t.
/// Weights are nonnegative and arithmetic must be exact, as for integers.
/// Lengths beyond numeric_limits<Weight>::max() are omitted. If fewer than
/// cnt representable walks exist, return all of them. Input is unchanged.
/// Edge count and the persistent heap's total node count must fit int.
template <class Weight>
std::vector<Weight>
k_shortest_walk_lengths(int n,
                        const std::vector<std::tuple<int, int, Weight>> &es,
                        int s, int t, int cnt) {
  assert(n >= 0 && 0 <= s && s < n && 0 <= t && t < n && cnt >= 0);
  if (cnt == 0) return {};
  assert(es.size() <= std::size_t(std::numeric_limits<int>::max()));
  int m = int(es.size());
  std::vector<int> head(std::size_t(n) + 1), ids(m);
  for (auto [u, v, w] : es) {
    assert(0 <= u && u < n && 0 <= v && v < n && w >= Weight{});
    ++head[v + 1];
  }
  for (int u = 0; u < n; ++u) head[u + 1] += head[u];
  auto cursor = head;
  for (int id = 0; id < m; ++id) ids[cursor[std::get<1>(es[id])]++] = id;

  const Weight maximum = std::numeric_limits<Weight>::max();
  std::vector<Weight> dis(n, maximum);
  std::vector<char> reached(n);
  std::vector<int> pe(n, -1), parent(n, -1), order;
  using state = std::pair<Weight, int>;
  std::priority_queue<state, std::vector<state>, std::greater<state>> queue;
  reached[t] = 1;
  dis[t] = Weight{};
  queue.emplace(Weight{}, t);
  while (!queue.empty()) {
    auto [du, u] = queue.top();
    queue.pop();
    if (du != dis[u]) continue;
    if (cnt == 1 && u == s) return {du};
    // A chosen tree parent was settled before its child, even on zero edges.
    order.push_back(u);
    for (int j = head[u]; j < head[u + 1]; ++j) {
      int id = ids[j];
      auto [v, to, w] = es[id];
      if (w > maximum - du) continue;
      Weight candidate = du + w;
      if (!reached[v] || candidate < dis[v]) {
        reached[v] = 1;
        dis[v] = candidate;
        parent[v] = u;
        pe[v] = id;
        queue.emplace(candidate, v);
      }
    }
  }
  if (!reached[s]) return {};

  // Reverse adjacency is no longer needed; reuse its buffers for outgoing IDs.
  std::fill(head.begin(), head.end(), 0);
  for (auto [u, v, w] : es) ++head[u + 1];
  for (int u = 0; u < n; ++u) head[u + 1] += head[u];
  cursor = head;
  for (int id = 0; id < m; ++id) ids[cursor[std::get<0>(es[id])]++] = id;

  k_shortest_walk_internal::persistent_leftist_heap<Weight> heap;
  std::vector<int> roots(n, -1);
  for (int u : order) {
    int first = int(heap.nd.size());
    for (int j = head[u]; j < head[u + 1]; ++j) {
      int id = ids[j];
      auto [from, v, w] = es[id];
      if (id == pe[u] || !reached[v]) continue;
      Weight sidetrack;
      // Evaluate w + dis[v] - dis[u] without overflowing a cancelling sum.
      if (dis[v] >= dis[u]) {
        Weight gap = dis[v] - dis[u];
        if (w > maximum - gap) continue;
        sidetrack = w + gap;
      } else {
        sidetrack = w - (dis[u] - dis[v]);
      }
      heap.append({sidetrack, v});
    }
    int local = heap.build(first);
    int inherited = parent[u] == -1 ? -1 : roots[parent[u]];
    roots[u] = heap.meld(inherited, local);
  }

  // A walk is its shortest base path plus an ordered sequence of sidetracks.
  std::priority_queue<state, std::vector<state>, std::greater<state>> candidates;
  candidates.emplace(dis[s], -1);
  std::vector<Weight> result;
  auto push = [&](Weight total, Weight delta, int node) {
    if (delta <= maximum - total) candidates.emplace(total + delta, node);
  };
  while (!candidates.empty()) {
    auto [total, id] = candidates.top();
    candidates.pop();
    result.push_back(total);
    if (int(result.size()) == cnt) break;
    int next;
    if (id == -1) {
      next = roots[s];
    } else {
      const auto &node = heap.nd[id];
      for (int child : {node.l, node.r}) if (child != -1)
        push(total, heap.nd[child].val - node.val, child);
      next = roots[node.to1];
    }
    if (next != -1) push(total, heap.nd[next].val, next);
  }
  return result;
}

} // namespace noya

#endif // NOYA_K_SHORTEST_WALK_HPP
/// @complexity Time: O((V + E) log(E + 2) + k log(k + 2)).
/// Space: O(V + E + V log(E + 2) + k).

#include <algorithm>
#include <cassert>
#include <functional>
#include <limits>
#include <queue>
#include <tuple>
#include <utility>
#include <vector>

namespace noya {
namespace k_shortest_walk_internal {

template <class Weight> struct persistent_leftist_heap {
  struct node {
    Weight val{};
    int to1 = -1, l = -1, r = -1, rk = 1;
  };
  std::vector<node> nd;
  int node_rank(int id) const { return id == -1 ? 0 : nd[id].rk; }
  int append(node x) {
    assert(nd.size() < std::size_t(std::numeric_limits<int>::max()));
    nd.push_back(x);
    return int(nd.size()) - 1;
  }
  int meld(int a, int b) {
    if (a == -1 || b == -1) return a == -1 ? b : a;
    if (nd[b].val < nd[a].val) std::swap(a, b);
    node x = nd[a];
    x.r = meld(x.r, b);
    if (node_rank(x.l) < node_rank(x.r)) std::swap(x.l, x.r);
    x.rk = node_rank(x.r) + 1;
    return append(x);
  }
  // Newly appended nodes have no external references. Heapify them in O(d);
  // a complete binary heap is leftist, so it can join the persistent heap.
  int build(int first) {
    std::size_t count = nd.size() - first;
    if (count == 0) return -1;
    std::make_heap(nd.begin() + first, nd.end(),
                   [](const node &a, const node &b) { return b.val < a.val; });
    for (std::size_t i = count; i-- > 0;) {
      auto &x = nd[first + i];
      std::size_t l = 2 * i + 1, r = l + 1;
      x.l = l < count ? int(first + l) : -1;
      x.r = r < count ? int(first + r) : -1;
      x.rk = node_rank(x.r) + 1;
    }
    return first;
  }
};

} // namespace k_shortest_walk_internal

/// @brief First cnt representable lengths of directed walks from s to t,
/// in nondecreasing order. Edges and vertices may repeat; parallel edges
/// define distinct walks. The empty walk is included when s == t.
/// Weights are nonnegative and arithmetic must be exact, as for integers.
/// Lengths beyond numeric_limits<Weight>::max() are omitted. If fewer than
/// cnt representable walks exist, return all of them. Input is unchanged.
/// Edge count and the persistent heap's total node count must fit int.
template <class Weight>
std::vector<Weight>
k_shortest_walk_lengths(int n,
                        const std::vector<std::tuple<int, int, Weight>> &es,
                        int s, int t, int cnt) {
  assert(n >= 0 && 0 <= s && s < n && 0 <= t && t < n && cnt >= 0);
  if (cnt == 0) return {};
  assert(es.size() <= std::size_t(std::numeric_limits<int>::max()));
  int m = int(es.size());
  std::vector<int> head(std::size_t(n) + 1), ids(m);
  for (auto [u, v, w] : es) {
    assert(0 <= u && u < n && 0 <= v && v < n && w >= Weight{});
    ++head[v + 1];
  }
  for (int u = 0; u < n; ++u) head[u + 1] += head[u];
  auto cursor = head;
  for (int id = 0; id < m; ++id) ids[cursor[std::get<1>(es[id])]++] = id;

  const Weight maximum = std::numeric_limits<Weight>::max();
  std::vector<Weight> dis(n, maximum);
  std::vector<char> reached(n);
  std::vector<int> pe(n, -1), parent(n, -1), order;
  using state = std::pair<Weight, int>;
  std::priority_queue<state, std::vector<state>, std::greater<state>> queue;
  reached[t] = 1;
  dis[t] = Weight{};
  queue.emplace(Weight{}, t);
  while (!queue.empty()) {
    auto [du, u] = queue.top();
    queue.pop();
    if (du != dis[u]) continue;
    if (cnt == 1 && u == s) return {du};
    // A chosen tree parent was settled before its child, even on zero edges.
    order.push_back(u);
    for (int j = head[u]; j < head[u + 1]; ++j) {
      int id = ids[j];
      auto [v, to, w] = es[id];
      if (w > maximum - du) continue;
      Weight candidate = du + w;
      if (!reached[v] || candidate < dis[v]) {
        reached[v] = 1;
        dis[v] = candidate;
        parent[v] = u;
        pe[v] = id;
        queue.emplace(candidate, v);
      }
    }
  }
  if (!reached[s]) return {};

  // Reverse adjacency is no longer needed; reuse its buffers for outgoing IDs.
  std::fill(head.begin(), head.end(), 0);
  for (auto [u, v, w] : es) ++head[u + 1];
  for (int u = 0; u < n; ++u) head[u + 1] += head[u];
  cursor = head;
  for (int id = 0; id < m; ++id) ids[cursor[std::get<0>(es[id])]++] = id;

  k_shortest_walk_internal::persistent_leftist_heap<Weight> heap;
  std::vector<int> roots(n, -1);
  for (int u : order) {
    int first = int(heap.nd.size());
    for (int j = head[u]; j < head[u + 1]; ++j) {
      int id = ids[j];
      auto [from, v, w] = es[id];
      if (id == pe[u] || !reached[v]) continue;
      Weight sidetrack;
      // Evaluate w + dis[v] - dis[u] without overflowing a cancelling sum.
      if (dis[v] >= dis[u]) {
        Weight gap = dis[v] - dis[u];
        if (w > maximum - gap) continue;
        sidetrack = w + gap;
      } else {
        sidetrack = w - (dis[u] - dis[v]);
      }
      heap.append({sidetrack, v});
    }
    int local = heap.build(first);
    int inherited = parent[u] == -1 ? -1 : roots[parent[u]];
    roots[u] = heap.meld(inherited, local);
  }

  // A walk is its shortest base path plus an ordered sequence of sidetracks.
  std::priority_queue<state, std::vector<state>, std::greater<state>> candidates;
  candidates.emplace(dis[s], -1);
  std::vector<Weight> result;
  auto push = [&](Weight total, Weight delta, int node) {
    if (delta <= maximum - total) candidates.emplace(total + delta, node);
  };
  while (!candidates.empty()) {
    auto [total, id] = candidates.top();
    candidates.pop();
    result.push_back(total);
    if (int(result.size()) == cnt) break;
    int next;
    if (id == -1) {
      next = roots[s];
    } else {
      const auto &node = heap.nd[id];
      for (int child : {node.l, node.r}) if (child != -1)
        push(total, heap.nd[child].val - node.val, child);
      next = roots[node.to1];
    }
    if (next != -1) push(total, heap.nd[next].val, next);
  }
  return result;
}

} // namespace noya

 // NOYA_K_SHORTEST_WALK_HPP