0x02 递推和递归


递归实现指数型枚举

题目

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

输入格式
输入一个整数n。

输出格式
每行输出一种方案。

同一行内的数必须升序排列,相邻两个数用恰好1个空格隔开。

对于没有选任何数的方案,输出空行。
(也就是暗示你肯定会有一种方案是什么都不选择,然后输出空行)

数据范围
1≤n≤15
输入样例:
3
输出样例:

3
2
2 3
1
1 3
1 2
1 2 3
要注意在输出里是有一个空行的

思路

对于这个题,有两种解决方案。

  1. 可以用二进制枚举。在 [ 0 , 2 n − 1 ] [0,2^n-1] [0,2n1]中每一个数字的二进制位上的数字都可以是一种合法的方案,这样的话,枚举 [ 0 , 2 n − 1 ] [0,2^n-1] [0,2n1]中的数字直接输出方案即可。
  2. 递归。对于递归而言,我们可以递归两条分支——选或不选。这样的话就可以把每一种方案都找到,当把所有的数字都确定(而不是每一个数字都选上)了之后就可以直接输出了。

注:对于第二种方案,由于不清楚最后的你的方案中有多少数字被选上,所以我们可以用 v e c o t r vecotr vecotr数组实现操作。

code

1.二进制枚举:

//用位运算解决方案问题 
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n;
int h[1<<20];

int main()
{
    cin>>n;
    for(int i=1;i<=18;++i)
        h[(ll)(1<<i)]=i;//hash的朴素操作
    cout<<endl;
    for(int i=1;i<(1<<n);++i)
    {
        int a=i;
        while(a>0)
        {
            ll ans=a&(-a);
            cout<<h[ans]+1<<' ';
            a=a-ans;
        }
        cout<<endl;
    }
    return 0;
}

2.递归实现:

注意:在递归的过程中,应该是先不选择这个数字,然后再选择这个数字。注意顺序,以及和其他题的区别。

//注意vector的用法 
#include<bits/stdc++.h>
using namespace std;
int n;

vector<int> order;

inline void calc(int x)
{
    if(x==n+1)
    {
        for(int i=0;i<order.size();++i)
            printf("%d ",order[i]);
        puts("");
        return ;
    }
    calc(x+1);
    order.push_back(x);
    calc(x+1);
    order.pop_back();
}

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

递归实现组合型枚举

题目

从 1~n 这 n 个整数中随机选出 m 个,输出所有可能的选择方案。

输入格式
两个整数 n,m ,在同一行用空格隔开。

输出格式
按照从小到大的顺序输出所有方案,每行1个。

首先,同一行内的数升序排列,相邻两个数用一个空格隔开。

其次,对于两个不同的行,对应下标的数一一比较,字典序较小的排在前面(例如1 3 5 7排在1 3 6 8前面)。

数据范围
n>0 ,
0≤m≤n ,
n+(n−m)≤25
输入样例:
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个即可。

在这道题中,除了限制数量,与指数型枚举最大的不同在于在递归中选择的方式不同
指数型枚举:首先是不选择该数,之后是选择该数;
组合型枚举:首先是选择该数,之后是不选择该数。

原因:若是组合性枚举的是**首先是不选择该数,之后是选择该数;**的方法,那么答案出来的时候就会是字典序大的先出来,因为回溯时都是先回到最后的的分支中去,然后继续向下递归。这样子的话不符合字典序的要求。

code

#include<bits/stdc++.h>
using namespace std;
int n,m;

vector<int> order;

inline void calculate(int x)
{
    if(order.size()>m||order.size()+(n-x+1)<m)  return ;//限制条件
    if(x==n+1)
    {
        for(int i=0;i<order.size();++i)
            cout<<order[i]<<' ';
        cout<<endl;
        return ;
    }
    order.push_back(x);
    calculate(x+1);//先选择该数
    order.pop_back();
    calculate(x+1);//后不选择该数
}

int main()
{
    cin>>n>>m;
    calculate(1);
    return 0;
}

递归实现排序型枚举

题目

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

输入格式
一个整数n。

输出格式
按照从小到大的顺序输出所有方案,每行1个。

首先,同一行相邻两个数用一个空格隔开。

其次,对于两个不同的行,对应下标的数一一比较,字典序较小的排在前面。

数据范围
1≤n≤9
输入样例:
3
输出样例:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

思路

  1. 暴力,求n的全排列问题。(博主不会)
  2. 递归。跟上面的思路大致相同,但是这次我们递归的是每个点可能会出现的位置,而不是选或者是不选的问题。也就是把每个数字可能出现的位置全部递归一遍,但是同一个数字不能出现两次,所以在选择一个数字的时候需要打上标记,在回溯的时候再取消标记。

注:这一次的递归的方案中的数字的个数是相同的且确定的,所以可以不使用比较慢的 v e c t o r vector vector数组,而是拿一个普通的数组存一下就好了。

code

#include<bits/stdc++.h>
using namespace std;
const int nn=20;
int n;
int order[nn];
bool chosen[nn];

inline void cal(int k)//k是遍历到第几个位置
{
	if(k==n+1)
	{
		for(int i=1;i<=n;++i)
			cout<<order[i]<<' ';
		cout<<endl;
	}
	for(int i=1;i<=n;++i)
	{
		if(chosen[i])	continue;
		//1.选择这个点作为第k个点
		chosen[i]=true;
		order[k]=i;
		cal(k+1);
		//2.不选择这个点作为第k个点,系统将会直接跳过这个点,有待于下一次的查找 
		chosen[i]=false;
		order[k]=0;
	}
}

int main()
{
	cin>>n;
	cal(1);
	return 0;
}

费解的开关

题目

你玩过“拉灯”游戏吗?25盏灯排成一个5x5的方形。每一个灯都有一个开关,游戏者可以改变它的状态。每一步,游戏者可以改变某一个灯的状态。游戏者改变一个灯的状态会产生连锁反应:和这个灯上下左右相邻的灯也要相应地改变其状态。

我们用数字“1”表示一盏开着的灯,用数字“0”表示关着的灯。下面这种状态

10111
01101
10111
10000
11011
在改变了最左上角的灯的状态后将变成:

01111
11101
10111
10000
11011
再改变它正中间的灯后状态将变成:

01111
11001
11001
10100
11011
给定一些游戏的初始状态,编写程序判断游戏者是否可能在6步以内使所有的灯都变亮

输入格式
第一行输入正整数n,代表数据中共有n个待解决的游戏初始状态。

以下若干行数据分为n组,每组数据有5行,每行5个字符。每组数据描述了一个游戏的初始状态。各组数据间用一个空行分隔。

输出格式
一共输出n行数据,每行有一个小于等于6的整数,它表示对于输入数据中对应的游戏状态最少需要几步才能使所有灯变亮。

对于某一个游戏初始状态,若6步以内无法使所有灯变亮,则输出“-1”

数据范围
0<n≤500
输入样例:
3
00111
01011
10001
11010
11100

11101
11101
11110
11111
11111

01111
11111
11111
11111
11111
输出样例:

3
2
-1

思路

首先,根据题目我们要提取出来一些有用的信息。

  1. 灯需要全亮,步数要控制在6步以内。
  2. 每一行的灯的状态只和该行的上一行或只是下一行有关。
  3. 每个位置的灯最多只会被点击一次(点击两次,状态相反,作用抵消)【第三条摘自蓝皮书】

然后,整合提取出来的信息。

  1. 因为有上面第二条的存在,所以一旦我们确定了01矩阵中一行状态就可以确定整个矩阵的状态。
  2. 针对于01矩阵中的一行,我们对于每一个数字都有点击或者是不点击的权利 (是不是感觉跟上面有点类似) ,从而可以产生很多种的方案,针对于每一种状态就可能有不同的步数使得该矩阵中的灯全亮。【方便起见,我们确定第一行的状态】
  3. 对于点击或者是不点击,我们就可以是使用二进制枚举的方法来确定状态。
  4. 那么对于确定第一行的状态而言,就只有最后一行就算是有灯为0,也没有最最后一行来使它变为1,所以,我们就可以是检验最后一行从而代替检验整个矩阵。(看不懂也么米有关系,你完全可以把整个矩阵遍历一遍,从而确定方案是否合法,笔者试过,复杂度是可以过得)

最后,想一下算法。可以二进制枚举+dfs搞一下。当然,你也可以是其他的枚举方式,比如gcf大佬的代码,在下面有体现。

code

二进制枚举的方法:

#include<bits/stdc++.h>
using namespace std;
int dx[10]={1,-1,0,0};
int dy[10]={0,0,1,-1};//向上,向下,向右,向左 
const int inf=1000000;
int n;
int a[10][10];
string s;
int ans;

inline int read()
{
	int x=0,f=1; char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0'; ch=getchar();}
	return x*f;
}

void changed (int x,int y)
{
	a[x][y]^=1;
	for(int i=0;i<4;++i)//对于方向数组只有4个数字,所以就只是[0,4) 
	{
		int xx=x+dx[i];
		int yy=y+dy[i];
		if(xx<0||yy<0)	continue;
		a[xx][yy]^=1;
	} 
}

bool check()
{
	for(int j=0;j<5;++j)
		if(!a[4][j])
			return false;
	return true;
}

void dfs(int layer,int tot)
{
	if(tot>6)	return ;
	if(layer==5&&check())
	{
		ans=min(ans,tot);
		return ;
	}
	int re=0;//re是用来恢复被修改过后的点 
	for(int i=0;i<5;++i)
		if(!a[layer-1][i])//用上一行的点的状况更新当前点的状况 
			re|=1<<i,changed(layer,i),tot++;
	dfs(layer+1,tot);
	for(int i=0;i<5;++i)
		if(re&(1<<i))
			changed(layer,i);
}

int main()
{
	n=read();
	while(n--)
	{
		ans=inf;
		//输入 
		for(int i=0;i<5;++i)
		{
			cin>>s;
			for(int j=0;j<5;++j)
				a[i][j]=s[j]-'0';
		}
		
		for(int i=0;i<(1<<5);++i)//二进制枚举每一行的状态 
		{
			int tot=0;
			int s=i;
			for(int j=0;j<5;++j)
				if(s&(1<<j))
					changed(0,j),tot++;//改变第一行的状态
			dfs(1,tot); 
			for(int j=0;j<5;++j)//复原你原来的01矩阵的状态 
				if(s&(1<<j))
					changed(0,j);
		}
		//判断是否又被更新 
		if(ans==inf)	printf("-1\n");
		else			printf("%d\n",ans);
	}
	return 0; 
} 

gcf大佬的代码 (我也看不懂)

#include<bits/stdc++.h>
using namespace std;
int n,a[10][10],vis[10],ans,b[10][10];
inline int read()
{
    int x=0,ff=1;
    char ch=getchar();
    while(!isdigit(ch))
    {
        if(ch=='-') ff=-1;
        ch=getchar();
    }
    while(isdigit(ch))
    {
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar();
    }
    return x*ff;
}
inline void put(int x)
{
    if(x<0) putchar('-'),x=-x;
    if(x>9) put(x/10);
    putchar(x%10+'0');
}
void work()
{
    int cnt=0;
    for(int i=1;i<=5;i++)
        for(int j=1;j<=5;j++) a[i][j]=b[i][j];
    for(int i=1;i<=5;i++) 
    {
        if(vis[i]) 
        {
            cnt++;
            a[1][i]=-a[1][i];
            a[2][i]=-a[2][i];
            if(i-1>0) a[1][i-1]=-a[1][i-1];
            if(i+1<6) a[1][i+1]=-a[1][i+1];
        } 
    }
    for(int i=1;i<=4;i++)
        for(int j=1;j<=5;j++)
        {
            if(a[i][j]==-1)
            {
                cnt++;
                a[i][j]=-a[i][j];
                a[i+1][j]=-a[i+1][j];
                if(j-1>0) a[i+1][j-1]=-a[i+1][j-1];
                if(j+1<6) a[i+1][j+1]=-a[i+1][j+1];
                if(i+2<6) a[i+2][j]=-a[i+2][j]; 
            }
        }
    for(int j=1;j<=5;j++)
    {
        if(a[5][j]==-1) {cnt=-1;break;}
    }
    if(cnt!=-1) ans=min(ans,cnt);    
}
inline void dfs(int x)
{
    if(x==5) {work();return;}
    dfs(x+1);
    vis[x+1]=1;
    dfs(x+1);
    vis[x+1]=0;
}
int main()
{
    freopen("1.in","r",stdin);
    n=read();
    for(int k=1;k<=n;k++) 
    {
        char ch;ans=INT_MAX;
        for(int i=1;i<=5;i++)
            for(int j=1;j<=5;j++)
            {
                cin>>ch;
                if(ch=='0') b[i][j]=-1;
                else        b[i][j]=1;
            }
        dfs(0);
        if(ans>6) put(-1),cout<<endl;
        else      put(ans),cout<<endl;
    }
    return 0;
}

未完待续……

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值