分层图最短路学习笔记

题单

引入

分层图最短路指的是类似于在一张图上可以进行若干次决策(决策不一定只有一种),每次决策会对原图产生影响不可逆,再求最短路的问题。
对于 \(n\) 个点,\(m\) 条边,可以进行 \(k\) 次决策的一张图,对于它跑分层图最短路的复杂度是 \(O(mk \log(nk))\)。这是一个非常优秀的复杂度。

如何实现 \(k\) 次决策

最短路是很简单的,分层图最短路的考点就在于建模和处理 \(k\) 次决策。每一题的建模都会有所不同,这里不予以讨论。

常规做法

对于这 \(k\) 次决策,它每次都会对原图产生影响,并且决策是不可逆的,那么我们就可以考虑建出一个分层图,每次决策我们进入下一层图,并且无法再返回,即建一条有向边,由当前图连向下一层图,代表一次决策,如何连边由决策改变的内容而定。
现在我们建出了一张分层图,那么在上面跑一遍 Dijkstraspfa (视情况而定)求出最短/长路即可得到进行任意次决策的最短/长路,最终的答案由题目求解内容而定。
由此我们便解决了这道分层图问题。

另一个做法

我们可以在跑最短路时增加一维状态 \(k'\),表示当前进行了 \(k'\) 个决策,如果当前这条边仍可以进行一次决策,那么我们就考虑进行这一次决策得到新的状态。再考虑当前边不进行决策的新状态进行转移。
这样做更好想,并且可以减少相当一部分的空间开销。

不限定决策次数

有些题目不会去限定决策次数,例如例题三。但是这种题目会在一定程度上限制总状态的个数,那么这样我们的分层图最短路还是能用的。思路差不多,都是建新图或加状态维度。这样的时间复杂度仍然是得到保证的。

几道例题

Luogu P4568 [JLOI2011] 飞行路线

简要题意

给出一张 \(n\) 个点,\(m\) 条边,可以进行 \(k\) 次决策的一张无向图。定义一次决策为将一条边的代价赋为 \(0\) 给出 \(s,t\)\(s\)\(t\) 的最短路。
\(2 \le n \le 10^4,1 \le m \le 5 \times 10^4,0 \le k \le 10\),节点从 \(0\) 开始编号。

算法分析

属于分层图最短路板子。根据题意不难建模出一道分层图问题。打板子就行。

code

#include <iostream>
#include <cstring>
#include <queue>

using namespace std;

template<class T> inline void read(T &n) {
	n = 0;
	bool _f = 0; char _c = getchar();
	while(_c < '0' || '9' < _c) {if(_c == '-') _f = 1; _c = getchar();}
	while('0' <= _c && _c <= '9') n = n * 10 + _c - '0', _c = getchar();
	if(_f) n = -n;
}

const int N = 1e4 + 5, M = 5e4 + 5;

int n, m, k, s, t;
int h[N], to[M << 1], nxt[M << 1], w[M << 1], tot;

inline void add(int u, int v, int val) {
	nxt[++tot] = h[u], to[h[u] = tot] = v, w[tot] = val;
	nxt[++tot] = h[v], to[h[v] = tot] = u, w[tot] = val;
}

struct NODE {
	int u, k, dis;
	NODE(int u = 0, int k = 0, int dis = 0):
	u(u), k(k), dis(dis){}
	bool operator < (const NODE &b) const {
		return dis > b.dis;
	}
};
int dis[N][11];
priority_queue<NODE> q;
bool used[N][11];


inline void dijkstra() {
	memset(dis, 0x3f, sizeof dis);
	dis[s][0] = 0;
	q.push(NODE(s, 0, 0));
	while(!q.empty()) {
		NODE now = q.top();
		q.pop();
		int u = now.u;
		if(used[u][now.k]) continue;
		used[u][now.k] = 1;
		for(int i = h[u]; i; i = nxt[i]) {
			int v = to[i];
			if(dis[v][now.k + 1] > dis[u][now.k] && now.k + 1 <= k) {
				dis[v][now.k + 1] = dis[u][now.k];
				q.push(NODE(v, now.k + 1, dis[v][now.k + 1]));
			}
			if(dis[v][now.k] > dis[u][now.k] + w[i]) {
				dis[v][now.k] = dis[u][now.k] + w[i];
				q.push(NODE(v, now.k, dis[v][now.k]));
			}
		}
	}
}

int main() {
	read(n), read(m), read(k);
	read(s), read(t);
	s++;t++;
	for(int i = 1; i <= m; i++) {
		int _x, _y, _c;
		read(_x), read(_y), read(_c);
		_x++;_y++;
		add(_x, _y, _c);
	}

	dijkstra();

	int res = 0x7fffffff;
	for(int i = 0; i <= k; i++) {
		res = min(res, dis[t][i]);
	}
	printf("%d\n", res);
}

吸氧后只要 \(98ms\) \kk。

双倍经验

Lougu P3119 [USACO15JAN]Grass Cownoisseur G

简要题意

给出一张 \(n\) 个点,\(m\) 条边,可以进行 \(1\) 次决策的一张有向图。定义一次决策为反向走一条边,求 \(1\)\(1\) 最多可以经过多少个不同的点。
\(1 \le n \le 10^5,1 \le m \le 10^5\),节点从 \(1\) 开始编号。

算法分析

首先,我们考虑到一个点只能贡献一次,并且同一个强连通分量里的点可以在不进行决策的情况下互相到达。那么自然可以想到对原图进行缩点,缩完点后建出一张新图,在这张新图中的边反向走才有意义,并且可以证明,那一次反向走的边不会是已经走过的边,那么我们就只要计算整个路径上的点权和就可以求解答案,读者可以自行证明,在本题代码后会给出简单证明。

由此,我们只要在新的图上跑分层图最短路即可。

数据太水了,这数据范围也能让 spfa 草过去

code

#include <iostream>
#include <queue>

using namespace std;

#define PIB pair<int, bool>
#define MP make_pair
#define fi first
#define se second

template<class T> inline void read(T &n) {
	n = 0;
	bool _f = 0; char _c = getchar();
	while(_c < '0' || '9' < _c) {if(_c == '-') _f = 1; _c = getchar();}
	while('0' <= _c && _c <= '9') n = n * 10 + _c - '0', _c = getchar();
	if(_f) n = -n;
}

const int N = 1e5 + 5, M = 1e5 + 5;

int n, m;
int h[N], to[N], nxt[N], tot;
vector<PIB> g[N];

inline void add(int u, int v) {
	nxt[++tot] = h[u], to[h[u] = tot] = v;
}

int dfn[N], low[N], stk[N], bel[N], siz[N << 1], top, cnt, idx;
bool vis[N];

void tarjan(int u) {
	dfn[u] = low[u] = ++cnt;
	stk[++top] = u;
	vis[u] = 1;

	for(int i = h[u]; i; i = nxt[i]) {
		int v = to[i];

		if(!dfn[v]) {
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}
		else if(vis[v]) low[u] = min(low[u], dfn[v]);
	}

	if(dfn[u] == low[u]) {
		idx++;
		while(stk[top] != u) {
			bel[stk[top]] = idx;
			vis[stk[top]] = 0;
			siz[idx]++;
			top--;
		}
		bel[stk[top]] = idx;
		vis[stk[top]] = 0;
		siz[idx]++;
		top--;
	}
}
void newgragh() {
	for(int u = 1; u <= n; u++) {
		for(int i = h[u]; i; i = nxt[i]) {
			int v = to[i];
			if(bel[u] == bel[v]) continue;
			g[bel[u]].push_back(MP(bel[v], 0));
			g[bel[v]].push_back(MP(bel[u], 1));
		}
	}
}

struct NODE {
	int u, k, dis;
	bool operator < (const NODE &b) const {
		return dis < b.dis;
	}
	NODE(int u, int k, int dis):
	u(u), k(k), dis(dis){}
};

int dis[N][2];
bool used[N][2];
priority_queue<NODE> q;

void spfa(int s) {
	used[s][0] = 1;
	q.push(NODE(s, 0, 0));
	while(!q.empty()) {
		NODE now = q.top();
		q.pop();
		int u = now.u;
		used[u][now.k] = 0;
		for(int i = 0; i < g[u].size(); i++) {
			PIB nextt = g[u][i];
			int v = nextt.fi, rev = nextt.se;
			if(rev && now.k) continue;
			if(!rev) {
				if(dis[v][now.k] < dis[u][now.k] + siz[u]) {
					dis[v][now.k] = dis[u][now.k] + siz[u];
					if(!used[v][now.k]) {
						q.push(NODE(v, now.k, dis[v][now.k]));
						used[v][now.k] = 1;
					}
				}
			}
			else {
				if(dis[v][1] < dis[u][0] + siz[u]) {
					dis[v][1] = dis[u][0] + siz[u];
					if(!used[v][1]) {
						q.push(NODE(v, 1, dis[v][1]));
						used[v][1] = 1;
					}
				}
			}
		}
	}
}

int main() {
	read(n), read(m);
	for(int i = 1; i <= m; i++) {
		int _x, _y;
		read(_x), read(_y);
		add(_x, _y);
	}

	for(int i = 1; i <= n; i++) {
		if(!dfn[i]) {
			tarjan(i);
		}
	}

	if(idx == 1) {
		printf("%d\n", n);
		return 0;
	}

	newgragh();
	spfa(bel[1]);

	printf("%d\n", dis[bel[1]][1]);
}

正确性

我们考虑,如果反向走了一条走过的边后会发生什么。首先,我们会到达一个到达过的点,当前我们已经失去了决策的资格,也就是说如果当前状态可以到达 \(1\) 更新答案,那么当前节点会有一条到达 \(1\) 的路径,但是从 \(1\) 出发是可以到达当前点的。这就意味着出现了一个。还记得我们在跑分层图最短路之前做了什么吗,没错,是缩点。缩点后原图成为一个 DAG,不可能出现环。所以不可能会出现反向走一条走过的边的情况。那么之前的结论成立。

Luogu P4009 汽车加油行驶问题

简要题意

一张 \(n \times n\) 的网格图,给出 \(k,a,b,c\),每个点有一个标记,在标记为 \(0\) 的点必须花费 \(a\) 的代价加满油,在标记为 \(0\) 的点可以花费 \(c+a\) 的代价加满油,向上或向左移动将花费 \(b\) 的代价。每走一步将花费 \(1\) 滴油,每次加油将油加到 \(k\),当没有油时不能移动。求只能上下左右移动从 \((1,1)\)\((n,n)\) 的最小代价。
\(2 \le n \le 100, 2 \le k \le 10\)

算法分析

本题没有明确地指出决策以及决策次数上限。但是有一个地方可以注意到:\(k\) 的值域非常小。并没有指出决策次数,但是由于 \(k\) 的限制,我们可以想到总状态数是非常小的。
我们将题目中的加油看做一次决策,在当前点建立并加满油也看做一次决策,那么这就是一个多种决策的分层图最短路问题。
对于这种问题,我们考虑建模去建一张新图是很麻烦的,那么我们在跑最短路时增加一维这样一个思路的优势便显现出来了。我们考虑设 \(dis_{x,y,oil}\) 为从 \((1,1)\) 出发,到达 \((x,y)\) 点且剩下了 \(oil\) 滴油的最短路。对于每个点,我们考虑向上下左右走,更新其他点,然后到达了标记为 \(1\) 的点时分类讨论,将油加上。然后在到达某个点后,如果在当前点建立一个加油站并加油的代价加上当前状态的代价小于当前点满油状态的代价,那么就可以更新当前点满油状态的代价。
这样我们便将题面中的几种操作全都放到最短路的转移中去了。
鉴于数据范围,我们可以直接 spfa 或是 Dijkstra。
笔者在这里给出 Dijkstra 的解法,spfa 差不多。
这一题和分层图其实没有多大的关系了,但是可以用来拓宽思维,知道分层图不一定会限定决策次数。

code

#include <iostream>
#include <cstring>
#include <queue>

using namespace std;

#define INF 0x7fffffff

template<class T> inline void read(T &n) {
    n = 0;
    bool _f = 0; char _c = getchar();
    while(_c < '0' || '9' < _c) {if(_c == '-') _f = 1; _c = getchar();}
    while('0' <= _c && _c <= '9') n = n * 10 + _c - '0', _c = getchar();
    if(_f) n = -n;
}

const int N = 105, NN = 1e4 + 5, K = 15;
const int dy[] = {1, -1, 0, 0}, dx[] = {0, 0, 1, -1};

int n, k, a, b, c;
bool petrol[N][N];

struct NODE {
	int x, y, oil, dis;
	bool operator < (const NODE &b) const {
		return dis > b.dis;
	}
	NODE (int x = 0, int y = 0, int oil = 0, int dis = 0):
	x(x), y(y), oil(oil), dis(dis){}
};

priority_queue<NODE> q;
int dis[N][N][K];
bool vis[N][N][K];

void dijkstra() {
	memset(dis, 0x3f, sizeof dis);
	q.push(NODE(1, 1, k, 0));
	dis[1][1][k] = 0;
	while(!q.empty()) {
		NODE now = q.top();
		q.pop();
		int x = now.x, y = now.y, oil = now.oil;
		
		if(vis[x][y][oil]) continue;
		vis[x][y][oil] = 1;
		
		if(dis[x][y][k] > dis[x][y][oil] + a + c) {
			dis[x][y][k] = dis[x][y][oil] + a + c;
			q.push(NODE(x, y, k, dis[x][y][k]));
		}
		
		for(int i = 0; i < 4; i++) {
			int vx = x + dx[i], vy = y + dy[i];
			if(vx < 0 || n < vx || vy < 0 || n < vy) continue;
			int w = ((vx < x || vy < y) ? b : 0);
			
			if(petrol[vx][vy]) {
				if(oil > 0 && dis[vx][vy][k] > dis[x][y][oil] + w + a) {
					dis[vx][vy][k] = dis[x][y][oil] + w + a;
					q.push(NODE(vx, vy, k, dis[vx][vy][k]));
				}
			}
			else {
				if(oil > 0 && dis[vx][vy][oil - 1] > dis[x][y][oil] + w) {
					dis[vx][vy][oil - 1] = dis[x][y][oil] + w;
					q.push(NODE(vx, vy, oil - 1, dis[vx][vy][oil - 1]));
				}
			}
		}
	}
}

int main() {
	read(n), read(k), read(a), read(b), read(c);
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= n; j++) {
			read(petrol[i][j]);
		}
	}
	
	dijkstra();
	
	int ans = INF;
	for(int i = 0; i <= k; i++) {
		ans = min(ans, dis[n][n][i]);
	}
	printf("%d\n", ans);
}
posted @ 2023-05-19 14:16  JR_ytxy  阅读(143)  评论(0)    收藏  举报