Chapter 3 搜索与图论
0:DFS和BFS的对比
数据结构 | 空间复杂度 | 最短性 | |
---|---|---|---|
DFS | stack | O(h) | 不具备 |
BFS | queue | O(2^h) | 具备 |
其中:h是树的高度
1:深度优先搜索DFS
题目:排列数字
给定一个整数 n,将数字 1∼n 排成一排,按照字典序将所有的排列方法输出。
# include <iostream>
# include <string.h>
# include <algorithm>
# include <stdio.h>
# include <vector>
using namespace std;
const int N = 10;
int n;
int path[N]; // 记录当前的depth路径
bool st[N]; // 记录是否被访问
void dfs(int u){
if(u == n){
//
for(int i=0;i<n;i++){
cout<<path[i]<<" ";
}
puts("");
return;
}
for(int i=1;i<=n;i++){
if(!st[i]){
// 没有访问过
path[u] = i;
st[i] = true;
dfs(u+1);
st[i] = false;
}
}
}
int main(){
cin >> n;
dfs(0);
return 0;
}
/*
3
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
*/
2:宽度优先搜索BFS
题目:走迷宫
1代表没有路,0代表有路
从左上角移动至右下角 (n,m) 处,至少需要移动多少次。
# include <iostream>
# include <string.h>
# include <algorithm>
# include <stdio.h>
# include <vector>
using namespace std;
const int N = 110;
typedef pair<int,int> PII;
int n, m;
int g[N][N], d[N][N];
//d数组记录该点到左上角的距离
PII q[N*N], Prev[N][N];
//Prev数组记录到达该点最短路中的上一个点
int bfs(){
int hh=0,tt=0;//队列的head和tail
q[0] = {0,0};//初始左上角
memset(d, -1, sizeof d);//初始距离左上角的距离为-1
d[0][0] = 0;
int dx[4]={-1,0,1,0}, dy[4]={0,1,0,-1};
//搜索的四个方向
while(hh<=tt){
auto t = q[hh++];//取出head的数据
//探索head的四个方向,是否可行
for(int i=0;i<4;i++){
int x = t.first+dx[i], y = t.second+dy[i];//当前x,y坐标
//如果没超过范围,且,有路,且,第一次访问该点
if(x>=0 && x<n && y>=0 && y<n && g[x][y]==0 && d[x][y]==-1){
d[x][y] = d[t.first][t.second] + 1;//更新该点的最短路距离
Prev[x][y]=t;//记录该点的上一个点,是t
q[++tt] = {x,y};//tail增加该点坐标,压入队列
}
}
//探索完整个队列,会退出循环
}
//输出最短路的经过的点
int x=n-1, y=m-1;
while(x||y){
cout<<x<<" "<<y<<endl;
auto t=Prev[x][y];
x=t.first;
y=t.second;
}
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()<<endl;
return 0;
}
/*
5 5
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0
4 4
3 4
2 4
2 3
2 2
2 1
2 0
1 0
8
*/
3:树与图的存储
树是无环连通图(特殊)
图的分类:(1)有向图;(2)无向图;
无向图是特殊的有向图
因此,针对树与图,最后只需要考虑有向图的存储
有向图的存储方法
【1】邻接矩阵
g[a,b]存储a到b
空间开销大,O(n^2)
【2】邻接表
图总共n个点,则有n个单链表
单链表中存储当前点可以到达的点的集合,次序无关
类似哈希表的拉链法
插入边[a,b]时,通常插入到a的单链表的head节点
4:树与图的BFS
图的BFS
题目:图中点的层次
# include <iostream>
# include <string.h>
# include <algorithm>
# include <stdio.h>
# include <vector>
using namespace std;
const int N = 1e5+10;
int n, m;
int h[N], e[N], ne[N], idx;
int d[N], q[N];
void add(int a, int b){
e[idx] = b; // 数据array中存放b
ne[idx] = h[a]; // b的编号在链表中先指向a的头节点的next
h[a] = idx; // a的头节点的next重新指向b的编号
idx++; // 更新下一次的编号
}
int bfs(){
int hh=0,tt=0;
q[0]=1;
memset(d, -1, sizeof d);
d[1]=0;
while(hh<=tt){
int t=q[hh++];
for(int i=h[t]; i!=-1; i=ne[i]){
int j=e[i];
if(d[j] == -1){
d[j] = d[t]+1;
q[++tt]=j;
}
}
}
return d[n];
}
int main(){
cin>>n>>m;
memset(h, -1, sizeof h);
for(int i=0;i<m;i++){
int a,b;
cin>>a>>b;
add(a,b);
}
cout<<bfs()<<endl;
return 0;
}
/*
4 5
1 2
2 3
3 4
1 3
1 4
1
*/
5:树与图的DFS
图的DFS
题目:树的中心
# include <iostream>
# include <string.h>
# include <algorithm>
# include <stdio.h>
# include <vector>
using namespace std;
const int N = 1e5+10, M = N*2;
int n, ans = N;
bool st[N];
int h[N], e[M], ne[M], idx;
void add(int a, int b){
e[idx] = b; // 数据array中存放b
ne[idx] = h[a]; // b的编号在链表中先指向a的头节点的next
h[a] = idx; // a的头节点的next重新指向b的编号
idx++; // 更新下一次的编号
}
int dfs(int u){
st[u] = 1; // u已经搜过了
int sum=1,res=0;
// 遍历u的链表
for(int i=h[u]; i!=-1; i=ne[i]){
int j=e[i]; // 当前编号的数值
if(!st[j]){
//没有访问过
int s = dfs(j); // s是子树的大小
res = max(res,s); // 更新res为最大子树
sum += s; // 当前结点+所有子树的大小
}
}
// 这个点删除后,剩余各个连通块中点数的最大值最小
res = max(res, n-sum); // 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++){
int a,b;
cin>>a>>b;
add(a,b);
add(b,a);
//无向边需要加两次
}
dfs(1);
cout<<ans<<endl;
return 0;
}
/*
9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6
4
*/
6:拓扑排序
只有有向图有拓扑序列
拓扑序列案例
1->2
2->3
1->3
有向无环图一定存在拓扑序列,即拓扑图
一个有向无环图,一定至少存在1个入度=0的点
求拓扑序列
1:入度=0的点,可以作为拓扑起点,则将其全部加入队列
2:根据BFS搜索
题目:有向图的拓扑排序
# include <iostream>
# include <string.h>
# include <algorithm>
# include <stdio.h>
# include <vector>
using namespace std;
const int N = 1e5+10;
int n, m;
int h[N], e[N], ne[N], idx;
int d[N], q[N];
//d数值表示i点的入度
void add(int a, int b){
e[idx] = b; // 数据array中存放b
ne[idx] = h[a]; // b的编号在链表中先指向a的头节点的next
h[a] = idx; // a的头节点的next重新指向b的编号
idx++; // 更新下一次的编号
}
int topsort(){
int 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 j=e[i];
d[j]--;
if(d[j]==0){
q[++tt]=j;
}
}
}
return tt==n-1;
}
int main(){
cin>>n>>m;
memset(h, -1, sizeof h);
for(int i=0;i<m;i++){
int a,b;
cin>>a>>b;
add(a,b);
d[b]++;
}
if(topsort()){
for(int i=0;i<n;i++){
cout<<q[i]<<" ";
}
puts("");
}
else puts("-1");
return 0;
}
/*
3 3
1 2
2 3
1 3
1 2 3
*/
7:最短路问题
源点——起点,汇点——终点
n:点的数量,m:边的数量
稠密图:m和n^2是一个级别
稀疏图:m和n是一个级别
稠密图用邻接矩阵存储,稀疏图用邻接表存储
单源最短路:求一个点到其他所有点的最短距离
(1)所有边权重都是正数
【1】朴素Dijkstra算法,时间复杂度O(n^2),适合稠密图(边多)
【2】堆优化Dijkstra算法,时间复杂度O(mlogn),适合稀疏图
(2)存在负权重的边
【1】Bellman-Ford算法,时间复杂度O(nm)
【2】SPFA算法,平均时间复杂度O(m),最坏O(nm)
多源汇最短路:任选两个点,计算其最短距离
Floyd算法,时间复杂度O(n^3)
最短路问题的核心:建图,把具体问题抽象成最短路问题
最短路问题不区分有向图和无向图,只考虑有向图算法
朴素Dijkstra
【1】初始化距离,dist[1]=0,dist[i]=+∞
【2】当前已经确定最短距离的点进入s数组,迭代n次
①找到第一个不在s中的距离最近的点t
②将t加入到s中
③用t更新其他点的距离(dist[x] > dist[t] + w)
题目:求含有重边和自环的最短路
# include <iostream>
# include <string.h>
# include <algorithm>
# include <stdio.h>
# include <vector>
using namespace std;
const int N = 510;
int n, m;
int g[N][N], dist[N];
// g记录每条边之间的距离,dist记录初始点到其他点的最短距离
bool st[N]; // 记录n个点是否被访问
int dijkstra(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0; //初始点到自身的距离是0
for(int i=0;i<n;i++){
int t=-1;
for(int j=1;j<=n;j++){
if(!st[j] && (t==-1 || dist[t]>dist[j])){
t=j;
break;
}
}
st[t] = 1;
for(int j=1;j<=n;j++){
dist[j] = min(dist[j], dist[t]+g[t][j]);
}
}
if(dist[n] == 0x3f3f3f3f){
return -1; // 不可达这个点
}
return dist[n];
}
int main(){
cin>>n>>m;
memset(g, 0x3f, sizeof g);
while(m--){
int a,b,c;
cin>>a>>b>>c;
g[a][b] = min(g[a][b], c); //消除重边,只留下最短的那个边
}
cout<<dijkstra()<<endl;
return 0;
}
/*
3 3
1 2 2
2 3 1
1 3 4
3
*/
堆优化Dijkstra
用堆实现找距离的最小值
堆的实现方式
【1】手写堆
【2】优先队列priority_queue(不支持修改元素)
题目:求含有重边和自环的最短路
# include <iostream>
# include <string.h>
# include <algorithm>
# include <stdio.h>
# include <vector>
# include <queue>
using namespace std;
const int N = 1e5+10;
int n, m;
int h[N], w[N], e[N], ne[N], idx, dist[N];
// w数组是点a链表中,点a到点b的权重
bool st[N];
typedef pair<int,int> PII;
void add(int a, int b, int c){
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx;
idx++;
}
int dijkstra(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0; //初始点到自身的距离是0
// 第一个元素是距离,第二个元素是结点编号
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0,1});
while(heap.size()){
// 取出队头元素
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if(st[ver]){
continue;
// 当前结点已经被访问
}
for(int i=h[ver]; i!=-1; i=ne[i]){
int j=e[i]; //结点编号
if(dist[j] > distance + w[i]){
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if(dist[n] == 0x3f3f3f3f){
return -1; // 不可达
}
return dist[n];
}
int main(){
cin>>n>>m;
memset(h, -1, sizeof h); // 初始化链表
while(m--){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c); //优先队列,自动处理重边
}
cout<<dijkstra()<<endl;
return 0;
}
/*
3 3
1 2 2
2 3 1
1 3 4
3
*/
Bellman-Ford
for n times:
backup [array]
for all edges(a, b, w):
dist[b] = min (dist[b], backup[a]+w)
更新过程为
松弛操作
如果图中有负权回路,就不一定有最短路
三角不等式
dist[b] <= dist[a] + w
题目:有边数限制的最短路
# include <iostream>
# include <string.h>
# include <algorithm>
# include <stdio.h>
# include <vector>
# include <queue>
using namespace std;
const int N = 510, M = 10010;
int n, m, k;
int dist[N], backup[N];
struct Edge{
int a,b,w;
}edges[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++){
int a=edges[j].a;
int b=edges[j].b;
int w=edges[j].w;
dist[b] = min(dist[b],backup[a]+w);
}
}
if(dist[n] > 0x3f3f3f3f / 2) return -1;
return dist[n];
}
int main(){
cin>>n>>m>>k;
for(int i=0;i<m;i++){
int a,b,w;
cin>>a>>b>>w;
edges[i]={a,b,w};
}
int t=bellman_ford();
if(t==-1) puts("impossible");
else cout<<t;
return 0;
}
/*
3 3 1
1 2 1
2 3 1
1 3 3
3
*/
SPFA
适用于所有没有负环的问题,优化Bellman-Ford
只有dist[a]变小了,dist[b]才会改变
quene <- 1
while queue not null:
//第一步
t <- q.front
q.pop()
//第二步
更新t的所有出边
queue <- b
题目:负权最短路
# include <iostream>
# include <string.h>
# include <algorithm>
# include <stdio.h>
# include <vector>
# include <queue>
using namespace std;
const int N = 1e5+10;
int n, m;
int h[N],w[N],e[N],ne[N],idx;
int dist[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++;
}
int spfa(){
memset(dist, 0x3f, sizeof dist);
dist[1]=0;
queue<int> q;
q.push(1);
st[1] = 1;
while(q.size()){
int t=q.front();
q.pop();
st[t]=0;
for(int i=h[t];i!=-1;i=ne[i]){
int j=e[i];
if(dist[j]>dist[t]+w[i]){
dist[j]=dist[t]+w[i];
if(!st[j]){
q.push(j);
st[j]=1;
}
}
}
}
if(dist[n]==0x3f3f3f3f) return -1;
return dist[n];
}
int main(){
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
int t=spfa();
if(t==-1) puts("impossible");
else cout<<t;
return 0;
}
/*
3 3
1 2 5
2 3 -3
1 3 4
2
*/
题目:判断负环
1h 52min
Floyd
邻接矩阵d[i, j]
for k in len(n):
for i in len(n):
for j in len(n):
d[i, j] = min(d[i, j], d[i, k] + d[k, j])
不能处理负权回路
本质:动态规划
题目:求最短路
# include <iostream>
# include <string.h>
# include <algorithm>
# include <stdio.h>
# include <vector>
# include <queue>
using namespace std;
const int N = 210, INF = 1e9;
int n, m, Q;
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(){
cin>>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;
}
}
while(m--){
int a,b,w;
cin>>a>>b>>w;
d[a][b]=min(d[a][b],w);
}
floyd();
while(Q--){
int a,b;
cin>>a>>b;
if(d[a][b]>INF/2) puts("impossible");
else cout<<d[a][b];
}
return 0;
}
/*
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
impossible
1
*/
8:最小生成树
Prim
朴素版prim,O(n^2)——稠密图
dist[i] <- 正无穷
for n times:
t <- 找到集合外距离最近的点
用t更新其他点到集合的距离
st[t] = 1
# include <iostream>
# include <string.h>
# include <algorithm>
# include <stdio.h>
# include <vector>
# include <queue>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;
int n, m;
int g[N][N];
int dist[N];
bool st[N];
int prim(){
memset(dist,0x3f,sizeof dist);
int res=0;
for(int i=0;i<n;i++){
int t=-1;
//t 记录集合外最近的点
for(int j=1;j<=n;j++){
// 如果j没访问过,且,t未更新或j的距离更小
if(!st[j]&&(t==-1||dist[t]>dist[j])){
t=j;
//更新
}
}
if(i && dist[t]==INF) return INF;
//如果是第一次找集合,且有不连通的块,则一定不能构成生成树
if(i) res+=dist[t];
//如果是第一次找集合,则更新res
for(int j=1;j<=n;j++){
dist[j]=min(dist[j],g[t][j]);
//更新其他点到集合的距离
}
st[t] = 1;
// t被访问,更新
}
return res;
}
int main(){
cin>>n>>m;
memset(g,0x3f,sizeof g);
while(m--){
int a,b,c;
cin>>a>>b>>c;
g[a][b]=g[b][a]=min(g[a][b],c);
}
int t=prim();
if(t==INF) puts("impossible");
else cout<<t;
return 0;
}
/*
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
6
*/
堆优化版prim,O(mlogn)——稀疏图
用优先队列优化集合数组,和dijkstra的优化类似
Kruskal
O(mlogm)——稀疏图
将所有边按照权重,升序排序,O(mlogm)
枚举每条边a——b,权重是c
if a、b不连通,则将这条边加入集合
# include <iostream>
# include <string.h>
# include <algorithm>
# include <stdio.h>
# include <vector>
# include <queue>
using namespace std;
const int N = 2e5+10;
int n, m;
int p[N];
//并查集的parent
struct Edge{
int a,b,w;
bool operator< (const Edge &W) const{
return w<W.w;
}
}edges[N];
int find(int x){
// 如果x不是祖宗结点,则继续查找
if(x != p[x])
p[x]=find(p[x]);
return p[x];
}
int main(){
cin>>n>>m;
for(int i=0;i<m;i++){
int a,b,w;
cin>>a>>b>>w;
edges[i]={a,b,w};
}
sort(edges,edges+m);
//升序排序
for(int i=1;i<=n;i++){
p[i]=i;
// 初始化p数组
}
int res=0,cnt=0;
//cnt记录已经加入的边数,res记录已加入的总权重
for(int i=0;i<m;i++){
int a=edges[i].a;
int b=edges[i].b;
int w=edges[i].w;
a=find(a);
b=find(b);
if(a!=b){
p[a]=b;
res+=w;
cnt++;
}
}
if(cnt<n-1) puts("impossible");
else cout<<res; //输出权重之和
return 0;
}
/*
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
6
*/
9:二分图
染色法
O(m+n)
染色法作用:判断一个图是否是二分图
一个图是二分图,当且仅当图中不含奇数环
# include <iostream>
# include <string.h>
# include <algorithm>
# include <stdio.h>
# include <vector>
# include <queue>
using namespace std;
const int N = 1e5+10, M = 2e5+10;
int n, m;
int h[N],e[M],ne[M],idx;
int color[N];
void add(int a,int b){
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
bool dfs(int u,int c){
color[u]=c; //当前点的颜色,记录
//遍历当前点的邻接点
for(int i=h[u];i!=-1;i=ne[i]){
int j=e[i];
//如果邻接点未染色
if(!color[j]){
//如果把邻接点染成另外一个颜色不行,就返回0
if(!dfs(j,3-c)) return 0;
}
//如果邻接点有染色
else{
//如果邻接点的颜色和当前点是一样的,就返回0
if(color[j]==c) return 0;
}
}
return 1;
}
int main(){
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--){
int a,b;
cin>>a>>b;
add(a,b);
add(b,a);
}
bool flag=1; //是否有矛盾
for(int i=1;i<=n;i++){
//如果没有被染过
if(!color[i]){
// dfs中有矛盾
if(!dfs(i,1)){
flag=0;
break;
}
}
}
if(flag) puts("Yes");
else puts("No");
return 0;
}
/*
4 4
1 3
1 4
2 3
2 4
Yes
*/
匈牙利算法
最坏O(mn),n个左侧堆的点需要查找n次,每次需要匹配m个右侧堆的点
实际运行时间远小于最坏情况
二部图中,没有2条边使用了同一个点进行匹配
作用:找到最多的边,满足左侧堆和右侧堆的匹配关系
典例:恋爱关系
题目:二分图的最大匹配
# include <iostream>
# include <string.h>
# include <algorithm>
# include <stdio.h>
# include <vector>
# include <queue>
using namespace std;
const int N = 510, M = 1e5+10;
int n1, n2, m;
int h[N],e[M],ne[M],idx;
int match[N];
bool st[N];
//加边
void add(int a,int b){
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
bool find(int x){
for(int i=h[x];i!=-1;i=ne[i]){
int j=e[i]; //还原右侧点
//如果右侧点没访问过
if(!st[j]){
st[j]=1; //设置访问状态
//如果右侧点没有匹配过,或匹配过的左侧点可以换一个右侧点匹配
if(match[j]==0 || find(match[j])){
match[j]=x; //记录右侧点匹配的左侧点
return 1; //成功匹配
}
}
}
return 0;
}
int main(){
cin>>n1>>n2>>m;
memset(h,-1,sizeof h);
while(m--){
int a,b;
cin>>a>>b;
add(a,b);
}
int res=0;
for(int i=1;i<=n1;i++){
memset(st,0,sizeof st);
if(find(i)) res++;
}
cout<<res;
return 0;
}
/*
2 2 4
1 1
1 2
2 1
2 2
2
*/