十一、Flood Fill算法
既可以宽搜实现,也可以深搜实现,宽搜具有最短性,深搜方便写,但容易爆栈。
宽搜思路:每次将周围的点放入队列,然后一圈一圈地往外扩展。
深搜思路:每次按四个方向分别进行扩展,直到一个方向无法进行扩展时,回溯。
题目链接:1113. 红与黑
11.1题目描述
有一间长方形的房子,地上铺了红色、黑色两种颜色的正方形瓷砖。
你站在其中一块黑色的瓷砖上,只能向相邻(上下左右四个方向)的黑色瓷砖移动。
请写一个程序,计算你 总共能够到达多少块黑色的瓷砖。
输入格式
输入包括多个数据集合。
每个数据集合的第一行是两个整数 W 和 H,分别表示 x 方向和 y 方向瓷砖的数量。
在接下来的 H 行中,每行包括 W 个字符。每个字符表示一块瓷砖的颜色,规则如下
1)‘.’:黑色的瓷砖;
2)‘#’:红色的瓷砖;
3)‘@’:黑色的瓷砖,并且你站在这块瓷砖上。该字符在每个数据集合中唯一出现一次。
当在一行中读入的是两个零时,表示输入结束。
输出格式
对每个数据集合,分别输出一行,显示你从初始位置出发能到达的瓷砖数(记数时包括初始位置的瓷砖)。
数据范围
1≤W,H≤20
输入样例:
6 9 ....#. .....# ...... ...... ...... ...... ...... #@...# .#..#. 0 0
输出样例:
45
11.2思路分析
利用Flood Fill算法,注意细节。
11.3代码实现
bfs代码
#include <iostream> #include <queue> #include <utility> using namespace std; typedef pair<int,int> PII; const int N=25; int w,h; int dx[]={1,-1,0,0},dy[]={0,0,1,-1}; //方向数组 char g[N][N]; int ans; int bfs(int x,int y){ queue<PII> q; q.push({x,y}); g[x][y]='#'; //修改当前格子内容,表示已遍历过 int res=0; while(!q.empty()){ PII t=q.front(); q.pop(); res++; //拓展队头 for(int i=0;i<4;i++){ int a=t.first+dx[i],b=t.second+dy[i]; if(a>=0&&a<h&&b>=0&&b<w&&g[a][b]=='.'){ res+=bfs(a,b); } } } return res; } int main(){ while(cin>>w>>h,w||h){ for(int i=0;i<h;i++){ for(int j=0;j<w;j++){ cin>>g[i][j]; } } for(int i=0;i<h;i++){ for(int j=0;j<w;j++){ if(g[i][j]=='@') ans=bfs(i,j); } } cout<<ans<<endl; } return 0; }
y总代码
#include <iostream> #include <queue> #include <utility> using namespace std; typedef pair<int,int> PII; const int N=25; int dx[]={1,-1,0,0},dy[]={0,0,1,-1}; char g[N][N]; int w,h; int ans; int bfs(int x,int y){ queue<PII> q; q.push({x,y}); g[x][y]='#'; int res=0; while(!q.empty()){ PII t=q.front(); q.pop(); res++; for(int i=0;i<4;i++){ int a=t.first+dx[i],b=t.second+dy[i]; if(a>=0&&a<h&&b>=0&&b<w&&g[a][b]=='.'){ g[a][b]='#'; q.push({a,b}); } } } return res; } int main(){ while(cin>>w>>h,w||h){ //while循环条件为w||h的值 for(int i=0;i<h;i++){ for(int j=0;j<w;j++){ cin>>g[i][j]; } } for(int i=0;i<h;i++){ for(int j=0;j<w;j++){ if(g[i][j]=='@') ans=bfs(i,j); } } cout<<ans<<endl; } return 0; }
dfs代码
#include <iostream> using namespace std; const int N=25; int w,h; int dx[]={1,-1,0,0},dy[]={0,0,1,-1}; //方向数组 char g[N][N]; int ans; int dfs(int x,int y){ int res=1; g[x][y]='#'; //修改当前位置的值,表示已经遍历过 //递归扩展四个方向 for(int i=0;i<4;i++){ int a=x+dx[i],b=y+dy[i]; if(a>=0&&a<h&&b>=0&&b<w&&g[a][b]=='.'){ res+=dfs(a,b); } } return res; } int main(){ while(cin>>w>>h,w||h){ for(int i=0;i<h;i++){ for(int j=0;j<w;j++){ cin>>g[i][j]; } } for(int i=0;i<h;i++){ for(int j=0;j<w;j++){ if(g[i][j]=='@') ans=dfs(i,j); } } cout<<ans<<endl; } return 0; }
十二、Kruskal算法
时间复杂度是O(mlogn),n表示点数,m表示边数
核心模板
模板1
int n,m; //n是点数,m是边数 int p[N]; //并查集的父结点数组 //存储边 struct Edge{ int a,b,w; bool operator< (const Edge &w) const{ return w<W.w; } }edges[M]; //并查集核心操作 int find(int x){ if(p[x]!=x) p[x]=find(p[x]); return p[x]; } int kruskal(){ sort(edges,edges+m); for(int i=1;i<=n;i++) p[i]=i; //初始化并查集 int res=0,cnt=0; for(int i=0;i<m;i++){ int a=edges[i].a,b=edges[i].b,w=edges[i].w; a=find(a),b=find(b); if(a!=b){ //如果两个连通块不连通,则将这两个连通块合并 p[a]=b; res+=w; cnt++; } } if(cnt<n-1) return INF; return res; }
模板2
int n,m; //n是点数,m是边数 int p[N]; //并查集的父结点数组 //存储边 struct Edge{ int a,b,w; }edges[M]; //手写比较函数,使sort能够为结构体排序 bool cmp(Edge A,Edge B){ return A.w<B.w; } //并查集核心操作 int find(int x){ if(p[x]!=x) p[x]=find(p[x]); return p[x]; } int kruskal(){ sort(edges,edges+m,cmp); for(int i=1;i<=n;i++) p[i]=i; //初始化并查集 int res=0,cnt=0; for(int i=0;i<m;i++){ int a=edges[i].a,b=edges[i].b,w=edges[i].w; a=find(a),b=find(b); if(a!=b){ //如果两个连通块不连通,则将这两个连通块合并 p[a]=b; res+=w; cnt++; } } if(cnt<n-1) return INF; return res; }
题目链接:859. Kruskal算法求最小生成树
12.1题目描述
给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。
给定一张边带权的无向图 G=(V,E),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|,m=|E|。
由 V 中的全部 n 个顶点和 E 中 n−1 条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。
输入格式
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含三个整数 u,v,w,表示点 u 和点 v 之间存在一条权值为 w 的边。
输出格式
共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。
数据范围
1≤n≤105,1≤m≤2∗105,图中涉及边的边权的绝对值均不超过 1000。
输入样例:
4 5 1 2 1 1 3 2 1 4 3 2 3 2 3 4 4
输出样例:
6
1
12.2思路分析
利用Kruskal算法,注意细节,具体思想见注释部分。
12.3代码实现
代码1
#include <iostream> #include <algorithm> using namespace std; const int N=200010; int n,m; int p[N]; //并查集祖宗结点数组 //结构体存储每条边 struct Edge{ int a,b,w; //重载小于号 bool operator< (const Edge &W) const{ return w<W.w; } }edges[N]; //并查集查找组总结点操作 int find(int x){ if(p[x]!=x) p[x]=find(p[x]); return p[x]; } int main(){ cin>>n>>m; for(int i=1;i<=n;i++){ //初始化并查集结点 p[i]=i; } for(int i=0;i<m;i++){ //输入边 int u,v,w; cin>>u>>v>>w; edges[i]={u,v,w}; } sort(edges,edges+m); //按边权重从小到大将每条边排序 int ans=0,cnt=0; //ans存储最小生成树边权重之和,cnt存储最小生成树中的边数 for(int i=0;i<m;i++){ int a=edges[i].a,b=edges[i].b,w=edges[i].w; if(find(a)!=find(b)){ //如果a,b不在一个集合中,则合并它们 p[find(a)]=find(b); ans+=w; cnt++; } } if(cnt<n-1) cout<<"impossible"; //如果边数小于n-1,则最小生成树不存在 else cout<<ans; return 0; }
代码2
#include <iostream> #include <algorithm> using namespace std; const int N=200010; int n,m; int p[N]; //并查集祖宗结点数组 //结构体存储每条边 struct Edge{ int a,b,w; }edges[N]; //手写比较函数,使sort能够为结构体排序 bool cmp(Edge A,Edge B){ return A.w<B.w; } //并查集查找组总结点操作 int find(int x){ if(p[x]!=x) p[x]=find(p[x]); return p[x]; } int main(){ cin>>n>>m; for(int i=1;i<=n;i++){ //初始化并查集结点 p[i]=i; } for(int i=0;i<m;i++){ //输入边 int u,v,w; cin>>u>>v>>w; edges[i]={u,v,w}; } sort(edges,edges+m,cmp); //按边权重从小到大将每条边排序 int ans=0,cnt=0; //ans存储最小生成树边权重之和,cnt存储最小生成树中的边数 for(int i=0;i<m;i++){ int a=edges[i].a,b=edges[i].b,w=edges[i].w; if(find(a)!=find(b)){ //如果a,b不在一个集合中,则合并它们 p[find(a)]=find(b); ans+=w; cnt++; } } if(cnt<n-1) cout<<"impossible"; //如果边数小于n-1,则最小生成树不存在 else cout<<ans; return 0; }
十三、染色法判别二分图
二分图当前仅当图中含奇数环
核心模板
时间复杂度是O(n+m),n表示点数,m表示边数
int n; //n表示点数 int h[N],e[M],ne[M],idx; //邻接表存储图 int color[N]; //表示每个点的颜色,-1表示未染色,0表示白色,1表示黑色 //参数:u表示当前结点,c表示当前点的颜色 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]==-1){ if(!dfs(j,!c)) return false; } else if(color[j]==c) return false; } return true; } bool check(){ memset(color,-1,sizeof color); bool flag=true; for(int i=1;i<=n;i++){ if(color[i]==-1){ if(!dfs(i,0)){ flag=false; break; } } } return flag; }
题目链接: 860. 染色法判定二分图
13.1题目描述
给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环。
请你判断这个图是否是二分图。
输入格式
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含两个整数 u 和 v,表示点 u 和点 v 之间存在一条边。
输出格式
如果给定图是二分图,则输出 Yes,否则输出 No。
数据范围
1≤n,m≤105
输入样例:
4 4 1 3 1 4 2 3 2 4
13.2思路分析
使用染色法判定二分图:相邻点一定是不同的颜色。注意细节,具体思想见注释部分。
13.3代码实现
#include <iostream> #include <cstring> using namespace std; const int N=100010,M=2*N; //存储的是无向边,存储两次,所以得是点数的二倍 int h[N],e[M],ne[M],idx; //邻接表存储图 int color[N]; int n,m; //邻接表加边 void add(int a,int b){ e[idx]=b; ne[idx]=h[a]; h[a]=idx++; } //dfs进行染色 bool dfs(int u,int c){ //u代表当前点的编号,c代表当前点的颜色:-1代表没染色,0代表为白色,1代表为黑色 color[u]=c; //将该点染色 //遍历其所有相邻点 for(int i=h[u];i!=-1;i=ne[i]){ int j=e[i]; if(color[j]==-1){ //如果其相邻点没有被染色 if(!dfs(j,!c)) return false; //如果该相邻点不能被染成与其不同的颜色,则无法完成染色 } else if(color[j]==c) return false; //如果其相邻点和其为相同颜色,则无法完成染色 } return true; } //判断每个点是否能够完成染色 bool check(){ memset(color,-1,sizeof color); //记得初始化 for(int i=1;i<=n;i++){ if(color[i]==-1){ if(!dfs(i,0)) return false; //如果存在点无法完成染色,则不是二分图 } } return true; } int main(){ cin>>n>>m; memset(h,-1,sizeof h); //记得初始化 while(m--){ int u,v; cin>>u>>v; add(u,v),add(v,u); } bool flag=check(); if(flag) cout<<"Yes"; else cout<<"No"; return 0; }
十四、匈牙利算法
核心模板
时间复杂度是O(nm),n表示点数,m表示边数
int n1,n2; //n1表示第一个集合中的点数,n2表示第二个集合中的点数
int h[N],e[M],ne[M],idx; //邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边
int match[N]; //存储第二集合中的每个点当前匹配的第一个集合中的点是哪个 bool st[N]; //表示第二个集合中的每个点是否已经被遍历过 bool find(int x){ for(int i=h[x];i!=-1;i=ne[i]){ int j=e[i]; if(!st[j]){ st[j]=true; if(match[j]==0||find(match[j])){ match[j]=x; return true; } } } return false; } //求最大匹配数,依次枚举第一个集合中的每个点能否匹配第二个集合中的点 int res=0; for(int i=1;i<=n1;i++){ memset(st,false,sizeof st); if(find(i)) res++; }
下图来自AcWing官网,作者们如图,侵权删。
题目链接:861. 二分图的最大匹配
14.1题目描述
给定一个二分图,其中左半部包含 n1 个点(编号 1∼n1),右半部包含 n2 个点(编号 1∼n2),二分图共包含 m 条边。
数据保证任意一条边的两个端点都不可能在同一部分中。
请你求出二分图的最大匹配数。
二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E}中的任意两条边都不依附于同一个顶点,则称 M 是一个匹配。
二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。
输入格式
第一行包含三个整数 n1、 n2 和 m。
接下来 m 行,每行包含两个整数 u 和 v,表示左半部点集中的点 u 和右半部点集中的点 v 之间存在一条边。
输出格式
输出一个整数,表示二分图的最大匹配数。
数据范围
1≤n1,n2≤500,1≤u≤n1,1≤v≤n2,1≤m≤105
输入样例:
2 2 4 1 1 1 2 2 1 2 2
输出样例:
2
14.2思路分析
使用匈牙利算法:枚举第一个集合中的点,每次都在第二个集合中找是否存在和它能够匹配成功的点,如果存在,结果加1。如果遇到第一个集合中的点在第二个集合中应该和它匹配的点已经被匹配了,就尝试是否可以使已经与第二个集合中的点匹配的第一个集合中的点换一个匹配点,如果可以,则将原匹配拆散,更新为新匹配,将当前枚举的点与拆散后的第二个集合中的点匹配;如果不可以,则该点无法完成匹配。
注意:“数据保证任意一条边的两个端点都不可能在同一部分中”即数据保证图是一个二分图,如果存在1到1的边,不是自环,而是从第一部分中的1指向第二部分中的1。
14.3代码实现
#include <iostream> #include <cstring> using namespace std; const int N=510,M=100010; int h[N],e[M],ne[M],idx; //邻接表存储每条边 int n1,n2,m; int match[N]; //存储当前与第二个集合匹配的第一个集合中的点是哪个 bool st[N]; //存储第二个集合中的点是否已经被遍历过 //邻接表加边 void add(int a,int b){ e[idx]=b; ne[idx]=h[a]; h[a]=idx++; } //查找是否存在与x匹配的点 bool find(int x){ //枚举x的每条出边 for(int i=h[x];i!=-1;i=ne[i]){ int j=e[i]; if(!st[j]){ //如果当前点没有被遍历过 st[j]=true; //将当前点设置为已遍历过 if(match[j]==0||find(match[j])){ //如果该点没有与第一个集合中的点匹配或者已经匹配但是匹配的第一个集合中的点可以更换一个新的匹配 match[j]=x; //则将原匹配拆散,j的新匹配为x return true; } } } return false; } int main(){ cin>>n1>>n2>>m; memset(h,-1,sizeof h); //记得初始化 while(m--){ int u,v; cin>>u>>v; add(u,v); } int ans=0; //枚举第一个集合中的每个点,看其是否能够匹配成功 for(int i=1;i<=n1;i++){ memset(st,false,sizeof st); //每次都先将第二个集合中的所有点设置为未遍历过,因为当前枚举的点要查看所有可以与其匹配的点是否能够和它匹配,而这些可以与它匹配的点可能也和其它的第一个集合中的点存在可匹配关系,所以每次都要清空st[] if(find(i)) ans++; //如果该点存在可以匹配的点,答案+1 } cout<<ans; return 0; }
补充题目
下述题目均来自蓝桥官网,侵删。
题目一
题目链接:
长草
1.1题目描述
1.2思路分析
利用bfs进行搜索,注意怎样控制扩展k层:第一次,首先把可以扩展的点放入队列中,然后在bfs中循环k次,每次都把队列中的元素进行拓展,注意只拓展队列中的元素,而且只拓展元素的数量次,所以循环的条件就是:第i次循环开始是队列中存在多少个可以拓展的点,就拓展多少次,(注意和一般宽搜条件不同,宽搜的条件是一直往外扩展,所以其条件是队列不空就往外拓展,这道题由于要控制拓展次数,所以我们的循环条件是外层循环开始时队列中有的元素个数)。
1.3代码实现
#include <iostream> #include <queue> #include <utility> using namespace std; typedef pair<int,int> PII; const int N=1010; int dx[]={1,-1,0,0},dy[]={0,0,1,-1}; //方向数组 queue<PII> q; char g[N][N]; int n,m,k; void bfs(){ while(k--){ //拓展k次 int cnt=q.size(); //利用cnt控制,每次只拓展队列中已有的元素,新加入的不拓展,属于下一次拓展的点 while(cnt--){ PII t=q.front(); //宽搜 q.pop(); for(int i=0;i<4;i++){ int a=t.first+dx[i],b=t.second+dy[i]; if(a>=0&&a<n&&b>=0&&b<m&&g[a][b]=='.'){ g[a][b]='g'; q.push({a,b}); } } } } } int main() { cin>>n>>m; for(int i=0;i<n;i++){ for(int j=0;j<m;j++){ cin>>g[i][j]; if(g[i][j]=='g') q.push({i,j}); //将最初的可以拓展的点加入队列 } } cin>>k; bfs(); //注意,别忘记调用函数 for(int i=0;i<n;i++){ for(int j=0;j<m;j++){ cout<<g[i][j]; } cout<<endl; } return 0; } 1
注:本题也可参考我的这篇文章
题目二
题目链接:
排列序数
2.1题目描述
2.2思路分析
dfs搜索所有情况,每次搜索到一种情况,将对应的序号记录在哈希表中,之后进行查找即可。(或者直接当找到该种情况直接输出,然后结束程序即可,即exit(0))
2.3代码实现
#include <iostream> #include <unordered_map> #include <string> using namespace std; const int N=15; bool st[N]; string s; int idx,len; void dfs(int u,string path){ if(u==len){ if(path==s){ cout<<idx; exit(0); //找到结束程序 } idx++; return ; } for(int i=0;i<len;i++){ if(!st[i]){ char in=char('a'+i),tmp=path[u]; path[u]=in; st[i]=true; dfs(u+1,path); st[i]=false; path[u]=tmp; } } } int main() { cin>>s; len=s.size(); dfs(0,s); return 0; }
隐藏回溯
#include <iostream> #include <unordered_map> #include <string> using namespace std; const int N=15; bool st[N]; string s; int idx,len; void dfs(int u,string path){ if(u==len){ if(path==s){ cout<<idx; exit(0); } idx++; return ; } for(int i=0;i<len;i++){ if(!st[i]){ char in=char('a'+i); st[i]=true; dfs(u+1,path+in); //隐藏回溯,dfs完path的值没有变 st[i]=false; } } } int main() { cin>>s; len=s.size(); dfs(0,""); return 0; }