搜索与图论
- 一、搜索
- 1、分治
- 矩阵二分 / 普通二分 模板
- 万能二分模板
- 2、DFS
- 例题1、AcWing 842. 排列数字
- 例题2、AcWing 843. n-皇后问题
- 3、BFS
- 例题1、AcWing 844. 走迷宫 (入门经典)
- 例题2、到达 "12345678x":AcWing 845. 八数码
- 二、图论
- 写在前面
- 1、图的存储
- 2、图的类型
- 3、基于DFS的图算法
- 模板框架
- 树与图的深度优先遍历 AcWing846. 树的重心
- 4、基于BFS的图算法
- 模板框架
- bfs求无权最短路:AcWing 847. 图中点的层次
- 求拓扑序列 :AcWing 848. 有向图的拓扑序列
- 5、最小生成树
- (1)朴素版 prim 算法 - 稠密图
- (2)Kruskal 算法 - 稀疏图
- 6、最短路算法
- (1) 单源最短路
- ① 所有边权重都是正数
- Ⅰ、 朴素 Dijkstra
- Ⅱ、 堆优化版 Dijkstra
- ② 存在负权边
- Ⅰ、 bellman-ford(有边数限制的最短路)
- Ⅱ、spfa
- AcWing 851. spfa求最短路
- AcWing 852. spfa判断负环
- (2) 多源汇最短路 - Floyd
- 7、二分图
- (1) 判定二分图 : 染色法
- (2) 求最大匹配 : 匈牙利算法
- 8、欧拉路
- 9、最大流
- 10、其他
- (1) 树的直径 - 树上距离最远的两点间的距离
- 模板题 : AcWing 1207. 大臣的旅费
一、搜索
1、分治
矩阵二分 / 普通二分 模板
原题链接:https://leetcode-cn.com/problems/sorted-matrix-search-lcci/
① 矩阵二分 :
/*
矩阵二分:从左下角开始二分,如果小了说明需要把列向右移,如果大了,说明需要把行向上移
*/
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int r = matrix.size();
if (r == 0) return false;
int c = matrix[0].size();
if (c == 0) return false;
int i = r - 1, j = 0;
while (i >= 0 && j < c) {
if (matrix[i][j] == target) return true;
else if (matrix[i][j] < target) ++ j;
else -- i;
}
return false;
}
};
② 暴力二分:
/*
暴力二分:直接二分查每一行,速度慢
*/
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int r = matrix.size();
if (r == 0) return false;
int c = matrix[0].size();
if (c == 0) return false;
for (int i = 0; i < r; ++ i) {
if (matrix[i][0] <= target && matrix[i][c - 1] >= target) {
if (judge(matrix, i, target)) return true;
}
}
return false;
}
//普通二分模板:在matrix[row]中判断是否存在target
bool judge(vector<vector<int>>& matrix, int row, int target) {
int l = 0, r = matrix[row].size();
while (l <= r) {
int mid = l + (r - l) / 2;
if (matrix[row][mid] == target) return true;
else if (matrix[row][mid] < target) l = mid + 1;
else r = mid - 1;
}
return false;
}
};
万能二分模板
AcWing 789. 数的范围
原题链接:https://www.acwing.com/problem/content/description/791/
思路
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int a[N], n, m;
int get_l(int num) {
int l = 0, r = n - 1;
while (l < r) {
int mid = l + r >> 1;
if (a[mid] >= num) r = mid;
else l = mid + 1;
}
//就算return r也是可以的,因为l == r才会跳出循环
if (l < n && l >= 0 && a[l] == num) return l;
else return -1;
}
int get_r(int num) {
int l = 0, r = n - 1;
while (l < r) {
//check()为true是l设置为mid,则需要+1防止死循环
int mid = l + r + 1 >> 1;
if (a[mid] <= num) l = mid;
else r = mid - 1;
}
//同上,就算return r也是可以的
if (l < n && l >= 0 && a[l] == num) return l;
else return -1;
}
int main() {
cin >> n >> m;
for (int i = 0; i < n; ++ i) cin >> a[i];
while (m --) {
int num;
cin >> num;
cout << get_l(num) << " " << get_r(num) << endl;
}
return 0;
}
2、DFS
例题1、AcWing 842. 排列数字
原题链接:https://www.acwing.com/problem/content/description/844/
#include<iostream>
using namespace std;
int ans[10], n;
bool st[10];
void dfs(int idx) { // idx - 第几个数
if (idx > n) {
for (int i = 1; i <= n; ++ i) printf("%d ", ans[i]);
printf("\n");
}
else {
for (int i = 1; i <= n; ++ i) {
if (!st[i]) {
st[i] = true;
ans[idx] = i;
dfs(idx + 1);
st[i] = false; //还原
}
}
}
}
int main() {
scanf("%d", &n);
dfs(1);
return 0;
}
例题2、AcWing 843. n-皇后问题
原题链接:https://www.acwing.com/problem/content/845/
注意
在处理对角线的时候,我们可以借助一次函数,即把两条对角线抽相成 y = ± x + b,移项得到:b = y - x 和 b = y + x,那么这里我们可以用截距 b 去判断两个皇后会不会被放在同一条对角线上。
当然,因为 y - x 可能为负,所以我们可以用 n + y - x 表示该截距 b,因为我们需要的只是判断是否在同一条对角线上,至于 b 的值是正是负不影响结果,而加上 n 后就可以直接用拿 b + n 映射到索引上,方便写代码。
代码
#include<iostream>
using namespace std;
int n;
bool col[10], p[20], unp[20], ans[10][10];
void dfs(int row) { //递归每一行
if (row > n) {
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) {
if (ans[i][j]) printf("Q");
else printf(".");
}
printf("\n");
}
printf("\n");
}
else {
for (int c = 1; c <= n; ++ c) { // 循环判断每一列是否能放皇后
// 该列以及两个对角线可以放皇后则进入if
if (!col[c] && !p[n + c - row] && !unp[row + c]) {
col[c] = p[n + c - row] = unp[row + c] = true;
ans[row][c] = true;
dfs(row + 1);
//记得还原
col[c] = p[n + c - row] = unp[row + c] = false;
ans[row][c] = false;
}
}
}
}
int main() {
scanf("%d", &n);
dfs(1);
return 0;
}
3、BFS
例题1、AcWing 844. 走迷宫 (入门经典)
原题链接:https://www.acwing.com/problem/content/846/
思路
因为每走一步的权重是 1 ,那么在 bfs 的模式下,最先到达终点的那条路必定是最短路,而且路径的长短 == bfs 的第一个 while 循环次数 - 1。
代码
#include <iostream>
using namespace std;
const int N = 110;
int n, m, g[N][N], ans;
bool st[N][N]; //判断坐标(x,y)是否已经走过
int hh = 0, tt = -1; // 模拟队列的头指针和尾指针
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1}; //向上下左右走
pair<int, int> q[N * N]; //队列
void bfs() {
while (tt - hh + 1 != 0) {
int length = tt - hh + 1;
while (length --) {
pair<int, int> temp = q[hh ++];
if (temp.first == n && temp.second == m) return; //找到终点
for (int i = 0; i < 4; ++ i) {
int x = temp.first + dx[i], y = temp.second + dy[i];
if (x > 0 && x <= n && y > 0 && y <= m && !st[x][y] && !g[x][y]) {
q[++ tt] = make_pair(x, y);
st[x][y] = true;
}
}
}
++ ans;
}
}
int main() {
//freopen("D:\\in.txt", "r", stdin);
//freopen("D:\\out.txt", "w", stdout);
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= m; ++ j) {
scanf("%d", &g[i][j]);
}
}
q[++ tt] = make_pair(1, 1);
bfs();
printf("%d\n", ans);
return 0;
}
例题2、到达 “12345678x”:AcWing 845. 八数码
原题链接:https://www.acwing.com/problem/content/847/
思路
对于一个初始状态,x 有上下左右四种走法,而这四种走法在当下也只需要交换一次位置即可得到,那么将其中合法的走法入队(如果是曾经走过的状态就不必入队了),再判断下一波,直到走到最终态 “12345678x” 或者队列为空(意味着无路可走了)。
代码
#include<iostream>
#include<string>
#include<queue>
#include<unordered_map>
using namespace std;
string start, ending = "12345678x";
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
int bfs() {
queue<string> q;
unordered_map<string, int> book;
q.push(start);
int ans = 0;
while (!q.empty()) {
int length = q.size();
while (length --) {
string temp = q.front();
q.pop();
if (temp == ending) return ans;
int idx = temp.find('x'); //返回'x'的下标
int x = idx / 3, y = idx % 3; //获得在3*3的横纵坐标
for (int i = 0; i < 4; ++ i) { //上下左右四个方向
int a = x + dx[i], b = y + dy[i];
if (a >= 0 && a < 3 && b >= 0 && b < 3) { //合法性
swap(temp[idx], temp[a * 3 + b]); //必须先判断a、b,不然a*3+b可能会非法访问
// 只有没经历过的字符串才需要添加进去
if (book.find(temp) == book.end()) {
book[temp] = 1;
q.push(temp);
}
swap(temp[idx], temp[a * 3 + b]); //还原
}
}
}
++ ans;
}
return -1;
}
int main() {
char op[3];
for (int i = 0; i < 9; ++ i) {
scanf("%s", op);
start += op[0];
}
cout << bfs() << endl;
return 0;
}
二、图论
写在前面
可以说,树是一种特殊的图,而无向图也是一种特殊的有向图,所以对于一些树和无向图的储存和遍历,可以抽象成是对特殊有向图的遍历。
1、图的存储
- 邻接矩阵:适合稠密图(m ~ n ^ 2)
int g[N][N]; //g[a][b] 表示从点a出发到点b的边
- 邻接表:适合稀疏图(m ~ n )
// 对于每个点k,开一个单链表,存储k所有可以走到的点
//h[k]存储这个单链表的头结点
int h[N], e[N], ne[N], idx = 1;
// 添加一条边a->b
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
//ne[idx] == 0 表示第idx个结点的next指向null
}
2、图的类型
3、基于DFS的图算法
模板框架
//时间复杂度 O(n+m)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);
}
}
树与图的深度优先遍历 AcWing846. 树的重心
原题链接:https://www.acwing.com/problem/content/description/848/
思路
因为该图是无向图,而且没有说明一定是以 1 为根节点,所以换句话说,把任一个结点拎起来,都可以自然垂下成为一棵树。换而言之,在 main 函数里面,dfs()里面传的参数可以是 1 到 n 之间的任意一个,得到的 ans 也会是一样的。
因为我们最终想要的 ans 是去掉树的重心后,数量最少的所有连通分支中最多结点的连通分支。比如说,假如上面以 1 为根的树的重心是 4,那么去掉重心后就有 3 个连通分支如下图:
而此时,连通分支结点数 res 最多是 5 个结点,所以 ans 就是要求最小的 res。
分析完题目要求,那么这道题的做法就是去 dfs,把每一个结点都先假设成是树的重心,进而去求去掉这个重心的数量最多的连通分支,最后对每一个 res 求 min 就得到了 ans。
所以这道题的关键就在于怎么求在重心 u 的前提下数量最多的连通分支。最直接的做法就是去求 max(所有连通分支的结点数),换句话说,如果知道所有连通分支的结点数,直接求 max 即可。
比如说,现在要来求以 4 为重心的各个连通分支的结点数,那么分别为:dfs(3)、dfs(6)、n - dfs(3) - dfs(6),而所谓的 n - dfs(3) - dfs(6)虽然结点 1 也是 4 的一个分支,但是因为是从 1 走向 4 ,所以 1 一定在 4 之前被遍历到,所以 1 一定会在 if ( ! st [ j ] ) 时排除在 if 之外,不会重复 dfs (1),这符合了 dfs 时每个点一般只走一次的原则。
综上。
代码
#include<iostream>
using namespace std;
const int N = 100010, M = 200010; //边数最大可达到 2N
int h[N], e[M], ne[M], idx = 1; //邻接表表示树
bool st[N]; //判断点是否已经遍历过
int ans = N, n; // ans最多也只能达到n,所以初始化为大于等于n即可
//增加a到b的边
void add(int a, int b) {
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
//对点u深搜,返回以u为根的树的结点总数
int dfs(int u) {
st[u] = true; //标记点u已经走过
//res - 假设u为重心,结点数最多的连通分量的结点数目
//sum - 以u为根节点的树的结点总数
int res = 0, sum = 1;
for (int i = h[u]; i != 0; i = ne[i]) { //迭代得到所有和u相连的结点
int j = e[i]; //获得在树中的编号
if (!st[j]) { //如果该结点还没走过
int s = dfs(j); //获得该连通分量的结点总数
sum += s; //累加以u为根结点的结点数
res = max(res, s); //求最多结点数的连通分量
}
}
res = max(res, n - sum);
ans = min(ans, res);
return sum;
}
int main() {
scanf("%d", &n);
int a, b;
for (int i = 1; i < n; ++ i) {
scanf("%d%d", &a, &b);
add(a, b); //无向图,所以相当于两条边
add(b, a);
}
dfs(4); // 1 ~ n皆可,这表示这棵树以哪一个为根节点
printf("%d\n", ans);
return 0;
}
4、基于BFS的图算法
模板框架
//树与图的广度优先遍历
queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);
while (q.size())
{
int t = q.front();
q.pop();
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true; // 表示点j已经被遍历过
q.push(j);
}
}
}
bfs求无权最短路:AcWing 847. 图中点的层次
原题链接:https://www.acwing.com/problem/content/submission/849/
思路
一圈圈往外走。
代码
#include<iostream>
#include<queue>
using namespace std;
const int N = 100010;
int g[N], e[N], ne[N], idx = 1; //邻接表
bool book[N]; //判断该点是否已经走过
//插入从a走到b的边
void insert(int a, int b) {
e[idx] = b;
ne[idx] = g[a];
g[a] = idx ++;
}
int bfs(int n) {
queue<int> q;
q.push(1); //从1出发
book[1] = true; //标记已经走过点1
int ans = 0;
while (!q.empty()) {
int length = q.size();
++ ans; //走了几层就说明最短路是几
while (length --) {
int a = q.front();
q.pop();
int aa = g[a];
while (aa != 0) {
if (e[aa] == n) return ans; //走到n,此时路程最短
else {
if (!book[e[aa]]) { //没走过的点才放进队列
book[e[aa]] = true;
q.push(e[aa]);
}
}
aa = ne[aa];
}
}
}
return -1; //走不到n
}
int main() {
int n, m, a, b;
scanf("%d%d", &n, &m);
for (int i = 0; i < m; ++ i) {
scanf("%d%d", &a, &b);
insert(a, b);
}
printf("%d\n", bfs(n));
return 0;
}
求拓扑序列 :AcWing 848. 有向图的拓扑序列
有向无环图必定可为拓扑图。
有环不可能是拓扑图(因为删减到最后是一个环,环没有一个入度为0的结点可以突破)。
无向图不可能是拓扑图(无向相当于有环)。
/*
入度为0才会被放进ans,如果搜索时出现a指向b,而b放进过ans里面,这是不可能存在的情况。
因为ans里面都是入度为0的点,为a指向b说明b入度大于0,自相矛盾
所以其实不用判断放入队列时,该点是否走过
*/
#include <bits/stdc++.h>
using namespace std;
#define getlen(array) {return (sizeof(array) / sizeof(array[0]));}
#define ll long long
#define ull unsigned long long
#define PII pair<ll, ll>
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rim int m; scanf("%d", &m)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf
const int INF = 0x3f3f3f3f;
const int N = 100010;
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
int ans[N], d[N], k = 0;
int h[N], ne[N], e[N], idx = 1;
void insert(int a, int b) {
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
void bfs(int n) {
queue<int> q;
//预先把入度为0的放进去
for (int i = 1; i <= n; ++ i) {
if (!d[i]) {
ans[k ++] = i;
q.push(i);
}
}
//一圈一圈往下走
while (!q.empty()) {
int num = q.front();
q.pop();
for (int i = h[num]; i != -1; i = ne[i]) {
int a = e[i];
-- d[a];
if (!d[a]) {
q.push(a);
ans[k ++] = a;
}
}
}
}
int main() {
//freopen("D:\\in.txt", "r", stdin);
//freopen("D:\\out.txt", "w", stdout);
rin;
rim;
MEM(h, -1);
for (int i = 0; i < m; ++ i) {
rit;
ria;
insert(t, a);
++ d[a];
}
bfs(n);
if (k == n) {
for (int i = 0; i < n; ++ i) {
cout << ans[i] << " ";
}
}
else cout << -1;
return 0;
}
5、最小生成树
(1)朴素版 prim 算法 - 稠密图
AcWing 858. Prim算法求最小生成树
原题链接:https://www.acwing.com/problem/content/description/860/
#include<bits/stdc++.h>
using namespace std;
const int N = 510;
const int INF = 0x3f3f3f3f;
int n, m;
//dis[i]表示i点所能确定的长度最小的边,不同于最短路里到起始点的距离
int dis[N], g[N][N];
bool st[N];
int prim() {
dis[1] = 0; //一个点本身没有距离
int ans = 0; //最终生成树的权重和
for (int i = 0; i < n; ++ i) {
int t = -1; //每一次找到还未确定的点里面权重最小的点
for (int j = 1; j <= n; ++ j) {
if (!st[j] && (t == -1 || dis[j] < dis[t])) {
t = j;
}
}
if (dis[t] == INF) return INF; //说明不是连通图
st[t] = true; //标记为已访问
ans += dis[t];
//更新点集
for (int j = 1; j <= n; ++ j) {
dis[j] = min(dis[j], g[t][j]);
}
}
return ans;
}
int main() {
cin >> n >> m;
//初始化
memset(dis, INF, sizeof dis);
memset(g, INF, sizeof g);
for (int i = 0; i < m; ++ i) {
int a, b, w;
cin >> a >> b >> w;
g[a][b] = g[b][a] = min(w, g[a][b]); //无向图 + 有重边
}
int ans = prim();
if (ans == INF) cout << "impossible";
else cout << ans;
return 0;
}
(2)Kruskal 算法 - 稀疏图
AcWing 859. Kruskal算法求最小生成树
原题链接:https://www.acwing.com/problem/content/description/861/
#include<bits/stdc++.h>
using namespace std;
const int N = 100010, M = 200010;
//表示从a到b权重为w的边
struct edge{
int a, b, w;
}es[M];
int n, m;
int p[N]; //表示图中节点根属于哪一个集合,其实就是并查集里的father
//找到根节点
int find(int a) {
if (p[a] != a) return p[a] = find(p[a]);
else return a;
}
bool cmp(edge a, edge b) {
return a.w < b.w;
}
void kruskal() {
for (int i = 1; i <= n; ++ i) p[i] = i; //初始化根节点
sort(es, es + m, cmp); //按照权重从小到大排序
int ans = 0, cnt = 1; //ans - 生成树权重和, cnt - 已经加入几个结点
for (int i = 0; i < m; ++ i) {
int a = es[i].a, b = es[i].b, w = es[i].w;
int x = find(a), y = find(b);
if (x != y) {
++ cnt;
ans += w;
p[x] = y; // 不可以是p[a] = y
}
}
if (cnt < n) cout << "impossible";
else cout << ans;
}
int main() {
cin >> n >> m;
for (int i = 0; i < m; ++ i) {
int a, b, w;
cin >> a >> b >> w;
es[i] = {a, b, w};
}
kruskal();
return 0;
}
6、最短路算法
(1) 单源最短路
① 所有边权重都是正数
Ⅰ、 朴素 Dijkstra
AcWing 849. Dijkstra求最短路 I
原题链接 : https://www.acwing.com/problem/content/851/
#include <bits/stdc++.h>
using namespace std;
#define getlen(array) {return (sizeof(array) / sizeof(array[0]));}
#define ll long long
#define ull unsigned long long
#define PII pair<ll, ll>
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rim int m; scanf("%d", &m)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf
const int INF = 0x3f3f3f3f;
const int N = 510;
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
//dis 为各个点到起点的最短距离
int g[N][N], dis[N];
bool st[N]; //判断是否已经更新到最短距离
int dijkstra(int n) {
dis[1] = 0;
//迭代n次
for (int i = 0; i < n; ++ i) {
int t = -1;
//找到当前还没更新成最短距离且距离起点最近的点
for (int j = 1; j <= n; ++ j) {
if (!st[j] && (t == -1 || dis[j] < dis[t]))
t = j;
}
//标记为已访问
st[t] = true;
//用贪心的思想更新所有点的距离
for (int j = 1; j <= n; ++ j) {
dis[j] = min(dis[j], dis[t] + g[t][j]);
}
}
if (dis[n] == INF) return -1; //不存在从起点到终点的路径
else return dis[n];
}
int main() {
//freopen("D:\\in.txt", "r", stdin);
//freopen("D:\\out.txt", "w", stdout);
rin;
rim;
MEM(g, INF);
MEM(dis, INF);
for (int i = 0; i < m; ++ i) {
int a, b, c;
sc("%d%d%d", &a, &b, &c);
g[a][b] = min(g[a][b], c); //因为有重边,所以需要取min
}
pr("%d", dijkstra(n));
return 0;
}
Ⅱ、 堆优化版 Dijkstra
AcWing 850. Dijkstra求最短路 II
原题链接:https://www.acwing.com/problem/content/submission/code_detail/6234491/
#include <bits/stdc++.h>
using namespace std;
#define getlen(array) {return (sizeof(array) / sizeof(array[0]));}
#define ll long long
#define ull unsigned long long
#define PII pair<int, int>
#define PLL pair<ll, ll>
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rim int m; scanf("%d", &m)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf
const int INF = 0x3f3f3f3f;
const int N = 200000;
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
// w存边的权重,dis为各个点到起点的最短距离
int h[N], e[N], ne[N], w[N], dis[N];
int idx = 1;
bool st[N]; //判断是否已经遍历过
//插入从a到b权重为c的边
void insert(int a, int b, int c) {
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
int dijkstra(int n) {
dis[1] = 0;
priority_queue<PII, vector<PII>, greater<PII> > heap; //小根堆
heap.push({0, 1});
while (!heap.empty()) {
// 获得当前堆内距离起点最近的点
PII p = heap.top();
heap.pop();
int distance = p.first, var = p.second;
if (st[var]) continue; //如果该点已有最短距离过便跳过
st[var] = true; //标记为已最短
//遍历与该点存在连边的点
for (int i = h[var]; i != -1; i = ne[i]) {
int j = e[i];
//更新距离且放在堆内
if (dis[j] > distance + w[i]) {
dis[j] = distance + w[i];
heap.push({dis[j], j});
}
}
}
if (dis[n] == INF) return -1; //不存在从起点到终点的路径
else return dis[n];
}
int main() {
//freopen("D:\\in.txt", "r", stdin);
//freopen("D:\\out.txt", "w", stdout);
rin;
rim;
MEM(h, -1);
MEM(dis, INF);
while (m --) {
int a, b, c;
sc("%d%d%d", &a, &b, &c);
insert(a, b, c);
}
pr("%d", dijkstra(n));
return 0;
}
② 存在负权边
Ⅰ、 bellman-ford(有边数限制的最短路)
AcWing 853. 有边数限制的最短路
原题链接:https://www.acwing.com/problem/content/description/855/
#include<bits/stdc++.h>
using namespace std;
const int N = 510, M = 10010;
const int INF = 0x3f3f3f3f;
//从a到b权重为w的边
struct edge{
int a, b, w;
};
int n, m, k;
int dis[N], cpy[N]; //距离数组和备份数组
edge es[M]; //边集
int bellman_ford() {
memset(dis, INF, sizeof dis);
dis[1] = 0;
for (int i = 0; i < k; ++ i) { //最多走k次
memcpy(cpy, dis, sizeof dis); //用备份数组去更新距离,防止更新时二次更新
for (int j = 0; j < m; ++ j) {
int a = es[j].a, b = es[j].b, w = es[j].w;
dis[b] = min(dis[b], cpy[a] + w);
}
}
//INF / 2是为了防止类似于1到a和1到n都是正无穷,但是a到n是负距离,
//导致从1到a再到n比1直接到n小,而更新了dis[n]
if (dis[n] > INF / 2) return -1;
else return dis[n];
}
int main() {
cin >> n >> m >> k;
for (int i = 0; i < m; ++ i) {
int a, b, w;
cin >> a >> b >> w;
es[i] = {a, b, w};
}
int ans = bellman_ford();
if (ans == -1) cout << "impossible";
else cout << ans;
return 0;
}
Ⅱ、spfa
AcWing 851. spfa求最短路
原题链接:https://www.acwing.com/problem/content/853/
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
const int INF = 0x3f3f3f3f;
int n, m;
int h[N], e[N], ne[N], w[N], dis[N];
int idx = 1;
void insert(int a, int b, int c) {
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
int spfa() {
dis[1] = 0;
queue<int> q;
q.push(1);
while (!q.empty()) {
int t = q.front();
q.pop();
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];
q.push(j); //和bellman—ford不同的是,只将有变化的边放进队列
}
}
}
//和bellmam-ford同理
if (dis[n] > INF / 2) return -1;
else return dis[n];
}
int main() {
memset(h, -1, sizeof h);
memset(dis, INF, sizeof dis);
cin >> n >> m;
for (int i = 0; i < m; ++ i) {
int a, b, c;
cin >> a >> b >> c;
insert(a, b, c);
}
int ans = spfa();
if (ans == -1) cout << "impossible";
else cout << ans;
return 0;
}
AcWing 852. spfa判断负环
原题链接:https://www.acwing.com/problem/content/854/
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
const int INF = 0x3f3f3f3f;
int n, m;
int h[N], e[N], ne[N], w[N], dis[N], cnt[N];
int idx = 1;
void insert(int a, int b, int c) {
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx ++;
}
bool spfa() {
dis[1] = 0;
queue<int> q;
for (int i = 1; i <= n; ++ i) {
q.push(i); //将所有点放入,因为可能从1出发到不了负环
cnt[i] = 1;
}
while (!q.empty()) {
int t = q.front();
q.pop();
if (cnt[t] > n) return true; //存在负环
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];
q.push(j); //只将有变化的边放进队列
cnt[j] = cnt[t] + 1;
}
}
}
return false;
}
int main() {
memset(h, -1, sizeof h);
memset(dis, INF, sizeof dis);
cin >> n >> m;
for (int i = 0; i < m; ++ i) {
int a, b, c;
cin >> a >> b >> c;
insert(a, b, c);
}
if (spfa()) cout << "Yes";
else cout << "No";
return 0;
}
(2) 多源汇最短路 - Floyd
AcWing 854. Floyd求最短路
原题链接:https://www.acwing.com/problem/content/856/
#include <bits/stdc++.h>
using namespace std;
#define getlen(array) {return (sizeof(array) / sizeof(array[0]));}
#define ll long long
#define ull unsigned long long
#define PII pair<ll, ll>
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rim int m; scanf("%d", &m)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf
const int INF = 0x3f3f3f3f;
const int N = 210;
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
int n, m, k;
int dis[N][N];
void floyd() {
for (int k = 1; k <= n; ++ k) {
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) {
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
}
}
}
int main() {
//freopen("D:\\in.txt", "r", stdin);
//freopen("D:\\out.txt", "w", stdout);
cin >> n >> m >> k;
//初始化
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) {
if (i == j) dis[i][j] = 0;
else dis[i][j] = INF;
}
}
//输入每条边
for (int i = 0; i < m; ++ i) {
int a, b, w;
cin >> a >> b >> w;
dis[a][b] = min(dis[a][b], w);
}
//寻找最短路
floyd();
//输出每组询问
while (k --) {
int a, b;
cin >> a >> b;
if (dis[a][b] > INF / 2) cout << "impossible" << endl;
else cout << dis[a][b] << endl;
}
return 0;
}
7、二分图
二分图:当且仅当图中不含奇数环。
奇数环:边的数量是奇数的环。
HDU 2458 Kindergarten :二分图用匈牙利求最大独立集
(1) 判定二分图 : 染色法
AcWing 860. 染色法判定二分图
原题链接:https://www.acwing.com/problem/content/submission/code_detail/6332360/
#include <bits/stdc++.h>
using namespace std;
#define MEM(x, y) memset(x, y, sizeof x)
const int INF = 0x3f3f3f3f;
const int N = 200010;
int n, m;
int color[N]; // 0-未染色 1-白色 2-黑色
int h[N], e[N], ne[N], idx = 1;
void insert(int a, int b) {
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
bool dfs(int a, int b) {
color[a] = b; //染色
for (int i = h[a]; i != -1; i = ne[i]) {
int j = e[i];
if (!color[j]) { //还没染过色
if (!dfs(j, 3 - b)) return false;
}
else if (color[j] == b) //已经染过色,但是出现矛盾
return false;
}
return true;
}
int main() {
cin >> n >> m;
MEM(h, -1);
for (int i = 0; i < m; ++ i) {
int a, b;
cin >> a >> b;
// if (a == b) continue; //自环也是奇数环,不可以continue
insert(a, b);
insert(b, a);
}
bool ans = true;
//可能不是连通图,所以需要逐一判断逐块染色
for (int i = 1; i <= n; ++ i) {
if (!color[i]) {
if (!dfs(i, 1)) {
ans = false;
break;
}
}
}
if (ans) cout << "Yes";
else cout << "No";
return 0;
}
(2) 求最大匹配 : 匈牙利算法
看似时间复杂度为O(nm),实际远远小于。
AcWing 861. 二分图的最大匹配
原题链接:https://www.acwing.com/problem/content/description/863/
#include<bits/stdc++.h>
using namespace std;
const int N = 510, M = 100010;
int n1, n2, m;
int h[N], e[M], ne[M], idx = 1;
int match[N]; //记录右边都是和左边的谁匹配了
bool st[N]; //每一趟,都需要确保每一个女生只被访问一次,相当于剪枝
void insert(int a, int b) {
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
//查询是否能找到与其匹配的
bool find (int a) {
for (int i = h[a]; i != -1; i = ne[i]) {
int j = e[i];
if (!st[j]) {
st[j] = true; //标记已访问
//要么从未匹配,要么可以通过易主原先的达到匹配
if (!match[j] || find(match[j])) {
match[j] = a;
return true;
}
}
}
return false;
}
int main() {
memset(h, -1, sizeof h);
cin >> n1 >> n2 >> m;
for (int i = 0; i < m; ++ i) {
int a, b;
cin >> a >> b;
insert(a, b);
}
int cnt = 0;
for (int i = 1; i <= n1; ++ i) {
//记得初始化,st只用于每一趟,而不是全局
memset(st, 0, sizeof st);
if (find(i)) ++ cnt;
}
cout << cnt;
return 0;
}
8、欧拉路
9、最大流
10、其他
(1) 树的直径 - 树上距离最远的两点间的距离
一般地,距离最远的这两点均为叶节点。特殊地,一棵仅含两个点的树,此时的直径为根节点与另一个节点的距离,直径为1;仅含一个根节点的树的直径为0。
模板题 : AcWing 1207. 大臣的旅费
( 原题是第四届蓝桥杯省赛C++A组 / 第四届蓝桥杯省赛JAVAA组 )
原题链接:https://www.acwing.com/problem/content/1209/
做法
先从任意一点a进入,dfs更新其余结点到这个结点的距离(加上权重),再遍历一下dis数组找到和a距离最远的结点b,再从结点b进入去同样dfs一次更新其余结点到b的距离(加上权重),最后遍历dis数组,此时数组中的最大值就是树的直径.
证明
主要反证法.
上面的做法其实换句话说就是,虽然 a 是任意选的, 但是 y 必然是直径的一个端点.
此时再从 y 进入寻找距离最远的结点, 那么此时 dis 里面保存的数值最大的点必然是直径的另一个点.
现反证如下:
xy 不是真正的直径, 设真正的直径是 uv , 且 xp 为 1, vp为 2, 其他依次类推.
情况一: uv 与 xy 有直接交点.
由于 y 是距离 x 最远的结点, 所以有 1 + 4 >= 1 + 3, 所以 4 >= 3
故而 2 + 4 >= 2 + 3, 所以当 2 + 4 == 2 + 3, 那么有多条直径, 如果 2 + 4 > 2 + 3, 那么存在比预设的直径长的路径, 故而预设有误. 但无论哪种情况都无法否认 y 是直径的一个端点.
情况二: uv 和 xy 没有直接相交.
但是由于这是一棵树,所以从一个结点出发,可以到达任意一个结点,所以必然存在图中的 pq, 而且由于 1 + 4 >= 1 + 3 + 5 , 所以 4 >= 3 + 5, 所以 4 >= 5.
故而 2 + 3 + 4 >= 2 + 5。换句话说,以 y 为直径的端点才符合直径的定义。
当然, x 并不一定就是直径的另一个端点, 有可能存在另外比 xy 长的.
所以这个时候就需要再从 y 出发, 更新除 y 的所有结点到 y 的距离, 此时的那个最大距离就是直径.
#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>
#include <unordered_set>
#include <unordered_map>
using namespace std;
#define getlen(array) {return (sizeof(array) / sizeof(array[0]));}
#define ll long long
#define ull unsigned long long
#define PII pair<ll, ll>
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf
const int INF = 0x3f3f3f3f;
const int N = 100010;
struct edge {
int id, weight;
};
vector<edge> e[N];
int dis[N];
void dfs (int idx, int fa, int d) {
dis[idx] = d;
int length = e[idx].size();
for (int i = 0; i < length; ++ i) {
edge ed = e[idx][i];
if (ed.id != fa) {
dfs(ed.id, idx, d + ed.weight);
}
}
}
int main() {
//freopen("D:\\in.txt", "r", stdin);
//freopen("D:\\out.txt", "w", stdout);
rin;
int a, b, c;
for (int i = 1; i < n; ++ i) {
sc("%d%d%d", &a, &b, &c);
e[a].push_back({b, c});
e[b].push_back({a, c});
}
dfs(1, -1, 0);
int u = 1;
for (int i = 2; i <= n; ++ i) {
if (dis[i] > dis[u]) u = i;
}
dfs(u, -1, 0);
u = 1;
for (int i = 2; i <= n; ++ i) {
if (dis[i] > dis[u]) u = i;
}
u = dis[u];
cout << u * 10 + (u + 1ll) * u / 2;
return 0;
}