算法学习:搜索技术

一、递归和排列
问题1:打印n个数的全排列,共n!个
问题2:打印n个数中任意m个数的全排列,共n!/m!个
问题3:打印n个数中任意m个数的组合,共n!/[m!(n-m)!]个

例题:
Ray又对数字的列产生了兴趣:
现有四张卡片,用这四张卡片能排列出很多不同的4位数,要求按从小到大的顺序输出这些4位数。

Input

每组数据占一行,代表四张卡片上的数字(0<=数字<=9),如果四张卡片都是0,则输入结束。

Output

对每组卡片按从小到大的顺序输出所有能由这四张卡片组成的4位数,千位数字相同的在同一行,同一行中每个四位数间用空格分隔。
每组输出数据间空一行,最后一组数据后面没有空行。

Sample Input

1 2 3 4
1 1 2 3
0 1 2 3
0 0 0 0
Sample Output

1234 1243 1324 1342 1423 1432
2134 2143 2314 2341 2413 2431
3124 3142 3214 3241 3412 3421
4123 4132 4213 4231 4312 4321

1123 1132 1213 1231 1312 1321
2113 2131 2311
3112 3121 3211

1023 1032 1203 1230 1302 1320
2013 2031 2103 2130 2301 2310
3012 3021 3102 3120 3201 3210

注意点:
遇到输入4个数字有0时,要保证0不为千分位,则将数字排序后,将0与后面数组中第一个不为0的数交换
代码如下:

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int a[4];
    int f=0;  
    while(1){
        int sum=0;
        for(int i=0;i<4;i++) cin>>a[i],sum+=a[i];
        if(!sum)break;
        if(f)cout<<endl;  //如果当前不是第一次输入,就输出空行 
        else f=1;         //是第一次输入,f改为一,下一次输出时候就能在两次之间产生空行 
                        //这个f判断放在sum判断之后,最后一组后面没有空行       
        sort(a,a+4);   //先对输入的数字排序 
        if(a[0]==0){        //如果首位是零,遍历把第一个不为零的数字和首位交换 
            for(int i=0;i<4;i++){        //,这样可以做到当前四位数的最小 
                if(a[i]!=0){
                    swap(a[0],a[i]);
                    break;
                } 
            }
        }
        int k=-1;
        do{
            if(k!=-1){
                if(k!=a[0])cout<<endl;
                else cout<<' ';
            }
            for(int i=0;i<4;i++)cout<<a[i];
            k=a[0];
        }while(next_permutation(a,a+4));//等到全排列结束后,跳出循环
        cout<<endl;    //每一次换行
    }
    return 0;
}

例题
利用递归,来求任意n个数中取出m个数的全排列
分析:

求n个数的全排列,也就是:

第一个数不动,将后面的n-1个数全排
将第二个数和第一个数交换,将后面n-1个数全排列(注意要换回来)
将第三个数和第一个数交换,然后将后面n-1个数全排

将第n个数和第一个数交换,然后将后面n-1个数全排
可以利用递归的思想来实现,直到n-1等于1

而取出m个数也就是让n-m=1时结束就可以
代码如下:

#include<bits/stdc++.h>
using namespace std;
bool is_swap(int start,int last,int *p)//判重,如果之间start~i间有重复的数字,则不进行交换,避免重复的情况出现
{
    for(int i=start;i<last;i++)
        if(p[i]==p[last])return false;
    return true;
}
void perm(int start,int last,int *p,int len)//进行全排列
{
    if(start==len){
        for(int i=0;i<len;i++)
        	printf("%d ",p[i]);
        printf("\n");
    }
    else
    {
        for(int i=start;i<=last;i++)
        if(is_swap(start,i,p))
        {
            swap(p[start],p[i]);
            perm(start+1,last,p,len);
            swap(p[start],p[i]);
        }
    }
}
int main()
{
    static int n;
    static int m;
    cin>>n;
    cin>>m;
    int *arr=new int[n];
    for(int i=0;i<n;i++)
    	cin>>arr[i];
    perm(0,n-1,arr,m);
    return 0;
}

要写出1-n中选择m个数的全排列,常用下列代码:

//输出1-5中任意3个数的全排列
#include<bits/stdc++.h>
using namespace std;
int star[]={0,1,2,3,4,5};
int perm(int begin,int end){
    int i;
    if(begin==4){
        for(int j=1;j<=3;j++)
            cout<<star[j];
        cout<<endl;
    }
    else
    for(i=begin;i<=5;i++){
        swap(star[begin],star[i]);
        perm(begin+1,end);
        swap(star[begin],star[i]);
    }
}
int main(){
    perm(1,5);
}

针对问题3:打印n个数中任意m个数的组合
先讨论子集生成问题:

一个包含n个元素的集合{a0,a1,a2,…,an-1}
它的子集有{},{a0},{a1},{a2},…,{a0,a1,a2},…,{a0,a1,a2,…an-1}
共2^n个

例如n=3的集合{a0,a1,a2},它的子集和二进制数的对应关系为
在这里插入图片描述
输出0-4的组合,代码如下:

#include<bits/stdc++.h>
using namespace std;
void print_subset(int n){
    for(int i=0;i<(1<<n);i++){
        //i:0~2n,每个i的二进制数对应一个子集
        for(int j=0;j<n;j++)//打印一个子集,即打印i的二进制数中所有的1
            if(i&(1<<j))
                cout<<j<<" ";
            cout<<endl;
    }
}
int main(){
    int n;
    cin>>n;
    print_subset(n);
}

回到问题3,对照子集生成的二进制方法,很容易看出,在n个元素的集合中找k个元素的子集,这个子集对应了1的个数为k个的二进制数。

如何判断二进制数中1的个数为k个?
操作:kk=kk&(kk-1),它能消除kk的二进制数的最后一个1
例如:7,二进制是111
111&(111-1)=111&110=110
110&(110-1)=110&101=100
100&(100-1)=100&011=000
所以有3个1

第三问代码如下:

#include<bits/stdc++.h>
using namespace std;
void print_set(int n,int k){
    for(int i=0;i<(1<<n);i++){
        int num=0,kk=i;
        while(kk){
            kk=kk&(kk-1);
            num++;
        }
        if(num==k){
            for(int j=0;j<n;j++)
                if(i&(1<<j))
                    cout<<j<<" ";
            cout<<endl;
        }
    }
}
int main(){
    int n,k;
    cin>>n>>k;
    print_set(n,k);
}

搜索的基本计数:BFS、DFS
DFS和BFS的模型:老鼠走迷宫
(1)一只老鼠走迷宫

在每个路口,它都选择先走右边(当然,先走左边也可以),能走多远就走多远
如果碰壁无法继续再往前走,就回退一步,这一次走左边,然后继续往下走
重复前面步骤,能走遍所有的卢,而且不会重复(回退不算重复走)
这就是DFS

(2)一群老鼠走迷宫

假设老鼠有无限多
在每个路口,都派出部分老鼠探索所有没走的过的路。
走某条路的老鼠,如果碰壁无法前行,就停下。
如果到达的路口已经有别的老鼠探索过了,也停下。
所有的道路都会走到,而且不会重复。
这就是BFS

二、BFS
1.BFS和队列(一般都队列来具体实现BFS)
例题:迷宫问题

题目描述
小明置身于一个迷宫,请你帮小明找出从起点到终点的最短路程。
小明只能向上下左右四个方向移动。
输入
输入包含多组测试数据。输入的第一行是一个整数T,表示有T组测试数据。
每组输入的第一行是两个整数N和M(1<=N,M<=100)。
接下来N行,每行输入M个字符,每个字符表示迷宫中的一个小方格。
字符的含义如下:
‘S’:起点
‘E’:终点
‘-’:空地,可以通过
‘#’:障碍,无法通过
输入数据保证有且仅有一个起点和终点。
输出
对于每组输入,输出从起点到终点的最短路程,如果不存在从起点到终点的路,则输出-1。

样例输入
在这里插入图片描述
样例输出
9

代码如下:

#include<bits/stdc++.h>
using namespace std;
typedef struct{
    int x,y,step;
}Node;
int cnt;
int m,n;
int d[4][2]={1,0,-1,0,0,1,0,-1};
char ma[105][105];//地图对应点元素
int vis[105][105];//判断点是否被遍历过
bool check(int x,int y){
   if(x<n&&x>=0&&y<m&&y>=0&&ma[x][y]!='#'&&!vis[x][y])
      return true;
   else
      return false;
}
void bfs(Node start){
   Node next;
   queue<Node>q;
   start.step=0;
   q.push(start);
   vis[start.x][start.y]=1;
   while(!q.empty()){
      start=q.front();
      q.pop();
      if(ma[start.x][start.y]=='E'){
         cnt=start.step;
         return;
      }
      for(int i=0;i<4;i++){
         next.x=start.x+d[i][0];
         next.y=start.y+d[i][1];
         if(check(next.x,next.y)){
            next.step=start.step+1;
            vis[next.x][next.y]=1;
            q.push(next);
         }
      }
   }
   cnt=-1;
}
int main(){
    int t;
    scanf("%d",&t);
    while(t--){
        Node start;
        memset(vis,0,sizeof(vis));
        memset(ma,0,sizeof(ma));
        scanf("%d%d",&n,&m);
        for(int i=0;i<n;i++)
            scanf("%s",ma[i]);
        for(int i=0;i<n;i++)
            for(int j=0;j<m;j++){
                if(ma[i][j]=='S'){
                    start.x=i;
                    start.y=j;
                    bfs(start);
                    break;
                }
            }
        printf("%d\n",cnt);
    }
}

迷宫问题也可用DFS解决,例题:

题目:
从s到t,.意味着可以走,*意味着不能走,如果能走,输出路径,如果不能走,输出no。最后输出走过的长度值。
输入:

5 6
....S*
.***..
.*..*.
*.***.
.T....

输出:

....m*                                                                        
.***mm                                                                        
.*..*m                                                                        
*.***m                                                                        
.Tmmmm  

代码如下:

#include<bits/stdc++.h>
using namespace std;
int n,m;
int num=0;
string maze[110];
bool vis[110][110];
int dir[4][2]={{-1,0},{0,-1},{1,0},{0,1}};
bool in(int x,int y) {
    return 0<=x&&x<n&&0<=y&&y<m;
}
bool dfs(int x,int y){
    if(maze[x][y]=='E')
        return true;
    num++;
    vis[x][y]=1;
    maze[x][y]='m';
    for(int i=0;i<4;++i){
        int tx=x+dir[i][0];
        int ty=y+dir[i][1];
        if(in(tx,ty)&&maze[tx][ty]!='#'&&!vis[tx][ty]){
            if(dfs(tx,ty))
                return true;
        }
    }
    vis[x][y]=0;
    num--;
    maze[x][y]='-';
    return false;
}
int main() {
    cin>>n>>m;
    for(int i=0;i<n;i++)
        cin>>maze[i];
    int x,y;
    for(int i=0;i<n;i++)
        for(int j=0;j<m;j++)
            if(maze[i][j]=='S')
                x=i,y=j;
    if(dfs(x,y)){
        for(int i=0;i<n;i++)
            cout<<maze[i]<<endl;
    }else
        cout<<"NO!"<<endl;
    printf("%d\n",num);
    return 0;
}

两种方法的比较:
(1)复杂度:DFS和BFS对大小为mn的图,做一次遍历,复杂度为O(mn)。
(2)应用场合:DFS用递归实现,程序比BFS更短,一般优先使用。
(3)但是像迷宫这样的求最短路径的问题,应该用BFS。

2.状态图搜索:八数码问题
(1)BFS搜索处理的对象,不仅可以是一个数,还可以是一种状态
(2)八数码问题是典型的状态图搜索问题。

在3×3的棋盘上,摆有八个棋子,每个棋子上标有1至8的某一数字。棋盘中留有一个空格,空格用0来表示。空格周围的棋子可以移到空格中。找到一种最少步骤的移动方法,实现从初始布局到目标布局的转变。

分析:
把空格看成0,一共有9个数字,样例输入:
起点:1 2 3 0 8 4 7 6 5
在这里插入图片描述

终点:1 0 3 8 2 4 7 6 5
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

上面过程可知道,有前面的基本BFS问题下的判重,前面是确定点没有走过才能遍历,而这题的判重更为复杂,总共9个数字,状态有9!=362880个,解决方法如下:
用数学方法:“康托展开”判重
在这里插入图片描述
如上表:

函数Cantor()实现的功能为:输入一个排列,即第一行某个排列;函数计算出它的Cantor值,即第二行对应的数。

在这里插入图片描述

在这里插入图片描述
注意:康托展开是一种特殊的哈希函数
代码如下:

#include<bits/stdc++.h>
const int LEN=362888;       //状态共9!=362880种
using namespace std;
struct node{
    int state[9];       //记录一个八数码的排列,即一个状态
    int dis;             //记录到起点的距离
};
int dir[4][2]={{-1,0},{0,-1},{1,0},{0,1}};
int visited[LEN]={0};  //与每个状态对应的记录,Cantor函数对它置数,并判重
int start[9];            //开始状态
int goal[9];             //目标状态
long int factory[]={1,1,2,6,24,120,720,5040,40320,362880};
                             //Cantor用到的常数
bool Cantor(int str[],int n){     //用康托展开判重
    long result=0;
    for(int i=0;i<n;i++){
        int counted=0;
        for(int j=i+1;j<n;j++)
            if(str[i]>str[j])       //当前未出现的元素中是排在第几个
                ++counted;
        result+=counted*factory[n-i-1];
    }
    if(!visited[result]){            //没有被访问过
        visited[result]=1;
        return 1;
    }
    else
        return 0;
}
int bfs(){
    node head;
    memcpy(head.state,start,sizeof(head.state));  //复制起点的状态
    head.dis=0;
    queue<node>q;          //队列中放状态
    Cantor(head.state,9);  //用康托展开判重,目的是对起点的visited[]赋初值
    q.push(head);             //第一个进队列的是起点状态
    while(!q.empty()){              //处理队列
        head=q.front();
        q.pop();                       //可在此处打印head.state,看弹出队列的情况
        int z;
        for(z=0;z<9;z++)        //找这个状态中元素0的位置
            if(head.state[z]==0)
                break;
            //转化为二维,左上角是原点(0, 0)。
        int x=z%3;          //横坐标
        int y=z/3;          //纵坐标
        for(int i=0;i<4;i++){   //上、下、左、右最多可能有4个新状态
            int newx=x+dir[i][0];    //元素0转移后的新坐标
            int newy=y+dir[i][1];
            int nz=newx+3*newy;    //转化为一维
            if(newx>=0&&newx<3&&newy>=0&&newy<3) {//未越界
                node newnode;
                memcpy(&newnode,&head,sizeof(struct node));//复制这新的状态
                swap(newnode.state[z],newnode.state[nz]);//把0移动到新的位置
                newnode.dis++;
                if(memcmp(newnode.state,goal,sizeof(goal))==0)
                                                           //与目标状态对比
                    return newnode.dis;             //到达目标状态,返回距离,结束
                if(Cantor(newnode.state,9))         //用康托展开判重
                    q.push(newnode);                   //把新的状态放进队列
             }
        }
    }
    return -1;            //没找到
}
int main(){
    for(int i=0;i<9;i++)cin>>start[i];       //初始状态
    for(int i=0;i<9;i++)cin>>goal[i];        //目标状态
    int num=bfs();
    if(num!=-1)cout<<num<<endl;
    else cout<<"Impossible"<<endl;
    return 0;
}

3.BFS和A*算法
求最短路:这是图论的一个基本问题,有很多复杂算法。
在特殊地图中,BFS是很好的最短路算法。该特殊地图指的是方格型的图,相邻两点的距离相同。而如果相邻点的距离不同,BFS就不适用了。下面介绍A*算法:
例子:求@到t的最短路:
在这里插入图片描述
引入曼哈顿距离:两个点在标准坐标系上的实际距离,在图中,就是@的坐标和t的坐标在横向和纵向的距离之和。

在这里插入图片描述

图b:从起点@开始的第1轮BFS搜索,邻居点上标注的数字3,是@到终点t的曼哈顿距离;
图c:第2轮搜索,标注2的点,是离终点更近的点,从这些点继续搜索;标注4和5的点距离终点远,先暂停搜索。 最后达到终点

而如果图例再复杂些,即起点和终点之间有很多障碍,此时情况如下:
在这里插入图片描述
A是起点,B是终点,黑色方块是障碍。浅色方块,视同曼哈顿距离进行启发式搜索所经过的部分;其它无色方块,是不需要搜索的。虚线为最短路。

在这里插入图片描述
上面例子中曼哈顿距离就是启发函数h(x)
A*算法=BFS+贪心

若h(x)=0,有f(x)=g(x),就是普通的BFS算法;
若g(x)=0,有f(x)=h(x),就是贪心算法,此时图中的*也会被访问到。

给个链接:https://www.redblobgames.com/pathfinding/a-star/introduction.html

4.双向广搜
等我以后研究研究。。。

三、DFS
1.DFS和递归
2.回溯和剪枝:八皇后
回溯不介绍,剪枝函数就是回溯中用于减少子结点扩展的函数。
八皇后代码:

#include<bits/stdc++.h>
using namespace std;
int n,tot=0;
int col[12]={0};
bool check(int c,int r){              //检查是否和已经放好的皇后冲突
    for(int i=0;i<r;i++)
        if(col[i]==c||(abs(col[i]-c)==abs(i-r))) //取绝对值
            return false;
    return true;
}
void DFS(int r) {                   //一行一行地放皇后,这一次是第r行
    if(r==n) {             //所有皇后都放好了,递归返回
       tot++;                  //统计合法的棋局个数
       return;
    }
    for(int c=0;c<n;c++)      //在每一列放皇后
        if(check(c,r)){              //检查是否合法
            col[r]=c;                //在第r行的c列放皇后
            DFS(r+1);                   //继续放下一行皇后
        }
}
int main(){
    int ans[12]={0};
    for(n=0;n<=10;n++){      //算出所有n皇后的答案。先打表不然会超时
        memset(col,0,sizeof(col)); //清空,准备计算下一个N皇后问题
        tot=0;
        DFS(0);
        ans[n]=tot;                //打表
    }
    while(cin>>n){
        if(n==0)
           return 0;
        cout<<ans[n]<<endl;
    }
    return 0;
}

3.迭代加深搜索IDDFS
有些搜索树不仅很深,而且很宽。若直接用DFS,会陷入递归无法返回;如果直接用BFS,队列空间会爆炸。
例题:埃及分数
水平不够,暂时无法解答。
4.IDA*
A*算法思想在IDDFS中的应用。
水平不够,暂时无法解答。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

布布要成为最负责的男人

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

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

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

打赏作者

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

抵扣说明:

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

余额充值