1 DFS
2 BFS
宽搜问题最重要的是可以寻找最短路(当边权相同时)。深搜保证可以搜到结果但是不一定是最短路!
当边权值不相同时,选择其他最短路算法。
2.1 走迷宫
从左上角走到右下角最短路径长度
#include<bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int g[N][N], d[N][N];
int n, m;
int bfs()
{
queue<PII> q;
q.push({0, 0});
memset(d, -1, sizeof d);
d[0][0] = 0;
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1}; //上 下 左 右
while(!q.empty())
{
PII t = q.front();
q.pop();
for(int i = 0; i < 4; i++)
{
int tx = t.first + dx[i];
int ty = t.second + dy[i];
//cout << tx << " " << ty << endl;
if(tx >= 0 && tx < n && ty >= 0 && ty < m && g[tx][ty] == 0 && d[tx][ty] == -1)
{
d[tx][ty] = d[t.first][t.second] + 1;
//cout << tx << " " << ty << endl;
q.push({tx, ty});
}
}
}
return d[n - 1][m - 1];
}
int main()
{
cin >> n >> m;
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
cin >> g[i][j];
cout << bfs();
return 0;
}
- 上下左右向量简便写法:
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1}; //上 下 左 右
- 坐标:
typedef pair<int, int> PII; q.push({tx, ty});
- 距离记录:
d[tx][ty] = d[t.first][t.second] + 1;
2.2 八数码
思路是:将每种情况的小方格看成是一个状态,那么转化为已知开始、结束状态和转移方式,求转移的最小步骤?
几个难点:
- 如何表示状态:字符串,like “12345678x”
- 如何状态转移:将字符串转化成九宫格,找到要交换的字符交换,再转换回来
- 如何统计距离:在上一个状态距离值上+1, 用哈希表存储
#include<bits/stdc++.h>
using namespace std;
int bfs(string start)
{
string end = "12345678x";
queue<string> q;
unordered_map<string, int> d;
q.push(start);
d[start] = 0;
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
while(!q.empty())
{
string t = q.front();
q.pop();
int dis = d[t];
if(t == end)
return dis;
//状态转移
for(int i = 0; i < 4; i++)
{
int k = t.find('x');
int tx = k / 3, ty = k % 3;
int a = tx + dx[i], b = ty + dy[i];
if(a < 3 && a >= 0 && b < 3 && b >= 0)
{
swap(t[k], t[a * 3 + b]);
if(!d.count(t))
{
d[t] = dis + 1;
q.push(t);
}
swap(t[k], t[a * 3 + b]);
}
}
}
return -1;
}
int main()
{
char ch;
string start;
for(int i = 0; i < 9; i++)
{
cin >> ch;
start += ch;
}
cout << bfs(start);
return 0;
}
3 树和图的BFS
847 图中点的层次
题目很简单,找到1到n的最短路径(边权为1),还是和走迷宫一样的遍历,只不过存储换成了图。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, m, a, b;
int d[N];
int h[N], e[N], ne[N], idx;
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx;
idx++;
}
int bfs()
{
queue<int> q;
q.push(1);
memset(d, -1, sizeof d);
d[1] = 0;
while(!q.empty())
{
int t = q.front();
q.pop();
for(int i = h[t]; i != -1; i = ne[i])
{
if(d[e[i]] == -1)
{
q.push(e[i]);
d[e[i]] = d[t] + 1;
}
}
}
return d[n];
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
while(m--)
{
cin >> a >> b;
add(a, b);
}
cout << bfs();
return 0;
}
- 图的存储
int h[N], e[N], ne[N], idx;
`void add(int a, int b){e[idx] = b; ne[idx] = h[a]; h[a] = idx;idx++;} - 寻找邻接节点
for(int i = h[t]; i != -1; i = ne[i])
4 树和图的DFS
846 树的重心
对树进行深度优先遍历可以求树的子树大小。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
const int M = N * 2;
int n, a, b, ans = N;
int h[N], e[M], ne[M], idx;
bool st[N];
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx;
idx++;
}
//返回以u为根的子树数量和
int dfs(int u)
{
st[u] = true;
int sum = 1; //包含u在内的下面子树的数量和
int res = 0; //以u为根的子树数量的最大值
for(int i = h[u]; i != -1; i = ne[i])
{
int p = e[i];
if(!st[p])
{
int s = dfs(p);
res = max(res, s);
sum += s;
}
}
res = max(res, n - sum);//父节点的那个子树数量
ans = min(ans, res);
return sum;
}
int main()
{
cin >> n;
memset(h, -1, sizeof h);
for(int i = 0; i < n - 1; i++)
{
cin >> a >> b;
add(a, b);
add(b, a);
}
dfs(1);
cout << ans;
return 0;
}
5 拓扑序列
有向图的拓扑序列,判断DAG图是否存在环?(BFS的应用)
有向无环图一定存在拓扑序列。
箭头都是从前往后指的,入度为0说明没有节点指向它,排在最前面
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, m, a, b;
int h[N], e[N], ne[N], idx, d[N];
int q[N];
int hh, tt;
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx;
idx++;
}
bool toposort()
{
hh = 0, tt = -1;
for(int i = 1; i <= n; i++)
{
if(!d[i])
q[++tt] = i;
}
while(hh <= tt)
{
int t = q[hh++];
for(int i = h[t]; i != -1; i = ne[i])
{
int p = e[i];
d[p]--;
if(!d[p])
q[++tt] = p;
}
}
if(tt == n - 1)
return true;
else
return false;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
while(m--)
{
cin >> a >> b;
add(a, b);
d[b]++;
}
if(toposort())
{
for(int i = 0; i <= tt; i++)
cout << q[i] << " ";
}
else
cout << -1;
return 0;
}
- 是否存在拓扑序列,就是看tt是否是n-1,包含全部n个节点即可
- 拓扑序列是什么:队列中的排列
数组模拟队列
- 初始化:
hh = 0, tt = -1;
- push:
q[++tt] = num;
- pop:
tmp = q[hh++];
- empty:
hh <= tt
今天立志排名进入4000!
6 最短路问题
6.1 dijkstra
朴素版dijkstra
这个简单代码是把第一个节点自动加入st数组
- 初始化,
dist[1] = 0; 其他为INF
- 迭代更新n次
- 找到不在st中dist最小的节点midx,加入st
- 使用midx更新松弛dist
#include<bits/stdc++.h>
using namespace std;
const int N = 510;
const int M = 1e5 + 10;
int n, m, a, b, c;
int g[N][N], dist[N], st[N];
//可以实现从第一个点开始自动加入st中
int dijkstra()
{
//初始化
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
//迭代n次,将n个点加入st中
for(int i = 0; i < n; i++)
{
//遍历dist数组,找到不在st数组中距离最小的节点midx
int midx = -1;
for(int j = 1; j <= n; j++)
{
if(!st[j] && (midx == -1 || dist[midx] > dist[j]))
midx = j;
}
st[midx] = true;
for(int j = 1; j <= n; j++)
dist[j] = min(dist[j], dist[midx] + g[midx][j]); //松弛dist数组
}
//无法到达n节点
if(dist[n] == 0x3f3f3f3f)
return -1;
else
return dist[n];
}
int main()
{
scanf("%d %d", &n, &m);
memset(g, 0x3f, sizeof g);
while(m--)
{
scanf("%d %d %d", &a, &b, &c);
g[a][b] = min(g[a][b], c); //存在重边
}
cout << dijkstra();
return 0;
}
- 没有必要加上!st[j],因为如果j被加入到里面去了,表面他已经在之前就是最小的了,不可能再用之后更大的去更新他。
堆优化的dijkstra
暂时还不是很清晰,我还需要再缕缕
- 为什么st判断写在外面?
- 什么时候用堆优化?邻接表? 复杂度?
- cnt++在哪里
#include<bits/stdc++.h>
using namespace std;
const int N = 150010;
int h[N], e[N], ne[N], w[N], idx;
int n, m, a, b, c;
int st[N], dist[N];
typedef pair<int, int> PII;
void add(int a, int b, int c)
{
e[idx] = b; ne[idx] = h[a]; h[a] = idx; w[idx] = c; idx++;
}
int dijkstra()
{
priority_queue<PII, vector<PII>, greater<PII>> heap;
memset(dist, 0x3f, sizeof dist);
heap.push({0, 1});//dist[0] = 1;
while(!heap.empty())
{
//heap中最多有m个元素,复杂度为mlogm
auto t = heap.top();
heap.pop();
int distance = t.first, ti = t.second;
if(st[ti]) continue;
st[ti] = true;
//对每一个头都进行了遍历,相当于遍历了所有边m,最多push了m次,外层while循环最多m次
for(int i = h[ti]; i != -1; i = ne[i])
{
int j = e[i];
if(distance + w[i] < dist[j]) //只有产生距离更小的点时才去更新j,减少堆中的数
{
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if(dist[n] == 0x3f3f3f3f)
return -1;
else
return dist[n];
}
int main()
{
scanf("%d %d", &n, &m);
memset(h, -1, sizeof h);
while(m--)
{
scanf("%d %d %d", &a, &b, &c);
add(a, b, c);
}
cout << dijkstra();
return 0;
}
6.2 bellman-ford
我感觉bf算法有点像暴力(带负权值),dijkstra像是贪心选择最小的(非负权值)。
算法流程O(nm):
- 初始化dist, 起始点为0其余为INF
- for k次迭代(k:最多使用k条边)
- 备份dist,防止更新是发生串联,一次外层迭代只能加一条边
- for 遍历所有边
- 松弛
dist[b] = min(dist[b], backup[a] + w);
- 松弛
有边数限制的最短路
#include<iostream>
#include<string.h>
using namespace std;
const int N = 510;
const int M = 10010;
int n, m, k, a, b, w;
int dist[N], backup[N];
struct edge
{
int a, b, w;
}e[M];
int bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for(int i = 0; i < k; i++)
{
memcpy(backup, dist, sizeof dist);
for(int j = 0; j < m; j++)
{
a = e[j].a, b = e[j].b, w = e[j].w;
dist[b] = min(dist[b], backup[a] + w);
}
}
return dist[n];
}
int main()
{
scanf("%d %d %d", &n, &m, &k);
for(int i = 0; i < m; i++)
scanf("%d %d %d", &e[i].a, &e[i].b, &e[i].w);
int ans = bellman_ford();
if(ans > 0x3f3f3f3f / 2)
puts("impossible");
else
cout << ans;
return 0;
}
这个题有负环没有关系是因为有k次数限制
其他点:
- 如果最短路存在,一定存在一个不含环的最短路。环分为正环、零环、负环。若有负环,可以一直走环减少路径长度,可能不存在最短路。正环和零环没必要走环,路径会增加。
- 判断是否存在负环?存在负环那么第n次还可以对其进行松弛,加一个flag判断一下
6. 3 spfa
spfa算法是对bf算法的优化,dist[b] = min(dist[b], backup[a] + w);
只有在backup[a]更新了,dist[b]才会更新。因此选择BFS对其进行优化,把变小了的backup[a]放进队列,然后取出更新其他的。
此题不能存在负环,存在负环可能会无限循环下去
spfa求最短路
#include<iostream>
#include<string.h>
#include<queue>
using namespace std;
const int N = 100010;
int n, m, a, b, ww;
int h[N], e[N], ne[N], w[N], idx;
int dist[N], st[N];
void add(int a, int b, int ww)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx;
w[idx] = ww;
idx++;
}
int spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;//表示是否在队列中
while(!q.empty())
{
a = q.front();
q.pop();
st[a] = false;//spfa可以多次更新,多次加入队列
for(int i = h[a]; i != -1; i = ne[i])
{
b = e[i];
ww = w[i];
if(dist[a] + ww < dist[b])
{
dist[b] = min(dist[b], dist[a] + ww);
if(!st[b])
{
q.push(b);
st[b] = true;
}
}
}
}
return dist[n];
}
int main()
{
scanf("%d %d", &n, &m);
memset(h, -1, sizeof h);
while(m--)
{
scanf("%d %d %d", &a, &b, &ww);
add(a, b, ww);
}
int t = spfa();
if(t == 0x3f3f3f3f)//加入队列的点都是从起点开始的,不会存在 不在最短路中且更新的dist[n]这种情况
puts("impossible");
else
cout << t;
return 0;
}
spfa判断负环
抽屉原理
如果经过了多于n条边,说明存在环,因为只有n-1条边
环存在的原因是最短路变小了,那环就是负环
cnt[b]表示从1到b中经过了多少条边,cnt[b] = cnt[a] + 1;//1是边a->b
#include<iostream>
#include<string.h>
#include<queue>
using namespace std;
const int N = 100010;
int n, m, a, b, ww;
int h[N], e[N], ne[N], w[N], idx;
int dist[N], st[N], cnt[N];
void add(int a, int b, int ww)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx;
w[idx] = ww;
idx++;
}
int spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;//可以不初始化
queue<int> q;
//加入所有节点
for(int i = 1; i <= n; i++)
{
st[i] = true;
q.push(i);
}
while(!q.empty())
{
a = q.front();
q.pop();
st[a] = false;
for(int i = h[a]; i != -1; i = ne[i])
{
b = e[i];
ww = w[i];
if(dist[a] + ww < dist[b])
{
dist[b] = min(dist[b], dist[a] + ww);
cnt[b] = cnt[a] + 1; //cnt更新
if(cnt[b] >= n)//判断是否存在负环
return true;
if(!st[b])
{
q.push(b);
st[b] = true;
}
}
}
}
return false;
}
int main()
{
scanf("%d %d", &n, &m);
memset(h, -1, sizeof h);
while(m--)
{
scanf("%d %d %d", &a, &b, &ww);
add(a, b, ww);
}
if(spfa()) puts("Yes");
else puts("No");
return 0;
}
评论里的一些问题:
- 是否存在负环:不是从1开始的最短路是否存在负环,因此将初始节点加入。这个y总说可以这样理解(nb!):想象一个虚拟源节点假设为0,距离所有节点距离都是0,这样在spfa第一次更新时就会把所有节点加入,dist[i]更新为0,这样有0的图找负环就等于没0的图找负环。
- dist为什么可以不初始化?不初始化dist都是0,甚至可以初始化任何值。因为只有在
dist[b] < dist[a] + w
时,才会cnt++,不初始化更新只可能是w < 0,从负边出现才开始更新统计cnt,由于如果有负环的话,一定会无数次更新,一定会导致cnt>=n的,所以不必在意是否初始化 - cnt >= m?说是只有m条边超过了肯定是有负环,是对的。但是其实只有n个顶点,每条最短路顶多有n-1条边,用cnt>=n即可(一般n < m)
6.4 Floyd
多源最短路径 每两个点之间的最短路径
算法思路:三重循环
- for k in (1, n)
- for i in (1, n)
- for j in (1, n)
d[i][j] = min(d[i][j], d[i][k] + d[k][j])
- for j in (1, n)
- for i in (1, n)
原理是个动态规划
d[k][i][j] = min(d[k][i][j], d[k - 1][i][k] + d[k - 1][k][j]
d[k][i][j]是指使用前k个节点中i和j的最短路径,可以使用前k-1个更新- 第一维可以省略,
d[i][j] = min(d[i][j], d[i][k] + d[k][j])
不使用之前的
#include<iostream>
using namespace std;
const int N = 210;
const int INF = 1e9;
int n, m, k, a, b, w;
int d[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++)
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
int main()
{
scanf("%d %d %d", &n, &m, &k);
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;
while(m--)
{
scanf("%d %d %d", &a, &b, &w);
d[a][b] = min(d[a][b], w);
}
floyd();
while(k--)
{
scanf("%d %d", &a, &b);
if(d[a][b] > INF / 2) puts("impossible");//存在负权边,可能导致不是最短路但是距离减少
else cout << d[a][b] << endl;
}
return 0;
}
7 最小生成树
7.1 Prim
朴素版Prim算法
算法原理和dikstra很像,差别在于dist数组含义不同(dij:到1号点的距离,prim到集合的距离)
- 初始化 dist=INF,选择第一个点为1
- for n次循环(选n个点加入mst)
- 从dist数组选择一个最小的节点t, 加入st
- (结束判断:dist[t]==INF)
- 用t更新到集合的距离
dist[j] = min(dist[j], g[t][j])
#include<iostream>
#include<string.h>
using namespace std;
const int N = 510;
const int INF = 0x3f3f3f3f;
int g[N][N];
int dist[N], st[N];
int n, m, u, v, w;
int prim()
{
//初始化为INF,选择第一个点为1
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
int ans = 0;
//选择最小的点加入
for(int i = 0; i < n; i++)//选n个点
{
int t = 0;
for(int j = 1; j <= n; j++)
{
if(!st[j] && dist[j] < dist[t])
t = j;
}
if(dist[t] == INF)//最小的是INF,全是INF,非连通
return INF;
ans += dist[t];
st[t] = true;
//更新到集合的距离
for(int j = 1; j <= n; j++)
{
if(!st[j] && g[t][j] < dist[j])
dist[j] = g[t][j];
}
}
return ans;
}
int main()
{
scanf("%d %d", &n, &m);
memset(g, 0x3f, sizeof g);
while(m--)
{
scanf("%d %d %d", &u, &v, &w);
g[u][v] = min(g[u][v], w);
g[v][u] = min(g[v][u], w);
}
int t = prim();
if(t == INF) puts("impossible");
else printf("%d\n", t);
return 0;
}
堆优化版prim算法
和堆优化版的dijkstra差不多,不过用cnt统计多少个点加入了mst
#include<iostream>
#include<string.h>
#include<queue>
using namespace std;
const int N = 510;
const int INF = 0x3f3f3f3f;
int g[N][N];
int dist[N], st[N];
int n, m, u, v, w;
typedef pair<int, int> PII;
int prim()
{
priority_queue<PII, vector<PII>, greater<PII>> q;
q.push({0, 1});
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
int ans = 0, cnt = 0;
while(q.size())//选n个点
{
auto t = q.top();
q.pop();
int distance = t.first, pos = t.second;//选择最小边
if(st[pos]) continue;
st[pos] = true;
cnt++;
ans += distance;
//cout << pos << " " << ans << endl;
for(int j = 1; j <= n; j++)
{
if(g[pos][j] < dist[j])
{
//cout << dist[j] << " " << j << endl;
dist[j] = g[pos][j];
q.push({dist[j], j});
}
}
}
if(cnt == n) return ans;
return INF;
}
int main()
{
scanf("%d %d", &n, &m);
memset(g, 0x3f, sizeof g);
while(m--)
{
scanf("%d %d %d", &u, &v, &w);
g[u][v] = min(g[u][v], w);
g[v][u] = min(g[v][u], w);
}
int t = prim();
if(t == INF) puts("impossible");
else printf("%d\n", t);
return 0;
}
7.2 Kruskal
算法思路:
之前各条边都分别在各自的集合里面,从小到大将其连通起来——并查集
- 从小到大排序
- 遍历所有边,a->b,如果a和b不连通,则将其连通加入mst
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 200010;
const int INF = 0x3f3f3f3f;
int n, m, a, b, w;
int f[N];
struct edge
{
int a, b, w;
bool operator<(const edge& e)
{
return w < e.w;
}
}e[N];
int find(int a)
{
if(f[a] != a)
f[a] = find(f[a]);//?
return f[a];
}
int kruskal()
{
sort(e, e + m);//默认less,重载operator<
int cnt = 0, ans = 0;
for(int i = 0; i < m; i++)
{
a = e[i].a, b = e[i].b, w = e[i].w;
int fa = find(a), fb = find(b);
//cout << fa << " " << fb << endl;
if(fa != fb)
{
cnt++;
ans += w;
f[fa] = fb;//union ?
}
}
//n - 1条边
if(cnt == n - 1) return ans;
return INF;
}
int main()
{
scanf("%d %d", &n, &m);
for(int i = 1; i < n; i++)
f[i] = i;
for(int i = 0; i < m; i++)
scanf("%d %d %d", &e[i].a, &e[i].b, &e[i].w);
int t = kruskal();
if(t == INF) puts("impossible");
else printf("%d\n", t);
return 0;
}