可持久化可并堆优化k短路

问题

对于带权有向图,定义路径的长度为经过的边的权值之和。两条路径不同当且仅当经过边的顺序不同。给一个带权有向图 G G G 以及起点和终点 s , t s,t s,t,求 G G G 中 s s s 到 t t t 权值前 k k k 小的路径。

朴素做法

问题转化

定义 d i s x dis_x disx​ 表示从 x x x 到 t t t 的最短路长度,权值为 w w w 的边 e : u → v e:u\to v e:u→v 的花费 δ ( e ) = d i s v − d i s u + w \delta(e)=dis_v-dis_u+w δ(e)=disv​−disu​+w

可以理解成若从 u u u 走向 t t t 时,沿着边 e e e 而不是最短路径上的边走,至少需要多走 δ ( e ) \delta(e) δ(e) 的距离。

先求出 G G G 中从 t t t 出发沿着反向边走的最短路树,那么一条从 s s s 到 t t t 的路径 P P P 是由若干条树边和非树边组成。 P P P 的长度可以表示为 l e n ( p ) = d i s s + ∑ e ∈ P δ ( e ) len(p)=dis_s+\sum_{e\in P}\delta(e) len(p)=diss​+e∈P∑​δ(e)

显然树边 e T e_T eT​ 满足 δ ( e T ) = 0 \delta(e_T)=0 δ(eT​)=0。若令 P ′ P' P′ 为 P P P 中的非树边构成的列表,则 l e n ( P ) = d i s s + ∑ e ′ ∈ P ′ δ ( e ′ ) len(P)=dis_s+\sum_{e'\in P'}\delta(e') len(P)=diss​+e′∈P′∑​δ(e′)

满足以下条件的所有列表 P ′ P' P′,与所有从 s s s 到 t t t 的路径 P P P 构成一一对应关系:

  • P ′ P' P′ 中相邻的两条边 a → b , c → d a\to b,c\to d a→b,c→d,满足 b = c b=c b=c 或者 c c c 是 b b b 的祖先。

那么求长度前 k k k 小的路径 P P P,等价于求边权和前 k k k 小的合法列表 P ′ P' P′。

求解

令 E ( x ) E(x) E(x) 表示 x x x 以及所有 x x x 的祖先的出边中的非树边构成的集合,考虑以下算法:

  • 用堆来维护合法的列表 P ′ P' P′。一开始堆中唯一的 P ′ P' P′ 为空。
  • 每次取出边权和最小的 P ′ P' P′,设 P ′ P' P′ 中的最后一条边为 u → v u\to v u→v。
  • 对于 E ( v ) E(v) E(v) 中的每一条边 e e e,将 P ′ ∪ e P'\cup e P′∪e 加入堆中。

这样可以保证每一个合法的 P ′ P' P′ 都恰好被生成一次。显然这是个正确的算法,但最坏情况下需要把所有合法的 P ′ P' P′ 都生成出来,这是无法接受的。

如果每次只取出 E ( v ) E(v) E(v) 中 δ ( e ) \delta(e) δ(e) 最小的一条边,并在更新堆的步骤中加入替换操作:

  • 设 P ′ P' P′ 中的最后一条边为 e e e,倒数第二条边为 u → v u\to v u→v,将 e e e 替换成 E ( v ) E(v) E(v) 中的下一条边(假设 E ( v ) E(v) E(v) 中的边按 δ ( e ) \delta(e) δ(e) 从小到大排序)。

则每一个合法的 P ′ P' P′ 同样都是恰好被生成一次,且每取出一个 P ′ P' P′,只会新生成 O ( 1 ) O(1) O(1) 个新的 P ′ P' P′。

可持久化可并堆优化

在上述做法中,如果用可持久化可并堆来维护 E ( v ) E(v) E(v),则建堆的总复杂度为 O ( m log ⁡ m ) O(m\log m) O(mlogm)。求解前 k k k 小的 P ′ P' P′ 的时间复杂度为 O ( k log ⁡ k ) O(k\log k) O(klogk)。那么总的复杂度为 O ( m log ⁡ m + k log ⁡ k ) O(m\log m+k\log k) O(mlogm+klogk)。

在实现的时候有一些小技巧:在堆中对每个 P ′ P' P′ 维护一个二元组。第一维表示 P ′ P' P′ 中的 δ ( e ) \delta(e) δ(e) 之和,第二维表示 P ′ P' P′ 中的最后一条边在可并堆中对应的节点编号。在替换操作中,将第二维分别换成原来节点的两个儿子,并修改第一维的值。加入操作中,设 P ′ P' P′ 中最后一条边为 u → v u\to v u→v,将第二维换成 E ( v ) E(v) E(v) 的根节点,并修改第一维的值。那么每次堆中最多新增三个元素。

例题

洛谷 P2483 【模板】k短路 / [SDOI2010]魔法猪学院

#include<bits/stdc++.h>
using namespace std;

typedef pair<double, int> pi;

const int N = 5005;
const int M = 200005;
const double eps = 1e-8;
const double inf = 1e15;

int n, m, sz, cnt, last[N], rev_last[N], rt[N], fa[N];
double W, dis[N];
bool vis[N];
vector<int> dfn;
struct edge{int from, to, next; double w; bool is_tree_edge;}e[M * 2];
struct tree{int l, r, h, val; double key;}t[M * 10];

void addedge(int u, int v, double w)
{
	e[++cnt].from = u; e[cnt].to = v; e[cnt].w = w; e[cnt].next = last[u]; last[u] = cnt;
	e[++cnt].from = v; e[cnt].to = u; e[cnt].w = w; e[cnt].next = rev_last[v]; rev_last[v] = cnt;
}

int newnode(double key, int val)
{
	sz++; t[sz].key = key; t[sz].val = val; t[sz].h = 1;
	return sz;
}

int merge(int x, int y)
{
	if (!x || !y) return x + y;
	if (t[x].key > t[y].key) swap(x, y);
	int z = ++sz; t[z] = t[x];
	t[z].r = merge(t[z].r, y);
	if (t[t[z].l].h < t[t[z].r].h) swap(t[z].l, t[z].r);
	t[z].h = t[t[z].r].h + 1;
	return z;
}

void dijkstra()
{
	priority_queue<pi, vector<pi>, greater<pi> > que;
	for (int i = 1; i <= n; i++) dis[i] = inf;
	dis[n] = 0; que.push(make_pair(0, n));
	while (!que.empty())
	{
		int x = que.top().second; que.pop();
		while (!que.empty() && vis[x]) x = que.top().second, que.pop();
		if (vis[x]) break;
		vis[x] = 1;
		for (int i = rev_last[x]; i; i = e[i].next)
			if (dis[x] + e[i].w < dis[e[i].to])
			{
				dis[e[i].to] = dis[x] + e[i].w;
				que.push(make_pair(dis[e[i].to], e[i].to));
			}
	}
}

void build_SPT(int x)
{
	vis[x] = 1; dfn.push_back(x);
	for (int i = rev_last[x]; i; i = e[i].next)
		if (!vis[e[i].to] && fabs(dis[x] + e[i].w - dis[e[i].to]) < eps)
		{
			fa[e[i].to] = x;
			build_SPT(e[i].to);
			e[i].is_tree_edge = e[i ^ 1].is_tree_edge = 1;
		}
}

void build_heap()
{
	for (int i = 2; i <= cnt; i += 2)
	{
		int x = e[i].from, y = e[i].to;
		if (!e[i].is_tree_edge && vis[x] && vis[y])
			rt[x] = merge(rt[x], newnode(dis[y] - dis[x] + e[i].w, y));
	}
	for (int i = 1; i < dfn.size(); i++)
		rt[dfn[i]] = merge(rt[dfn[i]], rt[fa[dfn[i]]]);
}

int solve()
{
	priority_queue<pi, vector<pi>, greater<pi> > que;
	int ans = 0;
	if (dis[1] < W + eps) W -= dis[1], ans++;
	if (rt[1]) que.push(make_pair(t[rt[1]].key, rt[1]));
	while (!que.empty())
	{
		double w = que.top().first; int x = que.top().second; que.pop();
		if (w + dis[1] > W) break;
		W -= w + dis[1]; ans++;
		if (t[x].l) que.push(make_pair(t[t[x].l].key + w - t[x].key, t[x].l));
		if (t[x].r) que.push(make_pair(t[t[x].r].key + w - t[x].key, t[x].r));
		if (rt[t[x].val]) que.push(make_pair(t[rt[t[x].val]].key + w, rt[t[x].val]));
	}
	return ans;
}

int main()
{
	scanf("%d%d%lf", &n, &m, &W);
	cnt = 1;
	for (int i = 1; i <= m; i++)
	{
		int x, y; double w; scanf("%d%d%lf", &x, &y, &w);
		if (x != n) addedge(x, y, w);
	}
	dijkstra();
	memset(vis, 0, sizeof(vis));
	build_SPT(n);
	build_heap();
	printf("%d\n", solve());
	return 0;
}
上一篇:leetcode116. 填充每个节点的下一个右侧节点指针


下一篇:k8s