数据结构和算法设计——搜索

搜索

宽度优先遍历:dfs

模板代码:

 queue<int> q;
 void dfs(){
 st[1] = true; // 表示1号点已经被遍历过
 q.push(1);
 while (q.size()){
     int t = q.front();
     q.pop();
     for (int i=h[t];i!=-1;i=ne[i]){//拓展结点:四联通、八联通
         int j=e[i];
         if (!st[j]){
             st[j]=true;//标记j
             q.push(j);
         }
     }
 }

Flood Fill 模型

池塘计数

农夫约翰有一片 N∗M的矩形土地。最近,由于降雨的原因,部分土地被水淹没了。现在用一个字符矩阵来表示他的土地。每个单元格内,如果包含雨水,则用”W”表示,如果不含雨水,则用”.”表示。现在,约翰想知道他的土地中形成了多少片池塘。
每组相连的积水单元格集合可以看作是一片池塘。每个单元格视为与其上、下、左、右、左上、右上、左下、右下八个邻近单元格相连。请你输出共有多少片池塘,即矩阵中共有多少片相连的”W”块。
数据范围
1≤N,M≤1000
输入样例:
10 12
W........WW.
.WWW.....WWW
....WW...WW.
.........WW.
.........W..
..W......W..
.W.W.....WW.
W.W.W.....W.
.W.W......W.
..W.......W.
输出样例:
3  

 const int N=1010,M=N*N;
 typedef pair<int,int>PII;/*坐标是两维*/
 queue<PII>q;
 char f[N][N];
 bool st[N][N];
 int n,m;
 /*每次出队队首元素,扩展八联通图,将结点符合条件的加入队列,直到队列为空*/
 void bfs(int x,int y){
     q.push({x,y});
     st[x][y]=true;
     while(q.size()){
         PII t=q.front();
         q.pop();
         /*八联通:挖去中间*/
         for(int i=t.first-1;i<=t.first+1;i++)
            for(int j=t.second-1;j<=t.second+1;j++){
                if(i==t.first&&j==t.second)  continue;/*注意*/
                if(f[i][j]=='.'||st[i][j])  continue;
                if(i<=0 || i>n || j<=0 || j>m)  continue;
                st[i][j]=true;
                q.push({i,j});        
            }
     }
 }
 int main(){
     cin>>n>>m;
     for(int i=1;i<=n;i++)  
       for(int j=1;j<=m;j++)cin>>f[i][j];
       /*bfs():只要符合条件且未遍历过,就dfs*/
       int cnt=0;
       for(int i=1;i<=n;i++)
          for(int j=1;j<=m;j++){
              if(!st[i][j]&&f[i][j]=='W'){
                  cnt++;
                  bfs(i,j);
              }
          }
       cout<<cnt<<endl;
 }

城堡问题:变量迭代+二进制

   1   2   3   4   5   6   7  
   #############################
 1 #   |   #   |   #   |   |   #
   #####---#####---#---#####---#
 2 #   #   |   #   #   #   #   #
   #---#####---#####---#####---#
 3 #   |   |   #   #   #   #   #
   #---#########---#####---#---#
 4 #   #   |   |   |   |   #   #
   #############################
   #  = Wall   |  = No wall  -  = No wall
请你编写一个程序,计算城堡一共有多少房间,最大的房间有多大。城堡被分割成 m?n个方格区域,每个方格区域可以有0~4面墙。注意:墙体厚度忽略不计。
输入格式:第一行包含两个整数 m 和 n,分别表示城堡南北方向的长度和东西方向的长度。接下来 m 行,每行包含 n 个整数,每个整数都表示平面图对应位置
的方块的墙的特征。每个方块中墙的特征由数字 P 来描述,我们用1表示西墙,2表示北墙,4表示东墙,8表示南墙,P 为该方块包含墙的数字之和。
例如,如果一个方块的 P 为3,则 3 = 1 + 2,该方块包含西墙和北墙。城堡的内墙被计算两次,方块(1,1)的南墙同时也是方块(2,1)的北墙。
输入的数据保证城堡至少有两个房间。
输出格式:共两行,第一行输出房间总数,第二行输出最大房间的面积(方块数)。
数据范围:1≤m,n≤50,0≤P≤15

 const int N=60;
 typedef pair<int ,int>PII;
 int g[N][N];
 bool st[N][N];
 queue <PII>q;
 int n,m;
 /*每个点就是一个一平方米的面积四面有或没有墙,若此方向没有墙,则将此方向扩展到队列中*/
 int dfs(int dx,int dy){
     int area=0;
     int sx[4]={0,-1,0,1},sy[4]={-1,0,1,0};
     q.push({dx,dy});
     st[dx][dy]=true;
     while(q.size()){
         PII t=q.front();
         area++;
         q.pop();
         for(int i=0;i<4;i++){//四联通
             int a=t.first+sx[i],b=t.second+sy[i];
             if(a<=0||a>n||b<=0||b>m) continue;
             if(st[a][b]||g[t.first][t.second]>>i&1)  continue;/*二进制求第k位的数:右移*/
             st[a][b]=true;
             q.push({a,b});
         }
     }
     return area;
 }
 int main(){
        int cnt=0,area;
        /*dfs*/
     for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++){
            if(!st[i][j]){
                area=max(area,dfs(i,j));
                cnt++;
            }
        }
        cout<<cnt<<endl;  cout<<area<<endl;
 }

山峰和山谷:原题

FGD小朋友特别喜欢爬山,在爬山的时候他就在研究山峰和山谷。为了能够对旅程有一个安排,他想知道山峰和山谷的数量。给定一个地图,为FGD想要旅行的区域,
地图被分为 n×n 的网格,每个格子 (i,j) 的高度 w(i,j)是给定的。
若两个格子有公共顶点,那么它们就是相邻的格子,我们定义一个格子的集合 S为山峰(山谷)当且仅当:S的所有格子都有相同的高度。S 的所有格子都连通。
对于s属于S与s相邻的 s′ 不属于 S,都有 ws>ws′(山峰),或者 ws< ws′(山谷)。
如果周围不存在相邻区域,则同时将其视为山峰和山谷。你的任务是,对于给定的地图,求出山峰和山谷的数量,如果所有格子都有相同的高度,那么整个地图即
是山峰,又是山谷。
输入格式:第一行包含一个正整数 nn,表示地图的大小。接下来一个 n×nn×n 的矩阵,表示地图上每个格子的高度 ww。
输出格式:共一行,包含两个整数,表示山峰和山谷的数量。
数据范围:1≤n≤10001≤n≤1000,0≤w≤109

 const int N=1010;
 typedef pair<int ,int>PII;
 int g[N][N];
 bool st[N][N];
 queue <PII>q;
 int n;
 /*每个点就是一个一平方米的面积四面有或没有墙,若此方向没有墙,则将此方向扩展到队列中*/
 void  dfs(int dx,int dy,bool & has_highter,bool &has_lower){
     q.push({dx,dy});
     st[dx][dy]=true;
     while(q.size())
     {
         PII t=q.front();
         q.pop();
         for(int i=t.first-1;i<=t.first+1;i++)
           for(int j=t.second-1;j<=t.second+1;j++){
               if(i<=0||i>n||j<=0||j>n)  continue;
               if(g[i][j]!=g[t.first][t.second])/*扩展结点高度不一样*/{
                   if(g[i][j]>g[t.first][t.second]) has_highter=true;
                   else has_lower=true;
               }else if(!st[i][j]){/*若相等且没有遍历过,必须放在上一个if的后边因为有遍历过但高度不同*/
                   st[i][j]=true;
                   q.push({i,j});
               } } } 
 }
 int main(){
        int peak=0,vally=0;
        /*dfs*/
     for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++){
            if(!st[i][j]){
                bool has_lower=false,has_highter=false;
                dfs(i,j,has_highter,has_lower);
                if(!has_highter)  peak++;
                if(!has_lower)  vally++;
            }
        }
        cout<<peak<<" "<<vally<<endl;
 }

最短路模型

走迷宫:记录距离

给定一个 n×m 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。最初,有一个人位于左上角 (1,1) 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。
请问,该人从左上角移动至右下角 (n,m) 处,至少需要移动多少次。数据保证 (1,1) 处和 (n,m) 处的数字为 0,且一定至少存在一条通路。
输入格式:第一行包含两个整数 n 和 m;接下来 n 行,每行包含 m 个整数(0 或 1),表示完整的二维数组迷宫。
输出格式:输出一个整数,表示从左上角移动至右下角的最少移动次数。
数据范围:1≤n,m≤100

 const int N=110;
 typedef pair<int,int >PII;
 int f[N][N],n,m,d[N][N];
 /*广度优先遍历*/
    int bfs(){
        memset(d,-1,sizeof d);
        d[1][1]=0;
        queue<PII> q;
        q.push({1,1});/*将源点到源点的距离设为0*/
        int dx[]={-1,0,1,0},dy[]={0,1,0,-1};
        /*广度优先遍历:使用的是队列*/
         while (q.size())
         {
            auto t=q.front();
            q.pop();
            for(int i=0;i<4;i++){/*遍历四个方向*/
                int x=t.first+dx[i],y=t.second+dy[i];
                if(x>0&&x<=n&&y>0&&y<=m&&f[x][y]==0&&d[x][y]==-1){
                   d[x][y]=d[t.first][t.second]+1;/*距离加1*/
                    q.push({x,y});/*将扩展点加入队列*/
                }
            }
        }
        return d[n][m];/*返回(n,m)到源点的距离*/
    }

迷宫问题:输出路径

给定一个 n×nn×n 的二维数组,如下所示:int maze[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,
};
它表示一个迷宫,其中的1表示墙壁,0表示可以走的路,只能横着走或竖着走,不能斜着走,要求编程序找出从左上角到右下角的最短路线。
数据保证至少存在一条从左上角走到右下角的路径。
输入格式:第一行包含整数 n。接下来n行,每行包含n个整数 0 或 1,表示迷宫。
输出格式:输出从左上角到右下角的最短路线,如果答案不唯一,输出任意一条路径均可。按顺序,每行输出一个路径中经过的单元格的坐标,左上角坐标为 (0,0),右下角坐标为 (n-1,n-1)。
数据范围:0≤n≤1000

 const int N=1010,M=N*N;
 typedef pair<int,int>PII;
 int g[N][N],n;
 PII  pre[N][N];/*记录前驱坐标(被谁扩展)*/
 queue<PII> q;
 bool st[N][N];
 /*bfs遍历*/
 void bfs(int sx,int sy){
     int dx[]={-1,0,1,0},dy[]={0,1,0,-1};
     q.push({sx,sy});
     st[sx][sy]=true;
     while(q.size()){
         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>n)  continue;
             if(g[a][b]||st[a][b])  continue;
             q.push({a,b});
             st[a][b]=true;
             pre[a][b]=t;
         }  }  }
 int main(){
        /*bfs*/
        bfs(n,n);
        PII end(1, 1);/*初始化end的值从终点(1,1)倒退(n,n)*/
     while (true){/*输出路径*/
         cout<<end.first-1<<" "<<end.second-1<<endl;
         if (end.first==n&&end.second==n) break;
         end = pre[end.first][end.second];
     }
 }

武士风度的牛

农民 John 有很多牛,他想交易其中一头被 Don 称为 The Knight 的牛。这头牛有一个独一无二的超能力,在农场里像 Knight 一样地跳(就是我们熟悉的象棋中马的走法)。虽然这头神奇的牛不能跳到树上和石头上,但是它可以在牧场上随意跳,我们把牧场用一个 x,y 的坐标图来表示。这头神奇的牛像其它牛一样喜欢吃草,给你一张地图,上面标注了 The Knight 的开始位置,树、灌木、石头以及其它障碍的位置,除此之外还有一捆草。
现在你的任务是,确定 The Knight 要想吃到草,至少需要跳多少次。The Knight 的位置用 K 来标记,障碍的位置用 * 来标记,草的位置用 H 来标记。
这里有一个地图的例子:
             11 | . . . . . . . . . .
             10 | . . . . * . . . . . 
              9 | . . . . . . . . . . 
              8 | . . . * . * . . . . 
              7 | . . . . . . . * . . 
              6 | . . * . . * . . . H 
              5 | * . . . . . . . . . 
              4 | . . . * . . . * . . 
              3 | . K . . . . . . . . 
              2 | . . . * . . . . . * 
              1 | . . * . . . . * . . 
              0 ----------------------
                                    1 
                0 1 2 3 4 5 6 7 8 9 0     、注意: 数据保证一定有解。
输入格式:第 1 行: 两个数,表示农场的列数 C 和行数 R。第 2..R+1 行: 每行一个由 C 个字符组成的字符串,共同描绘出牧场地图。
输出格式:一个整数,表示跳跃的最小次数。
数据范围:1≤R,C≤150

 const int N=160;
 typedef pair<int,int>PII;
 char g[N][N];
 bool st[N][N];
 int n,m,dist[N][N];
 queue <PII>q;
 int bfs(PII begin)
 {
     int dx[]={-2,-1,1,2,2,1,-1,-2};
     int dy[]={1,2,2,1,-1,-2,-2,-1};
     q.push(begin);
     st[begin.first][begin.second];
     while(q.size()){
         auto t=q.front();
         q.pop();
         /*特殊变换*/
         for(int i=0;i<8;i++)
         {
             int a=t.first+dx[i],b=t.second+dy[i];
             if(a<=0||a>n||b<=0||b>m)  continue;
             if(st[a][b]||g[a][b]=='*')  continue;
             if(g[a][b]=='H')    return dist[t.first][t.second]+1;
             dist[a][b]=dist[t.first][t.second]+1;/*记录距离:前驱距离+1*/
             st[a][b]=true;
             q.push({a,b});
         }
     }
   
 }
 int main(){
     cin>>m>>n;
     PII  begin,end;
     for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++){
            cin>>g[i][j];
            if(g[i][j]=='K')   begin={i,j};
        }
        cout<<bfs(begin)<<endl;
 }

抓住那头牛

农夫知道一头牛的位置,想要抓住它。农夫和牛都位于数轴上,农夫起始位于点 N,牛位于点 K。农夫有两种移动方式:
从 X 移动到 X1 或 X+1,每次移动花费一分钟
从 X 移动到 2X,每次移动花费一分钟
假设牛没有意识到农夫的行动,站在原地不动。农夫最少要花多少时间才能抓住牛?
输入格式:共一行,包含两个整数N和K。
输出格式:输出一个整数,表示抓到牛所花费的最少时间。
数据范围:0≤N,K≤105

 const int N=1e5+10;
 int dist[N],n,m;
 queue<int>q;
 bool st[N];
 int dfs(){
     q.push(n);
     while(q.size()){
         auto t=q.front();
         if(t==m)  return dist[m];
         q.pop();
         if(t+1<N&&!st[t+1]){
             dist[t+1]=dist[t]+1;
             q.push(t+1);
             st[t+1]=true;
         }  
         if(t-1>=0&&!st[t-1]){
             dist[t-1]=dist[t]+1;
             q.push(t-1);
             st[t-1]=true;
         }
         if(t*2<N&&!st[t*2]){
             dist[2*t]=dist[t]+1;
             q.push(2*t);
             st[2*t]=true;
         }
     }   
 }
 int main(){
     cin>>n>>m;
     cout<<dfs()<<endl;
 }

多源BFS

矩阵距离

给定一个 N 行 M 列的 01 矩阵 A,A[i][j] 与 A[k][l] 之间的曼哈顿距离定义为:横纵坐标之和 
输出一个 N 行 M 列的整数矩阵 B,其中:B[i][j]=min1≤x≤N,1≤y≤M,A[x][y]=1dist(A[i][j],A[x][y])
输入格式:第一行两个整数 N,M。接下来一个 N 行 M 列的 01 矩阵,数字之间没有空格。
输出格式:一个 N 行 M 列的矩阵 B,相邻两个整数之间用一个空格隔开。
数据范围:1≤N,M≤1000

 const int N=1010;
 typedef pair<int,int>PII;
 bool st[N][N];
 char g[N][N];
 int dist[N][N];
 int n,m;
 queue<PII>q;
 void bfs(){
   int dx[4]={-1,0,1,0}, dy[4]={0,1,0,-1};
   for(int i=1;i<=n;i++)/*找到所有的源点加入队列*/
       for(int j=1;j<=m;j++)  if(g[i][j]=='1'){
           q.push({i,j});
           st[i][j]=true;
       }
       while(q.size()){/*常规遍历*/
           auto 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)  continue;
               if(g[a][b]=='1'||st[a][b])  continue;
               dist[a][b]=dist[t.first][t.second]+1;
               st[a][b]=true;
               q.push({a,b});
           }
       }
 }
 int main(){
       bfs();
       for(int i=1;i<=n;i++){
              for(int j=1;j<=m;j++)  cout<<dist[i][j]<<" ";
              cout<<endl;
       }
 }

最小步数模型

八数码:需要回溯状态

在一个 3×3 的网格中,1~8 这 8 个数字和一个 x 恰好不重不漏地分布在这 3×3 的网格中。
例如:
1 2 3
x 4 6
7 5 8
在游戏过程中,可以把 x 与其上、下、左、右四个方向之一的数字交换(如果存在)。我们的目的是通过交换,
使得网格变为如下排列(称为正确排列):
1 2 3
4 5 6
7 8 x
现在,给你一个初始网格,请你求出得到正确排列至少需要进行多少次交换。
输入格式:输入占一行,将 3×3 的初始网格描绘出来。

 /*bfs()核心代码:将二维装换成一维,每个字符串就是一种状态*/
 int bfs(string str){
     string res="12345678x";
     queue<string >q;
     map<string,int>d;
     int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
 d[str]=0;/*从起始字符串到当前字符串需要0步*/
 q.push(str);
 while(q.size()){
     auto t=q.front();
     q.pop();
     if(t==res) return d[t];/*到结果状态的输出*/
     int distance =d[t];
     int k=t.find('x');/*找到x在一维中的位置*/
     int x=k/3,y=k%3;/*找到x在二维中的位置##*/
     for(int i=0;i<4;i++){/*遍历四个方向*/
         int a=x+dx[i],b=y+dy[i];
         if(a>=0&&b>=0&&a<3&&b<3){
             swap(t[a*3+b],t[k]);/*交换k和扩展字符*/
             if (!d.count(t)){/*如果当前字符串未被遍历过*/
                 d[t] = distance + 1;
                 q.push(t);
             }
             swap(t[a * 3 + b], t[k]);/*回溯状态##*/
         }   }    }

魔版:不需要回溯状态

Rubik 先生在发明了风靡全球的魔方之后,又发明了它的二维版本——魔板。这是一张有 8 个大小相同的格子的魔板:
1 2 3 4
8 7 6 5
我们知道魔板的每一个方格都有一种颜色。这 8 种颜色用前 8 个正整数来表示。可以用颜色的序列来表示一种魔板状态,规定从魔板的左上角开始,
沿顺时针方向依次取出整数,构成一个颜色序列。对于上图的魔板状态,我们用序列 (1,2,3,4,5,6,7,8) 来表示,这是基本状态。
这里提供三种基本操作,分别用大写字母 A,B,C 来表示(可以通过这些操作改变魔板的状态):
A:交换上下两行;
B:将最右边的一列插入到最左边;
C:魔板中央对的4个数作顺时针旋转。
对于每种可能的状态,这三种基本操作都可以使用。你要编程计算用最少的基本操作完成基本状态到特殊状态的转换,输出基本操作序列。注意:数据保证一定有解。
输入格式:输入仅一行,包括 8 个整数,用空格分开,表示目标状态。
输出格式:输出文件的第一行包括一个整数,表示最短操作序列的长度。如果操作序列的长度大于0,则在第二行输出字典序最小的操作序列。
数据范围:输入数据中的所有数字均为 1 到 8 之间的整数。

 char g[2][4];
 map<string, pair<char,string> > pre;/*前驱:扩展到当前状态的前驱操作和状态*/
 map<string,int> dist;
 queue<string>q;
 map<string,bool> st;
 string start="12345678";
 void set1(string state){/*字符串转字符数组*/
     for (int i = 0; i < 4; i ++ ) g[0][i] = state[i];
     for (int i = 7, j = 0; j < 4; i --, j ++ ) g[1][j] = state[i];
 }
 string get(){/*字符数组转字符串*/
     string res;
     for (int i = 0; i < 4; i ++ ) res += g[0][i];
     for (int i = 3; i >= 0; i -- ) res += g[1][i];
     return res;
 }
 string move0(string state){/*操作1*/
     set1(state);
     for (int i = 0; i < 4; i ++ ) swap(g[0][i], g[1][i]);
     return get();
 }
 ​
 string move1(string state){/*操作2*/
     set1(state);
     int v0 = g[0][3], v1 = g[1][3];
     for (int i = 3; i > 0; i -- )
     {
         g[0][i] = g[0][i - 1];
         g[1][i] = g[1][i - 1];
     }
     g[0][0] = v0, g[1][0] = v1;
     return get();
 }
 string move2(string state){/*操作3*/
     set1(state);
     int v = g[0][1];
     g[0][1] = g[1][1];
     g[1][1] = g[1][2];
     g[1][2] = g[0][2];
     g[0][2] = v;
     return get();
 }
 /*dfs遍历:为什么从开始推结果不行*/
 int dfs(string end)
 {
      if (start==end) return 0;
      q.push(start);
      st[start]=true;
      string state[3];
      while(q.size()){
          auto t=q.front();
          q.pop();
          state[0]=move0(t);
          state[1]=move1(t);
          state[2]=move2(t);
          for(int i=0;i<3;i++)
          {
              string temp=state[i];
              if(!st[temp]){
                  st[temp]=true;
                  dist[temp]=dist[t]+1;
                  pre[temp]={i+'A',t};/*i+'A'对应操作;t:对应被扩展*/
                  q.push(temp);
                  if(temp==end)  return dist[end];
              }
          }
      }
  } 
 int main(){
     string  end;
     int temp;
     for(int i=1;i<=8;i++){
         cin>>temp;
         end+=temp+'0';
     }
     int step=dfs(end);
     string res;
     /*操作存储*/
     while(end!=start){/*输出路径###*/
         res+=pre[end].first;
         end=pre[end].second;
     }
     reverse(res.begin(), res.end());/*取反res*/
     cout<<step<<endl;     if (step > 0) cout << res << endl;
 }

双端队列广搜

电路维修

达达是来自异世界的魔女,她在漫无目的地四处漂流的时候,遇到了善良的少女翰翰,从而被收留在地球上。翰翰的家里有一辆飞行车。有一天飞行车的电路板突然出现了故障,导致无法启动。电路板的整体结构是一个 R 行 C 列的网格(R,C≤500)
每个格点都是电线的接点,每个格子都包含一个电子元件。
电子元件的主要部分是一个可旋转的、连接一条对角线上的两个接点的短电缆。
在旋转之后,它就可以连接另一条对角线的两个接点。电路板左上角的接点接入直流电源,右下角的接点接入飞行车的发动装置。
达发现因为某些元件的方向不小心发生了改变,电路板可能处于断路的状态。她准备通过计算,旋转最少数量的元件,使电源与发动装置通过若干条短缆相连。不过,电路的规模实在是太大了,达达并不擅长编程,希望你能够帮她解决这个问题。
注意:只能走斜向的线段,水平和竖直线段不能走。
输入格式:输入文件包含多组测试数据。第一行包含一个整数 T,表示测试数据的数目。对于每组测试数据,第一行包含正整数 R 和 C,表示电路板的行数和列数。之后 R 行,每行 C 个字符,字符是"/"和"\"中的一个,表示标准件的方向。
输出格式:对于每组测试数据,在单独的一行输出一个正整数,表示所需的最小旋转次数。如果无论怎样都不能使得电源和发动机之间连通,输出 NO SOLUTION。
数据范围:1≤R,C≤500,1≤T≤5

 const int N=510;
 typedef pair<int,int>PII;
 int dist[N][N];
 char g[N][N];
 bool st[N][N];
 int n,m;
 /*Dijkstra:迪杰斯特拉版的dfs*/
 int bfs()
 {
     deque<PII>q;/*双端队列*/
     int dx[4]={-1,-1,1,1}, dy[4]={-1, 1, 1, -1};/*点的四联通*/
     int ix[4]={-1,-1,0,0}, iy[4]= {-1, 0, 0, -1};/*线路的四联通*/ 
     char cs[] = "\\/\\/";/*扩展正确线路*/
     memset(dist,0x3f,sizeof dist);/*多组输入*/
     /*多次输入需要初始化*/
     q.push_back({0,0});
     memset(st, 0, sizeof st);/*多组输入*/
     dist[0][0]=0;
 ​
     while(q.size())
     {
         auto t=q.front();/*dsf队列是dist是单调的所以每次出队的都是当前dist的mi*/
         q.pop_front();
         if(st[t.first][t.second])  continue;/*防止一个点多次扩展*/
         st[t.first][t.second]=true;
         if(t.first==n&&t.second==m) return dist[n][m];
 、
         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) continue;
             int ca = t.first + ix[i], cb = t.second + iy[i];/*扩展线路的坐标*/
             int d=dist[t.first][t.second]+(g[ca][cb]!=cs[i]);
             if(d<dist[a][b]){/*是否需要更新*/
                 dist[a][b]=d;
                 if (g[ca][cb] != cs[i]) q.push_back({a, b});/*0放队首1放队尾*/
                 else q.push_front({a, b});
             }
         }
     }
 }
 int main(){
     int T;
     cin>>T;
     while(T--){
         cin>>n>>m;
         for(int i=0;i<n;i++)  cin>>g[i];
         if((n+m)&1) cout<<"NO SOLUTION"<<endl;
         else cout<<bfs()<<endl;
     }  

双向搜索:

字符变换:如何实现双端的''同一函数''搜索

/*已知有两个字串 A, B 及一组字串变换的规则(至多 6 个规则):规则的含义为:在 A 中的子串 A1 可以变换为 B1、A2 可以变换为 B2…。
例如:A=abcd B=xyz变换规则为:abc → xu ud → y y → yz,则此时,A 可以经过一系列的变换变为 B,其变换的过程为:
abcd → xud → xy → xyz共进行了三次变换,使得 A 变换为 B。
输入格式:输入格式如下:
A B
A1 B1
A2 B2
第一行是两个给定的字符串 A 和 B。接下来若干行,每行描述一组字串变换的规则。所有字符串长度的上限为 20。
输出格式:若在 10 步(包含 10 步)以内能将 A 变换为 B ,则输出最少的变换步数;否则输出 NO ANSWER!。

 const int N=6;
 string a[N],b[N];/*起始规则;终止规则*/
 string A,B;/*起始和终止*/
 int n;/*规则数目*/
 int extend(queue<string>&qc,map<string,int>&dc,map<string,int>&dist,string a[],string b[]){
     /*1.扩展队列;2.扩展队列的dist; 3.另一队列dist; 4.变换条件字符串; 5.变化结果字符串*/
    string  t=qc.front();
     qc.pop();
     for(int i=0;i<t.size();i++)//枚举状态的的每个字符串
       for(int j=0;j<n;j++){//枚举每个规则
           if(t.substr(i,a[j].size() )==a[j])
           {//从状态的第i的字符串到规则j长度的字符串和规则j字符串相等,则可以扩展
               string temp=t.substr(0,i)+b[j]+t.substr(i+a[j].size());//扩展之后的字符串
               if(dist.count(temp))  return dc[t]+1+dist[temp];//在另一边已经扩展过
               if(dc.count(temp)) continue;//在当前队列已经扩展过
               qc.push(temp);
               dc[temp]=dc[t]+1;
           }
       }
       return 11;
 }
 int bfs(){
     if(A==B)  return 0;
     map<string,int >da,db;/*da:起始距离;db:终点距离*/
     queue<string>qa,qb;/*qa:起始队列;qb:终止队列*/
     qa.push(A);
     qb.push(B);
     da[A]=da[B]=0;
     int t=0;
     while(qa.size()&&qb.size()){/*必须两个都存在若一边结束而另一边未结束且没有相与即无解*/
        if(qa.size()<qb.size())  t=extend(qa,da,db,a,b);/*每次选择数目较小的扩展*/
        else t=extend(qb,db,da,b,a);
        if(t<=10) return t;/*只要在另一队列中匹配不到就返回11,直到在另一队列找到才会返回真正的t*/
     }
     return 11;
 }
 int main(){
     cin>>A>>B;
     while(cin>>a[n]>>b[n])  n++;
     int step=bfs();
     if(step>10) puts("NO ANSWER!");
     else cout<<step<<endl;
 }

A*:利用启发函数缩小搜索范围

八数码

 /*八数码:A-stars*/
 #include<bits/stdc++.h>
 using namespace std;
 typedef pair<int,string>PIS;
 #define x first
 #define y second
 map<string,pair<string,char>>pre;//记录前驱及操作
 map<string,int>dist;     
 priority_queue< PIS,vector<PIS>, greater<PIS>> heap;
  /*小根堆,将元素的估计终点距离从小到大排序,第一维是估计距离,第二维是状态*/ 
 ​
 int calculation(string state){//A-star:估价函数 
  int res=0;
  for(int i=0;i<9;i++)
    if(state[i]!='x'){
        int t=state[i]-'1';//对应下标 
        res+=abs(i/3-t/3)+abs(i%3-t%3);//曼哈顿距离 
        
    } 
     return res;
 }
 ​
  string bfs(string start) {
      int dx[4]={-1,1,0,0};int dy[4]={0,0,-1,1};
      string end="12345678x";
      char op[]="udlr";//操作
      dist[start]=0;
      heap.push( {0+calculation(start),start} );//起点到t的距离+t估计到终点距离
      
      while(heap.size())
      {
          auto t=heap.top();
          heap.pop();
          string state=t.y;
          if(state==end) break;
         int x,y;
         for(int i=0;i<9;i++)/*找到x在3x3矩阵下的坐标*/
             if(state[i]=='x'){
                 x=i/3,y=i%3;
                 break;
             }
             
         string source =state;//暂时保存当前字符串状态 
         for(int j=0;j<4;j++){//四宫格扩展 
             int a=x+dx[j],b=y+dy[j];
             if(a<0||a>=3||b<0||b>=3) continue;
             swap(state[a*3+y],state[x*3+b]);//交换位置:将在数组中位置转换成字符串中位置 
             
             if(!dist.count(state)||dist[state]>dist[source]+1){/*未扩展过或距离变小*/
                 dist[state]=dist[source]+1;//更新距离
                 heap.push({dist[state]+calculation(state),state}); 
                 pre[state]={source,op[j]};//存储前驱,扩展操作; 
             }
             state=source; //要扩展四个方向,需要还原 
             
         } 
         
      }
      //从终点回溯到起点 
      string res;
      while(end!=start){
          res+=pre[end].y;
          end =pre[end].x;
      }
      reverse(res.begin(),res.end());//数组倒置 
      return res;
  }
  
 int main()
 {
     string start,seq;
     char c;
     while(cin>>c){
         start+=c;
         if(c!='x') seq+=c;
     }
     int cent=0;
     //求逆序对 :若i<j,且a[i]>a[j],则称(a[i],a[j])是一对逆序对 
     for(int i=0;i<8;i++)
       for(int j=i+1;j<8;j++)
          if(seq[i]>seq[j]) cent++; 
       if(cent%2) puts("unsolvable");
       else cout<<bfs(start)<<endl;
       return 0;
 }

第k最短路

给定一张 N 个点(编号 1,2…N),M 条边的有向图,求从起点 S 到终点 T 的第 K 短路的长度,路径允许重复经过点或边。注意: 每条最短路中至少要包含一条边。
输入格式:第一行包含两个整数 N 和 M。接下来 M 行,每行包含三个整数 A,B 和 L,表示点 A 与点 B 之间存在有向边,且边长为 L。
最后一行包含三个整数 S,T 和 K,分别表示起点 S,终点 T 和第 K 短路。
输出格式:输出占一行,包含一个整数,表示第 K 短路的长度,如果第 K 短路不存在,则输出 ?1。
数据范围:1≤S,T≤N≤1000,0≤M≤104,1≤K≤1000,1≤L≤100

 const int N=1000,M=200010;
 #define x first
 #define y second
 typedef pair<int,int>PII;
 typedef pair<int,PII>PIII;
 ​
 int n,m,S,T,k;//点数、边数、起点、终点
 int h[N],rh[N],e[M],w[M],ne[M],idx;//正向邻接表的表头,反向邻接表的表头,边权 
 int dist[N],cnt[N];
 int st[N];
 int K;
 //邻接表创建 
 void add(int h[],int a,int b,int c){
     e[idx]=b,w[idx]=c;ne[idx]=h[a],h[a]=idx++; //画图 
 } 
 //dijkstra():估价函数,从终点到其他点的最小距离
 void dijkstra(){
     priority_queue<PII,vector<PII>,greater<PII>>heap;
     heap.push({0,T});
     memset(dist,0x3f,sizeof dist);
     dist[T]=0;
     while (heap.size()){
         auto t=heap.top();
         heap.pop();
         int ver =t.y;
         if(st[ver]) continue;
         for(int i=rh[ver];~i;i=ne[i]){/*从反边开始扩展*/
             int j=e[i];
             if(dist[j]>dist[ver]+w[i])
             {
                 dist[j]=dist[ver]+w[i];
                 heap.push({dist[j],j});
             }
         }
         
     }
 }
 ​
 int astar(){
     priority_queue<PIII,vector<PIII>,greater<PIII>> heap;
     heap.push({dist[S],{0,S}});/*估价值,真实值+编号*/
     int cent;//遍历了几次
     while(heap.size())
     {
         auto t=heap.top();
         heap.pop();
         int ver=t.y.y,distance=t.y.x;
         cnt[ver]++; 
         if(cnt[T]==K) return distance;
         for(int i=h[ver];~i;i=ne[i])//从正向图扩展,把能找到的边全部加入来,不管大小 
         {
             int j=e[i];
              if (cnt[j] < K)
             heap.push({distance+w[i]+dist[j],{distance+w[i],j} });
         }
     } 
     return -1;
 }
 int main()
 {
     scanf("%d%d",&n,&m);
     memset(h,-1,sizeof h);//初始化表头;
     memset(rh,-1,sizeof rh);
     
     for(int i=0;i<m;i++){
         int a,b,c;
         scanf("%d%d%d",&a,&b,&c);//起点、终点、边权
         add(h,a,b,c);//正向邻接表 
         add(rh,b,a,c); //反向邻接表 
     } 
     scanf("%d%d%d",&S,&T,&K);
     if(S==T)   K++; 
     dijkstra(); 
     printf("%d\n",astar());

深度优先遍历

连通性模型:

迷宫:原题

一天Extense在森林里探险的时候不小心走入了一个迷宫,迷宫可以看成是由 n?n 的格点组成,每个格点只有2种状态,.和#,前者表示可以通行后者表示不能通行。
同时当Extense处在某个格点时,他只能移动到东南西北(或者说上下左右)四个方向之一的相邻格点上,Extense想要从点A走到点B,问在不走出迷宫的情况下能不能办到。
如果起点或者终点有一个不能通行(为#),则看成无法办到。
注意:A、B不一定是两个不同的点。
输入格式:第1行是测试数据的组数 k,后面跟着 k 组输入。每组测试数据的第1行是一个正整数 n,表示迷宫的规模是 n?n 的。接下来是一个 n?n 的矩阵,矩阵
中的元素为.或者#。再接下来一行是 4 个整数 ha,la,hb,lb,描述 A 处在第 ha 行, 第 la 列,B 处在第 hb 行, 第 lb 列。
注意到 ha,la,hb,lb 全部是从 0 开始计数的。
输出格式:k行,每行输出对应一个输入。能办到则输出“YES”,否则输出“NO”。
数据范围:1≤n≤100

 const int N=110;
 char g[N][N];
 bool st[N][N];
 int ha,la,hb,lb,n ;
 int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};
 ​
 bool dfs(int x,int y){
     if(g[x][y]=='#')return false;/*结束当前分支的递归*/
     
     if(x==hb&&y==lb) return true;
     st[x][y]=true;
     for(int i=0;i<4;i++)/*四联通*/
     {
         int a=x+dx[i],b=y+dy[i];
         if(a<0||a>=n||b<0||b>=n) continue;
         if(st[a][b])  continue;
         // if(g[a][b]=='#') continue;
         if(dfs(a,b))  return true;
     }
     return false;
 }
 int main(){
     int k;
     cin>>k;
     while(k--){
         cin>>n;
         memset(st,0,sizeof st);
         for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)  cin>>g[i][j];
            cin>>ha>>la>>hb>>lb;
            if(dfs(ha,la)) cout<<"YES"<<endl;
            else cout<<"NO"<<endl;
     }
 }

红与黑:树深度

有一间长方形的房子,地上铺了红色、黑色两种颜色的正方形瓷砖。你站在其中一块黑色的瓷砖上,只能向相邻(上下左右四个方向)的黑色瓷砖移动。
请写一个程序,计算你总共能够到达多少块黑色的瓷砖。
输入格式输入包括多个数据集合。每个数据集合的第一行是两个整数 W 和 H,分别表示 x 方向和 y 方向瓷砖的数量。在接下来的 H 行中,每行包括 W 个字符。
每个字符表示一块瓷砖的颜色,规则如下
1)‘.’:黑色的瓷砖;
2)‘#’:红色的瓷砖;
3)‘@’:黑色的瓷砖,并且你站在这块瓷砖上。该字符在每个数据集合中唯一出现一次。当在一行中读入的是两个零时,表示输入结束。
输出格式:对每个数据集合,分别输出一行,显示你从初始位置出发能到达的瓷砖数(记数时包括初始位置的瓷砖)。
数据范围:1≤W,H≤20

 const int N=30;
 char g[N][N];
 bool st[N][N];
 int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};
 int n,m;
 int dfs(int x,int y){/*返回子树的深度*/
     int cnt=1;/*子树根就一行*/
     st[x][y]=true;
     for(int i=0;i<4;i++){
         int a=x+dx[i],b=y+dy[i];
         if(a<0||a>=n||b<0||b>=m)  continue;
         if(g[a][b]!='.') continue;
         if(st[a][b]) continue;
         cnt+=dfs(a,b);
     }
     return cnt;
 }
 int main(){
     while(cin>>m>>n,m||n)
     {
         memset(st,0,sizeof st);
         int x,y;
         for(int i=0;i<n;i++)
       for(int j=0;j<m;j++)   {
           cin>>g[i][j];
           if(g[i][j]=='@')  x=i,y=j;
       }
       cout<<dfs(x,y)<<endl;
     }
     return 0;   
 }

搜索顺序

马走日

马在中国象棋以日字形规则移动。请编写一段程序,给定 n?m 大小的棋盘,以及马的初始位置 (x,y),要求不能重复经过棋盘上的同一个点,计算马可以有多少
途径遍历棋盘上的所有点。
输入格式:第一行为整数 T,表示测试数据组数。每一组测试数据包含一行,为四个整数,分别为棋盘的大小以及初始位置坐标 n,m,x,y。
输出格式:每组测试数据包含一行,为一个整数,表示马能遍历棋盘的途径总数,若无法遍历棋盘上的所有点则输出 0。
数据范围:1≤T≤9,

const int N=10;
int n,m;
bool st[N][N];
int ans;/*全局变量*/
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};
void  dfs(int x,int y,int cnt){
    if(cnt==n*m){/*cnt:点数*/
        ans++;
        return;
    }
    st[x][y]=true;
    for(int i=0;i<8;i++){
        int a=x+dx[i],b=y+dy[i];
        if(a<0||a>=n||b<0||b>=m)  continue;
        if(st[a][b])  continue;
        dfs(a,b,cnt+1);
    }
    st[x][y]=false;
}
int main(){
    int T;
    cin>>T;
    while(T--){
        int x,y;
        cin>>n>>m>>x>>y;
        memset(st,0,sizeof st);
        ans=0;
        dfs(x,y,1);
        cout<<ans<<endl;
    }
    return 0;
}

单词接龙

单词接龙是一个与我们经常玩的成语接龙相类似的游戏。现在我们已知一组单词,且给定一个开头的字母,要求出以这个字母开头的最长的“龙”,每个单词最多
被使用两次。在两个单词相连时,其重合部分合为一部分,例如 beast 和 astonish ,如果接成一条龙则变为 beastonish。
我们可以任意选择重合部分的长度,但其长度必须大于等于1,且严格小于两个串的长度,例如 at 和 atide 间不能相连。
输入格式:输入的第一行为一个单独的整数 nn 表示单词数,以下 nn 行每行有一个单词(只含有大写或小写字母,长度不超过20),输入的最后一行为一个单个字符,表示“龙”开头的字母。
你可以假定以此字母开头的“龙”一定存在。
输出格式:只需输出以此字母开头的最长的“龙”的长度。
数据范围:n≤20 

#include<bits/stdc++.h>
using namespace std;
const int N=30;
string word[N];
int used[N],g[N][N];
int n,ans;/*ans全局变量爆搜*/
void dfs(string dragon,int last)/*last:龙最后字母*/
{
    ans=max((int)dragon.size(),ans);
    used[last]++;
    for(int i=0;i<n;i++){
        if(g[last][i]&&used[i]<2)
             dfs(dragon + word[i].substr(g[last][i]), i);
    }
    used[last]--;
    
}
int main(){
    cin>>n;
    for(int i=0;i<n;i++)  cin>>word[i];
    char start;  cin>>start;
    /*字符串i的后缀和字符串j的前缀最小重合数:从i的末尾,j的开头开始*/
    for(int i=0;i<n;i++)
      for(int j=0;j<n;j++)
          for(int k=1;k<min(word[i].size(),word[j].size()) ;k++)
              if(word[i].substr(word[i].size()-k,k)==word[j].substr(0,k)){
                  g[i][j]=k;
                  break;
              }
          /*遍历所有的龙头*/    
          for(int i=0;i<n;i++)
             if(word[i][0]==start){
                 dfs(word[i],i);
             }
      cout<<ans<<endl;
      return 0;
}

分成质数组

给定 n个正整数,将它们分组,使得每组中任意两个数互质。至少要分成多少个组?
输入格式:第一行是一个正整数 n第二行是 n个不大于10000的正整数。
输出格式:一个正整数,即最少需要的组数。
数据范围:1≤n≤10 

#include<bits/stdc++.h>
using namespace std;
const int N=10;
int n;
int group[N][N],a[N];
int ans;
bool st[N];
int gcd(int a,int b){
    return b?gcd(b,a%b):a;
}
bool check(int g[],int gc,int num){
    for(int i=0;i<gc;i++){
        if(gcd(g[i],a[num])>1) return false;
    }
    return true;
}
void dfs(int g,int gc,int tc,int start)
/*gc:最后一组的序号,gc:组内最后一元素序号;
  tc:当前组内数;     start:搜到第几个数*/
{
    if(g>=ans) return ;/*当前组数已经小于已知的最小组数,直接舍弃当前子树*/
    if(tc==n)  {
        ans=g;
        return;
    }/*当前组已经搜完所有数,更新ans,并返回*/
    bool  flag=true;
    for(int i=start;i<n;i++){
        if(!st[i]&&check(group[g],gc,i))
        {
            group[g][gc] = a[i];
            st[i]=true;
            dfs(g,gc+1,tc+1,i+1);
            st[i]=false;/*恢复现场,不选这个数而是将这个数加入到后边的组*/
            flag=false;/**/
        }
    }
    if(flag) dfs(g+1,0,tc,0);
}
/*深度优先遍历每次我们只要考虑最后一组:第一次将第一个放入第一组,深度优先遍历直到
遍历完所有的数,并将所有符合互质的加入第一组,递归下一层因为下一层因为下一层的start
==n且flag==true,直接开数组,重新递归未遍历的数,直到tc==n即所有数都在所有组中,更新
最大组数并返回到上一层的dfs处:
1.若上一层未开新数组:做出不选择第i个数的决定并且flag置为false,并判断i之后的数是否
有符合条件的,若有加入当前组,继续递归下一层,若没有符合互质的,则说明当前组除了第i
个数其余数不符合,直接返回上一层*/
int main()
{
    cin>>n;
    ans=n;
    for(int i=0;i<n;i++)  scanf("%d", a+ i);
    dfs(1,0,0,0);
    cout<<ans<<endl;
    return 0;
}

剪枝与优化

迭代加深

双向dfs

IDA*A

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixin_56920434

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值