单源最短路-施工完毕
单源最短路建图,应用,扩展。
重新给图论提高课做一个总结。
建图方式
对于一个含有个点,条边的无向图,边权都是正值,求解起点到终点的最短距离。
根据的数据范围选择邻接表或者邻接矩阵直接建图跑最短路就行,属于裸的板子题,难点在于如何抽象出图论模型来改板子吧。
多做多见多积累吧,灵光一现或许就是平时的日积月累的结果。
例题
题目:热浪
地址:https://www.acwing.com/problem/content/1131/
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 2510, M = 2*6500+10;
int h[N],e[M],ne[M],w[M],dis[N],q[N];
int n,m,S,T,idx;
bool st[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void spfa() {
memset(dis, 0x3f, sizeof dis);
dis[S] = 0;
st[S] = true;
int hh = 0, tt = 1;
q[0] = S;
while( hh != tt ) {
int t = q[hh++];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (dis[j] > dis[t] + w[i]) {
dis[j] = dis[t] + w[i];
if( !st[j] ) {
q[tt++] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
}
int main(void) {
cin >> n >> m >> S >> T;
memset(h, -1, sizeof h);
for (int i = 0; i < m; ++i ) {
int a,b,c;
cin >> a >> b >> c;
add(a,b,c), add(b,a,c);
}
spfa();
cout << dis[T] << endl;
return 0;
}
题目:信使
地址:https://www.acwing.com/problem/content/1130/
求从起点出发,到所有点的最短距离的集合中的最大值,最远的都到了,其他点肯定早或同时收到信号。
本题求的是最长路,把代码的比较符号改改就行,数据量比较小。
Code
#include <bits/stdc++.h>
using namespace std;
int g[200][200];
int n, m;
int main() {
scanf("%d%d",&n,&m);
memset(g,0x3f,sizeof g);
for (int i = 1; i <= n; i++) g[i][i] = 0;
for (int i = 1; i <= m; i++) {
int a, b, c;
scanf("%d%d%d",&a,&b,&c);
g[a][b] = g[b][a] = min(g[a][b], c);
}
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
g[i][j] = min(g[i][j], g[i][k] + g[k][j]);
}
}
}
int res = -1;
for (int i = 1; i <= n; i++) {
if (g[1][i] == 0x3f3f3f3f) {
res = -1;
break;
} else {
res = max(res, g[1][i]);
}
}
printf("%d\n",res);
return 0;
}
题目:香甜的黄油
地址:https://www.acwing.com/problem/content/1129/
有个点,条边,边权都是正值。
求给定起点下,到达所有牧场的最短距离。
spfa可以解决,但是关于spfa它已经死了,所以保险起见我们最好跑堆优化的dijksra算法。
本题枚举所有点为起点,找一下该点到达所有点的最小值即可,数据范围较小。
Code
#include <bits/stdc++.h>
using namespace std;
#define debug freopen("in.in","r",stdin);freopen("out.out","w",stdout);
#define IO ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
#define rep(i, l, r) for(int i = (l); i <= (r); i++)
#define per(i, r, l) for(int i = (r); i >= (l); i--)
#define pb push_back
#define SZ(x) ((int)(x).size())
#define fi first
#define se second
#define all(x) (x).begin(), (x).end()
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
const int inf = 123456789;
int n, p, c;
int dist[803], id[803];
bool vis[803];
vector<PII> e[1000];
int spfa(int start) {
memset(dist,0x3f,sizeof dist);
memset(vis,false,sizeof vis);
queue<int> q;
q.push(start);
dist[start] = 0;
vis[start] = 0;
while (q.size()) {
int now = q.front();
q.pop();
vis[now] = false;
for (auto ver : e[now]) {
if (dist[ver.fi] > dist[now] + ver.se) {
dist[ver.fi] = dist[now] + ver.se;
if (!vis[ver.fi]) {
vis[ver.fi] = true;
q.push(ver.fi);
}
}
}
}
int sum = 0;
for (int i = 1; i <= n; i++) {
if (dist[id[i]] == 0x3f3f3f3f) return 0x3f3f3f3f;
else sum += dist[id[i]];
}
return sum;
}
int main(void) {
scanf("%d%d%d",&n,&p,&c);
for (int i = 1; i <= n; i++) {
scanf("%d",&id[i]);
}
for (int i = 1; i <= c; i++) {
int u, v, w;
scanf("%d%d%d",&u,&v,&w);
e[u].pb({v,w});
e[v].pb({u,w});
}
int ans = inf;
for (int i = 1; i <= p; i++) {
ans = min(ans, spfa(i));
}
printf("%d\n",ans);
return 0;
}
题目:最小花费
地址:https://www.acwing.com/problem/content/1128/
,求X的最小值,即求后面一连串的最大值。
类比成求最长路,跑一个图论板子即可。
Code
#include <bits/stdc++.h>
using namespace std;
#define debug freopen("in.in","r",stdin);freopen("out.out","w",stdout);
#define IO ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
#define rep(i, l, r) for(int i = (l); i <= (r); i++)
#define per(i, r, l) for(int i = (r); i >= (l); i--)
#define pb push_back
#define SZ(X) ((int)(x).size())
#define fi first
#define se second
#define all(x) (x).begin(), (x).end()
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
const int inf = 123456789;
int n, m, s, e;
double dist[503];
bool vis[503];
double g[503][503];
void dijskra() {
dist[s] = 1;
for (int i = 1; i <= n; i++) {
int ver = -1;
for (int j = 1; j <= n; j++) {
if (!vis[j] && (ver == -1 || dist[ver] > dist[j])) {
ver = j;
}
}
vis[ver] = true;
for (int j = 1; j <= n; j++) {
dist[j] = max(dist[j], dist[ver] * g[ver][j]);
}
}
}
int main(void) {
scanf("%d%d",&n,&m);
for (int i = 1; i <= m; i++) {
int a, b, c;
scanf("%d%d%d",&a,&b,&c);
double w = (100.0-c)/100;
g[a][b] = g[b][a] = max(g[a][b],w);
}
scanf("%d%d",&s,&e);
dijskra();
printf("%.8lf\n",100/dist[e]);
return 0;
}
题目:最优乘车
地址:https://www.acwing.com/problem/content/922/
题意:
某个旅客想去公园游玩,但是他目前所在的地点没有一辆直达的巴士,他可能需要换乘几次后才能到达公园,现在给出所有巴士站编号和所有巴士路线,询问旅客到达公园的最少换乘次数。
思路:
换乘几次 = 乘车几次-1
可以看出,一条巴士路线上的所有巴士站,都可以沿着路线一直往后走,只算一次乘车,即答案+1。
那么我们可以建立一个权值都是1的图来进行求解,在这里如果我们直接依据题意建图,即一条线路上两两站点连线来求这个答案,显然是非常麻烦的,比如一个站点有多条线路时,你到下一站是属于换乘还是一条线路的判断,非常麻烦。
所以我们可以换一种灵活的建图方法,我们将一条线路上前面车站到后边可达车站建上一条边权为1的边即可,然后直接跑BFS即可。
#include <bits/stdc++.h>
using namespace std;
int n, m;
int stop[1500], dist[503];
bool g[503][503];
void bfs() {
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
while (q.size()) {
int v = q.front();
q.pop();
for (int i = 1; i <= n; i++) {
if (g[v][i] && dist[i] > dist[v] + 1) {
dist[i] = dist[v] + 1;
q.push(i);
}
}
}
}
int main() {
cin >> m >> n;
string line;
getline(cin,line);
while (m--) {
getline(cin,line);
stringstream ssin(line);
int cnt = 0, p;
while (ssin >> p) { stop[cnt++] = p; }
for (int j = 0; j < cnt; j++) {
for (int k = j + 1; k < cnt; k++) {
g[stop[j]][stop[k]] = true;
}
}
}
bfs();
if (dist[n] == 0x3f3f3f3f) {
cout << "NO" << endl;
} else {
cout << dist[n]-1 << endl;
}
return 0;
}
扩展
例题
题目:选择最佳线路
地址:https://www.acwing.com/problem/content/1139/
题意:
n个点m条边的有向图,不知道起点S,但知道终点E,求解从每个起点出发到达任意终点的所有路线的最短距离。
分析:
-
算法1
人为确定一个虚拟源点,并且虚拟源点练到所有起点一条边权为0的边,即原问题转化为从虚拟源点出发到达终点E的最短路。
-
算法2
反向建图,把终点置为起点,求终点开始到所有起点的单源最短路,再在之中取个最小值。
Code
感觉用SPFA的话,可以把所有起点压入队里跑。
#include <bits/stdc++.h>
using namespace std;
#define debug freopen("in.in","r",stdin);freopen("out.out","w",stdout);
#define IO ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
#define rep(i, l, r) for(int i = (l); i <= (r); i++)
#define per(i, r, l) for(int i = (r); i >= (l); i--)
#define pb push_back
#define SZ(X) ((int)(x).size())
#define fi first
#define se second
#define all(x) (x).begin(), (x).end()
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
const int inf = 0x3f3f3f3f;
const int N = 10010, M = 200010;
int n, m, s, idx;
int h[N], ne[M], e[M], w[M];
int dist[N];
bool vis[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void spfa() {
memset(dist,0x3f,sizeof dist);
queue<int> q;
q.push(0);
vis[0] = true;
dist[0] = 0;
while (q.size()) {
int u = q.front();
q.pop();
vis[u] = false;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[u] + w[i]) {
dist[j] = dist[u] + w[i];
if (!vis[j]) {
vis[j] = true;
q.push(j);
}
}
}
}
}
int main(void) {
IO
while (cin >> n >> m >> s) {
memset(h,-1,sizeof h);
idx = 0;
rep(i,1,m) {
int a, b, c;
cin >> a >> b >> c;
add(a,b,c);
}
int T;
cin >> T;
while (T--) {
int ver;
cin >> ver;
add(0,ver,0);
}
spfa();
if (dist[s] == inf) {
cout << "-1\n";
} else {
cout << dist[s] << '\n';
}
}
return 0;
}
题目:拯救大兵瑞恩
地址:https://www.acwing.com/problem/content/1133/
题意:
巴拉巴拉巴拉太长了自己看。
分析:
简易版本:一个迷宫,从起点S走到终点E的最短路长度,每个格子可上下左右四个方向扩展,格子的属性有两种,连通和不连通(有障碍物),需要记录的状态就是当前坐标以及到达当前坐标的长度,可扩展坐标和可扩展坐标最短路长度。
到本题这里,格子的属性更复杂了,可以直达的格子里分为有钥匙和没钥匙两种,拿去钥匙不计时间,有障碍物的格子分为不可达的障碍物,或者需要某一类钥匙才可以通过的门。
那么我们需要记录的状态也就更多了,除了上述状态,还需要记录此时持有钥匙的状态,即我们需要维护一个三维状态,来表明在这个坐标时的持有钥匙状态。
接下来我们考虑走到下一个格子后,如何进行状态处理。本题因为可以走回头路,不存在拓扑序,存在环形路径的可能,所以用DP无法计算状态转移,需要使用最短路算法。
我们定义当前点为,状态为。
- 如果存在钥匙,我们一定会拿起来,因为拿钥匙不消耗时间(等于边权为0),多多益善。那么会得到状态,比较当前状态的最短路长度,如果,则不进行任何处理,新状态更优,且已在队伍里。反之将更新为目前状态的最短路径长度,将其压入队头。
- 没有钥匙,就照常往四个方向进行扩展延伸,将新的可以到达状态压入队列尾部,等待下一次扩展。
本题空间为64MB,直接开三维数组存状态会MLE,所以我们将状态压缩一下,将压缩成一个独立的点来表示,每类钥匙可以用二进制来表示,拿起钥匙就是|一下,将对应位置1即可,判断有无钥匙则将状态右移对应位看是不是1就可以。
建图细节看代码,这里不再赘述。
Code
#include <bits/stdc++.h>
using namespace std;
#define debug freopen("in.in","r",stdin);freopen("out.out","w",stdout);
#define IO ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
#define rep(i, l, r) for(int i = (l); i <= (r); i++)
#define per(i, r, l) for(int i = (r); i >= (l); i--)
#define pb push_back
#define SZ(X) ((int)(x).size())
#define fi first
#define se second
#define all(x) (x).begin(), (x).end()
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
const int inf = 123456789;
const int N = 11, M = 400, P = 1 << 10;
int n, m, k, p;
int h[N * N], e[M], w[M], ne[M], idx;
int g[N][N], key[N * N];
int dist[N*N][P];
bool vis[N * N][P];
set<PII> Edge;
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void build() {
int dx[] = {-1,0,1,0}, dy[] = {0,1,0,-1};
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
for (int u = 0; u < 4; u++) {
int x = i + dx[u], y = j + dy[u];
if (!x || x > n || !y || y > m) continue;
int a = g[i][j], b = g[x][y];
if (!Edge.count({a,b})) {
add(a,b,0);
}
}
}
}
}
int bfs() {
memset(dist,0x3f,sizeof dist);
dist[1][0] = 0;
deque<PII> q;
q.push_back({1,0});
while (q.size()) {
PII u = q.front();
q.pop_front();
if (vis[u.fi][u.se]) continue;
vis[u.fi][u.se] = true;
if (u.fi == n * m) {
return dist[u.fi][u.se];
}
if (key[u.fi]) {
int state = u.se | key[u.fi];
if (dist[u.fi][state] > dist[u.fi][u.se]) {
dist[u.fi][state] = dist[u.fi][u.se];
q.push_front({u.fi, state});
}
}
for (int i = h[u.fi]; i != -1; i = ne[i]) {
int j = e[i];
if (w[i] && !(u.se >> w[i]-1 & 1)) continue;
if (dist[j][u.se] > dist[u.fi][u.se] + 1) {
dist[j][u.se] = dist[u.fi][u.se] + 1;
q.push_back({j,u.se});
}
}
}
return -1;
}
int main(void) {
IO
cin >> n >> m >> p >> k;
for (int i = 1, t = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
g[i][j] = t++;
}
}
memset(h,-1,sizeof h);
while (k--) {
int x1, y1, x2, y2, c;
cin >> x1 >> y1 >> x2 >> y2 >> c;
int a = g[x1][y1], b = g[x2][y2];
Edge.insert({a,b}), Edge.insert({b,a});
if (c) {
add(a,b,c), add(b,a,c);
}
}
build();
int s;
cin >> s;
while (s--) {
int x, y, c;
cin >> x >> y >> c;
key[g[x][y]] |= 1 << c-1;
}
cout << bfs() << '\n';
return 0;
}
题目:最短路计数
地址:https://www.acwing.com/problem/content/1136/
题意:
给出一个 N 个顶点 M 条边的无向无权图,顶点编号为 1到 N。
问从顶点 1 开始,到其他每个点的最短路有几条。
分析:
先前条件:最短路中不存在值为0的环,否则可以一直在环里跑,无解。
先前概念:最短路径树指的是以某个节点为根,根节点到其他点距离最小形成的一棵树。
从每个点的前一个路径考虑,看看该点时从哪个路径转移过来的,累加上来就好,但是我们需要先弄出来一个最短路径树(拓扑图)。对于转移过来的边,我们根据长度为两种。
- 长度比当前大许多,即沿着这条路线伸展出来的路才是我们需要的最短路。
- 长度差1,说明是我们当前节点延伸出去且是当前长度为当前最短路,直接累加上去。
对于最短路径树,我们可以通过BFS算法或者dijkstra算法实现。
- BFS每次扩展一层,最后一定能够得到拓扑图。
- dijkstra每次出队距离起点最近的点,如果知道用的是哪条边,连起来就是一颗最短路树。
Code
#include <bits/stdc++.h>
using namespace std;
#define debug freopen("in.in","r",stdin);freopen("out.out","w",stdout);
#define IO ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
#define rep(i, l, r) for(int i = (l); i <= (r); i++)
#define per(i, r, l) for(int i = (r); i >= (l); i--)
#define pb push_back
#define SZ(X) ((int)(x).size())
#define fi first
#define se second
#define all(x) (x).begin(), (x).end()
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
const int inf = 123456789;
const int N = 100010, M = 400010;
const int mod = 100003;
int n, m;
int h[N], e[M], ne[M], q[M];
int dist[N], cnt[N];
int idx;
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void bfs() {
memset(dist,0x3f,sizeof dist);
dist[1] = 0, cnt[1] = 1;
int front = 0, rear = 0;
q[0] = 1;
while (front <= rear) {
int t = q[front++];
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[t] + 1) {
dist[j] = dist[t] + 1;
cnt[j] = cnt[t];
q[++rear] = j;
} else if (dist[j] == dist[t] + 1) {
cnt[j] = (cnt[j] + cnt[t]) % mod;
}
}
}
}
int main(void) {
IO
cin >> n >> m;
memset(h,-1,sizeof h);
while (m--) {
int a, b;
cin >> a >> b;
add(a,b),add(b,a);
}
bfs();
for (int i = 1; i <= n; i++) {
cout << cnt[i] << "\n";
}
return 0;
}
题目:观光
地址:https://www.acwing.com/problem/content/385/
题意:
求次短路长度数目。
分析:
和上一题差不多,只不过状态转移麻烦很多。
- 当前最短路变次短路,最短路得到更优结果,双双入队。
- 找到一条新的最短路,次数更新。
- 找到一条更优次短路,更新当前次短路状态,入队。
- 找到一条新的次短路,次数更新。
Code
#include <bits/stdc++.h>
using namespace std;
#define debug freopen("in.in","r",stdin);freopen("out.out","w",stdout);
#define IO ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
#define rep(i, l, r) for(int i = (l); i <= (r); i++)
#define per(i, r, l) for(int i = (r); i >= (l); i--)
#define pb push_back
#define SZ(X) ((int)(x).size())
#define fi first
#define se second
#define all(x) (x).begin(), (x).end()
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
const int inf = 123456789;
const int N = 1010, M = 100010;
int n, m, S, E, idx;
int h[N],ne[M],e[M],w[M];
int dist[N][2], cnt[N][2];
bool vis[N][2];
struct Node {
int id, type, dist;
bool operator > (const Node& tmp) const {
return dist > tmp.dist;
}
};
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
int dij() {
memset(vis,false,sizeof vis);
memset(dist, 0x3f, sizeof dist);
memset(cnt, 0, sizeof cnt);
dist[S][0] = 0, cnt[S][0] = 1;
priority_queue<Node, vector<Node>, greater<Node>> heap;
heap.push({S,0,0});
while (heap.size()) {
Node u = heap.top();
heap.pop();
int ver = u.id, type = u.type, distance = u.dist, count = cnt[ver][type];
if (vis[ver][type]) continue;
vis[ver][type] = true;
for (int i = h[ver]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j][0] > distance + w[i]) {
dist[j][1] = dist[j][0];
cnt[j][1] = cnt[j][0];
heap.push({j,1,dist[j][1]});
dist[j][0] = distance + w[i];
cnt[j][0] = count;
heap.push({j,0,dist[j][0]});
} else if (dist[j][0] == distance + w[i]) {
cnt[j][0] += count;
} else if (dist[j][1] > distance + w[i]) {
dist[j][1] = distance + w[i];
cnt[j][1] = count;
heap.push({j,1,dist[j][1]});
} else if (dist[j][1] == distance + w[i]) {
cnt[j][1] += count;
}
}
}
int res = cnt[E][0];
if (dist[E][1] - 1 == dist[E][0]) {
res += cnt[E][1];
}
return res;
}
int main(void) {
IO
int T;
cin >> T;
while (T--) {
cin >> n >> m;
memset(h,-1,sizeof h);
idx = 0;
while (m--) {
int a,b,c;
cin >> a >> b >> c;
add(a,b,c);
}
cin >> S >> E;
cout << dij() << '\n';
}
return 0;
}
综合应用
跳脱出一个题考单一知识点的局限。
例题
题目:新年好
地址:https://www.acwing.com/problem/content/1137/
题意:
个点,条边的无向图,求解从起点出发到达其余个点所需要的最短路的最小值。
分析
我们可以预处理出来以第号点作为起点时候的最短路情况。
然后枚举所有的拜访路线,即种拜访途径,取其中的最小值即为答案。
本题是单源最短路结合dfs求解问题。
Code
#include <bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
const int N = 50010, M = 2e5 + 10, inf = 0x3f3f3f3f;
int h[N], ne[M], e[M], w[M], idx;
int n, m;
int source[6], dist[6][N];
bool st[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void dijskra(int start, int dist[]) {
fill(dist,dist+N,inf);
memset(st,0,sizeof st);
dist[start] = 0;
priority_queue<PII,vector<PII>,greater<PII>> heap;
heap.push({0,start});
while (heap.size()) {
auto t = heap.top();
heap.pop();
int ver = t.second;
if (st[ver]) continue;
st[ver] = true;
for (int i = h[ver]; ~i ; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[ver] + w[i]) {
dist[j] = dist[ver] + w[i];
heap.push({dist[j],j});
}
}
}
}
int dfs(int u, int start, int distance) {
if (u > 5) {
return distance;
}
int ans = inf;
for (int i = 1; i <= 5; i++) {
if (!st[i]) {
int v = source[i];
st[i] = true;
ans = min(ans, dfs(u + 1, i, distance + dist[start][v]));
st[i] = false;
}
}
return ans;
}
int main() {
cin >> n >> m;
source[0] = 1;
for (int i = 1; i <= 5; i++) {
cin >> source[i];
}
memset(h,-1,sizeof h);
while (m--) {
int a, b, c;
cin >> a >> b >> c;
add(a,b,c), add(b,a,c);
}
for (int i = 0; i < 6; i++) {
dijskra(source[i],dist[i]);
}
memset(st,0,sizeof st);
cout << (dfs(1,0,0)) << endl;
return 0;
}
题目:通信线路
地址:https://www.acwing.com/problem/content/342/
题意:
个点,条边的无向图,求解只经过条边的从到的最短路径的中的最长边的最小值。
分析
抓住字眼:最大值最小,考虑一下可不可以用二分来做。
答案是可以的,我们可以二分答案,用最短路算法来进行验证。
对于二分点,我们需要满足的条件是:1到n的最短路中,大于mid的边的条数小于等于k。
如果满足,意味着我们可以最坏情况可以留下mid,最好情况是把mid这个预设的整条路经的权值也去掉。那么我们可以尝试缩小范围,反之扩大范围。
那么我们如何统计最短路中大于mid的边的条数,我们可以把权值变成0和1两种可能来求解,这样子当我们求到n点的最短路时,存储的就是大于mid的边的条数,即使用双端队列进行最短路搜索。
Code
小细节:因为存在不可能的情况,所以二分的边界+1,作为标记。
#include <bits/stdc++.h>
using namespace std;
const int N = 1010, M = 20010;
int n, p, k, idx;
int h[N], ne[M], e[M], w[M];
int dist[N];
bool vis[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool check(int mid) {
memset(dist, 0x3f, sizeof dist);
memset(vis,false,sizeof vis);
deque<int> q;
dist[1] = 0, q.push_back(1);
while (q.size()) {
int now = q.front();
q.pop_front();
if (vis[now]) continue;
vis[now] = true;
for (int i = h[now]; i != -1; i = ne[i]) {
int j = e[i], v = w[i] > mid;
if (dist[j] > dist[now] + v) {
dist[j] = dist[now] + v;
if (v) {
q.push_back(j);
} else {
q.push_front(j);
}
}
}
}
return dist[n] <= k;
}
int main() {
cin >> n >> p >> k;
memset(h,-1,sizeof h);
for (int i = 1; i <= p; i++) {
int a, b, c;
cin >> a >> b >> c;
add(a,b,c), add(b,a,c);
}
int l = 0, r = 1e6 + 1;
while (l < r) {
int mid = (l + r) / 2;
if (check(mid)) {
r = mid;
} else {
l = mid + 1;
}
}
if (l == 1e6 + 1) {
cout << "-1" << endl;
} else {
cout << l << endl;
}
return 0;
}
题目:道路与航线
地址:https://www.acwing.com/problem/content/344/
题意:
巴拉巴拉。
分析
一个结论:在DAG图上可能存在负边权,可以用拓扑序来计算最短路径。
证明待补充。
那么我们就可以解决这个问题了。
-
缩点
把所有道路分成一个个连通块,那么整个图也就抽象成了了若干个块与块之间通过航道进行转移。
-
按拓扑序在块内跑最短路。
Code
#include <bits/stdc++.h>
using namespace std;
#define debug freopen("in.in","r",stdin);freopen("out.out","w",stdout);
#define IO ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
#define rep(i, l, r) for(int i = (l); i <= (r); i++)
#define per(i, r, l) for(int i = (r); i >= (l); i--)
#define pb push_back
#define SZ(X) ((int)(x).size())
#define fi first
#define se second
#define all(x) (x).begin(), (x).end()
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
const int inf = 0x3f3f3f3f;
const int N = 25010, M = 150010;
int n, m1,m2,s;
int id[N];
int h[N], ne[M], e[M], w[M], idx;
int dist[N], din[N], bcnt;
bool st[N];
vector<int> block[N];
queue<int> q;
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u, int bid) {
id[u] = bid, block[bid].push_back(u);
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if (!id[j]) dfs(j, bid);
}
}
void dijkstra(int bid) {
priority_queue<PII, vector<PII>, greater<PII>> heap;
for (auto u : block[bid]) {
heap.push({dist[u], u});
}
while (heap.size()) {
auto t = heap.top();
heap.pop();
int ver = t.second;
if (st[ver]) continue;
st[ver] = true;
for (int i = h[ver]; i != -1; i = ne[i]) {
int j = e[i];
if (id[j] != id[ver] && --din[id[j]] == 0) {
q.push(id[j]);
}
if (dist[j] > dist[ver] + w[i]) {
dist[j] = dist[ver] + w[i];
if (id[j] == id[ver]) {
heap.push({dist[j],j});
}
}
}
}
}
void topsort() {
memset(dist,0x3f,sizeof dist);
dist[s] = 0;
for (int i = 1; i <= bcnt; i++) {
if (!din[i]) {
q.push(i);
}
}
while (q.size()) {
int t = q.front();
q.pop();
dijkstra(t);
}
}
int main(void) {
IO
memset(h, -1, sizeof h);
cin >> n >> m1 >> m2 >> s;
while (m1--) {
int a, b, c;
cin >> a >> b >> c;
add(a,b,c), add(b,a,c);
}
for (int i = 1; i <= n; i++) {
if (!id[i]) {
bcnt++;
dfs(i,bcnt);
}
}
while (m2--) {
int a, b, c;
cin >> a >> b >> c;
add(a,b,c);
++din[id[b]];
}
topsort();
for (int i = 1; i <= n; i++) {
if (dist[i] > inf / 2) cout << "NO PATH" << endl;
else cout << dist[i] << '\n';
}
return 0;
}
题目:最优贸易
地址:https://www.acwing.com/problem/content/description/343/
题意:
芭拉芭拉芭太长忽略
分析:
可以利用dp思想看出来,我们需要求出从1到i的前缀最小值,i到n中的后缀最大值。
但是由于不是拓扑图,所以不能用p解,但能使用最短路算法。
对于dijksra算法,我们必须得保证起点是最小值,才能满足贪心的性质,但显然我们这里不满足,所以我们只能使用spfa算法。
这里需要注意求n到i的时候,需要反向求解,因为我们的分界点不一定能到终点,求后缀最大值的时候可能就会断开了,我们必须要保证i前后都是连续的。
Code
#include <bits/stdc++.h>
using namespace std;
#define debug freopen("in.in","r",stdin);freopen("out.out","w",stdout);
#define IO ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
#define rep(i, l, r) for(int i = (l); i <= (r); i++)
#define per(i, r, l) for(int i = (r); i >= (l); i--)
#define pb push_back
#define SZ(x) ((int)(x).size())
#define fi first
#define se second
#define all(x) (x).begin(), (x).end()
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
const int inf = 123456789;
const int N = 100010, M = 200010;
int n, m;
int price[N];
int idx, h[M], h1[M], e[M], ne[M];
int dmin[N], dmax[N];
bool vis[N];
void add(int *h, int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void spfa(int *d, int start, int *h, bool sign) {
queue<int> q;
memset(vis,false,sizeof vis);
if (sign) {
memset(d,0x3f,sizeof dmin);
}
q.push(start);
vis[start] = true;
d[start] = price[start];
while (q.size()) {
int u = q.front();
q.pop();
vis[u] = false;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (sign && d[j] > min(d[u], price[j]) || !sign && d[j] < max(d[u], price[j])) {
if (sign) {
d[j] = min(d[u],price[j]);
} else {
d[j] = max(d[u],price[j]);
}
if (!vis[j]) {
vis[j] = true;
q.push(j);
}
}
}
}
}
int main(void) {
scanf("%d%d",&n,&m);
memset(h,-1,sizeof h);
memset(h1,-1,sizeof h1);
for (int i = 1; i <= n; i++) {
scanf("%d",&price[i]);
}
while (m--) {
int a, b, c;
scanf("%d%d%d",&a,&b,&c);
add(h,a,b), add(h1,b,a);
if (c == 2) {
add(h,b,a), add(h1,a,b);
}
}
spfa(dmin, 1, h, true);
spfa(dmax, n, h1, false);
int res = 0;
for (int i = 1; i <= n; i++) {
res = max(res, dmax[i] - dmin[i]);
}
printf("%d\n",res);
return 0;
}
单源最短路建图,应用,扩展。
重新给图论提高课做一个总结。
建图方式
对于一个含有个点,条边的无向图,边权都是正值,求解起点到终点的最短距离。
根据的数据范围选择邻接表或者邻接矩阵直接建图跑最短路就行,属于裸的板子题,难点在于如何抽象出图论模型来改板子吧。
多做多见多积累吧,灵光一现或许就是平时的日积月累的结果。
例题
题目:热浪
地址:https://www.acwing.com/problem/content/1131/
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 2510, M = 2*6500+10;
int h[N],e[M],ne[M],w[M],dis[N],q[N];
int n,m,S,T,idx;
bool st[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void spfa() {
memset(dis, 0x3f, sizeof dis);
dis[S] = 0;
st[S] = true;
int hh = 0, tt = 1;
q[0] = S;
while( hh != tt ) {
int t = q[hh++];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (dis[j] > dis[t] + w[i]) {
dis[j] = dis[t] + w[i];
if( !st[j] ) {
q[tt++] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
}
int main(void) {
cin >> n >> m >> S >> T;
memset(h, -1, sizeof h);
for (int i = 0; i < m; ++i ) {
int a,b,c;
cin >> a >> b >> c;
add(a,b,c), add(b,a,c);
}
spfa();
cout << dis[T] << endl;
return 0;
}
题目:信使
地址:https://www.acwing.com/problem/content/1130/
求从起点出发,到所有点的最短距离的集合中的最大值,最远的都到了,其他点肯定早或同时收到信号。
本题求的是最长路,把代码的比较符号改改就行,数据量比较小。
Code
#include <bits/stdc++.h>
using namespace std;
int g[200][200];
int n, m;
int main() {
scanf("%d%d",&n,&m);
memset(g,0x3f,sizeof g);
for (int i = 1; i <= n; i++) g[i][i] = 0;
for (int i = 1; i <= m; i++) {
int a, b, c;
scanf("%d%d%d",&a,&b,&c);
g[a][b] = g[b][a] = min(g[a][b], c);
}
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
g[i][j] = min(g[i][j], g[i][k] + g[k][j]);
}
}
}
int res = -1;
for (int i = 1; i <= n; i++) {
if (g[1][i] == 0x3f3f3f3f) {
res = -1;
break;
} else {
res = max(res, g[1][i]);
}
}
printf("%d\n",res);
return 0;
}
题目:香甜的黄油
地址:https://www.acwing.com/problem/content/1129/
有个点,条边,边权都是正值。
求给定起点下,到达所有牧场的最短距离。
spfa可以解决,但是关于spfa它已经死了,所以保险起见我们最好跑堆优化的dijksra算法。
本题枚举所有点为起点,找一下该点到达所有点的最小值即可,数据范围较小。
Code
#include <bits/stdc++.h>
using namespace std;
#define debug freopen("in.in","r",stdin);freopen("out.out","w",stdout);
#define IO ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
#define rep(i, l, r) for(int i = (l); i <= (r); i++)
#define per(i, r, l) for(int i = (r); i >= (l); i--)
#define pb push_back
#define SZ(x) ((int)(x).size())
#define fi first
#define se second
#define all(x) (x).begin(), (x).end()
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
const int inf = 123456789;
int n, p, c;
int dist[803], id[803];
bool vis[803];
vector<PII> e[1000];
int spfa(int start) {
memset(dist,0x3f,sizeof dist);
memset(vis,false,sizeof vis);
queue<int> q;
q.push(start);
dist[start] = 0;
vis[start] = 0;
while (q.size()) {
int now = q.front();
q.pop();
vis[now] = false;
for (auto ver : e[now]) {
if (dist[ver.fi] > dist[now] + ver.se) {
dist[ver.fi] = dist[now] + ver.se;
if (!vis[ver.fi]) {
vis[ver.fi] = true;
q.push(ver.fi);
}
}
}
}
int sum = 0;
for (int i = 1; i <= n; i++) {
if (dist[id[i]] == 0x3f3f3f3f) return 0x3f3f3f3f;
else sum += dist[id[i]];
}
return sum;
}
int main(void) {
scanf("%d%d%d",&n,&p,&c);
for (int i = 1; i <= n; i++) {
scanf("%d",&id[i]);
}
for (int i = 1; i <= c; i++) {
int u, v, w;
scanf("%d%d%d",&u,&v,&w);
e[u].pb({v,w});
e[v].pb({u,w});
}
int ans = inf;
for (int i = 1; i <= p; i++) {
ans = min(ans, spfa(i));
}
printf("%d\n",ans);
return 0;
}
题目:最小花费
地址:https://www.acwing.com/problem/content/1128/
,求X的最小值,即求后面一连串的最大值。
类比成求最长路,跑一个图论板子即可。
Code
#include <bits/stdc++.h>
using namespace std;
#define debug freopen("in.in","r",stdin);freopen("out.out","w",stdout);
#define IO ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
#define rep(i, l, r) for(int i = (l); i <= (r); i++)
#define per(i, r, l) for(int i = (r); i >= (l); i--)
#define pb push_back
#define SZ(X) ((int)(x).size())
#define fi first
#define se second
#define all(x) (x).begin(), (x).end()
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
const int inf = 123456789;
int n, m, s, e;
double dist[503];
bool vis[503];
double g[503][503];
void dijskra() {
dist[s] = 1;
for (int i = 1; i <= n; i++) {
int ver = -1;
for (int j = 1; j <= n; j++) {
if (!vis[j] && (ver == -1 || dist[ver] > dist[j])) {
ver = j;
}
}
vis[ver] = true;
for (int j = 1; j <= n; j++) {
dist[j] = max(dist[j], dist[ver] * g[ver][j]);
}
}
}
int main(void) {
scanf("%d%d",&n,&m);
for (int i = 1; i <= m; i++) {
int a, b, c;
scanf("%d%d%d",&a,&b,&c);
double w = (100.0-c)/100;
g[a][b] = g[b][a] = max(g[a][b],w);
}
scanf("%d%d",&s,&e);
dijskra();
printf("%.8lf\n",100/dist[e]);
return 0;
}
题目:最优乘车
地址:https://www.acwing.com/problem/content/922/
题意:
某个旅客想去公园游玩,但是他目前所在的地点没有一辆直达的巴士,他可能需要换乘几次后才能到达公园,现在给出所有巴士站编号和所有巴士路线,询问旅客到达公园的最少换乘次数。
思路:
换乘几次 = 乘车几次-1
可以看出,一条巴士路线上的所有巴士站,都可以沿着路线一直往后走,只算一次乘车,即答案+1。
那么我们可以建立一个权值都是1的图来进行求解,在这里如果我们直接依据题意建图,即一条线路上两两站点连线来求这个答案,显然是非常麻烦的,比如一个站点有多条线路时,你到下一站是属于换乘还是一条线路的判断,非常麻烦。
所以我们可以换一种灵活的建图方法,我们将一条线路上前面车站到后边可达车站建上一条边权为1的边即可,然后直接跑BFS即可。
#include <bits/stdc++.h>
using namespace std;
int n, m;
int stop[1500], dist[503];
bool g[503][503];
void bfs() {
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
while (q.size()) {
int v = q.front();
q.pop();
for (int i = 1; i <= n; i++) {
if (g[v][i] && dist[i] > dist[v] + 1) {
dist[i] = dist[v] + 1;
q.push(i);
}
}
}
}
int main() {
cin >> m >> n;
string line;
getline(cin,line);
while (m--) {
getline(cin,line);
stringstream ssin(line);
int cnt = 0, p;
while (ssin >> p) { stop[cnt++] = p; }
for (int j = 0; j < cnt; j++) {
for (int k = j + 1; k < cnt; k++) {
g[stop[j]][stop[k]] = true;
}
}
}
bfs();
if (dist[n] == 0x3f3f3f3f) {
cout << "NO" << endl;
} else {
cout << dist[n]-1 << endl;
}
return 0;
}
扩展
例题
题目:选择最佳线路
地址:https://www.acwing.com/problem/content/1139/
题意:
n个点m条边的有向图,不知道起点S,但知道终点E,求解从每个起点出发到达任意终点的所有路线的最短距离。
分析:
-
算法1
人为确定一个虚拟源点,并且虚拟源点练到所有起点一条边权为0的边,即原问题转化为从虚拟源点出发到达终点E的最短路。
-
算法2
反向建图,把终点置为起点,求终点开始到所有起点的单源最短路,再在之中取个最小值。
Code
感觉用SPFA的话,可以把所有起点压入队里跑。
#include <bits/stdc++.h>
using namespace std;
#define debug freopen("in.in","r",stdin);freopen("out.out","w",stdout);
#define IO ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
#define rep(i, l, r) for(int i = (l); i <= (r); i++)
#define per(i, r, l) for(int i = (r); i >= (l); i--)
#define pb push_back
#define SZ(X) ((int)(x).size())
#define fi first
#define se second
#define all(x) (x).begin(), (x).end()
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
const int inf = 0x3f3f3f3f;
const int N = 10010, M = 200010;
int n, m, s, idx;
int h[N], ne[M], e[M], w[M];
int dist[N];
bool vis[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void spfa() {
memset(dist,0x3f,sizeof dist);
queue<int> q;
q.push(0);
vis[0] = true;
dist[0] = 0;
while (q.size()) {
int u = q.front();
q.pop();
vis[u] = false;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[u] + w[i]) {
dist[j] = dist[u] + w[i];
if (!vis[j]) {
vis[j] = true;
q.push(j);
}
}
}
}
}
int main(void) {
IO
while (cin >> n >> m >> s) {
memset(h,-1,sizeof h);
idx = 0;
rep(i,1,m) {
int a, b, c;
cin >> a >> b >> c;
add(a,b,c);
}
int T;
cin >> T;
while (T--) {
int ver;
cin >> ver;
add(0,ver,0);
}
spfa();
if (dist[s] == inf) {
cout << "-1\n";
} else {
cout << dist[s] << '\n';
}
}
return 0;
}
题目:拯救大兵瑞恩
地址:https://www.acwing.com/problem/content/1133/
题意:
巴拉巴拉巴拉太长了自己看。
分析:
简易版本:一个迷宫,从起点S走到终点E的最短路长度,每个格子可上下左右四个方向扩展,格子的属性有两种,连通和不连通(有障碍物),需要记录的状态就是当前坐标以及到达当前坐标的长度,可扩展坐标和可扩展坐标最短路长度。
到本题这里,格子的属性更复杂了,可以直达的格子里分为有钥匙和没钥匙两种,拿去钥匙不计时间,有障碍物的格子分为不可达的障碍物,或者需要某一类钥匙才可以通过的门。
那么我们需要记录的状态也就更多了,除了上述状态,还需要记录此时持有钥匙的状态,即我们需要维护一个三维状态,来表明在这个坐标时的持有钥匙状态。
接下来我们考虑走到下一个格子后,如何进行状态处理。本题因为可以走回头路,不存在拓扑序,存在环形路径的可能,所以用DP无法计算状态转移,需要使用最短路算法。
我们定义当前点为,状态为。
- 如果存在钥匙,我们一定会拿起来,因为拿钥匙不消耗时间(等于边权为0),多多益善。那么会得到状态,比较当前状态的最短路长度,如果,则不进行任何处理,新状态更优,且已在队伍里。反之将更新为目前状态的最短路径长度,将其压入队头。
- 没有钥匙,就照常往四个方向进行扩展延伸,将新的可以到达状态压入队列尾部,等待下一次扩展。
本题空间为64MB,直接开三维数组存状态会MLE,所以我们将状态压缩一下,将压缩成一个独立的点来表示,每类钥匙可以用二进制来表示,拿起钥匙就是|一下,将对应位置1即可,判断有无钥匙则将状态右移对应位看是不是1就可以。
建图细节看代码,这里不再赘述。
Code
#include <bits/stdc++.h>
using namespace std;
#define debug freopen("in.in","r",stdin);freopen("out.out","w",stdout);
#define IO ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
#define rep(i, l, r) for(int i = (l); i <= (r); i++)
#define per(i, r, l) for(int i = (r); i >= (l); i--)
#define pb push_back
#define SZ(X) ((int)(x).size())
#define fi first
#define se second
#define all(x) (x).begin(), (x).end()
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
const int inf = 123456789;
const int N = 11, M = 400, P = 1 << 10;
int n, m, k, p;
int h[N * N], e[M], w[M], ne[M], idx;
int g[N][N], key[N * N];
int dist[N*N][P];
bool vis[N * N][P];
set<PII> Edge;
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void build() {
int dx[] = {-1,0,1,0}, dy[] = {0,1,0,-1};
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
for (int u = 0; u < 4; u++) {
int x = i + dx[u], y = j + dy[u];
if (!x || x > n || !y || y > m) continue;
int a = g[i][j], b = g[x][y];
if (!Edge.count({a,b})) {
add(a,b,0);
}
}
}
}
}
int bfs() {
memset(dist,0x3f,sizeof dist);
dist[1][0] = 0;
deque<PII> q;
q.push_back({1,0});
while (q.size()) {
PII u = q.front();
q.pop_front();
if (vis[u.fi][u.se]) continue;
vis[u.fi][u.se] = true;
if (u.fi == n * m) {
return dist[u.fi][u.se];
}
if (key[u.fi]) {
int state = u.se | key[u.fi];
if (dist[u.fi][state] > dist[u.fi][u.se]) {
dist[u.fi][state] = dist[u.fi][u.se];
q.push_front({u.fi, state});
}
}
for (int i = h[u.fi]; i != -1; i = ne[i]) {
int j = e[i];
if (w[i] && !(u.se >> w[i]-1 & 1)) continue;
if (dist[j][u.se] > dist[u.fi][u.se] + 1) {
dist[j][u.se] = dist[u.fi][u.se] + 1;
q.push_back({j,u.se});
}
}
}
return -1;
}
int main(void) {
IO
cin >> n >> m >> p >> k;
for (int i = 1, t = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
g[i][j] = t++;
}
}
memset(h,-1,sizeof h);
while (k--) {
int x1, y1, x2, y2, c;
cin >> x1 >> y1 >> x2 >> y2 >> c;
int a = g[x1][y1], b = g[x2][y2];
Edge.insert({a,b}), Edge.insert({b,a});
if (c) {
add(a,b,c), add(b,a,c);
}
}
build();
int s;
cin >> s;
while (s--) {
int x, y, c;
cin >> x >> y >> c;
key[g[x][y]] |= 1 << c-1;
}
cout << bfs() << '\n';
return 0;
}
题目:最短路计数
地址:https://www.acwing.com/problem/content/1136/
题意:
给出一个 N 个顶点 M 条边的无向无权图,顶点编号为 1到 N。
问从顶点 1 开始,到其他每个点的最短路有几条。
分析:
先前条件:最短路中不存在值为0的环,否则可以一直在环里跑,无解。
先前概念:最短路径树指的是以某个节点为根,根节点到其他点距离最小形成的一棵树。
从每个点的前一个路径考虑,看看该点时从哪个路径转移过来的,累加上来就好,但是我们需要先弄出来一个最短路径树(拓扑图)。对于转移过来的边,我们根据长度为两种。
- 长度比当前大许多,即沿着这条路线伸展出来的路才是我们需要的最短路。
- 长度差1,说明是我们当前节点延伸出去且是当前长度为当前最短路,直接累加上去。
对于最短路径树,我们可以通过BFS算法或者dijkstra算法实现。
- BFS每次扩展一层,最后一定能够得到拓扑图。
- dijkstra每次出队距离起点最近的点,如果知道用的是哪条边,连起来就是一颗最短路树。
Code
#include <bits/stdc++.h>
using namespace std;
#define debug freopen("in.in","r",stdin);freopen("out.out","w",stdout);
#define IO ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
#define rep(i, l, r) for(int i = (l); i <= (r); i++)
#define per(i, r, l) for(int i = (r); i >= (l); i--)
#define pb push_back
#define SZ(X) ((int)(x).size())
#define fi first
#define se second
#define all(x) (x).begin(), (x).end()
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
const int inf = 123456789;
const int N = 100010, M = 400010;
const int mod = 100003;
int n, m;
int h[N], e[M], ne[M], q[M];
int dist[N], cnt[N];
int idx;
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void bfs() {
memset(dist,0x3f,sizeof dist);
dist[1] = 0, cnt[1] = 1;
int front = 0, rear = 0;
q[0] = 1;
while (front <= rear) {
int t = q[front++];
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[t] + 1) {
dist[j] = dist[t] + 1;
cnt[j] = cnt[t];
q[++rear] = j;
} else if (dist[j] == dist[t] + 1) {
cnt[j] = (cnt[j] + cnt[t]) % mod;
}
}
}
}
int main(void) {
IO
cin >> n >> m;
memset(h,-1,sizeof h);
while (m--) {
int a, b;
cin >> a >> b;
add(a,b),add(b,a);
}
bfs();
for (int i = 1; i <= n; i++) {
cout << cnt[i] << "\n";
}
return 0;
}
题目:观光
地址:https://www.acwing.com/problem/content/385/
题意:
求次短路长度数目。
分析:
和上一题差不多,只不过状态转移麻烦很多。
- 当前最短路变次短路,最短路得到更优结果,双双入队。
- 找到一条新的最短路,次数更新。
- 找到一条更优次短路,更新当前次短路状态,入队。
- 找到一条新的次短路,次数更新。
Code
#include <bits/stdc++.h>
using namespace std;
#define debug freopen("in.in","r",stdin);freopen("out.out","w",stdout);
#define IO ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
#define rep(i, l, r) for(int i = (l); i <= (r); i++)
#define per(i, r, l) for(int i = (r); i >= (l); i--)
#define pb push_back
#define SZ(X) ((int)(x).size())
#define fi first
#define se second
#define all(x) (x).begin(), (x).end()
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
const int inf = 123456789;
const int N = 1010, M = 100010;
int n, m, S, E, idx;
int h[N],ne[M],e[M],w[M];
int dist[N][2], cnt[N][2];
bool vis[N][2];
struct Node {
int id, type, dist;
bool operator > (const Node& tmp) const {
return dist > tmp.dist;
}
};
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
int dij() {
memset(vis,false,sizeof vis);
memset(dist, 0x3f, sizeof dist);
memset(cnt, 0, sizeof cnt);
dist[S][0] = 0, cnt[S][0] = 1;
priority_queue<Node, vector<Node>, greater<Node>> heap;
heap.push({S,0,0});
while (heap.size()) {
Node u = heap.top();
heap.pop();
int ver = u.id, type = u.type, distance = u.dist, count = cnt[ver][type];
if (vis[ver][type]) continue;
vis[ver][type] = true;
for (int i = h[ver]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j][0] > distance + w[i]) {
dist[j][1] = dist[j][0];
cnt[j][1] = cnt[j][0];
heap.push({j,1,dist[j][1]});
dist[j][0] = distance + w[i];
cnt[j][0] = count;
heap.push({j,0,dist[j][0]});
} else if (dist[j][0] == distance + w[i]) {
cnt[j][0] += count;
} else if (dist[j][1] > distance + w[i]) {
dist[j][1] = distance + w[i];
cnt[j][1] = count;
heap.push({j,1,dist[j][1]});
} else if (dist[j][1] == distance + w[i]) {
cnt[j][1] += count;
}
}
}
int res = cnt[E][0];
if (dist[E][1] - 1 == dist[E][0]) {
res += cnt[E][1];
}
return res;
}
int main(void) {
IO
int T;
cin >> T;
while (T--) {
cin >> n >> m;
memset(h,-1,sizeof h);
idx = 0;
while (m--) {
int a,b,c;
cin >> a >> b >> c;
add(a,b,c);
}
cin >> S >> E;
cout << dij() << '\n';
}
return 0;
}
综合应用
跳脱出一个题考单一知识点的局限。
例题
题目:新年好
地址:https://www.acwing.com/problem/content/1137/
题意:
个点,条边的无向图,求解从起点出发到达其余个点所需要的最短路的最小值。
分析
我们可以预处理出来以第号点作为起点时候的最短路情况。
然后枚举所有的拜访路线,即种拜访途径,取其中的最小值即为答案。
本题是单源最短路结合dfs求解问题。
Code
#include <bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
const int N = 50010, M = 2e5 + 10, inf = 0x3f3f3f3f;
int h[N], ne[M], e[M], w[M], idx;
int n, m;
int source[6], dist[6][N];
bool st[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void dijskra(int start, int dist[]) {
fill(dist,dist+N,inf);
memset(st,0,sizeof st);
dist[start] = 0;
priority_queue<PII,vector<PII>,greater<PII>> heap;
heap.push({0,start});
while (heap.size()) {
auto t = heap.top();
heap.pop();
int ver = t.second;
if (st[ver]) continue;
st[ver] = true;
for (int i = h[ver]; ~i ; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[ver] + w[i]) {
dist[j] = dist[ver] + w[i];
heap.push({dist[j],j});
}
}
}
}
int dfs(int u, int start, int distance) {
if (u > 5) {
return distance;
}
int ans = inf;
for (int i = 1; i <= 5; i++) {
if (!st[i]) {
int v = source[i];
st[i] = true;
ans = min(ans, dfs(u + 1, i, distance + dist[start][v]));
st[i] = false;
}
}
return ans;
}
int main() {
cin >> n >> m;
source[0] = 1;
for (int i = 1; i <= 5; i++) {
cin >> source[i];
}
memset(h,-1,sizeof h);
while (m--) {
int a, b, c;
cin >> a >> b >> c;
add(a,b,c), add(b,a,c);
}
for (int i = 0; i < 6; i++) {
dijskra(source[i],dist[i]);
}
memset(st,0,sizeof st);
cout << (dfs(1,0,0)) << endl;
return 0;
}
题目:通信线路
地址:https://www.acwing.com/problem/content/342/
题意:
个点,条边的无向图,求解只经过条边的从到的最短路径的中的最长边的最小值。
分析
抓住字眼:最大值最小,考虑一下可不可以用二分来做。
答案是可以的,我们可以二分答案,用最短路算法来进行验证。
对于二分点,我们需要满足的条件是:1到n的最短路中,大于mid的边的条数小于等于k。
如果满足,意味着我们可以最坏情况可以留下mid,最好情况是把mid这个预设的整条路经的权值也去掉。那么我们可以尝试缩小范围,反之扩大范围。
那么我们如何统计最短路中大于mid的边的条数,我们可以把权值变成0和1两种可能来求解,这样子当我们求到n点的最短路时,存储的就是大于mid的边的条数,即使用双端队列进行最短路搜索。
Code
小细节:因为存在不可能的情况,所以二分的边界+1,作为标记。
#include <bits/stdc++.h>
using namespace std;
const int N = 1010, M = 20010;
int n, p, k, idx;
int h[N], ne[M], e[M], w[M];
int dist[N];
bool vis[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool check(int mid) {
memset(dist, 0x3f, sizeof dist);
memset(vis,false,sizeof vis);
deque<int> q;
dist[1] = 0, q.push_back(1);
while (q.size()) {
int now = q.front();
q.pop_front();
if (vis[now]) continue;
vis[now] = true;
for (int i = h[now]; i != -1; i = ne[i]) {
int j = e[i], v = w[i] > mid;
if (dist[j] > dist[now] + v) {
dist[j] = dist[now] + v;
if (v) {
q.push_back(j);
} else {
q.push_front(j);
}
}
}
}
return dist[n] <= k;
}
int main() {
cin >> n >> p >> k;
memset(h,-1,sizeof h);
for (int i = 1; i <= p; i++) {
int a, b, c;
cin >> a >> b >> c;
add(a,b,c), add(b,a,c);
}
int l = 0, r = 1e6 + 1;
while (l < r) {
int mid = (l + r) / 2;
if (check(mid)) {
r = mid;
} else {
l = mid + 1;
}
}
if (l == 1e6 + 1) {
cout << "-1" << endl;
} else {
cout << l << endl;
}
return 0;
}
题目:道路与航线
地址:https://www.acwing.com/problem/content/344/
题意:
巴拉巴拉。
分析
一个结论:在DAG图上可能存在负边权,可以用拓扑序来计算最短路径。
证明待补充。
那么我们就可以解决这个问题了。
-
缩点
把所有道路分成一个个连通块,那么整个图也就抽象成了了若干个块与块之间通过航道进行转移。
-
按拓扑序在块内跑最短路。
Code
#include <bits/stdc++.h>
using namespace std;
#define debug freopen("in.in","r",stdin);freopen("out.out","w",stdout);
#define IO ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
#define rep(i, l, r) for(int i = (l); i <= (r); i++)
#define per(i, r, l) for(int i = (r); i >= (l); i--)
#define pb push_back
#define SZ(X) ((int)(x).size())
#define fi first
#define se second
#define all(x) (x).begin(), (x).end()
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
const int inf = 0x3f3f3f3f;
const int N = 25010, M = 150010;
int n, m1,m2,s;
int id[N];
int h[N], ne[M], e[M], w[M], idx;
int dist[N], din[N], bcnt;
bool st[N];
vector<int> block[N];
queue<int> q;
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u, int bid) {
id[u] = bid, block[bid].push_back(u);
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if (!id[j]) dfs(j, bid);
}
}
void dijkstra(int bid) {
priority_queue<PII, vector<PII>, greater<PII>> heap;
for (auto u : block[bid]) {
heap.push({dist[u], u});
}
while (heap.size()) {
auto t = heap.top();
heap.pop();
int ver = t.second;
if (st[ver]) continue;
st[ver] = true;
for (int i = h[ver]; i != -1; i = ne[i]) {
int j = e[i];
if (id[j] != id[ver] && --din[id[j]] == 0) {
q.push(id[j]);
}
if (dist[j] > dist[ver] + w[i]) {
dist[j] = dist[ver] + w[i];
if (id[j] == id[ver]) {
heap.push({dist[j],j});
}
}
}
}
}
void topsort() {
memset(dist,0x3f,sizeof dist);
dist[s] = 0;
for (int i = 1; i <= bcnt; i++) {
if (!din[i]) {
q.push(i);
}
}
while (q.size()) {
int t = q.front();
q.pop();
dijkstra(t);
}
}
int main(void) {
IO
memset(h, -1, sizeof h);
cin >> n >> m1 >> m2 >> s;
while (m1--) {
int a, b, c;
cin >> a >> b >> c;
add(a,b,c), add(b,a,c);
}
for (int i = 1; i <= n; i++) {
if (!id[i]) {
bcnt++;
dfs(i,bcnt);
}
}
while (m2--) {
int a, b, c;
cin >> a >> b >> c;
add(a,b,c);
++din[id[b]];
}
topsort();
for (int i = 1; i <= n; i++) {
if (dist[i] > inf / 2) cout << "NO PATH" << endl;
else cout << dist[i] << '\n';
}
return 0;
}
题目:最优贸易
地址:https://www.acwing.com/problem/content/description/343/
题意:
芭拉芭拉芭太长忽略
分析:
可以利用dp思想看出来,我们需要求出从1到i的前缀最小值,i到n中的后缀最大值。
但是由于不是拓扑图,所以不能用p解,但能使用最短路算法。
对于dijksra算法,我们必须得保证起点是最小值,才能满足贪心的性质,但显然我们这里不满足,所以我们只能使用spfa算法。
这里需要注意求n到i的时候,需要反向求解,因为我们的分界点不一定能到终点,求后缀最大值的时候可能就会断开了,我们必须要保证i前后都是连续的。
Code
#include <bits/stdc++.h>
using namespace std;
#define debug freopen("in.in","r",stdin);freopen("out.out","w",stdout);
#define IO ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
#define rep(i, l, r) for(int i = (l); i <= (r); i++)
#define per(i, r, l) for(int i = (r); i >= (l); i--)
#define pb push_back
#define SZ(x) ((int)(x).size())
#define fi first
#define se second
#define all(x) (x).begin(), (x).end()
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
const int inf = 123456789;
const int N = 100010, M = 200010;
int n, m;
int price[N];
int idx, h[M], h1[M], e[M], ne[M];
int dmin[N], dmax[N];
bool vis[N];
void add(int *h, int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void spfa(int *d, int start, int *h, bool sign) {
queue<int> q;
memset(vis,false,sizeof vis);
if (sign) {
memset(d,0x3f,sizeof dmin);
}
q.push(start);
vis[start] = true;
d[start] = price[start];
while (q.size()) {
int u = q.front();
q.pop();
vis[u] = false;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (sign && d[j] > min(d[u], price[j]) || !sign && d[j] < max(d[u], price[j])) {
if (sign) {
d[j] = min(d[u],price[j]);
} else {
d[j] = max(d[u],price[j]);
}
if (!vis[j]) {
vis[j] = true;
q.push(j);
}
}
}
}
}
int main(void) {
scanf("%d%d",&n,&m);
memset(h,-1,sizeof h);
memset(h1,-1,sizeof h1);
for (int i = 1; i <= n; i++) {
scanf("%d",&price[i]);
}
while (m--) {
int a, b, c;
scanf("%d%d%d",&a,&b,&c);
add(h,a,b), add(h1,b,a);
if (c == 2) {
add(h,b,a), add(h1,a,b);
}
}
spfa(dmin, 1, h, true);
spfa(dmax, n, h1, false);
int res = 0;
for (int i = 1; i <= n; i++) {
res = max(res, dmax[i] - dmin[i]);
}
printf("%d\n",res);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】