2022蓝桥杯学习——1.递归和递推

递归

关于递归

所有的递归都可以转换成一棵递归搜索树 我们需要考虑的是枚举的顺序

例题

1.递归实现指数型枚举

题目描述
从 1∼n 这 n 个整数中随机选取任意多个,输出所有可能的选择方案。

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

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

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

对于没有选任何数的方案,输出空行。

本题有自定义校验器(SPJ),各行(不同方案)之间的顺序任意。

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

3

输出样例:

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

枚举顺序:考虑1~n每个数选还是不选,这样每个数都对应两个可能,一共就是2的n次方的方案数
转换成一棵递归树
在这里插入图片描述
代码实现+注释 C++

#include<iostream>
using namespace std;
const int N=16;
int st[N];//st数组有三种值,1表示选,2表示不选,0表示初始状态
int n;
void dfs(int u){//u代表我们枚举到第u个数
    if(u>n){//当n个数都枚举完了就开始打印
        for(int i=1;i<=n;i++){
            if(st[i]==1) cout<<i<<" ";
        }
        cout<<endl;
        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);//从一开始枚举
    return 0;
}

2.递归实现排列型枚举

题目描述:
把 1∼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个位置选什么数
转换成递归树:
在这里插入图片描述
代码实现+注释 C++

#include<iostream>
#include<algorithm>
using namespace std;
const int N=10;
bool st[N];//用来判断1~n这n个数是否被选。true代表已经被选了,false反之
int p[N];//用来记录1~n个位置选的是哪个数
int n;
void dfs(int u){
    if(u>n){//当n个位置都确定之后就打印
        for(int i=1;i<=n;i++){
            cout<<p[i]<<" ";
        }
        cout<<endl;
        return;
    }
    for(int i=1;i<=n;i++){//第u个位置开始选数
        if(!st[i]){//如果这个数没有被选
            st[i]=true;//选择这个数打上标记
            p[u]=i;//记录
            dfs(u+1);//开始枚举下一个位置
            st[i]=false;//恢复现场
        }
    }
}
int main()
{
    cin>>n;
    dfs(1);//从第一个位置开始遍历
    return 0;
}

3.递归实现组合型枚举

题目描述:
从 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

枚举顺序: 枚举1~m个位置选什么,但是,为了保证不重复且按字典序,我们要加上一个条件,每一种组合后一个数一定比前一个数小
递归树:
在这里插入图片描述
代码实现+注释 C++

#include<iostream>
#include<algorithm>
using namespace std;
const int N=25;
int p[N];//记录每个位置填的数字
int n,m;//n个数字里选m个
void dfs(int u,int start){//枚举第u个位置,可枚举的数字从start~n
    if(n-start<m-u) return;//当剩下的数字不够枚举的时候,就退出
    if(u==m+1){//当m个位置都确定了,开始打印
        for(int i=1;i<=m;i++){
            cout<<p[i]<<" ";
        }
        cout<<endl;
        return;
    }
    for(int i=start;i<=n;i++){//枚举第u个位置 这里不用st记录是因为我们从小到大枚举,保证后一个数比前一个大,不会出现重复
        p[u]=i;//记录所选的数
        dfs(u+1,i+1);//开始枚举下一个位置,为了保证下一个数比第i个大,从i+1开始枚举
    }
}
int main()
{
    cin>>n>>m;
    dfs(1,1);//从一个位置开始枚举,数字从 1 开始
    return 0;
}

蓝桥杯真题

带分数

题目描述:
100 可以表示为带分数的形式:100=3+69258/714
还可以表示为:100=82+3546/197
注意特征:带分数中,数字 1∼9 分别出现且只出现一次(不包含 0)。

类似这样的带分数,100 有 11 种表示法。

输入格式
一个正整数。

输出格式
输出输入数字用数码 1∼9 不重复不遗漏地组成带分数表示的全部种数。

数据范围
1≤N<106
输入样例1:

100

输出样例1:

11

输入样例2:

105

输出样例2:

6

枚举顺序: 先枚举a,再枚举b,由表达式计算出c,判断能成立的表达式是否也满足数字 1∼9 分别出现且只出现一次(n=a+b/c => cn-ac=b)

代码+注释 C++

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;

const int N=100010;
bool st[20],back[20];
int n,res;

bool check(int a,int c){//用来判断此时的a和c能否满足条件,a和c确定之后,若想满足表达式,b是唯一确定的
    int b=n*c-a*c;//计算出b
    if(!a||!b||!c) return false;//如果 a或者b或者c是0,则一定不满足条件
    
    memcpy(back,st,sizeof(st));//将原来的st数组拷贝一份,接下来用的是拷贝的back,不改变原数组
    while(b){//求出b的每一位
        int x=b%10;
        if(!x||back[x]) return false;//如果改数字是0或者改数字已经被用了,一定不满足条件
        back[x]=true;//将用到的数字打上标记
        b/=10;
    }
    for(int i=1;i<=9;i++){//最后,如果b满足条件,看看是否a,b,c用了1~9每一个数字
        if(!back[i]) return false;//只要有数字没有用 就一定不满足条件
    }
    return true;
}
void dfs_c(int u,int a,int c){//第一个参数用来表示选到数字几了,第二个参数表示a的值,第三个参数表示c的值
    if(u>9) return;//如果a把9个数用完了,一定不满足条件
    
    if(check(a,c)){//用此时的a和c来判断是否有b时表达式和条件成立
        res++;
    }
    for(int i=1;i<=9;i++){//继续枚举c
        if(!st[i]){
            st[i]=true;
            dfs_c(u+1,a,c*10+i);
            st[i]=false;
        }
    }
}
void dfs_a(int u,int a){//第一个参数用来表示选到数字几了,第二个参数表示a的值
    if(a>=n) return;
    if(a) dfs_c(u,a,0);//当a大于0开始枚举c
    
    for(int i=1;i<=9;i++){//继续枚举a
        if(!st[i]){
            st[i]=true;
            dfs_a(u+1,a*10+i);
            st[i]=false;
        }
    }
}
int main()
{
    cin>>n;
    dfs_a(0,0);//枚举a,从第0位开始,此时a=0
    
    cout<<res<<endl;
    return 0;
}

递推

例题

1.简单斐波那契

题目描述:
以下数列 0 1 1 2 3 5 8 13 21 … 被称为斐波纳契数列。

这个数列从第 3 项开始,每一项都等于前两项之和。

输入一个整数 N,请你输出这个序列的前 N 项。

输入格式
一个整数 N。

输出格式
在一行中输出斐波那契数列的前 N 项,数字之间用空格隔开。

数据范围
0<N<46
输入样例:

5

输出样例:

0 1 1 2 3

递推 当n=0 f[n]=0; 当n=1 f[n]=1; n>=2 f[n]=f[n-1]+f[n-2]

代码实现+注释:C++

#include<iostream>
using namespace std;
const int N=50;
int f[N];
int main()
{
    int n;
    cin>>n;
    f[0]=0,f[1]=1;
    for(int i=0;i<n;i++){
        if(i>=2) f[i]=f[i-1]+f[i-2];
        cout<<f[i]<<" ";
    }
    return 0;
}

2.费解的开关

题目描述:
你玩过“拉灯”游戏吗?

25 盏灯排成一个 5×5 的方形。

每一个灯都有一个开关,游戏者可以改变它的状态。

每一步,游戏者可以改变某一个灯的状态。

游戏者改变一个灯的状态会产生连锁反应:和这个灯上下左右相邻的灯也要相应地改变其状态。

我们用数字 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

在这里插入图片描述

代码实现+注释:C++

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=6;
char g[N][N],back[N][N];
int dx[]={0,0,-1,1,0},dy[]={1,-1,0,0,0};//对应 右、左、上、下、它自己
int t;
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||b<0||a>=5||b>=5) continue;
        g[a][b]^=1;//因为0和1的编码对应的二进制只有最后一位不一样,差1,‘0’异或1就是‘1’
    }
}
int main()
{
    cin>>t;
    while(t--){
        int res=10,step=0;
        for(int i=0;i<5;i++) cin>>g[i];
        for(int i=0;i<32;i++){//枚举第一行的32中状态,每一种状态都可能对应一种结果,从里面找出操作最小的
            step=0;
            memcpy(back,g,sizeof(g));//拷贝一份,下一次枚举的时候用原来的数组
            for(int j=0;j<5;j++){//j代表第一行的五个灯,当该位置为‘0’时代表不操作,为‘1’代表操作
                if(i>>j&1){
                    step++;//记录操作步数
                    turn(0,j);//改变这个灯和相邻四个灯的状态
                }
            }
            for(int j=0;j<4;j++){//枚举前四行
                for(int k=0;k<5;k++){//每行五个灯
                    if(g[j][k]=='0'){//如果这一个灯是关的,就改变它下面的灯
                        step++;//记录状态
                        turn(j+1,k);
                    }
                }
            }
            bool dark=false;//用来判断最后是否成功
            for(int j=0;j<5;j++)//遍历最后一行灯的状态
                if(g[4][j]=='0'){//如果是‘0’就说明这一组操作不能使灯全部变亮
                    dark=true;
                    break;
                }
            if(!dark) res=min(res,step);//如果灯全亮,更新操作最小值
            memcpy(g,back,sizeof(back));//还原g数组
        }
        if(res>6) cout<<-1<<endl;
        else cout<<res<<endl;
    }
}

3.飞行员兄弟

题目描述
“飞行员兄弟”这个游戏,需要玩家顺利的打开一个拥有 16 个把手的冰箱。

已知每个把手可以处于以下两种状态之一:打开或关闭。

只有当所有把手都打开时,冰箱才会打开。

把手可以表示为一个 4×4 的矩阵,您可以改变任何一个位置 [i,j] 上把手的状态。

但是,这也会使得第 i 行和第 j 列上的所有把手的状态也随着改变。

请你求出打开冰箱所需的切换把手的次数最小值是多少。

输入格式
输入一共包含四行,每行包含四个把手的初始状态。

符号 + 表示把手处于闭合状态,而符号 - 表示把手处于打开状态。

至少一个手柄的初始状态是关闭的。

输出格式
第一行输出一个整数 N,表示所需的最小切换把手次数。

接下来 N 行描述切换顺序,每行输出两个整数,代表被切换状态的把手的行号和列号,数字之间用空格隔开。

注意:如果存在多种打开冰箱的方式,则按照优先级整体从上到下,同行从左到右打开。

数据范围
1≤i,j≤4

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

输出样例:

6
1 1
1 3
1 4
4 1
4 3
4 4

这个题不能像费解的开关一样 通过确定一行就确定其他行的状态,因为每次受影响的是一行+一列,但可以发现数据范围很小,所以可以通过暴力枚举来解题,我们直接枚举所有可能的操作就好了,十六把锁对应2的十六次方种可能,用二进制加位运算,十六位,每一位是0(不操作) 或 1(操作),每次对所有锁通过0和1确定之后操作一遍,操作完之后判断是否能打开,如果能就更新最小值

代码实现+详细注释 C++:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<vector>
using namespace std;
const int N=5;
char g[N][N],back[N][N];
typedef pair<int,int> PII;
vector<PII> res;
void turn_one(int x,int y){//改变某个灯的状态,'+ -> -'' or  '- -> +''
    if(g[x][y]=='-') g[x][y]='+';
    else if(g[x][y]=='+') g[x][y]='-';
}
void turn(int x,int y)//通过改变一个点之后改变这个点所在的一行和一列的锁
{
    for(int i=0;i<4;i++){
        turn_one(x,i);
        turn_one(i,y);
    }
    turn_one(x,y);
}
bool check(){//检查是否所有锁都打开,是返回true
    for(int i=0;i<4;i++){
        for(int j=0;j<4;j++){
            if(g[i][j]=='+') return false;//只要有一把没打开就返回false
        }
    }
    return true;
}
int main()
{
    for(int i=0;i<4;i++) cin>>g[i];
    memcpy(back,g,sizeof(g));
    for(int op=0;op<1<<16;op++){//枚举所有可能的情况
        vector<PII> cur;//用来保存操作的锁
        for(int i=0;i<4;i++){//每一次操作对应十六个位置,将一维转化成二维
            for(int j=0;j<4;j++){
                if(op>>(i*4+j)&1) {//如果是1 就操作 是0 就不操作
                    turn(i,j);
                    cur.push_back({i,j});
                }
            }
        }
        if(check()){//操作完十六个位置之后检查是否可打开锁
            if(res.empty()||res.size()>cur.size()){//可以就更新最小值
                res=cur;
            }
        }
        memcpy(g,back,sizeof(back));//恢复原来的状态
    }
    cout<<res.size()<<endl;
    for(int i=0;i<res.size();i++) cout<<res[i].first+1<<" "<<res[i].second+1<<endl;
    return 0;
}

蓝桥杯真题

翻硬币

题目描述
小明正在玩一个“翻硬币”的游戏。

桌上放着排成一排的若干硬币。我们用 * 表示正面,用 o 表示反面(是小写字母,不是零)。

比如,可能情形是: **oo ***oooo

如果同时翻转左边的两个硬币,则变为:oooo***oooo

现在小明的问题是:如果已知了初始状态和要达到的目标状态,每次只能同时翻转相邻的两个硬币,那么对特定的局面,最少要翻动多少次呢?

我们约定:把翻动相邻的两个硬币叫做一步操作。

输入格式
两行等长的字符串,分别表示初始状态和要达到的目标状态。

输出格式
一个整数,表示最小操作步数

数据范围
输入字符串的长度均不超过100。
数据保证答案一定有解。

输入样例1:
在这里插入图片描述

输出样例1:

5

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

1

思路:

其实对于每一枚硬币,我们如果是反转这枚硬币和它右边的硬币与翻转它右边的硬币和它右边的硬币左边的硬币,都是同时翻转了两个相同的硬币,所以我们只需要考虑同时翻转某一个硬币和它右边的硬币就行,而不需要再考虑翻转它左边的硬币,然后就要确定什么时候翻转,要想翻成某种状态,其实只有一种可能,因为从前往后遍历每一个硬币,只要这枚硬币和目标状态不同我们就翻转,这样在遍历到最后一个的时候,因为已经不能再翻转的(后面没硬币了),只要最后一个硬币和目标状态相同,就是翻转成功。

代码实现+详细注释 C++:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<string>
using namespace std;
string a,b,c;
int res;
void turn(int i){//翻转这枚硬币和它右边的硬币
    if(a[i]=='*') a[i]='o';
    else a[i]='*';
    if(a[i+1]=='*') a[i+1]='o';
    else a[i+1]='*';
}
int main()
{
   cin>>a>>b;
   for(int i=0;i<a.size()-1;i++){//从左往右遍历每一枚硬币
       if(a[i]!=b[i])//和目标状态不同就翻转
       {
           turn(i);
           res++;//记录操作次数
       }
   }
   cout<<res<<endl;
   return 0;
}

终于整理完了,我发现递归和递推真的很难用语言来描述清楚,大家学习的时候也可以多画图,手动模拟,希望可以帮到大家!!
有什么疑问都可以问,或者有错误的地方也感谢指出,接下来会不断更新蓝桥杯学习内容,如果想报名蓝桥杯,推荐学习网站AcWing,所以题目来源均是ACWing。(不是广告hh 良心推荐)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值