搜索与图论(一)
文章目录
深度优先搜索 DFS
底层结构:栈(递归)
在树的搜索时尽量向叶子节点搜索,搜到尽头时回朔,找下一个叶子结点,直到遍历所有数
842排列数字
DFS(爆搜)样例
回朔操作画图理解
每层都去枚举1~n的数,用bool st[n]去存储是否使用过该数
并有在回朔时(完成一条路径)的复原操作,即把标记用过的数再改回来
#include <iostream>
using namespace std;
const int N=10;
int path[N]; //定义一条搜索路径,深度从0开始
int n; //数据从1到n path层数从0到n-1
bool st[N]; //st下标代表1~n点 存储数据false表示没有用过,,true表示使用过
void dfs(int u){
if(n==u){
for(int i=0;i<n;i++)printf("%d ",path[i]);
printf("\n");
return;
}
for(int i=1;i<=n;i++){
if(!st[i])
{
path[u]=i; //u是深度,从第0层开始爆搜,如果i没有使用过,把他赋值给当前路径元素
st[i]=true; //在这条路径使用过后,赋值为true
dfs(u+1); //爆搜整条路径直到u==n时会返回上一层(回朔),这时将使用过的元素恢复
st[i]=false; //递归函数结束之后,当前层要立即恢复状态
}
}
}
int main(){
cin>>n;
dfs(0);
return 0;
}
843皇后问题
解决:按行爆搜,不需要考虑行不重复
只需要考虑列和正反对角线,额外开数组存储他们是否有数据
对角线转化的公式
已知正对角线y=-x+b b=x+y
反对角线公式y=x+b b=y-x
用截距b对应表示即可
正对角线 dg[N]=dg[x+y]=dg[i+u] (i是列对应横坐标 u是行对应纵坐标)
反对角线 udg[N]=udg[y-x]=udg[u-i+n]考虑到u-i可能为负数,把他加上n
#include <iostream>
using namespace std;
const int N =20;
int n;
char g[N][N]; //开二维字符数组存放最后结果
char col[N],dg[N],udg[N]; //分别代表当前列,当前正对角线,反对角线 默认false表示没有数据
void dfs(int u){ //按行开始搜索
if(u==n)
{
for(int i=0;i<n;i++)cout<<g[i]<<endl; //按行输出
printf("\n");
return ;
}
for(int i=0;i<n;i++){ //遍历当前行的所有列
if(!col[i]&&!dg[i+u]&&!udg[u-i+n]){ //当插入位置的所在列所在正反对角线都没有元素,那么插入Q
g[u][i]='Q';
col[i]=dg[i+u]=udg[u-i+n]=true;
dfs(u+1);
col[i]=dg[i+u]=udg[u-i+n]=false;
g[u][i]='.'; //注意与排列数字不同,8皇后是二维数组,需要将每一层的数据还原
}
}
}
int main(){
cin>>n;
for(int i=0;i<n;i++) //字符数组初始全部为.
for(int j=0;j<n;j++)
g[i][j]='.';
dfs(0);
return 0;
}
解决2:按元素逐个爆搜
注意一定要挨个遍历,不然可能会漏查
#include <iostream>
using namespace std;
const int N =20;
char g[N][N];
int n;
char row[N],col[N],dg[N],udg[N]; //统计当前行,列,正反对角线是否有重复元素
void dfs(int u,int i,int s){ //行,列,Q的总数
if(i==n){i=0;u++;}
if(u==n){
if(s==n){
for(int i=0;i<n;i++)cout<<g[i]<<endl;
cout<<endl;
}
return;
}
//插入皇后
if(!row[u]&&!col[i]&&!udg[u-i+n]&&!dg[u+i]){
g[u][i]='Q';
row[u]=col[i]=udg[u-i+n]=dg[u+i]=true;
dfs(u,i+1,s+1);
row[u]=col[i]=udg[u-i+n]=dg[u+i]=false;
g[u][i]='.';
}
//不插入皇后
dfs(u,i+1,s);
}
int main(){
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
g[i][j]='.';
dfs(0,0,0);
return 0;
}
总结一下两种方法,第一种方法按行爆搜,第二种方法逐个爆搜
为什么第一种方法没有定义皇后个数s?
因为按行爆搜保证每行只有一个元素,如果能搜到最后一行,那么一定插入了n个皇后
而按个搜索可能会枚举错误的答案导致某行插入不了皇后,个数缺失
注意爆搜存在的常规操作———递归和复原
宽度优先搜索 BFS
底层结构:队列
在树的搜索时按层搜索,遍历完当前层再找下一层
具有最短路特性
844走迷宫
定义一个队列然后将要找的点逐个入队,寻找满足条件的“下一层的点”
将它们入队,更新他们到起点的距离,直到找到最后一层
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef pair <int,int> PII;
const int N=200;
int g[N][N], d[N][N]; //g存放地图,d存放地图各个点到左上的距离
int n,m;
PII q[N*N]; //开一个pair数组用于存放各个点的坐标
void bfs(){
memset(d,-1,sizeof d); //将d数组初始化为-1代表没有遍历过这个点(宽搜只能每次找距离为1的最近的点,不能回头,不能重复找)
d[0][0]=0; //第一个点已经走过,初始化为0,代表距离为0
q[0]={0,0}; //把第一个点入队
int hh=0,tt=0; //队头已经有元素,把tt初始化为0
while(hh<=tt){
auto t=q[hh++]; //让队头出队hh是队头指针
int dx[4]={-1,1,0,0}, dy[4]={0,0,-1,1}; //定义方位向量{{-1,0},{1,0},{0,-1},{0,1}}分别代表向上下左右四个方向走
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<m && d[x][y]==-1 &&g[x][y]==0){ //如果x,y在边界内,并且可以走(g[x][y]==0)并且没有被遍历过(d[x][y]==-1)
q[++tt]={x,y};
d[x][y]=d[t.first][t.second]+1; //让它入队,并且更新这个点到左上角的距离
}
}
}
}
int main(){
cin>>n>>m;
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
scanf("%d",&g[i][j]);
bfs();
cout<<d[n-1][m-1]; //输出右下角的点到起点的距离
return 0;
}
845八数码
1.本题求最少步数,所以应当用bfs来做
2.如果状态为目标状态,那么返回步数,如果更新不到目标状态,返回-1
3我们可以想到,3*3的矩阵可以表示为一个长度为9的字符串
用12345678x表示最终状态,每改变一次就使那个状态与初始状态(start)
的距离加一
4.使用hashmap 将字符串string 映射到int表示距离distance
使用队列queue保存和释放每一步状态
5.bfs需要把遍历过的状态标记,以防止死循环
本题我们是用map的count函数查找是否存在当前字符串(状态)的映射
#include <iostream>
#include <cstring>
#include<algorithm>
#include <queue>
#include <unordered_map>
using namespace std;
string start;
queue<string> q; //定义状态字符串队列q
unordered_map<string,int> d; //字符串状态的距离映射
int bfs(string start){
q.push(start); //初始状态
d[start]=0; //初始状态距离为0
string end="12345678x"; //目标状态
int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1}; //二位数组中的方向向量
while(q.size()){
auto t=q.front(); //出队q,寻找下一个状态
q.pop();
if(t == end)return d[t];
int k=t.find('x'); //用k保存当前状态中x的位置
int distance=d[t]; //distance保存当前状态的距离
int x= k / 3, y= k % 3; //将字符串中x的位置映射到二维坐标
for(int i=0;i<4;i++)
{
int a=x+dx[i], b=y+dy[i]; //遍历x可能要交换的元素
if(a>=0&&a<3&&b>=0&&b<3)
{
swap(t[k],t[a*3+b]); //交换得到新的状态串t
if(!d.count(t)) //如果map中存在t的键值,返回1否则返回-1
{
d[t]=distance+1;
q.push(t); //没有遍历过就把他入队并且更新距离
}
swap(t[k],t[a*3+b]); //与dfs不同,这里恢复是为了下次循环字符串t一致
}
}
}
return -1; //没有找到目标状态返回-1
}
int main(){
char c[2];
for(int i=0;i<9;i++){
cin>>c;
start +=c[0];
}
cout<<bfs(start);
return 0;
}
总结一下宽搜
相对于暴搜宽搜不存在递归过程,理解是因为它按层搜索,搜索就必定把当前层搜索完,不会回头,且每次前进最短路径,常用于解决最小路径问题
解宽搜问题必存在一队列用于保存各个搜索到的状态
同时必存在一路径数组用于保存“距离”
必须要解决重复搜索的问题(定义初值-1,或者讨论是否存在)
树与图的存储
无向图是一种特殊的有向图
有向图分为邻接矩阵(数组存储)和邻接表(几乎和拉链法哈希表相同)
树与图的深度优先遍历 O(n+m)点数+边数
846树的重心
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10,M=2*N;
int h[N],e[M],ne[M],idx; //因为是无向图(双边)所以数据和指针开两倍
int n;
bool st[N]; //st[]代表是否遍历过
int ans=N; //最后结果,一堆最大值里面的最小值
void insert(int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++; //存储方式:拉链法哈希
}
//返回以u作为根节点的节点数量,同时u作为要删的节点
int dfs(int u){
int sum=1,res=0; //当前节点算一个,节点数量和sum计1,res是连通块的最大值
st[u]=true;
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i]; //j是下一个要找的头结点
if(!st[j])
{
int s= dfs(j);
sum+=s;
res=max(res,s);
}
}
res=max(res,n-sum); //去掉根节点的另外一连通块是n-sum-1
ans=min(ans,res);
return sum;
}
int main(){
memset(h,-1,sizeof h); //拉链哈希初始化头结点全部指向-1
cin>>n;
int m=n-1;
while(m--){
int a,b;
cin>>a>>b;
insert(a,b),insert(b,a); //双向导通左右都要插入
}
dfs(1); //从任意节点开始搜:因为是双向的所以从哪个节点开搜都无所谓
cout<<ans;
return 0;
}
树与图的广度优先遍历
847图中点的层次
#include <iostream>
#include<algorithm>
#include <cstring>
using namespace std;
const int N=1e5+10;
int h[N],e[N],ne[N],idx;
int n,m;
int q[N],d[N]; //队列,距离
//a指向b
void add(int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int bfs(){
memset(d,-1,sizeof d);
q[0]=1; //第一个点入队
d[1]=0; //起始节点是1,距离是0
int hh,tt;
hh=tt=0;
while(hh<=tt){
int t = q[hh++];
if(t==n)return d[n];
for(int i=h[t];i!=-1;i=ne[i]){ //每一层入队,再遍搜下一层
int j=e[i];
if(d[j]==-1){
q[++tt]=j;
d[j]=d[t]+1;
}
}
}
return -1;
}
int main(){
cin>>n>>m;
memset(h,-1,sizeof h); //初始化头结点
while(m--){
int a,b;
cin>>a>>b;
add(a,b);
}
cout<<bfs();
return 0;
}
拓扑排序
图的宽搜的应用(有向图)
一个拓扑序列的所有边 的指向一定是从前向后的(如上图)
所以123是一个拓扑序列,并且拓扑序列不是唯一的
可以证明,一个有向无环图一定存在一个拓扑序列
图的入度和出度的概念
848有向图的拓扑序列
思路:将入度为0的点搜索出来加入队头
利用宽搜找到下一层的点,使他们的入度减一(相当于删除前一节点)
如果入度为0则入队,否则拓展其他的点
最后如果全部的点都入队,则存在拓扑序列
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10;
int n,m;
int q[N],d[N]; //队列q,入度d,初始入度都是0
int h[N],e[N],ne[N],idx;
void insert (int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
bool topsort(){
int hh,tt;
hh=0,tt=-1;
for(int i=1;i<=n;i++){ //找到1到n中入度是0的点,其必在拓扑序列中的第一个,让他们入队
if(d[i]==0)
q[++tt]=i;
}
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];i!=-1;i=ne[i]){ //找到t所指向的所有点,并使他们的入度-1(使他们的入度-1查找是否还有除t以外的点指向他们)
int j=e[i]; //如果有且仅有t指向他们,让他们入队
d[j]--;
if(!d[j])q[++tt]=j;
}
}
return tt==n-1; //队列若有n个点,说明存在拓扑序列
}
int main(){
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--){
int a,b;
cin>>a>>b;
insert (a,b);
d[b]++; //a指向b所以b的入度加一
}
if(topsort()){
for(int i=0;i<n;i++)cout<<q[i]<<" ";
}
else cout<<-1;
return 0;
}