递归算法思路以及题目总结(未完待续...)

2018-3-10

之前在一个网站上看到了一些递归题目的合集,题目来源是:
http://bailian.openjudge.cn/
是北京大学ACM训练和相关程序课程在线考试系统。
我先列出所有的题目编号,具体代码会在下面:

题目名称/题目ID
菲波那契数列 2753
二叉树 2756
逆波兰表达式 2694
放苹果 1664
红与黑 2816
八皇后问题 2754
木棍问题 2817
城堡 2815
分解因数 2749
迷宫 279024 2787
文件结构"图" 2775
小游戏 2802
碎纸机 2803
棋盘分割 1191
棋盘问题 1321

1.斐波那契数列
这个对我们来说应该并不陌生,f(n)=f(n-1)+f(n-2),初始化f(1)=1,f(2)=1,当n比较大的时候,我们会发现有些f(m)会被我们计算了多次,那么我们可以先将它们存到数组里,如果我们需要用的话直接去数组里面取就可以了。

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

const int N = 20;
int fib[N+1];

int dfs(int p){
    if (fib[p]) return fib[p];
    fib[p]=dfs(p-1)+dfs(p-2);
    return fib[p];
}

int main(){
    memset(fib,0,sizeof(fib));
    fib[1]=1;fib[2]=1;
    int t,n;
    cin>>t;
    while(t--){
        cin>>n;
        if (fib[n]) cout<<fib[n]<<endl;
        else cout<<dfs(n)<<endl;
    }
    return 0;
}

2.二叉树
这个题目让我们找到两个节点的最大公共节点,我们不难发现二叉树的性质,父节点的值是子节点值的1/2,利用这个特点我们可以一步步的向上找。

#include<iostream>
using namespace std;

int x,y;

int dfs(int i,int j){
    if (i==j) return i;
    if (i>j){
        return dfs(i/2,j);
    }else{
        return dfs(i,j/2);
    }
}

int main(){
    while (cin>>x>>y){
        cout<<dfs(x,y)<<endl;
    }
    return 0;
}

3.逆波兰表达式
说实话,我觉得这个题目本身就很神奇,我在想这个题目的时候在纠结输入到底应该如何处理,后来在小伙伴的提醒之下写出了答案。
我们可以在函数里面等待输入,当输入的是运算符的时候,我会等待输入两个数字来进行运算,如果输入的还是运算符,我们可以继续等待,直至输入数字,返回结果与相应的运算符进行运算。

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

char x[10];

double dfs(){
    cin>>x;
    switch(x[0]){
        case '+':{
            return dfs()+dfs();
            break;
        }
        case '-':{
            return dfs()-dfs();
            break;
        }
        case '*':{
            return dfs()*dfs();
            break;
        }
        case '/':{
            return dfs()/dfs();
            break;
        }
        default:
            return atof(x);
    }
}

int main(){
    printf ("%f\n",dfs());
    return 0;
}

4.放苹果
这道题目老早就见过,但是在这一次写的时候又出现了 一个问题,我知道如果第n个盘子不用的话就等同于m个苹果放在n-1个盘子中,但是如果我们第n个盘子用了呢?那我们就得把所有的n个盘子里面都放一个才可以,因为我们这个是不考虑顺序的,我们必须保证得到的两个是完全没有交集的,我们不能把相同的情况计算多遍!如果说我们没有把n个盘子都放一个的话,那么我们在f(m,n-1)里面也会出现空的盘子,那可能就出现重复的情况了。

#include<iostream>
using namespace std;

int dfs(int m,int n){
    if (m<0||n<0){
        return 0;
    }
    if (m==1||n==1||m==0||n==0) return 1;
    return dfs(m,n-1)+dfs(m-n,n);
}

int main(){
    int t;
    cin>>t;
    while (t--){
        int m,n;
        cin>>m>>n;
        cout<<dfs(m,n)<<endl;
    }
    return 0;
}

5.红与黑
这种题目应该屡见不鲜了,就是走迷宫类的题目,实现起来的套路似乎差不多。

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

const int N = 20;
int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};
char x[N+1][N+1];
int res;

void dfs(int p,int q){
    for (int k=0;k<4;k++){
        int ii=p+dx[k],jj=q+dy[k];
        if (x[ii][jj]=='.'){
            x[ii][jj]='#';
            res++;
            dfs(ii,jj);
        }
    }
}

int main(){
    int p,q,w,h;
    while (cin>>w>>h){
        if (w==0&&h==0){
            break;
        }
        memset(x,0,sizeof(x));
        res=0;
        for (int i=1;i<=h;i++){
            for (int j=1;j<=w;j++){
                cin>>x[i][j];
                if (x[i][j]=='@'){
                    p=i;q=j;
                }
            }
        }
        dfs(p,q);
        cout<<res+1<<endl;
    }
    return 0;
}

6.八皇后问题
n皇后问题之前在蓝桥杯集训的时候有位学长给我们讲过,有一种听过了就再也忘不了的感觉。。。

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

const int N = 8;
bool h[N+1],z[2*N+1],y[2*N+1];
int r[N+1];
int res,n;
bool flag;

bool isvalid(int i,int j){
    if (h[j]||z[i-j+N]||y[i+j]) return false;
    return true;
}

void dfs(int step){
    if (flag) return ;
    if (step==N+1){
        res++;
        if (res==n){
            for (int i=1;i<=N;i++){
                cout<<r[i];
            }
            cout<<endl;
            flag=true;
        }
        return ; 
    }
    for (int j=1;j<=N;j++){
        if (isvalid(step,j)){
            h[j]=true;
            z[step-j+N]=true;
            y[j+step]=true;
            r[step]=j;
            dfs(step+1);
            h[j]=false;
            z[step-j+N]=false;
            y[j+step]=false;
        }
    }
}

int main(){
    int t;
    cin>>t;
    while (t--){
        cin>>n;
        res=0;flag=false;
        memset(h,false,sizeof(h));
        memset(z,false,sizeof(z));
        memset(y,false,sizeof(y));
        dfs(1);
    }
    return 0;
}

7.木棍问题
说一下我思路的转变?!
首先我们得知道可能的答案的范围,应该是木棒的最大长度max与木棒长度之和sum之间,我们需要对这之间的值进行枚举直到找到满足条件的即可。
对于某一个特定的长度p,q=sum/p即为根数,我们需要求出给定的木棒能不能组合成q根长度为p的木棒即可。
我一开始觉得只要像八皇后那样即可,看能不能找到q个和为p的木棒即可,后来我发现,这是不可行的,因为我们在回溯的时候只有在当前循环都不满足条件(或者都计算过了)才会返回至上一层,那么我们必然会使用之前已经有过的木棒了,这当然是不可行的,比如说1,2,3,4,8,8,8…,当我们的p为11,q为3时,我们到1,2,8时是满足s=p=11的,然后我们就来到了第二个8,也满足,然后来到了第三个8也满足,我们会发现这里的1,2,被我们使用了多次,这当然是不可行的。那我们肯定会想我们把当前使用到的都存起来,然后在满足s=p=11时对这些值进行标记,以后不能再使用了,如果还是用当前的这个方法当然是不可行的,因为不难发现即使我们对它们进行了标记也晚了。不仅要解决上面的问题,就算我们对它们进行了标记,我们标记的那些也可能再次失去标记,比如说我们只是求得了s=p的一个情况,剩下的也不一定可以满足条件,反之,我们可以再换一种组合方式才可以使剩下的满足条件,这需要我们再一次进行回溯,大概应该就是这么个意思吧!

1.以一个小棒为开头,用dfs看看能否把这个小棒拼凑成p长,如果可以,
  用f[i]记录下用过的小棒,然后继续以另外一个小棒为开头,以此类推。

2.小棒的长度从大到小排序。

3.如果当前最长的小棒不能拼成p长,那么就返回前一步,更改前一步的
  最长小棒的组合情况(这里不能是全部退出),不用再继续搜索下去了。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N = 64;
int n,x[N+1];
bool flag,f[N+1];

bool cmp(int a,int b){
    return a>b;
}

bool dfs(int total,int unused,int left,int len){
    if (unused==0&&left==0) return true;
    if (left==0) left=len;
    for (int i=0;i<total;i++){
        if (f[i]) continue;
        if (x[i]>left) continue;
        f[i]=true;
        if (dfs(total,unused-1,left-x[i],len)){
            return true;
        }
        f[i]=false;
        if (x[i]==left||left==len) break;
    }
    return false;
}

int main(){
    while (cin>>n){
        if (n==0) break;
        int su=0;
        for (int i=0;i<n;i++){
            cin>>x[i];
            su+=x[i];
        }
        sort(x,x+n,cmp);
        for (int i=x[0];i<=su;i++){
            memset(f,false,sizeof(f));
            if (su%i!=0) continue;
            if (dfs(n,n,0,i)){
                cout<<i<<endl;
                break;
            }
        }
    }
    return 0;
}

8.城堡
比较简单的一个题目,可以进行处理的是:它给我们的是1,2,4,8分别对应数二进制表示的第0,1,2,3位,所以我们在判断的时候只要作相应的位运算就可以判断那个方向有没有墙了。

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

const int N = 50;
int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};
int num[4]={2,8,1,4};
int x[N+1][N+1];
bool f[N+1][N+1];
int sum,m,n;

bool isvalid(int i,int j){
    if (i<1||j<1||i>m||j>n) return false;
    return true;
}

void dfs(int i,int j){
    for (int k=0;k<4;k++){
        int ii=i+dx[k],jj=j+dy[k];
        if (isvalid(ii,jj)&&(x[i][j]&num[k])==0&&!f[ii][jj]){
            f[ii][jj]=true;
            sum++;
            dfs(ii,jj);
        }
    }
}

int main(){
    while (cin>>m>>n){
        int res=0,num=0;
        memset(f,false,sizeof(f));
        for (int i=1;i<=m;i++){
            for (int j=1;j<=n;j++){
                cin>>x[i][j];
            }
        }
        for (int i=1;i<=m;i++){
            for (int j=1;j<=n;j++){
                if (!f[i][j]){
                    f[i][j]=true;
                    num+=1;
                    sum=1;
                    dfs(i,j);
                    res=max(sum,res);
                }
            }
        }
        cout<<num<<endl<<res<<endl;
    }
    return 0;
}

9.分解因数
如果说某个数i能够被n整除的话那么它就是n的一个因数,然后我们只要再找到n=n/i的因数就可以了,这是一个递归的想法,由于题目要求1 < a1 <= a2 <= a3 <= … <= an,那么我们得记住当前的ai,下一个ai+1必须满足大于等于ai即可,当然,递归结束的条件是n=1。

#include<iostream>
using namespace std;

int num;

void dfs(int m,int n){
    if (n<=0) return ;
    if (n==1){
        num++;
        return ;
    }
    for (int i=m;i<=n;i++){
        if (n%i==0){
            dfs(i,n/i);
        }
    }
}

int main(){
    int t,n;
    cin>>t;
    while (t--){
        num=0;
        cin>>n;
        dfs(2,n);
        cout<<num<<endl;
    }
    return 0;
}

10.迷宫
类似的题目,唯一需要注意的是:如果起点或者终点有一个不能通行(为#),则看成无法办到。

#include<iostream>
using namespace std;

const int N = 100;
char x[N+1][N+1];
int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};
int n,ha,la,hb,lb;
bool flag;

bool isvalid(int i,int j){
    if (i<0||j<0||i>=n||j>=n) return false;
    return true;
}

void dfs(int i,int j){
    if (flag) return ;
    if (i==hb&&j==lb){
        flag=true;
        return ;
    }
    for (int k=0;k<4;k++){
        int ii=i+dx[k],jj=j+dy[k];
        if (isvalid(ii,jj)&&x[ii][jj]=='.'){
            x[ii][jj]='#';
            dfs(ii,jj);
        }
    }
}

int main(){
    int t;
    cin>>t;
    while (t--){
        flag=false;
        cin>>n;
        for (int i=0;i<n;i++){
            for (int j=0;j<n;j++){
                cin>>x[i][j];
            }
        }
        cin>>ha>>la>>hb>>lb;
        if (x[ha][la]=='#'||x[hb][lb]=='#'){
            cout<<"NO"<<endl;
            continue;
        }
        dfs(ha,la);
        if (flag) cout<<"YES"<<endl;
        else cout<<"NO"<<endl;
    }
    return 0;
}

11.算24
说实话,我觉得这个题目蛮好的。递归真的是个神奇的东西,我们不妨假设x[i]和x[j]是参加运算的两个数,且我们假设i是小于j的,只要我们在进行x[i]-x[j]的时候同时也进行一次x[j]-x[i]就可以了,我们将每次x[i]和x[j]操作后的值放在x[i]里面,那么我们每次进行num-1的操作时,需要将num-1对应的值保存在x[j]里面,因为我们每次i是从0到num-1进行遍历的,如果不保存的话我们将要失去它。当然,我们还要进行回溯来恢复x[i]和x[j]的值。

#include<iostream>
#include<cmath>
#define eps 1e-5
using namespace std;

const int N = 4;
double x[N+1];

int dfs(int num){
    if (num==1){
        if (fabs(x[0]-24)<eps) return 1;
        return 0;
    }
    for (int i=0;i<num-1;i++){
        for (int j=i+1;j<num;j++){
            double p=x[i],q=x[j];
            x[j]=x[num-1];
            x[i]=p+q;
            if (dfs(num-1)) return 1;
            x[i]=p-q;
            if (dfs(num-1)) return 1;
            x[i]=q-p;
            if (dfs(num-1)) return 1;
            x[i]=p/q;
            if (dfs(num-1)) return 1;
            x[i]=q/p;
            if (dfs(num-1)) return 1;
            x[i]=p*q;
            if (dfs(num-1)) return 1;
            x[i]=p;
            x[j]=q;
        }
    }
    return 0;
}

int main(){
    while (cin>>x[0]>>x[1]>>x[2]>>x[3]){
        if (x[0]==0&&x[1]==0&&x[2]==0&&x[3]==0) break;
        if (dfs(4)){
            cout<<"YES"<<endl;
        }else{
            cout<<"NO"<<endl;
        }
    }
    return 0;
}

12.棋盘问题
先说最后一个棋盘问题,感觉这个像是八皇后题目的变形,其实思想都是差不多的,唯一需要我们注意的是,不是所有的地方都任由我们放置棋子了,当然如果我们用列的视角来看这个题目的话,换言之就会存在某一列的任意位置都不能放置棋子的问题了,所以说如果我们输入的k大于n的话是一定没有满足题意的解的,这时我们不能只用一个参数step来标记当前列了,我们需要两个参数,一个是当前列,另一个是当前放置的棋子的个数,如果说在l列有满足条件的,那么我们就step+1,l+1,但是如果说某一列不存在满足条件的,那么我们就step不变,再去下一列寻找即可。

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

const int N = 8;
char x[N+1][N+1];
bool h[N+1],flag;
int n,k,sum,p;

void dfs(int step,int l){
    if (step==k){
        sum++;
        return ;
    }
    if (l>n) return ;
    for (int i=1;i<=n;i++){
        if (!h[i]&&x[i][l]!='.'){
            h[i]=true;
            dfs(step+1,l+1);
            h[i]=false;
        }
    }
    dfs(step,l+1);
}

int main(){
    while (cin>>n>>k){
        if (n==-1&&k==-1){
            break;
        }
        memset(h,false,sizeof(h));
        for (int i=1;i<=n;i++){
            for (int j=1;j<=n;j++){
                cin>>x[i][j];
            }
        }
        if (k>n){
            cout<<0<<endl;
            continue;
        }
        sum=0;
        dfs(0,1);
        cout<<sum<<endl;
    }
    return 0;
}

13.小游戏
在TLE了n多次之后还是没有AC…
我觉得思路还是比较明确的,有三个值得我们注意的地方:

1)输入里面是有空格的,而scanf在遇到空格时会结束输入。
(2)我们求的次数等同于需要拐弯的次数,而不是经过的空白区域的个数,
   所以我们需要两个数来标记当前是从哪一个方向的格子过来的,但是
   我们经过观察可以发现,只要用一个数(0~3)就能表示了。
(3)这个题目横纵坐标可能和我们平时的不太一样(我是这么觉得的)。

虽然还是TLE,还是要附上代码的。

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

const int N = 80;
char x[N+1][N+1];
bool f[N+1][N+1];
int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};
int w,h,x1,y1,x2,y2,res;

void dfs(int i,int j,int sum,int pp){
    if (sum>=res) return ;
    if (i==y2&&j==x2){
        if (sum<res) res=sum;
        return ;
    }
    for (int k=0;k<4;k++){
        int ii=i+dx[k],jj=j+dy[k];
        if (ii>=0&&jj>=0&&ii<=h+1&&jj<=w+1&&!f[ii][jj]&&x[ii][jj]!='X'){
            f[ii][jj]=true;
            if (pp==k){
                dfs(ii,jj,sum,k);
            }else{
                dfs(ii,jj,sum+1,k);
            }
            f[ii][jj]=false;
        }
    }
}

int main(){
    int n1=1;
    while (scanf ("%d%d",&w,&h)!=EOF){
        if (!w&&!h) break;
        for (int i=1;i<=h;i++){
            getchar();
            for (int j=1;j<=w;j++){
                x[i][j]=getchar();
            }
        }
        printf ("Board #%d:\n",n1++);
        int n2=1;
        while (scanf ("%d%d%d%d",&x1,&y1,&x2,&y2)!=EOF){
            if (!x1&&!y1&&!x2&&!y2) break;
            memset(f,false,sizeof(f));
            if (x[y1][x1]==' '||x[y2][x2]==' '){
                printf ("Pair %d: impossible.\n",n2++);
                continue;
            }
            res=10000;
            x[y2][x2]=' ';
            dfs(y1,x1,0,-1);
            x[y2][x2]='X';
            if (res==10000){
                printf ("Pair %d: impossible.\n",n2++);
            }else{
                printf ("Pair %d: %d segments.\n",n2++,res);
            }
        }
        printf ("\n");
    }
    return 0;
}

14.碎纸机
大体的意思就是让你把一串数字随便分开来,给出能够组成小于给定数字的最大的组合方式。
递归的回溯真是一个神奇的东西,首先我们要把给的那个数拆分开来。如果每一位上的数字之和都大于给定的m的话,那么是无解的;如果说对于某一个已经求得的满足条件的最大小于m的和,存在与他相等的,就把flag=true,也就是说这是是大于一个满足条件的,如果再存在比它大的满足条件的,就把flag再置为false,因为我们的值已经被更新过了。
我们得用一个数标记我们来到了第几个数,当来到最后一个数的时候我们就要进行判断了。

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

const int N = 6;
int x[N+1],r[N+1],p[N+1];
int m,n,num,res_long,res_sum;
bool flag;

bool cut(int a){
    int sum=0;num=1;
    while (a){
        x[num++]=a%10;
        sum+=a%10;
        a/=10;
    }
    if (sum>m) return false;
    return true;
}

void dfs(int step,int sum_now,int k){
    if (sum_now>m) return ;
    if (step==0){
        if (sum_now>res_sum){
            memcpy(r,p,sizeof(p));
            res_long=k-1;
            res_sum=sum_now;
            flag=false;
            return;
        }
        if (sum_now==res_sum){
            flag=true;
            return ;
        }
        return ;
    }
    int tmp=0;
    for (int i=step;i>=1;i--){
        tmp=10*tmp+x[i];
        p[k]=tmp;
        dfs(i-1,sum_now+tmp,k+1);
    }
}

int main(){
    while (cin>>m>>n){
        if (m==0&&n==0) break;
        if (m==n){
            cout<<m<<" "<<m<<endl;
            continue;
        }
        if (!cut(n)){
            cout<<"error"<<endl;
            continue;
        }
        flag=false;
        res_long=0;
        res_sum=0;
        dfs(num-1,0,1);
        if (flag){
            cout<<"rejected"<<endl;
        }else{
            cout<<res_sum<<" ";
            for (int i=1;i<res_long;i++){
                cout<<r[i]<<" ";
            }
            cout<<r[res_long]<<endl;
        }
    }
    return 0;
}

棋盘分割和文件结构”图” 这两道题暂时还没有解决…

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值