dfs暴搜三种模式

三种搜索模式

指数型/排列型/组合型,根据不同题目情形灵活应用
用dfs递归暴搜,重点在于,想一种枚举顺序,使其不重不漏地输出每一种组合

1.指数型枚举

从 1~n 这 n 个整数中随机选取任意多个,输出所有可能的选择方案

输入样例:3

输出样例:

3
2
2 3
1
1 3
1 2
1 2 3

思路:有n个空位,每一个位置1选/0不选,一串长度为n的0/1序列为一个方案,从第一个开始做决策,第一个选(填1),接下来第二个选(填1)…一直到第n个选(填1),得到了一种方案,回溯,第n个不选(填0),又得到一种方案;回溯到上一层,第n-1不选(填0),继续往下走,想象顺着一棵树,一条路走到黑,然后又回一下头改变一下选择,再继续走到黑。

怎么写代码?

树中的每个位置状态需要记录,因为到了最后的叶子节点需要从跟一直读下来,作为一个方案。用dfs(1),表示从第1个位置开始做决策。

#include<iostream>
#include<cstring>
#include<cstdio> 
#include<algorithm>

using namespace std;

const int N = 16;

int n;
int st[N];//记录每个位置状态 0:未考虑 1:选他 2:不选他
int ways[1<<15][16],cnt;
void dfs(int u)
{
    if( u > n ){//选完了,输出
        for(int i = 1; i <= n; i++ )
        {
            
            if(st[i]==1)
            //ways[cnt][i]=i;//记录每个方案
                printf("%d ",i);
        }
        //cnt++;
        printf("\n");
        return;
    }
    
    st[u] = 2;
    dfs(u + 1);//第一个分支 不选
    st[u] = 0;//恢复现场
    
    st[u] = 1;
    dfs(u + 1);//第二个分支 选
    st[u] = 0;
}
int main(){
    
    cin>>n;
    dfs(1);//从第1位开始选择
	return 0;
} 

95.费解的开关

一下子不知道如何下手,就从迈出第一步开始,我不知道究竟怎么改变可以让灯从全亮变成,最直接的,先对第一行动手,枚举在第一行上所能做的所有操作,此时这个枚举可以用指数型枚举的搜索模式,因为方案中每一个位代表选/不选,在本题中就可以当做按开关/不按开关。注意,枚举的是对第一行的操作,根据枚举的结果要对第一行进行一顿操作。继续分析发现,要是按照行的顺序来,第一行操作完之后,第二行的操作目标就是让第一行灭的灯亮起来,故第二行操作是固定的,以此类推,到了n-1行,最后一行无法操作了,若是全亮则此次尝试成功,否则不成功。

代码实现思路

  1. 枚举第一行操作
  2. 根据第一行操作,操作完1~n-1行
  3. 判断最后一行是不是全亮

注:

  1. 枚举还可以用二进制方式,从0~2n-1每一个数对应一个n位二进制,可以代表一种方案
  2. 根据枚举的操作,如何实现turn(x,y)——事先规定好方向

代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;

const int N=6;

char g[N][N],backup[N][N];
int dx[5]={-1,0,1,0,0},dy[5]={0,1,0,-1,0};

void turn(int x,int y)
{
    for(int i=0; i<5; i++ )
    {
        int a=x+dx[i],b=y+dy[i];
        if(a<0 || a>=5 || b<0 || b>=5 ) continue;//出界
        g[a][b]^=1;//二进制异或1,0变1askii码,1变0
    }
}
int main()
{
    int T;
    cin>>T;
    
    while(T--)
    {
        for(int i=0; i<5; i++ ) cin>>g[i];//读入棋盘
        int res=10;
        for(int op=0; op<32; op++ )//枚举第一行所有操作方案
        {
            memcpy(backup,g, sizeof g);//备份
            int step=0;
            
            //操作第一行
            for(int i=0;i< 5; i++ )
            {
                if(op>>i&1)
                {
                    step++;
                    turn(0,i);
                }
            }
                
            //下一行操作受限于上一行
                for(int i=0; i<4; i++ )
                {
                    for(int j=0;j<5;j++)
                    if(g[i][j]=='0')//若上一行某一格为‘0’,则下一行对应格子必定要按下,让该格子变为‘1’
                    {
                        step++;
                        turn(i+1,j);//按下
                    }
                }
                
                bool dark=false;//依次做完前4行后,判断最后一行是否符合要求
                for(int i=0; i<5;i++)
                {
                    if(g[4][i]=='0'){
                        dark=true;
                        break;
                    }
                }
                
                if(!dark) res=min(res,step);
                memcpy(g,backup,sizeof g);
          
        } 
        if(res>6) res=-1;
            cout<< res<<endl;
    }  
    return 0;
}

116.飞行员兄弟

看上去与费解的开关类似,又不一样,因为类似的,先枚举第一行操作后,下一行的操作并不能确定,还有很多行都可以对第一行造成影响,由于矩阵小,直接二进制枚举矩阵每一个单元的操作

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<vector>

#define x first
#define y second

using namespace std;

const int N =5;
char g[N][N];
char backup[N][N];
typedef pair<int,int> PII;

 
void turn_one(int x,int y)
{

	if(g[x][y]=='+') g[x][y]='-';
	else g[x][y]='+';
		
}

void turn_all(int x,int y)
{
    for(int i=0;i<=3; i++)
    {
        turn_one(x,i);
        turn_one(i,y);
    }
    
    turn_one(x,y);//消除重复的
}

int get(int x,int y)//返回x,y坐标的标号,为了可以按照字典序排序
{
    return x*4+y;
}
int main()
{
	for(int i=0;i<4;i++) cin>>g[i];//读状态
	
	vector<PII> res;
	for(int op=0;op< 1<<16 ; op++ )//对16个位置的所有方案进行枚举
	{
		memcpy(backup,g,sizeof g);
        vector<PII> temp;
        
        //进行操作
		for(int i=0;i<=3;i++)//依次根据op的方案,按照op每个位的指示进行操作
		    for(int j=0;j<=3; j++)
                if(op>>get(i,j)&1)//get(i,j)移到某一位查看op指示的操作
                {
                    temp.push_back({i,j});//有操作即记录
                    turn_all(i,j);
                }
        //判断所有开关是否全开
			
		bool unfinish=false;
		
		for(int i=0;i<=3;i++)
    		for(int j=0;j<=3;j++)
    			if(g[i][j]=='+')
    				unfinish=true;
    				
		if(!unfinish)
		{
		    if(res.empty()|| res.size()> temp.size() )  res=temp;
		}

			
		memcpy(g,backup,sizeof g);
	}
	
	cout<<res.size()<<endl;
	for(auto op :res ) cout<<op.x+1<<' '<<op.y+1<<endl;
	return 0;
} 
2.排列型枚举

把 1~nn 个整数排成一行后随机打乱顺序,输出所有可能的次序

输入样例:3

输出样例:

1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

思路:重点还是想一个枚举的顺序,怎么不重不漏的写出来

  1. 先想好n个位置,依次枚举这个位置上放的数,例如,第一个放1,往下走第二个可以放除了1以外的其他数,直到位置放完,回溯,想象一棵树
  2. 先想一个数,把他依次放到每一个位置上,例如,先选1,1放1号位置,往下走,2放第2个…回溯1放第2个位置的情况
  3. 到叶子节点才会有一个方案出来

注:排列型枚举,在树往下生长的时候,要注意所选数字不能被重复利用,故增加一个判重的数组,记录i在这条路上是否用过used[i],因为每次对stu[u],u位置放数的时候都是从头开始找的。

代码怎么写? 与指数型类似

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N =10;

int n;
int st[N];// 0:未放置数,1~n表示放了那个数
bool used[N];//true 表示该数用过,false表示没有用过
void dfs(int u){//表示现在探索u的位置放的数
    
    if(u>n){
        for(int i=1; i<=n; i++ ) printf("%d ",st[i]);//打印方案
        puts("");
        
        return;
    }
    
    //依次枚举每个分支,即当前位置可以填哪些数
    for(int i=1;i<=n;i++)
    {
        if(!used[i]){
            st[u]=i;
            used[i]=true;
            dfs(u+1);
            
            //恢复现场
            
            st[u]=0;
            used[i]=false;
        }
    }
    
}

int main()
{
    
    scanf("%d",  &n);
    dfs(1);
    return 0;
}

1209.带分数

  1. 暴力把1~ 9切成三段,带入验证,先枚举1~9的全排列,用到排列型枚举,再双重循环砍两刀,获得a,b,c三个数,然后验证
  2. 把数组里的数字组合起来,例如a[0]=1,a[1]=2,a[3]=[3],想得到123的话,只需要一重循环,循环内不断*10+a[i]即可,如下:
   int cut(int s,int e)
   {
       int res=0;
       for(int i=s;i<=e;i++)
       res=res*10+st[i];
       
       return res;
   }

暴力代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;


const int N=10;
int st[N];
bool used[N];
int n;
int cnt;

int cut(int s,int e)
{
    int res=0;
    for(int i=s;i<=e;i++)
    res=res*10+st[i];
    
    return res;
}

void dfs(int u)
{
    if(u==N)
    {
        for(int i=1;i<=10; i++ )
            for(int j=i+1; j<=10; j++ )
            {
                int a=cut(1,i);
                int b=cut(i+1,j);
                int c=cut(j+1,9);   
                
                if(c*n==c*a+b) cnt++;
            }
        
    }
    
    for(int i=1;i<=9; i++ )
    {
        if(!used[i])
        {
            st[u]=i;
            used[i]=true;
            dfs(u+1);
            st[u]=-1;
            used[i]=false;
        }
    }
    
}

int main()
{
    cin>>n;
    dfs(1);
    cout<<cnt<<endl;
    
}
2:双层嵌套dfs()
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>

using namespace std;

const int N=20;
int n;
int ans;
bool used[N],backup[N];

bool check(int a,int c)
{
   int b=n*c-a*c;
   //看b中是否有重复数字
   
   if(!a || !b || !c ) return false;
   
   //把数组复制到backup里面,重开一个判重数组
   memcpy(backup,used,sizeof used);
   //分离b的每一位
   while(b)
   {
       int x= b % 10;//取个位
        b=b/10;//个位删掉
        if(!x || backup[x]) return  false;
        backup[x]=true;
   }
    for(int i=1;i<=9; i++)
        if(!backup[i])
        return false;
    
    return true;
}
void dfs_c(int u,int a, int c)
{
    if(u==n) return;
    if(check(a,c)) ans++;
    
    for(int i=1;i<=9;i++)
    {
        if(!used[i])
        {
            used[i]=true;
            dfs_c(u+1,a,c*10+i);
            used[i]=false;
        }
    }
}

void dfs_a(int u,int a)
{
    if(a>=n) return;
    
    if(a) dfs_c(u,a,0);//对于每一个枚举的a都枚举c,相当于在a 的递归搜索树的叶子节点再开辟一个搜索树
    
    for(int i=1;i<=9;i++)
    {
        if(!used[i])
        {
            used[i]=true;
            dfs_a(u+1,a*10+i);
            used[i]=false;
        }
    }
}
int main()
{
    cin>>n;
    dfs_a(0,0);
    cout<<ans<<endl;
    return 0;
}
3.组合型枚举

从 1~n 这 n 个整数中随机选出 m 个,输出所有可能的选择方案,要求每一行升序。

输入样例:

5 3

输出样例:

1 2 3
1 2 4
1 2 5
1 3 4
1 3 5
1 4 5
2 3 4
2 3 5
2 4 5
3 4 5

思路:与排列型枚举一致,不过限制了位置m个,并且升序的话,需要保证下一次的选取>父亲,从父亲的下一个数开始选取,故参数传递上多了一个父亲的值,少了一个判重数组

代码?

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>

using namespace std;
const int N =25;
int n,m;
int st[N];
vector< vector<int> > ways;

void dfs(int u,int limit)
{
    
    if(u+n - limit < m ) return;//剪枝
    if(u>m)
    {
        vector<int>way;
        for(int i=1; i<= m; i++ )
        { 
            way.push_back(st[i]);
        }
        
        ways.push_back(way);
        return;
    }
    
    for(int i=limit;i<=n;i++)
    {
        st[u]=i;
        dfs(u+1,i+1);
        st[u]=0;
    }
    
}
int main(){
    
    scanf("%d %d",&n,&m);
    dfs(1,1);
    
    for(int i=0; i<ways.size();i++)
    {
      for(int j=0;j<ways[i].size();j++)
        {
            printf("%d ",ways[i][j]);
        }
        puts("");
    }
    
    return 0;
    
}

来源于y总的AcWing
(超棒的!

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值