最短路径问题选做选讲

最短路径问题选做选讲

一.luogu P4822 冻结

看起来非常可做的样子,但仔细一想,这道题的难点在于我们怎么实现缩短路径后继续维护最短路径。显然,这道题是无法贪心的,一个显然的错误是,考虑将最短路径里经过的较长的路径减半,然而如果对其他路径组合也进行减半完全可能出现比此更优的答案。

观察数据范围,发现堂堂一个最短路问题数据范围居然只有 50 ,暗示我们可能需要维护一个很大的图。

考虑构造一个分层图。

什么是分层图呢?

简单来说,在这道题目中,假定题目给出的图是第一层图,由于我们无法确定到底减半哪条边,所以我们需要给每个节点上都插上一个第一层图上这个节点相连的原边的一半,也就是说,我们把一个二维空间向上扩展,拓展到 k+1 维空间,每层图的每个节点都有在本层图继续跑和升高维度的两个选择。这就是一个很大的图。这个图的节点数是 n * (k + 1) ,边数 (k + 1) * m + k * n (终于有点最短路数据范围的样子了)

然后怎么写呢?这个题就先建个图,然后跑模板的 dij 就可以了。然而这个建图史称建图伟业qwq。

需要注意一点是:你不必使用完所有的 SpellCard。

所以我们对于最后得到的 lc[] 数组还需要进行一个松弛操作,这个松弛针对的是每层图的 lc[n],一个是因为如果直接输出 lc[n * (k + 1)] 等于默认使用了所有的魔法卡片,这显然是与题意相违背的;另一个是你必须要跑到终点 n 才行啊。

所以代码也就呼之欲出了。

Code:

/*
1.建图:先建原层,再建高层

2.跑dij
*/
#include <bits/stdc++.h>
#define MAXN 51357 // 数据范围建议手算一下
#define MAXM 202000 + 5
#define INF 0x3f3f3f3f

using namespace std;

int head[MAXN], cnt, n, m, k, lc[MAXN];
bool vis[MAXN];

struct edge{
    int st, to, next, val;
}edge[4 * MAXM];

inline void add(int u, int v, int w) {
    edge[++cnt].st = u, edge[cnt].to = v, edge[cnt].val = w;
    edge[cnt].next = head[u], head[u] = cnt;
}

struct po{
    int a, b;
};

bool operator < (po x, po y){
	return x.b > y.b;
}

priority_queue <po> q;

void dij() {
    for(int i = 1; i <= n * (k + 1); i++) lc[i] = INF;/* 初始化的时候要记得不是 n 个点 */
    lc[1] = 0;
    q.push((po){1, 0});
    while(!q.empty()) {
        po now = q.top(); q.pop();
        if(vis[now.a]) continue; vis[now.a] = true;
        for(int i = head[now.a]; i; i = edge[i].next) {
            if(lc[edge[i].to] > lc[now.a] + edge[i].val) {
                lc[edge[i].to] = lc[now.a] + edge[i].val;
                q.push((po){edge[i].to, lc[edge[i].to]});
            }
        }
    }
}

int main() {
    scanf("%d%d%d", &n, &m, &k);
    for(int i = 1, u, v, w; i <= m; i++) {
        scanf("%d%d%d", &u, &v, &w);
        add(u, v, w); /* 建本层(第一层)图 */
        add(v, u, w);
        for(int j = 1; j <= k; j++) { /* 把第 j - 1 层图和第 j 层图连接起来 */
            add(u + (j - 1) * n, v + j * n, w / 2);
            add(v + (j - 1) * n, u + j * n, w / 2);
        }
        for(int j = 1; j <= k; j++) { /* 建其他层图 */
            add(j * n + u, j * n + v, w);
            add(j * n + v, j * n + u, w);
        }
    }

    dij();

    int ans = INF; /* 重要的松弛 */
    for(int i = 1; i <= k + 1; i++) {
        ans = min(ans, lc[n * i]);
    } 
    printf("%d", ans);
    return 0;
}

还是很轻松的~


二.luoguP5683 道路拆除

这道题......为什么我第一反应会想到最小生成树?(bushi)

其实依然是最短路。

考虑正难则反

我们不太好直接求最多要拆多少条路,但我们可以求最少要保留多少条路。题目中明确给出边权为1,也就是最短路径的长度就等于这条最短路径上的边数。所以这个问题就转化成了要求 1 号节点到 s1 号节点 + s2 号节点的最短路问题。

难道是节点 1 到节点 s1 的最短路 + 节点 1 到节点 s2 的最短路吗?

显然不是。

我们发现,两个最短路径可能有重合的部分,也就是下面这个简单样例。

道路拆除_P1

当节点 A 与 节点 1 重合时,这幅图就变成了一个“分岔路口”

道路拆除_P2

当想到这一点后,我们就可以开始窃喜了。

为什么呢?因为这是个无向图啊!也就是说 dis[1][s1] = dis[1][i] + dis[i][s1] = dis[1][i] + dis[s1][i]s2 同理。所以我们只需要分别维护出从节点 1 , s1 , s2 出发的每个节点的最短路径,然后先判断是否符合 t1 , t2 的条件,再直接维护出一个最小的 ans 就可以了。

实现方法也很显然。

Code:

#include <bits/stdc++.h>
#define INF 0x3f3f3f3f
#define MAXN 100010
#define gc getchar

using namespace std;

int n, m, s1, t1, s2, t2, ans = INF, lc[MAXN], dis1[MAXN], dis2[MAXN], dis3[MAXN]; // lc[]是个工具人,dis1[]表示从1出发,dis2[]表示从s1出发,dis3[]表示从s2出发
int head[MAXN], cnt;
bool vis[MAXN];

struct edge{
    int st, next, to;
}e[MAXN * 2];

inline int read() {
    int x=0,f=1;char ch=gc();
    while(!isdigit(ch)){if(ch=='-')f=-1;ch=gc();}
    while(isdigit(ch)){x=x*10+ch-'0';ch=gc();}
    return x*f;
}

inline void add(int u, int v) {
    e[++cnt].st = u, e[cnt].to = v;
    e[cnt].next = head[u], head[u] = cnt; 
}

struct po
{
    int a, b;
};

bool operator < (po x, po y) {
    return x.b > y.b;
}

priority_queue <po> q;

void dij(int x) {
    for(int i = 1; i <= n; i++) lc[i] = INF;
    memset(vis, false, sizeof(vis));
    lc[x] = 0;
    q.push((po){x, 0});
    while(!q.empty()) {
        po now = q.top(); q.pop();
        if(vis[now.a]) continue; vis[now.a] = true;
        for(int i = head[now.a]; i; i = e[i].next) {
            if(lc[e[i].to] > lc[now.a] + 1) {
                lc[e[i].to] = lc[now.a] + 1;
                q.push((po){e[i].to, lc[e[i].to]});
            }
        }
    }
}

int main() {
    n = read(), m = read();
    for(int i = 1, x, y; i <= m; i++) {
        x = read(), y = read();
        add(x, y);
        add(y, x);
    }
    s1 = read(), t1 = read(), s2 = read(), t2 = read();
    
	/*分别维护出从节点 1 , s1 , s2 出发的每个节点的最短路径*/
    dij(1);
    for(int i = 1; i <= n; i++) dis1[i] = lc[i]; 
    dij(s1);
    for(int i = 1; i <= n; i++) dis2[i] = lc[i];
    dij(s2);
    for(int i = 1; i <= n; i++) dis3[i] = lc[i];

    for(int i = 1; i <= n; i++) { // 维护出最小的ans
        if(dis1[i] + dis2[i] <= t1 && dis1[i] + dis3[i] <= t2) {
            ans = min(ans, dis1[i] + dis2[i] + dis3[i]);
        }
    }

    if(ans == INF) printf("-1\n");
        else printf("%d", m - ans);
    return 0;
}

三.luoguP1948 Telephone Lines S

大致读了一下题,发现这是个很显然的分层图么。

唯一的不同在于 需要维护出最短路径上的最大值。

所以只需要把松弛稍微修改一下就可以了。

几乎就是板子。

#include <bits/stdc++.h>
#define MAXN 2001000 + 7
#define MAXM 50020000 + 7
#define INF 0x3f3f3f3f

using namespace std;

int n, p, k, lc[MAXN];
int head[MAXN], cnt;
bool vis[MAXN];

struct edge {
    int st, to, w, next;
}e[MAXM];

inline void add(int x, int y, int z) {
    e[++cnt].st = x, e[cnt].to = y, e[cnt].w = z;
    e[cnt].next = head[x], head[x] = cnt;
}

struct po {
    int a, b;
};

bool operator < (const po x, const po y) {
    return x.b > y.b;
}

priority_queue <po> q;

void dij(int s) {
    for(int i = 1; i <= n * (k + 1); i++) lc[i] = INF;
    lc[s] = 0;
    q.push((po){s, 0});
    while(!q.empty()) {
        po now = q.top(); q.pop();
        int u = now.a;
        if(vis[u]) continue; vis[u] = 1;
        for(int i = head[u]; i; i = e[i].next) {
            if(lc[e[i].to] > max(lc[u], e[i].w)) {
                lc[e[i].to] = max(lc[u], e[i].w);
                q.push((po){e[i].to, lc[e[i].to]});
            }
        }
    }
}

int main() {
    scanf("%d%d%d", &n, &p, &k);
    for(int i = 1, u, v, w; i <= p; i++) {
        scanf("%d%d%d", &u, &v, &w);
        add(u, v, w);
        add(v, u, w);
        for(int j = 1; j <= k; j++) {
            add(u + (j - 1) * n, v + j * n, 0);
            add(v + (j - 1) * n, u + j * n, 0);
        }
        for(int j = 1; j <= k; j++) {
            add(u + j * n, v + j * n, w);
            add(v + j * n, u + j * n, w);
        }
    }
    
    dij(1);
    if(lc[n * (k + 1)] == INF) cout << "-1" << endl;
        else cout << lc[n*(k+1)] << endl;

    return 0;
}

四.luogu P4568 飞行路线

有一说一这也是个板子题,没啥意思。

但是这道题的数据范围是挺奇怪的,我WA了好几次都是WA在数据范围上,不知道为什么。

#include <bits/stdc++.h>
#define MAXN 410000 + 5
#define MAXM 3100000 + 5
#define INF 0x3f3f3f3f

using namespace std;

int n, m, k, s, t, lc[MAXN], ans = INF;
int head[MAXN], cnt;
bool vis[MAXN];

struct edge{
    int st, to, w, next;
}e[MAXM];

struct po{
    int a, b;
};

bool operator < (po x, po y){
	return x.b > y.b;
}

priority_queue <po> q;

inline void add(int x, int y, int z) {
    e[++cnt].st = x, e[cnt].to = y, e[cnt].w = z;
    e[cnt].next = head[x], head[x] = cnt;
}

void dij(int s) {
    for(int i = 1; i <= n * (k + 1); i++) lc[i] = INF;//*
    lc[s] = 0;
    q.push((po){s, 0});
    while(!q.empty()) {
        po now = q.top(); q.pop();
        if(vis[now.a]) continue; vis[now.a] = true;
        for(int i = head[now.a]; i; i = e[i].next) {
            if(lc[e[i].to] > lc[now.a] + e[i].w) {
                lc[e[i].to] = lc[now.a] + e[i].w;
                q.push((po){e[i].to, lc[e[i].to]});
            }
        }
    }
}

int main() {
    scanf("%d%d%d", &n, &m, &k);
    scanf("%d%d", &s, &t);
    s++, t++;
    for(int i = 1, u, v, w; i <= m; i++) {
        scanf("%d%d%d", &u, &v, &w);
        u++, v++;
        add(u, v, w);
        add(v, u, w);
        for(int j = 1; j <= k; j++) {
            add(u + (j - 1) * n, v + j * n, 0);
            add(v + (j - 1) * n, u + j * n, 0);
        }
        for(int j = 1; j <= k + 1; j++) {
            add(u + j * n, v + j * n, w);
            add(v + j * n, u + j * n, w);
        }
    }
    //for(int i=1;i<=k;i++)add(t+(i-1)*n,t+i*n,0);
    dij(s);
    for(int i = 1; i <= k + 1; i++) {
        ans = min(ans, lc[(i - 1) * n + t]);
    }

    printf("%d", ans);

    //printf("%d\n",lc[t+n*k]);
    return 0;
}

这道题数组开多大我有点不太会算,按照我算的答案会RE两个点,挺奇怪的。

P1875 佳佳的魔法药水

题目大意:

给定 n 个药水,每个药水有一个初始权值 c[i],有若干条关系例如:用一份 A 药水 + 一份 B 药水 = 一份 C 药水。
现给出以上,求 0 号药水的最小权值为多少。

输入格式:
第一行 n
第二行 0 ~ n-1 表示 c[i]
接下来若干行,每行三个数字A B C,表示 A + B -> C

可以发现这是一个图论题,考察最短路,但是略有不同的是这道题给出了点权而非边权,并且没有一个给定的起点。

但是最短路 \(dij\)思想不变。

思路:

对于普通的最短路问题,一般会给出边权而非点权,并且给出点数 \(n\) 和边数 \(m\) ,所以在使用堆优化的迪杰斯特拉算法处理这类问题时,一般开两个结构体,分别为 \(edge\) (存边) 和 \(po\) (存点),需要维护一个链式前向星,点的编号以及该点距离起点的最短距离即 \(lowcost\)

对于这个不普通的最短路问题,我们依然可以考虑维护一个链式前向星来存图,但是不再存储边权,而是存储两个起点。在 A + B = C 这个关系里,A 、 B 相当于是两个起点共同指向 C。由于依然要使用堆优化,所以结构体 \(po\) 存储信息不变。

不同的是,需要再用一个结构体 \(Node\) 来记录每个节点的 \(cost\)\(ans\) 其中 \(ans\) 为方案数。

以上部分用代码实现可写为:

struct Edge {
    int A, B, C;
    int next;
}e[MAXM];

struct Node {
    int ans; // 方案数
    int cost; // 最小花费
    Node () { ans = 1; cost = 0; }
}node[MAXN];

struct po {
    int a, b; // 点的编号,点的最短距离(即最小花费)。
};

然后需要注意的是,这道题目有以下三种关系:

A + B = C
B + A = C
A + A = C

这分别启示我们这是无向图以及存图时防止自环的出现。

具体的dij函数怎么写呢?由于没有起点,所以在输入每个节点的点权时就可以直接把这些点和它们的权值压入优先队列里,然后每次取出队列的最小值(重载运算符实现),然后按照题目的要求更新每个节点的 \(cost\)\(ans\) 即可。

更为具体的,需要注意更新的条件:即在更新 C 药水的方案数时,要确定已经更新过了 A 药水以及 B 药水的最小花费。更为具体的,

\[\begin{cases} node[C].ans = node[A].ans * node[B].ans, node[C].cost = node[A].cost + node[B].cost & \text {if node[C].cost > node[A].cost + node[B].cost} \\ node[c].ans += node[A].ans * node[B].ans & \text{if node[C].cost = node[A].cost + node[B]} \end{cases} \]

可以用乘法原理解释以上公式。(略长......

\(code\) :

#include <bits/stdc++.h>
#define MAXN 1005
#define MAXM 1000007
 
using namespace std;

int n, cnt, head[MAXN];
bool vis[MAXN];

struct Edge {
	int A, B, C;
	int next;
	Edge () { A = 0, B = 0, C = 0; next = 0; }
}e[MAXM];

struct po {
	int a, b;
};

struct Node {
	int cost, ans;
	Node () { cost = 0; ans = 1; }
}node[MAXN];

void inline add(int x, int y, int z) {
	e[++cnt].A = x, e[cnt].B = y, e[cnt].C = z;
	e[cnt].next = head[x], head[x] = cnt;
}

bool operator < (const po x, const po y) {
	return x.b > y.b;
}

priority_queue <po> q;

void dij() {
	memset(vis, false, sizeof(vis));
	
	while(!q.empty()) {
		po now = q.top(); q.pop();
		int u = now.a, w = now.b; 
		if(vis[u]) continue; vis[u] = true;
		for(int i = head[u]; i; i = e[i].next) {
			int tmp = e[i].B, v = e[i].C;
			if(vis[tmp]) { // 注意 此时 u 一定是访问过的,所以只要判断 tmp 是否是访问过的即可
				if(node[v].cost > node[tmp].cost + w) {
					node[v].cost = node[tmp].cost + w;
					node[v].ans = node[tmp].ans * node[u].ans;
					q.push((po){v, node[v].cost});
				} else if(node[v].cost == node[tmp].cost + w) {
					node[v].ans += node[tmp].ans * node[u].ans;
				}				
			}

		}
	}
	return;
}

int main() {
	int A = 0, B = 0, C = 0;
	
	scanf("%d", &n);
	for(int i = 0; i < n; ++i) {
		scanf("%d", &node[i].cost);
		q.push((po){i, node[i].cost});
	}
	
	while(scanf("%d%d%d", &A, &B, &C) != EOF) {
		add(A, B, C);
		if(A == B) continue;
		add(B, A, C); 
	}
	
	dij();
	
	printf("%d %d\n", node[0].cost, node[0].ans);
	return 0;
}
posted @ 2021-08-01 09:34  许江一墨  阅读(105)  评论(0编辑  收藏  举报