搜索与图论(一)

搜索与图论(一)

深度优先搜索 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;
    
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值