算法模板(3):搜索(2):bfs与图论基础

news2024/11/25 4:55:17

bfs

在搜索题中,一般来讲,bfs和dfs都有一个最优选择。

基础bfs

走迷宫

  • 注:这个模板具有还原路径的功能。
  • 其实,还可以反向搜(从终点走到起点),就不用
    reverse数组了。
  • 其实,bfs是不用把路径标为INF的,也用不到vis数组的。只需要将d初始化为-1就可以,想想是不是?!
typedef pair<int, int> P;
int g[maxn][maxn], N, d[maxn][maxn], dx[] = { 0, 0, 1, -1 }, dy[] = {1, -1, 0, 0};
P pre[maxn][maxn];
queue<P> que;
vector<P> path;
void bfs() {
	memset(d, -1, sizeof d);
	que.push({ 0, 0 });
	d[0][0] = 0, pre[0][0] = { -1, -1 };
	while (que.size()) {
		auto p = que.front(); que.pop();
		int x = p.first, y = p.second;
		for (int i = 0; i < 4; i++) {
			int nx = x + dx[i], ny = y + dy[i];
			if (nx < 0 || nx >= N || ny < 0 || ny >= N) continue;
			if (g[nx][ny] || d[nx][ny] != -1) continue;
			d[nx][ny] = d[x][y] + 1;
			pre[nx][ny] = { x, y };
			que.push({ nx, ny });
		}
	}
	for (auto p = P(N - 1, N - 1); p != P(-1, -1); p = pre[p.first][p.second]) {
		path.push_back(p);
	}
	reverse(path.begin(), path.end());
	for (auto p : path) {
		printf("%d %d\n", p.first, p.second);
	}
}

920. 最优乘车

  • 题意:每条单程巴士线路从某个巴士站出发,依次途经若干个巴士站,最终到达终点巴士站。一名旅客最近到 H H H 城旅游,他很想去 S S S 公园游玩,但如果从他所在的饭店没有一路巴士可以直接到达 S S S 公园,则他可能要先乘某一路巴士坐几站,再下来换乘同一站台的另一路巴士,这样换乘几次后到达 S S S 公园。现在用整数 1 , 2 , … N 1,2,…N 1,2,N H H H 城的所有的巴士站编号,约定这名旅客所在饭店的巴士站编号为 1 1 1 S S S 公园巴士站的编号为 N N N。写一个程序,帮助这名旅客寻找一个最优乘车方案,使他在从饭店乘车到 S S S 公园的过程中换乘的次数最少。

  • 输入格式:第一行有两个数字 M M M N N N,表示开通了 M M M 条单程巴士线路,总共有 N N N 个车站。从第二行到第 M + 1 M+1 M+1 行依次给出了第 1 1 1 条到第 M M M 条巴士线路的信息,其中第 i + 1 i+1 i+1 行给出的是第 i i i 条巴士线路的信息,从左至右按运行顺序依次给出了该线路上的所有站号,相邻两个站号之间用一个空格隔开。

  • 目前发现(大雪菜也说过),当边权都为1的时候,通常就是用 bfs 来写,而不是用 spfa,dijkstra 这种方法。

  • 其实,这和迷宫的那个模型差不太多,而且还简单一些。

  • 这道题保留的意义其实是掌握以下 stringstream 和 getline(cin, line) 的用法。

#include<sstream>
#include<iostream>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
const int maxn = 510, INF = 1e9;
int g[maxn][maxn], N, M, d[maxn];
void bfs() {
	fill(d, d + maxn, INF);
	queue<int> que;
	que.push(1);
	d[1] = 0;
	while (que.size()) {
		int u = que.front(); que.pop();
		for (int j = 1; j <= N; j++) {
			if (g[u][j] && d[j] > d[u] + 1) {
				d[j] = d[u] + 1;
				que.push(j);
			}
		}
	}
}
int main() {
	cin >> M >> N;
	string line;
	getline(cin, line);
	for (int i = 0; i < M; i++) {
		getline(cin, line);
		stringstream ss(line);
		vector<int> v;
		int u;
		while (ss >> u) v.push_back(u);
		for (int i = 0; i < v.size(); i++) {
			for (int j = 0; j < i; j++) g[v[j]][v[i]] = 1;
		}
	}
	bfs();
	int ans = d[N];
	if (ans == INF) printf("NO\n");
	else printf("%d\n", ans - 1);
	return 0;
}

Flood Fill

1097. 池塘计数

  • 题意: n ∗ m n * m nm 的园子,雨后起了积水。八连通的积水被认为是连在一起的。问总共有多少水洼。

  • dfs可能会爆栈,因此这里展示bfs的做法。

  • 连通块分为四连通和八连通。

  • 注意,这个题是可以把 queue 当作全局变量的。因为在 bfs 中,只有 queue 为空是才能跳出循环。

int N, M;
char g[maxn][maxn];
bool vis[maxn][maxn];
typedef pair<int, int> P;
queue<P> que;
void bfs(int x, int y) {
	que.push({ x, y });
	vis[x][y] = true;
	while (que.size()) {
		auto p = que.front(); que.pop();
		int x = p.first, y = p.second;
		for (int dx = -1; dx <= 1; dx++) {
			for (int dy = -1; dy <= 1; dy++) {
				int nx = x + dx, ny = y + dy;
				if (nx < 0 || nx >= N || ny < 0 || ny >= M) continue;
				if (vis[nx][ny] || g[nx][ny] == '.') continue;
				vis[nx][ny] = true;
				que.push({ nx, ny });
			}
		}
	}
}
void solve() {
	int ans = 0;
	for (int i = 0; i < N; i++) {
		for (int j = 0; j < M; j++) {
			if (g[i][j] == 'W' && !vis[i][j]) {
				bfs(i, j);
				ans++;
			}
		}
	}
	printf("%d\n", ans);
}

173. 矩阵距离

  • 给定一个N行M列的01矩阵A, A [ i ] [ j ] A[i][j] A[i][j] A [ k ] [ l ] A[k][l] A[k][l] 之间的曼哈顿距离定义为:

d i s t ( A [ i ] [ j ] , A [ k ] [ l ] ) = ∣ i − k ∣ + ∣ j − l ∣ dist(A[i][j],A[k][l])=|i-k|+|j-l| dist(A[i][j],A[k][l])=ik+jl

  • 输出一个N行M列的整数矩阵B,其中:

B [ i ] [ j ] = m i n 1 ≤ x ≤ N , 1 ≤ y ≤ M , A [ x ] [ y ] = 1 ⁡ d i s t ( A [ i ] [ j ] , A [ x ] [ y ] ) B[i][j]=min_{1≤x≤N,1≤y≤M,A[x][y]=1}⁡{dist(A[i][j],A[x][y])} B[i][j]=min1xN,1yM,A[x][y]=1dist(A[i][j],A[x][y])

  • 多源 BFS 问题.
  • 虚拟源点,无需解释,直接上代码。
queue<P> que;
void bfs() {
	memset(d, -1, sizeof d);
	for (int i = 0; i < N; i++) {
		for (int j = 0; j < M; j++) {
			if (g[i][j] == '1') {
				d[i][j] = 0;
				que.push(P(i, j));
			}
		}
	}
	while (que.size()) {
		auto p = que.front(); que.pop();
		int x = p.first, y = p.second;
		for (int i = 0; i < 4; i++) {
			int nx = x + dx[i], ny = y + dy[i];
			if (nx < 0 || nx >= N || ny < 0 || ny >= M || d[nx][ny] != -1) continue;
			d[nx][ny] = d[x][y] + 1;
			que.push(P(nx, ny));
		}
	}
}

双端队列广搜

  • 双端队列广搜就是边权可以是0或1
  • 在一个边权只有01的无向图中搜索最短路径可以使用双端队列进行BFS。其原理是当前可以扩展到的点的权重为0时,将其加入队首;权重为1时,将其加入队尾

175. 电路维修

  • 这道题并不是所有节点都可以访问到的。只能访问到下标值和为偶数的节点。当中点下标之和为奇数的时候就访问不到。
  • 注意这个偏移量数组的灵活运用 (dx, ix, ok).
  • 这迷宫的终点是(N, M),一定注意!而且下标是从0开始的。而且注意要取出队头pop_front()
  • 这道题判断重复访问的位置只能放到从 deque 取出的地方,不可以往后放。因为这个节点访问顺序不好保证。
  • 双端队列广搜有一个和bfs不一样的地方,即第一次从双端队列弹出的是最短距离,但是第一次更新的时候并不是!因此,判断节点是否被访问过,以及更新距离的方式,都要改变。而bfs第一次更新的时候就已经是最短距离了。
bool vis[maxn][maxn];
char g[maxn][maxn];
int dx[] = { 1, 1, -1, -1 }, dy[] = { 1, -1, 1, -1 };
int ix[] = { 0, 0, -1, -1 }, iy[] = { 0, -1, 0, -1 };
const char* ok = "\\//\\";
int bfs() {
	for (int i = 0; i <= N; i++) {
		for (int j = 0; j <= M; j++) {
			d[i][j] = INF;
		}
	}
	memset(vis, false, sizeof vis);
	deque<P> dq;
	d[0][0] = 0;
	dq.push_back(P(0, 0));

	while (dq.size()) {
		auto p = dq.front(); dq.pop_front();
		int x = p.first, y = p.second;
		if (x == N && y == M) return d[x][y];
		
		if (vis[x][y]) continue;
		vis[x][y] = true;
		for (int i = 0; i < 4; i++) {
			int nx = x + dx[i], ny = y + dy[i];
			if (nx < 0 || nx > N || ny < 0 || ny > M) continue;
			int w = (g[x + ix[i]][y + iy[i]] != ok[i]);
			if (d[nx][ny] >= d[x][y] + w) {
				d[nx][ny] = d[x][y] + w;
				if (w) dq.push_back(P(nx, ny));
				else dq.push_front(P(nx, ny));
			}
		}
	}
}

双向广搜

#include<iostream>
#include<string>
#include<unordered_map>
#include<queue>
using namespace std;
string a[10], b[10], A, B;
int N;
int extend(queue<string>& que, unordered_map<string, int>& da, unordered_map<string, int>& db, string a[], string b[]) {
	string st = que.front(); que.pop();
	string ed;
	for (int i = 0; i < st.size(); i++) {
		for (int j = 0; j < N; j++) {
			if (st.substr(i, a[j].size()) == a[j]) {
				ed = st.substr(0, i) + b[j] + st.substr(i + a[j].size());
				if (db.count(ed)) return da[st] + 1 + db[ed];
				if (da.count(ed)) continue;
				da[ed] = da[st] + 1;
				que.push(ed);
			}
		}
	}
	return 11;
}
int bfs() {
	queue<string> qa, qb;
	unordered_map<string, int> da, db;
	qa.push(A), qb.push(B), da[A] = 0, db[B] = 0;
	//因为只要又一个队列为空,那么必然从起点无法转移到终点了。
	while (qa.size() && qb.size()) {
		int t;
		if (qa.size() < qb.size()) t = extend(qa, da, db, a, b);
		else t = extend(qb, db, da, b, a);
		if (t <= 10) return t;
	}
	return 11;
}
int main() {
	cin >> A >> B;
	while (cin >> a[N] >> b[N]) N++;
	int ans = bfs();
	if (ans > 10) cout << "NO ANSWER!\n";
	else cout << ans << endl;
	return 0;
}

A*

  • 用一个小根堆,维护两个值,一个是从起点到当前点的最短距离,另一个是从当前点到终点的估计距离。
  • A* 算法就是挑一个估计距离最小的点去扩展。估计距离必须不能超过真实距离,而且要非负,这样才能保证A*算法一定正确。百度百科:距离估计与实际值越接近,最终搜索速度越快。
  • A* 算法只有在有解的情况下才是高效的。否则还不如朴素的bfs效率高。因为A*是用优先队列来实现的。
  • A* 只能保证终点出队的时候,距离最小,但不能保证每一个点出队的时候都是最小的。
  • A* 算法边权只要没有负权回路就是用。Dijkstra在形式上可以看成估计距离为0的A*算法。

178. 第K短路

在这里插入图片描述

  • 整个搜索空间非常庞大。
  • 由于百科中说估计值与真实值越接近,最终搜索速度越快。所以这里选择到终点的最短路作为估计值(可以建反图来求得)。
  • 第几次出队就是第几短,于是终点出了k次就是第k短路了。
  • 这道题容易漏掉两种无解的情况:起点与终点重合,起点与终点不连通。
#include<cstdio>
#include<queue>
#include<cstring>
#include<algorithm>
using namespace std;
typedef pair<int, int> P;
typedef pair<int, P> P3;
const int maxn = 1010, maxm = 100010, INF = 0x3f3f3f3f;
int h[maxn], hr[maxn], ne[maxm], e[maxm], w[maxm], idx;
int N, M, S, T, K, d[maxn];
bool vis[maxn];
void add(int h[], int a, int b, int c) {
	e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
void dijkstra() {
	priority_queue<P, vector<P>, greater<P>> que;
	fill(d, d + maxn, INF);
	d[T] = 0; que.push(P(0, T));
	while (que.size()) {
		auto p = que.top(); que.pop();
		int u = p.second;
		if (vis[u]) continue;
		vis[u] = true;
		for (int i = hr[u]; i != -1; i = ne[i]) 
        {
			int v = e[i];
			if (d[v] > d[u] + w[i]) 
            {
				d[v] = d[u] + w[i];
				que.push(P(d[v], v));
			}
		}
	}
}
int astar() {
	//若起点与终点不连通,也是无解吖。。。
	if (d[S] == INF) return -1;
	priority_queue<P3, vector<P3>, greater<P3>> que;
	que.push(P3(d[S], P(0, S)));
	int cnt = 0;
	while (que.size()) {
		auto p = que.top(); que.pop();
		int u = p.second.second, dis = p.second.first;
		if (u == T) cnt++;
		if (cnt == K) return dis;
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			//如何判断所有路径?就是不经判断,把所有经过的路径全部加进来。
			que.push(P3(dis + w[i] + d[v], P(dis + w[i], v)));
		}
	}
	return -1;
}
int main() {
	memset(h, -1, sizeof h);
	memset(hr, -1, sizeof hr);
	scanf("%d%d", &N, &M);
	for (int i = 0; i < M; i++) {
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(h, a, b, c);
		add(hr, b, a, c);
	}
	scanf("%d%d%d", &S, &T, &K);
	//因为题中说路径至少包含一条边,因此,对于起点和终点重合的情况,若想让路径至少包含一条边,要这样处理:
	if (S == T) K++;
	dijkstra();
	printf("%d\n", astar());
}

图论

遍历问题

(1)树和图的深度优先遍历

  • 这里讲一下树的存储。综上来看,还是要掌握用单链表实现临界点存储,因为比较快。
  • h[i]储存的是头结点,其实就是i节点指向的一条边的编号idx。而这个链表存储的是第i个节点指向的所有的边。注意,idx是边的编号。
  • 注意,h 储存的是节点信息,e 和 ne 储存的是边的信息。e存储的是边指向的节点(相当于to),而ne存储的是这条边指向该头结点的下一条边。如果再有w[idx]的话,指的是编号idx的边的权重。
  • 由于h[i]存储的指向的边顺序无所谓,因此插入边的时候用前插法,更好实现。
  • 链表初始化,就是全部初始化为-1,这样当ne[i]为-1时,表示结束。
  • 邻接表存储:
int h[N], e[N], ne[N], idx;

// 添加一条边a->b
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

// 初始化
idx = 0;
memset(h, -1, sizeof(h));
//树与图的遍历(深度优先遍历)
//时间复杂度 O(n+m) n 表示点数,m 表示边数

int dfs(int u)
{
    st[u] = true; // st[u] 表示点u已经被遍历过

    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j]) dfs(j);
    }
}

树的重心

  • 给定一颗树。请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
  • 这道题一直WA的原因是双向边,但是边的数组最开始开小了。双向边的话,数组一定要开到两倍!
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 100005, maxm = maxn * 2;
int h[maxn], e[maxm], ne[maxm], idx, N, ans;
bool vis[maxn];  //该节点是否访问过。
void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int dfs(int u) {
	vis[u] = true;
	//res储存各个连通块中点数的最大值,sum储存从节点u出发的子树的大小。
	int res = 0, sum = 1;
	for (int i = h[u]; i != -1; i = ne[i]) {
		int v = e[i];
		if (!vis[v]) {
			int s = dfs(v);
			res = max(res, s);
			sum += s;
		}
	}
	//N - sum是上面的那个连通块的大小
	res = max(res, N - sum);
	ans = min(res, ans);
	return sum;
}
int main() {
	for (int i = 0; i < maxn; i++) h[i] = -1;
	int a, b;
	scanf("%d", &N);
	ans = N;
	for (int i = 0; i < N - 1; i++) {
		scanf("%d%d", &a, &b);
		add(a, b), add(b, a);
	}
	dfs(1);
	printf("%d\n", ans);
	return 0;
}

树的同构

树是一种很常见的数据结构。我们把 N N N 个点, N − 1 N-1 N1 条边的连通无向图称为树。若将某个点作为根,从根开始遍历,则其它的点都有一个前驱,这个树就成为有根树。对于两个树 T 1 T_1 T1 T 2 T_2 T2,如果能够把树 T 1 T_1 T1 的所有点重新标号,使得树 T 1 T_1 T1 和树 T 2 T_2 T2 完全相同,那么这两个树是同构的。也就是说,它们具有相同的形态。

现在,给你 M M M 个无根树,请你把它们按同构关系分成若干个等价类.

注意这个题的输入,树的编号是 1 ∼ N 1 \sim N 1N,结点父结点编号为 0 0 0 说明该点没有父结点.

(wsy) 最难卡掉的树哈希方法: r d rd rd 数组是一个随机数生成的数组:h[i] = rnd() % M,其中 M M M 是一个大质数.

叶节点的哈希值定义为1.
H a s h ( T R ) = ∏ i ∈ s o n ( R ) ( r d ( s i z e ( T R ) + H a s h ( T i ) ) m o d    M Hash(T_R) = \prod\limits_{i \in son(R)}(rd(size(T_R) + Hash(T_i)) \mod M Hash(TR)=ison(R)(rd(size(TR)+Hash(Ti))modM
对于无根树,可以先求树的重心作为根,如果重心有两个就建立虚拟节点连接两个重心,把虚拟节点当作根节点.

判断无根树同构,也可以跑两遍树形DP,求出每个点为根时的Hash值,排序后比较即可。不过应该需要用到 f x = 1 + ∑ y ∈ s o n x f y × p r i m e ( s i z e y ) f_x = 1 + \sum_{y\in son_x}{f_y \times prime(size_y)} fx=1+ysonxfy×prime(sizey)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 1e9 + 7;
const int N = 60, M = 110;

int h[N], e[M], ne[M], idx, sz[N], weight[N];
ll Hash[N], Hash_tree[N];
int centroid[2];
inline void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
mt19937 rnd(0);
int n, m;

//求树的重心
int getCentroid(int u, int fa)
{
    sz[u] = 1, weight[u] = 0;
    for(int i = h[u]; i != -1; i = ne[i])
    {
        int v = e[i];

        if(v == fa) continue;

        sz[u] += getCentroid(v, u);
        //找到size最大的子树.
        weight[u] = max(weight[u], sz[v]);
    }
   	//子树还有来自父结点的那棵子树.
    weight[u] = max(weight[u], n - sz[u]);
    if(weight[u] <= n / 2)
    {
        centroid[centroid[0] != 0] = u;
    }
    return sz[u];
}

void getHash(int u, int fa)
{
    Hash_tree[u] = sz[u] = 1;

    for(int i = h[u]; i != -1; i = ne[i])
    {
        int v = e[i];
        if(v == fa) continue;
        getHash(v, u);
        sz[u] += sz[v];
    }
    for(int i = h[u]; i != -1; i = ne[i])
    {
        int v = e[i];
        if(v == fa) continue;
        Hash_tree[u] = Hash_tree[u] * (Hash[sz[u]] + Hash_tree[v]) % mod;
    }
}

int main()
{
    for(int i = 1; i < N; i++)
    {
        Hash[i] = rnd() % mod;
    }
    unordered_map<ll, int> Map;
    scanf("%d", &m);
    for(int id = 1; id <= m; id++)
    {
        memset(h, -1, sizeof h);
        idx = 0;
        scanf("%d", &n);
        for(int i = 1; i <= n; i++)
        {
            int p;
            scanf("%d", &p);
            if(!p) continue;
            add(p, i);
            add(i, p);
        }
        centroid[0] = centroid[1] = 0;
        getCentroid(1, -1);

        int rt0 = centroid[0], rt1 = centroid[1];
        ll res = 1;
        if(!rt1)
        {
            getHash(rt0, -1);
            res = (Hash[sz[rt0]] + Hash_tree[rt0]) % mod;
        }
        else
        {
            getHash(rt0, rt1);
            res = (Hash[n] + Hash_tree[rt0]) % mod;
            getHash(rt1, rt0);
            res = res * (Hash[n] + Hash_tree[rt1]) % mod;
        }
        if(!Map.count(res)) Map[res] = id;
        printf("%d\n", Map[res]);
    }
}

(2) 树和图的宽度优先遍历

847. 图中点的层次

  • 给定一个 n n n 个点 m m m 条边的有向图,图中可能存在重边和自环。所有边的长度都是 1,点的编号为 1 1 1 n n n。请你求出 1 号点到 n n n 号点的最短距离,如果从 1 号点无法走到 n n n 号点,输出 −1。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 100010, maxm = 100010, INF = 0x3f3f3f3f;
int h[maxn], e[maxm], ne[maxm], idx;
int N, M, d[maxn];
void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int bfs() {
	queue<int> que;
	memset(d, 0x3f, sizeof d);
	que.push(1);
	d[1] = 0;
	while (que.size()) {
		int u = que.front(); que.pop();
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			if (d[v] != INF) continue;
			d[v] = d[u] + 1;
			que.push(v);
		}
	}
	if(d[N] == INF) return -1;
	return d[N];
}
int main() {
	scanf("%d%d", &N, &M);
	memset(h, -1, sizeof h);
	for (int i = 0; i < M; i++) {
		int a, b;
		scanf("%d%d", &a, &b);
		add(a, b);
	}
	bfs();
	printf("%d\n", bfs());
	return 0;
}

最短路问题

(1)Dijkstra朴素算法( O ( n 2 ) O(n ^ 2) O(n2)

  • 适用范围:单源最短路,不存在负权边,稠密图。
  • 稠密图嘛,用邻接矩阵来存储。
  • 还是不乱用memset函数了,一用就错。还是老老实实用for循环吧。
  • memset似乎中间可以填的数有:0, -1, 0x3f
  • 此方法建图时一定要:g[i, j] =min(g[i, j], cost).
  • 小心dij中的第二层循环,总是嵌套错
const int INF = 1e9, maxn = 510;
int G[maxn][maxn], d[maxn], vis[maxn];
int N, M;
int dij(int s, int e) {
	for (int i = 1; i <= N; i++) d[i] = INF;
	d[s] = 0;
	for (int i = 0; i < N; i++) {
		int t = -1;
		for (int j = 1; j <= N; j++) {
			if (!vis[j] && (t == -1 || d[t] > d[j])) t = j;
		}
		vis[t] = true;
		for (int j = 1; j <= N; j++) {
			d[j] = min(d[j], d[t] + G[t][j]);
		}
	}
	if (d[e] == INF) return -1;
	return d[e];
}

(2)堆优化版dijkstra (O(mlogn))

  • 适用范围:单源最短路,不存在负权边,稀疏图。
typedef long long ll;
typedef pair<ll, int> P;
const ll INF = 1e16;
int h[maxn], e[maxm], ne[maxm], idx;

int vis[maxn], N, M;
ll w[maxm], d[maxn];

void add(int a, int b, int c) {
	e[idx] = b, ne[idx] = h[a], w[idx] = (ll)c, h[a] = idx++;
}
ll dij(int s, int t) {
	for (int i = 1; i <= N; i++) d[i] = INF;
	priority_queue<P, vector<P>, greater<P> > que;
	d[s] = 0;
	que.push({ 0, 1 });
	while (que.size()) {
		P p = que.top(); que.pop();
		int u = p.second;
		if (vis[u]) continue;
		vis[u] = 1;
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			if (d[v] > d[u] + w[i]) {
				d[v] = d[u] + w[i];
				que.push({ d[v], v });
			}
		}
	}
	if (d[t] == INF) return -1;
	return d[t];
}

(3)Bellman-Ford算法(O(nm))

  • 适用范围:单源最短路,存在负权边
  • 这个算法几乎用不到,因为可以被spfa取代
  • 可以加上限制条件:经过的边不超过k条。若有这个限制的话,即使存在负圈(负权回路),也可以算出最短距离。但是,若无经过边数限制,则一定无最短距离。
  • 这道题函数返回的那个地方留意一下,是判断是否大于 INF / 2.
  • 再次提醒,图论问题两个易错点:别忘初始化!别弄混边和顶点(N 和 M)!
int n, m;       // n表示点数,m表示边数
int dist[N];        // dist[x]存储1到x的最短路距离

struct Edge     // 边,a表示出点,b表示入点,w表示边的权重
{
    int a, b, w;
}edges[M];

// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    // 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
    for (int i = 0; i < n; i ++ )
    {
        for (int j = 0; j < m; j ++ )
        {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w;
            if (dist[b] > dist[a] + w)
                dist[b] = dist[a] + w;
        }
    }

    if (dist[n] > 0x3f3f3f3f / 2) return -1;
    return dist[n];
}

(4)spfa 算法(队列优化的Bellman-Ford算法)

spfa求最短路

  • 适用范围:单源最短路,存在负权边(平均情况下O(m),最坏情况下O(nm))。
  • 这个函数返回值依然是判断是否等于INF,因为这个是用队列维护的,没有更新到的点不会加入队列,自然也不会去更新d[t]的值。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 100010, INF = 0x3f3f3f3f;
int h[maxn], e[maxn], ne[maxn], w[maxn], idx;
int N, M, d[maxn];
bool vis[maxn];
void add(int a, int b, int c) {
	e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
void spfa() {
	queue<int> que;
	que.push(1);
	memset(d, 0x3f, sizeof d);
	d[1] = 0;
	vis[1] = true;
	while (que.size()) {
		int u = que.front(); que.pop();
		vis[u] = false;
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			if (d[v] > d[u] + w[i]) {
				d[v] = d[u] + w[i];
				if (!vis[v]) {
					que.push(v);
					vis[v] = true;
				}
			}
		}
	}
}
int main() {
	scanf("%d%d", &N, &M);
	memset(h, -1, sizeof h);
	for (int i = 0; i < M; i++) {
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c);
	}
	spfa();
	if (d[N] == INF) printf("impossible\n");
	else printf("%d\n", d[N]);
	return 0;
}

spfa判断负环

  • 注意,从头到尾变化还是不少的
  • 原理:统计当前某个点的最短路所包含的边数。若边数大于等于N,则说明有负环。
  • 经验表明,spfa求负环的时候,复杂度通常会高达 O ( n m ) O(nm) O(nm),那么,经验表明,当所有点入队次数超过2n次的时候,很可能存在负环。虽然不能保证一定正确,但是用spfa超时的时候,这个方法效果还不错。
  • d不初始化也无所谓,d的初值并不影响答案。
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int maxn = 2010, maxm = 10010;
int h[maxn], e[maxm], ne[maxm], w[maxm], idx;
int N, M, d[maxn], cnt[maxn];
bool vis[maxn];
void add(int a, int b, int c) {
	e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
//有负环的话返回true.
bool spfa() {
	queue<int> que;
	for (int i = 1; i <= N; i++) {
		//防止图不连通。其实等效加了一个虚拟超级源点。
		que.push(i);
		vis[i] = true;
	}
	while (que.size()) {
		int u = que.front(); que.pop();
		vis[u] = false;
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			if (d[v] > d[u] + w[i]) {
				d[v] = d[u] + w[i];
				cnt[v] = cnt[u] + 1;
				if (cnt[v] >= N) return true;
				if (!vis[v]) {
					que.push(v);
					vis[v] = true;
				}
			}
		}
	}
    return false;
}
int main() {
	scanf("%d%d", &N, &M);
	memset(h, -1, sizeof h);
	for (int i = 0; i < M; i++) {
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c);
	}
	if (spfa()) printf("Yes\n");
	else printf("No\n");
	return 0;
}

(5)floyd算法( O ( n 3 ) O(n ^ 3) O(n3)

  • 适用范围:多源最短路,允许有负权边,不能有负环。
  • 看看这个怎么初始化的,怎么处理重边与负权边的。
  • 应用:最短路,传递闭包,最小环,恰好经过k条边的最短路。
int d[maxn][maxn], N, M, Q;
void flo(){

	for (int k = 1; k <= N; k++) {
		for (int i = 1; i <= N; i++) {
			for (int j = 1; j <= N; j++) {
				d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
			}
		}
	}
}
int main() {
	scanf("%d%d%d", &N, &M, &Q);
	for (int i = 1; i <= N; i++) {
		for (int j = 1; j <= N; j++) {
			if (i == j) d[i][j] = 0;
			else d[i][j] = INF;
		}
	}
	for (int i = 0; i < M; i++) {
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		d[a][b] = min(c, d[a][b]);
	}
	flo();
	while (Q--) {
		int s, t;
		scanf("%d%d", &s, &t);
		//因为存在负权边
		if (d[s][t] > INF / 2) printf("impossible\n");
		else printf("%d\n", d[s][t]);
	}
	return 0;
}

(6)更复杂的最短路问题

Floyd 求最小环:344. 观光之旅

  1. 给定一张无向图,求图中一个至少包含3个点的环,环上的节点不重复,并且环上的边的长度之和最小。该问题称为无向图的最小环问题。你需要输出最小环的方案,若最小环不唯一,输出任意一个均可。
  2. 按照环的编号最大的节点 k k k 的编号分类。那么环一定可以写成边 i k ik ik 、边 k j kj kj,加上一个路径 i i i j j j。这样,我们可以确定 k k k 时,循环所有 ( i , j ) (i, j) (i,j) 对。
  3. d [ i , j ] = d [ i , k ] + d [ k , j ] d[i, j] = d[i, k] + d[k, j] d[i,j]=d[i,k]+d[k,j],这个 k k k 就本身具备了记录路径的功能,每当跟新的时候,我们可以把它记录下来,即 p o s [ i , j ] = k pos[i, j] = k pos[i,j]=k.
  4. 凡是涉及到三个相加的, I N F INF INF 一定不要写成 0 x 3 f 3 f 3 f 3 f 0x3f3f3f3f 0x3f3f3f3f,因为又溢出的风险。
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 110, INF = 1e7;
int N, M;
int d[maxn][maxn], g[maxn][maxn], cnt, pos[maxn][maxn], ans[maxn];
void get_path(int i, int j) {
	if (pos[i][j] == 0) return;
	int k = pos[i][j];
	get_path(i, k);
	ans[cnt++] = k;
	get_path(k, j);
}
int main() {
	scanf("%d%d", &N, &M);
	for (int i = 1; i <= N; i++) {
		for (int j = 1; j <= N; j++) g[i][j] = INF;
	}
	for (int i = 1; i <= N; i++) g[i][i] = 0;
	for (int i = 0; i < M; i++) {
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		g[a][b] = g[b][a] = min(c, g[a][b]);
	}
	memcpy(d, g, sizeof d);
	int res = INF;
	for (int k = 1; k <= N; k++) {
		for (int i = 1; i < k; i++) {
			for (int j = i + 1; j < k; j++) {
				if (d[i][j] + g[i][k] + g[k][j] < res) {
					res = d[i][j] + g[j][k] + g[k][i];
					cnt = 0;
					ans[cnt++] = k;
					ans[cnt++] = i;
					get_path(i, j);
					ans[cnt++] = j;
				}
			}
		}
		for (int i = 1; i <= N; i++) {
			for (int j = 1; j <= N; j++) {
				if (d[i][j] > d[i][k] + d[k][j]) {
					d[i][j] = d[i][k] + d[k][j];
					pos[i][j] = k;
				}
			}
		}
	}
	if (res == INF) printf("No solution.\n");
	else {
		for (int i = 0; i < cnt; i++) printf("%d ", ans[i]);
	}
	return 0;
}

Floyd相关的倍增算法:345. 牛站

  • 给定一张由 T T T 条边构成的无向图,点的编号为 1 ∼ 1000 1 \sim 1000 11000 之间的整数。求从起点 S S S 到终点 E E E 恰好经过 N N N 条边(可以重复经过)的最短路。

  • f l o y d floyd floyd 算法是 d [ k , i , j ] d[k, i, j] d[k,i,j] 表示从i到j,只经过 1 ∼ k 1\sim k 1k 的距离最小值。倍增算法略作改动, d [ k , i , j ] d[k, i, j] d[k,i,j] 表示从 i i i j j j,只经过 k k k 条边的最短路径。

  • 那么,设从 i i i k k k 经过 a a a 条边,从 k k k j j j 经过 b b b 条边,那么 d [ a + b , i , j ] = min ⁡ ( d [ a , i , k ] + d [ b , k , j ] ) d[a + b, i, j] = \min(d[a, i, k] + d[b, k, j]) d[a+b,i,j]=min(d[a,i,k]+d[b,k,j])

  • 注意,sizeof参数必须是一个真正的数组,不可以是函数传进来的指针

  • 复杂度是 O ( n 3 log ⁡ n ) O(n^3 \log n) O(n3logn)。运用了快速幂的思想。快速幂可以成立的一个重要原因是结合律是成立的。包括矩阵的快速幂也是因为矩阵之间乘法的结合律是成立的。具体的解释可以看看代码注释:

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<map>
using namespace std;
const int maxn = 110, INF = 0x3f3f3f3f;
int g[maxn][maxn], res[maxn][maxn];
int N, M, K, st, ed;
map<int, int> id;

void mul(int c[][maxn], int a[][maxn], int b[][maxn]) {
	//因为c与a或b可能是同一数组,防止访问冲突,再开一个数组。
	static int tmp[maxn][maxn];
	for (int i = 0; i < maxn; i++) fill(tmp[i], tmp[i] + maxn, INF);
	for (int k = 0; k < N; k++) {
		for (int i = 0; i < N; i++) {
			for (int j = 0; j < N; j++) {
				tmp[i][j] = min(tmp[i][j], a[i][k] + b[k][j]);
			}
		}
	}
	//这一步其实相当于重新建图,把经过边数为1的路径去掉,把边数为2的路径作为新的边连起来。
	memcpy(c, tmp, sizeof tmp);
}
void qmi() {
	for (int i = 0; i < maxn; i++) {
		fill(res[i], res[i] + maxn, INF);
		//这个地方必须要把res[i][i]初始化为0.因为这个存的是答案,不只是路径的问题。
		res[i][i] = 0;
	}
	//就像快速幂一样。
	while (K) {
		//mul(res, res, g) 和 mul(res, g, res) 都可以 AC
		if (K & 1) mul(res, res, g);
		mul(g, g, g);
		K >>= 1;
	}
}
int main() {
	scanf("%d%d%d%d", &K, &M, &st, &ed);
	if (!id.count(st)) id[st] = N++;
	st = id[st];
	if (!id.count(ed)) id[ed] = N++;
	ed = id[ed];
	for (int i = 0; i < maxn; i++) {
		fill(g[i], g[i] + maxn, INF);
	}
	//注意,这里不可以把g[i][i]初始化为0.因为从i走到i表示一个自环呀,算经过1个节点。
	for (int i = 0; i < M; i++) {
		int a, b, c;
		scanf("%d%d%d", &c, &a, &b);
		if (!id.count(a)) id[a] = N++;
		if (!id.count(b)) id[b] = N++;
		a = id[a], b = id[b];
		g[a][b] = g[b][a] = min(g[a][b], c);
		
	}
	qmi();
	printf("%d\n", res[st][ed]);
	return 0;
}
  • 其实还可以这样写倍增算法:
void qmi() {
	memset(res, 0x3f, sizeof res);
	for (int i = 1; i <= N; i++) res[i][i] = 0;
	while (K) {
	//差别在这里,把结果存在res里面,这样更像快速幂。
	//而且此时写成mul(g, res, res)不影响结果。感觉这样子好像更合理一些。
		if (K & 1) mul(res, g, res);
		mul(g, g, g);
		K >>= 1;
	}
}

外部拓扑排序+内部dijkstra:342. 道路与航线

在这里插入图片描述

  • 可以分块。块儿内部都是非负权边,可以用dijkstra来算。团外部又负权边但是无环,可以按照拓扑图来做。块儿的内部是不可能有航线的。因为若A和B节点之间有航线,就不可能有道路,否则无法满足“不能从B到A”这一条件。
  • 另外,我在想,最开始的时候,如果一个连通块入度为0,是不是等价于S在这个连通块儿内部。不然,这个连通块儿是不可能从S走的到的呀。
  • 当然要入度减到0的时候入队啊,不然其他的点根本就没办法用dijkstra计算啊。
  • 最后还是要提醒,又负权边的话,只要不是 spfa,最后判断距离都要 > INF / 2。
  1. 先输入所有双向道路,然后 DFS 出所有连通块,计算两个数组: id[] 存储每个点属于哪个连通块; vector< int > block[] 存储每个连通块里有哪些点;
  2. 输入所有航线,同时统计出每个连通块的入度。
  3. 按照拓扑序依次处理每个连通块。先将所有入度为0的连通块的编号加入队列中。
  4. 每次从队头取出一个连通块的编号bid.
  5. 将该block[bid]中的所有点加入堆中,然后对堆中所有点跑dijkstra算法。注:这相当于建立了超级源点(即起点),然后将该块中的所有点都加入队列中.
  6. 每次取出堆中距离最小的点ver.
  7. 然后遍历ver的所有邻点。如果 i d [ v e r ] = = i d [ j ] id[ver] == id[j] id[ver]==id[j],那么如果 j j j 能被更新,则将其插入堆中;如果 i d [ v e r ] ≠ i d [ j ] id[ver] \ne id[j] id[ver]=id[j] ,则将 i d [ j ] id[j] id[j] 这个连通块的入度减 1 1 1,如果减成 0 0 0了,则将其插入拓扑排序的队列中。
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<vector>
using namespace std;
const int maxn = 25010, maxm = 150010, INF = 0x3f3f3f3f;
int h[maxn], e[maxm], ne[maxm], w[maxm], idx;
int N, M1, M2, S, d[maxn];
bool vis[maxn];
int id[maxn], in[maxn], bcnt;
vector<int> blocks[maxn];
void add(int a, int b, int c){
	e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
queue<int> que;
void dfs(int u) {
	blocks[bcnt].push_back(u);
	id[u] = bcnt;
	for (int i = h[u]; i != -1; i = ne[i]) {
		int v = e[i];
		if (!id[v]) dfs(v);
	}
}
typedef pair<int, int> P;
void dijkstra(int x) {
	priority_queue<P, vector<P>, greater<P> > pq;
	for (auto u : blocks[x]) pq.push(P(d[u], u));
	while (pq.size()) {
		auto p = pq.top(); pq.pop();
		int u = p.second, dis = p.first;
		if (vis[u]) continue;
		vis[u] = true;
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			if (d[v] > d[u] + w[i]) {
				d[v] = d[u] + w[i];
				if (id[u] == id[v]) pq.push(P(d[v], v));
			}
			if (id[u] != id[v] && --in[id[v]] == 0) que.push(id[v]);
		}
	}
}
void toposort() {
	fill(d, d + maxn, INF);
	for (int i = 1; i <= bcnt; i++) {
		if (in[i] == 0) que.push(i);
	}
	d[S] = 0;
	while (que.size()) {
		int x = que.front(); que.pop();
		dijkstra(x);
	}
}
int main() {
	memset(h, -1, sizeof h);
	scanf("%d%d%d%d", &N, &M1, &M2, &S);
	for (int i = 0; i < M1; i++) {
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c);
		add(b, a, c);
	}
	for (int i = 1; i <= N; i++) {
		if (!id[i]) {
			bcnt++;
			dfs(i);
		}
	}
	for (int i = 0; i < M2; i++) {
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c);
		in[id[b]]++;
	}
	toposort();
	for (int i = 1; i <= N; i++) {
		if (d[i] > INF / 2) printf("NO PATH\n");
		else printf("%d\n", d[i]);
	}
	return 0;
}

活用spfa:341. 最优贸易

  • 题意: n n n 个点 m m m 条边,任意两个点之间至多一条道路,有单向边也有双向边,每个点都有一个点权,要求找到两个结点 u u u v v v,使得 u u u 的点权 减去 v v v 的点权的结果最大,并且可以从1号点走到 u u u,从 u u u 走到 v v v,再从 v v v 走到 n n n 号点.
  • 这个可以从 d p dp dp 的角度思考问题。以 k k k 为分界点,买在 1 ∼ k 1\sim k 1k 中(在这之间寻找买入最小值 d m i n [ k ] dmin[k] dmin[k] ),卖在 k ∼ N k \sim N kN 中(在这之间寻找卖出最大值 d m a x [ k ] dmax[k] dmax[k]),不过这里的 k k k 也可以作为买入点或卖出点。
  • F [ x ] F[x] F[x] 为点权, D [ x ] D[x] D[x] 存储从起点到 x x x 的访问到的点权的最大值或最小值。建图以及建反图,可以用 SPFA 或者 Dijkstra 算法,把 d [ x ] + w ( x , y ) d[x]+w(x,y) d[x]+w(x,y) 换成 d [ x ] = min ⁡ { d [ x ] , p r i c e [ y ] } d[x] = \min\{d[x],price[y]\} d[x]=min{d[x],price[y]} 来更新。正反分别跑一遍最短路;最后枚举每个节点 x x x,用 F [ x ] − D [ x ] F[x] - D[x] F[x]D[x] 更新答案。
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int maxn = 100010, maxm = 2000010, INF = 0x3f3f3f3f;
int hs[maxn], ht[maxn], e[maxm], ne[maxm], w[maxn], idx;
int dmin[maxn], dmax[maxn], N, M;
bool vis[maxn];
void add(int a, int b, int type) 
{
	if (type) e[idx] = b, ne[idx] = ht[a], ht[a] = idx++;
	else e[idx] = b, ne[idx] = hs[a], hs[a] = idx++;
}
void spfa(int h[], int d[], int type) 
{
	queue<int> que;
	//spfa是不用将vis初始化的。因为队列为空的时候才会跳出while循环嘛。
	if (!type) {
		fill(d, d + maxn, INF);
		d[1] = w[1], vis[1] = true;
		que.push(1);
	}
	else {
	//这一步将d初始化为0或-INF都是对的。因为点权的最小值是1。
	//一开始我还以为是边权变成负数然后求最大值,然后发现不是这样。
	//因为这个不是在跑最短路,只是用了spfa的板子而已。
		fill(d, d + maxn, -INF);
		d[N] = w[N], vis[N] = true;
		que.push(N);
	}
	while (que.size()) {
		int u = que.front(); que.pop();
		vis[u] = false;
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			if (!type && d[v] > min(d[u], w[v]) || type && d[v] < max(d[u], w[v])) {
				if (!type) d[v] = min(d[u], w[v]);
				else d[v] = max(d[u], w[v]);
				if (!vis[v]) {
					vis[v] = true;
					que.push(v);
				}
			}
		}
	}
}
int main() {
	memset(hs, -1, sizeof hs);
	memset(ht, -1, sizeof ht);
	scanf("%d%d", &N, &M);
	for (int i = 1; i <= N; i++) scanf("%d", &w[i]);
	for (int i = 0; i < M; i++) {
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, 0), add(b, a, 1);
		if (c == 2) add(b, a, 0), add(a, b, 1);
	}
	spfa(hs, dmin, 0);
	spfa(ht, dmax, 1);
	int ans = 0;
	for (int i = 1; i <= N; i++) ans = max(dmax[i] - dmin[i], ans);
	printf("%d\n", ans);
	return 0;
}

拆点(分层图):1131. 拯救大兵瑞恩

  • 题意:整个迷宫被划分为 n ∗ m n*m nm 个单元。南北或东西方向相邻的 2 2 2 个单元之间可能互通,也可能有一扇锁着的门,或者是一堵不可逾越的墙。迷宫中有一些单元存放着钥匙,同一个单元可能存放多把钥匙,并且所有的门被分成 P P P 类,打开同一类的门的钥匙相同,不同类门的钥匙不同。求从 ( 1 , 1 ) (1,1) (1,1)出发 到 ( n , m ) (n,m) (n,m) 的最少移动距离。

  • 1 ≤ n , m , p ≤ 10 1 \le n,m,p \le 10 1n,m,p10,墙和门的总数最多150个.

  • d p dp dp 角度思考,设 d ( x , y , s t a t e ) d(x, y, state) d(x,y,state) 就是在 ( x , y ) (x, y) (x,y) 这个点,拥有钥匙状态时 s t a t e state state (因为只有10把钥匙🔑,所以用二进制表示很方便),这个值储存的是最短步数。可以变成两种情况:

  1. 捡钥匙: d ( x , y , s t a t e ) = min ⁡ { d ( x , y , s t a t e ) , d ( x , y , s t a t e ∣ k e y ) } d(x, y, state) = \min\{d(x, y, state), d(x, y, state | key)\} d(x,y,state)=min{d(x,y,state),d(x,y,statekey)}
  2. 移动(没有门或墙,有门且有匹配的钥匙): d ( a , b , s t a t e ) = max ⁡ { d ( x , y , s t a t e ) + 1 , d ( a , b , s t a t e ) } d(a, b, state) = \max\{d(x, y, state) + 1, d(a, b, state)\} d(a,b,state)=max{d(x,y,state)+1,d(a,b,state)}
  • 所以状态的转移可以是走 1 1 1 步,也可以是走 0 0 0 步。就可以变成双端队列广搜了。于是处理步骤如下:
  1. 建图:先把墙和门的信息保存在 s e t set set 里面,然后用邻接表建图。用 w [ i ] w[i] w[i] 表示边的信息, 0 0 0 为没有门也没有墙, > 0 > 0 >0 时为需要哪个钥匙,这里多说一句,输入0时表示有墙,但是有墙的时候是不建立边的。
  2. 先捡钥匙,此过程就是走权重为 0 0 0 的边。第二步向四个方向移动,就是走权重为 1 1 1 的边。然后答案就出来了。
  • 小心左移右移别搞混。
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<deque>
#include<set>
using namespace std;
const int maxn = 110, maxm = 410, maxp = 1 << 10, INF = 0x3f3f3f3f;
int h[maxn], e[maxm], ne[maxm], w[maxm], idx;
bool vis[maxn][maxp];
typedef pair<int, int> P;
set<P> s;
int g[15][15], N, M, T, K, S, key[maxn], d[maxn][maxp];
void add(int a, int b, int c) {
	e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
deque<P> dq;
int dx[] = { 1, -1, 0, 0 }, dy[] = { 0, 0, 1, -1 };
void build() {
	for (int x = 1; x <= N; x++) {
		for (int y = 1; y <= M; y++) {
			for (int i = 0; i < 4; i++) {
				int nx = x + dx[i], ny = y + dy[i];
				if (nx < 1 || nx > N || ny < 1 || ny > M) continue;
				int a = g[x][y], b = g[nx][ny];
				//没有墙或门,则记边权为0。
				if (!s.count(P(a, b))) add(a, b, 0);
			}
		}
	}
}
int bfs() {
	for (int i = 1; i <= N * M; i++) {
		fill(d[i], d[i] + maxp, INF);
	}
	d[1][0] = 0;
	dq.push_back(P(1, 0));
	while (dq.size()) {
		auto p = dq.front(); dq.pop_front();
		int u = p.first, state = p.second;
		if (vis[u][state]) continue;
		vis[u][state] = true;
		if (u == N * M) return d[u][state];
		if (key[u]) {
			int new_state = state | key[u];
			if (d[u][new_state] > d[u][state]) {
				d[u][new_state] = d[u][state];
				dq.push_front(P(u, new_state));
			}
		}
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			if (d[v][state] > d[u][state] + 1) {
				if (w[i] && !((state >> w[i] - 1) & 1)) continue;
				d[v][state] = d[u][state] + 1;
				dq.push_back(P(v, state));
			}
		} 
	}
	return -1;
}
int main() {
	memset(h, -1, sizeof h);
	int id = 1;
	scanf("%d%d%d%d", &N, &M, &T, &K);
	for (int i = 1; i <= N; i++) {
		for (int j = 1; j <= M; j++) g[i][j] = id++;
	}
	
	for (int i = 0; i < K; i++) {
		int x1, y1, x2, y2, c;
		scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &c);
		int a = g[x1][y1], b = g[x2][y2];
		s.insert(P(a, b)), s.insert(P(b, a));
		if (c) add(a, b, c), add(b, a, c);
	}
	scanf("%d", &S);
	for (int i = 0; i < S; i++) {
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		int u = g[a][b];
		key[u] |= 1 << c - 1;
	}
	build();
	printf("%d\n", bfs());
	return 0;
}

最短路的数量:1134. 最短路计数

  • 给出一个 N N N 个顶点 M M M 条边的无向无权图,顶点编号为 1 1 1 N N N. 问从顶点 1 1 1 开始,到其他每个点的最短路有几条. 答案对 100003 100003 100003 取模.

  • D A G DAG DAG 的起点到终点的道路数量很容易算,因此如何构建出一个最短路拓扑图就是关键。并且,构建出最小路的图(只保留最短路含的边),那么构造出的图必然没有环,可用反证法证明。所以一定具有拓扑序。无向边边权不可能是0,否则无解(路径数量无穷大).

  • BFS 与 Dijkstra 本身就是按照最短路树往下跑的(每个点只会出队1次),也就是说出队顺序具有拓扑序的。但是 SPFA 出队顺序是不具备拓扑序的(可能入队出队多次)。因此,只能用 BFS 和 Dijkstra。但是,如果又负权边呢?大雪菜说,可以先用 SPFA 把拓扑图构建出来。

  • 注意这个地方,就是用 d [ u ] + w ( u , v ) d[u] + w(u, v) d[u]+w(u,v) 更新 d [ v ] d[v] d[v] 的时候,如果更新的话,就说明之前到达 v v v 的路径不是最短路,就这样赋值: c n t [ v ] = c n t [ u ] cnt[v] = cnt[u] cnt[v]=cnt[u];若 d [ v ] = = d [ u ] + w ( u , v ) d[v] == d[u] + w(u, v) d[v]==d[u]+w(u,v) 的时候,当前到达 u u u 的路径已经是最小值(Dijkstra 从堆中出来的结点已经是求出最短路的结点),那么可以 c n t [ v ] + = c n t [ u ] cnt[v] += cnt[u] cnt[v]+=cnt[u].

#include<bits/stdc++.h>
using namespace std;
const int maxn = 100010, maxm = 400010, INF = 0x3f3f3f3f, mod = 100003;
int h[maxn], ne[maxm], e[maxm], idx;
int N, M, d[maxn], cnt[maxn];
void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void bfs() {
	fill(d, d + maxn, INF);
	d[1] = 0, cnt[1] = 1;
	queue<int> que;
	que.push(1);
	while (que.size()) {
		int u = que.front(); que.pop();
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			if (d[v] > d[u] + 1) {
				d[v] = d[u] + 1;
				cnt[v] = cnt[u];
				que.push(v);
			}
			else if (d[v] == d[u] + 1) {
				cnt[v] = (cnt[u] + cnt[v]) % mod;
			}
		}
	}
}
int main() {
	memset(h, -1, sizeof h);
	scanf("%d%d", &N, &M);
	for (int i = 0; i < M; i++) {
		int a, b;
		scanf("%d%d", &a, &b);
		add(a, b), add(b, a);
	}
	bfs();
	for (int i = 1; i <= N; i++) printf("%d\n", cnt[i]);
	return 0;
}

最短、次短路径计数:383. 观光

  • 题意:给一个有向有权图,求 最短路的数量 与 比最短路长度多1的路径数量 之和
  • 到达v次短路两种:到达u的最短路加上 u -> v,或是到达u的次短路加上 u -> v。
  • 第一次从优先队列中 pop 出来,一定是最优解了,不会再被更新,不管是最短路还是次短路。因此,结构体 P 中存储的值和数组中保存的值一定是一致的。因此,后面更新答案的时候不管写 d [ u ] [ t ] , c n t [ u ] [ t ] d[u][t], cnt[u][t] d[u][t],cnt[u][t] 还是写 p . d , p . t y p e p.d,p.type p.d,p.type 都是一样的。
  • 奆鶸,一定要注意优先队列的重载,居然tm反过来了
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int maxn = 1010, maxm = 100010, INF = 0x3f3f3f3f;
int h[maxn], e[maxm], ne[maxm], w[maxm], idx;
int N, M, st, ed, d[maxn][2], cnt[maxn][2];
bool vis[maxn][2];

struct P 
{
	int u, type, d;
	bool operator > (const P& rhp)const {
		return d > rhp.d;
	}
};

void add(int a, int b, int c) 
{
	e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

int bfs() 
{
	for (int i = 1; i <= N; i++) 
    {
		for (int j = 0; j < 2; j++) d[i][j] = INF;
	}
	priority_queue<P, vector<P>, greater<P>> que;
	que.push({ st, 0, 0 });
	d[st][0] = 0, cnt[st][0] = 1;
	while (que.size()) 
    {
		auto p = que.top(); que.pop();
		int u = p.u, t = p.type, dis = p.d; int count = cnt[u][t];
		if (vis[u][t]) continue;
		vis[u][t] = true;
		for (int i = h[u]; i != -1; i = ne[i]) 
        {
			int v = e[i];
			//到达一个节点的次短路不可能比到达前驱节点的最短路还要早。
			//因此下一个if最后更新的时候,一定是在更新最短距离。对应的type一定是0.
			if (d[v][0] > dis + w[i]) 
            {
				//到达u的最短路加上 u -> v
				d[v][1] = d[v][0], cnt[v][1] = cnt[v][0];
				que.push({ v, 1, d[v][1] });
				d[v][0] = dis + w[i], cnt[v][0] = count;
				que.push({ v, 0, d[v][0] });
			}
			else if (d[v][0] == dis + w[i]) cnt[v][0] += count;
			else if (d[v][1] > dis + w[i]) 
            {
				//到达u的次短路加上 u -> v
				d[v][1] = dis + w[i], cnt[v][1] = count;
				que.push({ v, 1, d[v][1] });
			}
			else if (d[v][1] == dis + w[i]) cnt[v][1] += count;
		}
	}
	int res = cnt[ed][0];
	if (d[ed][0] + 1 == d[ed][1]) res += cnt[ed][1];
	return res;
}
int main() {
	int T;
	scanf("%d", &T);
	while (T--) {
		memset(h, -1, sizeof h);
		memset(vis, false, sizeof vis);
		memset(cnt, 0, sizeof cnt);
		idx = 0;
		scanf("%d%d", &N, &M);
		for (int i = 0; i < M; i++) {
			int a, b, c;
			scanf("%d%d%d", &a, &b, &c);
			add(a, b, c);
		}
		scanf("%d%d", &st, &ed);
		printf("%d\n", bfs());
	}
	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/417740.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Qt Quick - Dialog

Dialog使用总结一、概述二、对话框标题和按钮三、模态对话框四、非模态的对话框五、standardButtons 属性一、概述 对话框是一种弹出式对话框&#xff0c;主要用于短期任务和与用户的简短交流。与ApplicationWindow和Page类似&#xff0c;Dialog被组织成三个部分:header、cont…

PyTorch深度学习实战 | 基于线性回归、决策树和SVM进行鸢尾花分类

鸢尾花数据集是机器学习领域非常经典的一个分类任务数据集。它的英文名称为Iris Data Set&#xff0c;使用sklearn库可以直接下载并导入该数据集。数据集总共包含150行数据&#xff0c;每一行数据由4个特征值及一个标签组成。标签为三种不同类别的鸢尾花&#xff0c;分别为&…

【AI帮我写代码,上班摸鱼不是梦】手摸手图解CodeWhisperer的安装使用

IDEA插件 除了借助ChatGPT通过问答的方式生成代码&#xff0c;也可以通过IDEA插件在写代码是直接帮助我们生成代码。 目前&#xff0c;IDEA插件有CodeGeeX、CodeWhisperer、Copilot。其中&#xff0c;CodeGeeX和CodeWhisperer是完全免费的&#xff0c;Copilot是收费的&#x…

数据分析:麦当劳食品营养数据探索并可视化

系列文章目录 作者&#xff1a;i阿极 作者简介&#xff1a;Python领域新星作者、多项比赛获奖者&#xff1a;博主个人首页 &#x1f60a;&#x1f60a;&#x1f60a;如果觉得文章不错或能帮助到你学习&#xff0c;可以点赞&#x1f44d;收藏&#x1f4c1;评论&#x1f4d2;关注…

c++模板整理

目录 一.泛型编程​​​​​​​ 二.函数模板 2.1 函数模板概念 2.2函数模板格式 2.3 函数模板的原理 2.4 函数模板的实例化 2.5 模板参数的匹配原则 三.类模板 3.1 类模板的定义格式 3.2 类模板的实例化 3.3模板类 一.泛型编程​​​​​​​ 如何实现一个通用的交…

【前端之旅】快速上手Echarts

一名软件工程专业学生的前端之旅,记录自己对三件套(HTML、CSS、JavaScript)、Jquery、Ajax、Axios、Bootstrap、Node.js、Vue、小程序开发(Uniapp)以及各种UI组件库、前端框架的学习。 【前端之旅】Web基础与开发工具 【前端之旅】手把手教你安装VS Code并附上超实用插件…

「高并发业务必读」深入剖析 Java 并发包中的锁机制

故事 程序员小张&#xff1a; 刚毕业&#xff0c;参加工作1年左右&#xff0c;日常工作是CRUD 架构师老李&#xff1a; 多个大型项目经验&#xff0c;精通各种屠龙宝术&#xff1b; 小张和老李一起工作已有数月&#xff0c;双方在技术上也有了很多的交流&#xff0c;但是却总是…

GB28181 协议 SIP

2、注册信令 2.1基本注册 2.1.1 抓包过程 2.1.2 详细步骤 2.1.2.1、REGISTER REGISTER sip:34020000002000000001192.168.9.186:15060 SIP/2.0Via: SIP/2.0/TCP 192.168.9.186:42860;rport;branchz9hG4bK1557586049From: <sip:30514805331320000140192.168.9.186:5060>…

手写Spring框架-前奏-反射获取Annotation

目录 所谓反射 反射机制的作用 反射依赖reflect和Class 反射依赖的Class Class类的特点 获取Class对象的三种方式 获取类的构造方法并使用 获取类的成员变量并使用 获取类的成员方法并使用 问题引入 解析类的注解 解析成员变量的注解标签 解析方法上的注解 注解获…

Java类加载

类加载的时机 一个类型从被加载到虚拟机内存中开始&#xff0c;到卸载出内存为止&#xff0c;它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用和卸载七个阶段。其中验证、准备、解析三个阶段统称为连接。 图中加载、验证、准备、初始化和卸载这五个阶段的顺序是…

CDGP数据治理专家认证含金量如何?值得考一个吗?

CDGP&#xff08;Certified Data Governance Professional&#xff09;数据治理专家认证的含金量非常高。该认证证明了持有人拥有数据治理方面的专业知识和技能&#xff0c;能够有效地管理和保护组织的数据资产。 CDGP认证考试内容涵盖数据治理的各个方面&#xff0c;包括数据…

看这家在线教育企业如何通过DHTMLX Scheduler,实现培训管理系统优化

“我们公司目前有一套培训管理系统&#xff0c;用于管理培训学员。目前学员越来越多&#xff0c;老旧的系统已经没法满足需求&#xff0c;导致我们经常需要手动记录学员出勤培训情况&#xff0c;除此之外&#xff0c;系统课程安排只展示时间&#xff0c;没法展示诸如主题&#…

macOS Big Sur 11.7.6 (20G1231) 正式版 ISO、PKG、DMG、IPSW 下载

本站下载的 macOS 软件包&#xff0c;既可以拖拽到 Applications&#xff08;应用程序&#xff09;下直接安装&#xff0c;也可以制作启动 U 盘安装&#xff0c;或者在虚拟机中启动安装。另外也支持在 Windows 和 Linux 中创建可引导介质。 2023 年 4 月 10 日&#xff08;北京…

【Vue全家桶】Pinia状态管理

【Vue全家桶】Pinia状态管理 文章目录【Vue全家桶】Pinia状态管理写在前面一、认识Pinia1.1 认识Pinia1.2 为什么使用Pinia&#xff1f;二、 Store2.1 定义Store2.2 Option对象2.3 setup函数2.4 使用定义的Store三、Pinia核心概念State3.1 定义State3.2 操作State3.3 使用选项式…

基于小生境粒子群优化算法的考虑光伏波动性的主动配电网有功无功协调优化(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密…

C语言基础——指针

文章目录一、指针1.指针的意义2.指针类型表示3.一些操作3.1打印1个变量地址3.2通过地址查看改地址的内容以及修改改地址的内容3.3操作某个空间 -- 4个字节,给他赋值为100&#xff0c;只知道该空间的地址0x8000 00004.指针变量的定义5.指针类型的大小6.指针变量的使用6.1 指针变…

python数据分析-matplotlib折线图知识总结01

python绘图库matplotlib的知识总结一.matplotlib是什么二.matplotlib的安装与导入三.matplotlib的常用函数四.matplotlib绘制折线图的使用方法1.设置图形大小2. 利用数据绘图3.调整x,y轴的刻度,旋转角度,显示描述信息,绘制网格,添加图例4.图形的样式5.绘制多条折线6.显示绘制的…

python知识记录:灵活使用numpy提高python数据分析效率!

NumPy是Python语言的一个第三方库&#xff0c;其支持大量高维度数组与矩阵运算。 作为python科学计算领域的三剑客之一&#xff0c;numpy在数据分析处理方面有着独特的魅力&#xff01; numpy模块的出现更多的是在数组处理的操作上面&#xff0c;并且支持和python常用的数据结…

Transformer在时序预测的应⽤第一弹——Autoformer

Transformer在时序预测的应⽤第一弹——Autoformer 原文地址&#xff1a;Autoformer: Decomposition Transformers with Auto-Correlation for Long-Term Series Forecasting&#xff08;NIPS 2021&#xff09; 做长时间序列的预测 Decomposition把时间序列做拆分&#xff0c…

目标检测——YOLOv7(十三)

简介&#xff1a; 继美团发布YOLOV6之后&#xff0c;YOLO系列原作者也发布了YOLOV7。主要从两点进行模型的优化&#xff1a;模型结构重参化和动态标签分配。 YOLOv7的特点是快&#xff01;相同体量下比YOLOv5精度更高&#xff0c;速度快120%&#xff0c;比YOLOX快180%。 Github…