蓝桥杯赛前冲刺补习第二课——《递归及其构造技巧》

第一题

小明刚刚看完电影《第39级台阶》。离开电影院的时候,他数了数礼堂前的台阶数,恰好是39级!
站在台阶前,他突然又想着一个问题:如果我每一步只能迈上1个或2个台阶。
先迈左脚,然后左右交替,最后一步是迈右脚,也就是说一共要走偶数步。
那么,上完39级台阶,有多少种不同的上法呢?请你利用计算机的优势,帮助小明寻找答案。


解法:
  第一个递归练习题,我们总结一下题目的一些重要因素,39级台阶,一步可走1阶或2阶,一共要走偶数步
  这里要注意一下,正式的题目中很可能说完左右脚之后并不会给出偶数步的提示,而是需要自己去发掘题目的隐含条件,这种思考方式是非常重要的,要习惯于抽象问题,寻找隐藏条件,以便于优化问题逻辑,加快解题速度。

static int count = 0;
/**
 * @param times: 迈脚的次数
 * @param steps:台阶数
 */
public static void oneOrTwoSteps(int times,int steps) {
    /*
    oneOrTwoSteps(0, 0);
    System.out.println(count);
     */
    if(steps > 39){//如果走过的步数超过39则返回
        return;
    }

    if(steps == 39){//如果恰好为39阶
        /*
            且走的步数为偶数步
            视为此次行走方案符合要求
         */
        if(times%2 == 0){
            count++;
        }
        return;
    }

    //除去递归出口情况,特殊情况,递归的普通步骤
    oneOrTwoSteps(times+1, steps+1);//一步迈1个台阶
    oneOrTwoSteps(times+1, steps+2);//一步迈2个台阶
}

第二题

X星球特别讲究秩序,所有道路都是单行线。
一个甲壳虫车队,共16辆车,按照编号先后发车,夹在其它车流中,缓缓前行。
路边有个死胡同,只能容一辆车通过,是临时的检查站,如图所示。
图为一个丁字路口。
X星球太死板,要求每辆路过的车必须进入检查站,也可能不检查就放行,也可能仔细检查。
如果车辆进入检查站和离开的次序可以任意交错。那么,该车队再次上路后,可能的次序有多少种?
为了方便起见,假设检查站可容纳任意数量的汽车。
显然,如果车队只有1辆车,可能次序1种;2辆车可能次序2种;3辆车可能次序5种。


解法:
  车必须进检查站,死胡同先进后出的情况可以类比为一个栈,在单个过程中会出现两种情况:
  1.等待检查的车进1
  2.正在检查的车出1
  在状态总结完之后,我们开始考虑递归出口以及特殊情况:
  1.当等待检查的车辆数量为0时,即此刻将不会有进栈的操作,只能一一出栈,只有1中排列情况
  2.当正在检查的车辆数量为0时,即此刻将不会有出栈的操作,只能进栈

/**
 * 有三种情况:
 * 1.wait>0, checking<16 可进可出
 * 2.wait==0 剩下的车都进去了,return 1
 * 3.checking==0 没有车进去,无法出栈,只能进栈
 *
 * @param wait 等待检查的车辆数量
 * @param checking 正在检查的车辆数量
 * @return
 */
public static int starXCarCheck(int wait, int checking) {
    if (wait == 0) return 1;
    if (checking == 0) {//无法出栈,只能进栈
        return starXCarCheck(wait - 1, checking + 1);
    }
    return starXCarCheck(wait - 1, checking + 1) + starXCarCheck(wait, checking - 1);
}

第三题

匪警请拨110,即使手机欠费也可拨通!
为了保障社会秩序,保护人民群众生命财产安全,警察叔叔需要与罪犯斗智斗勇,因而需要经常性地进行体力训练和智力训练!
某批警察叔叔正在进行智力训练:
1 2 3 4 5 6 7 8 9 = 110
请看上边的算式,为了使等式成立,需要在数字间填入加号或者减号(可以不填,但不能填入其它符号)。
之间没有填入符号的数字组合成一个数,例如:12+34+56+7-8+9 就是一种合格的填法;123+4+5+67-89 是另一个可能的答案。
请你利用计算机的优势,帮助警察叔叔快速找到所有答案。
每个答案占一行。形如:
12+34+56+7-8+9
123+4+5+67-89
……


解法:
  可以这么想,在每个数字之间都需要进行一次选择填充,“+”、“-”或者“”(即什么都不填),这样就可以通过递归得到所有可能的表达式:
  1+2+3+4+5+6+7+8+9,
  1+2+3+4+5+6+7+8-9,
  1+2+3+4+5+6+7+89
  ……
  然后再对得出的表达式进行计算判断结果是否等于110,这样就能得到所有满足条件的表达式了。


  再按照笔者思考的过程来说一下这道题递归的具体思想吧:
  初始输入:”123456789”, “”, 9
  笔者一开始想的是每次递归将输入字符串截取为两个部分,比如第一次就是截取成”1”和”23456789”,此时”23456789”作为输入字符串传入下一次递归状态,选择一种填充符号和”1”一起拼接进结果字符串。
  此时就有问题了,因为这样结果字符串会变成符号在前:”+1”
  这时我选择将初始输入改为”123456789”, “0”, 9
  但是这样还是有问题,因为会出现:”0-1”
  以最后我将初始输入改为了:”23456789”, “1”, 8

这里以全填”+”号做例子展示递归过程:
“23456789”, “1”, 8
“3456789”, “1+2”, 7
“456789”, “1+2+3”, 6
“56789”, “1+2+3+4”, 5
“6789”, “1+2+3+4+5”, 4
“789”, “1+2+3+4+5+6”, 3
“89”, “1+2+3+4+5+6+7”, 2
“9”, “1+2+3+4+5+6+7+8”, 1
“9”, “1+2+3+4+5+6+7+8+9”, 0

将position==0作为递归出口,此时得到了此次DFS的最终表达式,开始计算结果

/**
 * 递归出口:position==0
 * 普通过程:在当前位置填入"+","——",""
 *
 * @param s 
 * @param r 
 * @param position 
 * @return
 */
public static int use1To10AndAddOrMinusToGet110(String s, String r, int position) {
    if (position == 0) {
        /*
            开始计算
         */
        int result = calc(r);
        if (result == 110) {
            System.out.println(r);
            return 1;
        } else {
            return 0;
        }
    }

    return use1To10AndAddOrMinusToGet110(s.substring(1, s.length()), r + "+" + s.substring(0, 1), position - 1) +
            use1To10AndAddOrMinusToGet110(s.substring(1, s.length()), r + "-" + s.substring(0, 1), position - 1) +
            use1To10AndAddOrMinusToGet110(s.substring(1, s.length()), r + s.substring(0, 1), position - 1);
}
/**
 * 计算中序加减表达式
 *
 * @param r
 * @return
 */
private static int calc(String r) {
    Stack<Integer> numStack    = new Stack<>();
    Stack<String>  symbolStack = new Stack<>();

    String[] a = r.split("\\+");
    for (int i = a.length - 1; i >= 0; i--) {
        String[] b = a[i].split("-");
        for (int j = b.length - 1; j >= 0; j--) {
            numStack.push(Integer.valueOf(b[j]));
        }
    }
    char[] ch = r.toCharArray();
    for (int i = ch.length - 1; i >= 0; i--) {
        if (ch[i] == '+' || ch[i] == '-') {
            symbolStack.push(String.valueOf(ch[i]));
        }
    }

    while (numStack.size() > 1) {
        int result = 0;
        int q      = numStack.pop();
        int p      = numStack.pop();

        String s = symbolStack.pop();
        switch (s) {
            case "+":
                result += q + p;

                break;
            case "-":
                result += q - p;

                break;
        }
        numStack.push(result);
    }

    return numStack.pop();
}

  这里再多讲一下老师在课堂上给出的算法:
  首先说一下笔者通过一次debug得出的结论,该算法的表达式的核心思想和是上述笔者自己的方法的思想完全不同的。
  笔者的方法是从1开始限制之后的表达式排序,而老师给出的这个方法是从+9开始向前做subProblem分解,最顶层的subProblem是+9,-9和“9和前面的字串拼接”,然后再分别是+8,-8,“8和前面的字串拼接”……等等

这里给出一段so表达式的例子:
+2+3+4+5+6+7+8+9 a[0] = 1
-2+3+4+5+6+7+8+9 a[0] = 1
+3+4+5+6+7+8+9 a[0] = 12 至此+3的subProblem已解完
+2-3+4+5+6+7+8+9 a[0] = 1
-2-3+4+5+6+7+8+9 a[0] = 1
-3+4+5+6+7+8+9 a[0] = 12 至此-3的subProblem已解完
+23+4+5+6+7+8+9 a[0] = 1
-23+4+5+6+7+8+9 a[0] = 1
+4+5+6+7+8+9 a[0] = 123 至此“3和前面的字串拼接”的subProblem已解完

更为巧妙的是,在每次向下传递so表达式串时,goal都将当前的加减决定已经完成
比如在做+9决定时,110已经减了9,向下传递的参数变成了101
这样走到最后只需要将a[0]与goal进行比较,则可以得出当前so表达式是否正确的结论

/**
 * @param a 数字数组
 * @param k 当前考虑的位置
 * @param so 当前的表达式
 * @param goal 当前的目标结果
 *
 */
public static void use1To10AndAddOrMinusToGet110Teachar(int[] a, int k, String so, int goal) {
        /*main方法部分
            int[] a = {1,2,3,4,5,6,7,8,9};
            use1To10AndAddOrMinusToGet110Teachar(a,8,"",110);
         */
        if (k == 0) {
            if (a[0] == goal) System.out.println(a[0] + so);
            return;
        }

        use1To10AndAddOrMinusToGet110Teachar(a, k - 1, "+" + a[k] + so, goal - a[k]);
        use1To10AndAddOrMinusToGet110Teachar(a, k - 1, "-" + a[k] + so, goal + a[k]);

        int old = a[k - 1];
        a[k - 1] = Integer.valueOf("" + a[k - 1] + a[k]);
        use1To10AndAddOrMinusToGet110Teachar(a, k - 1, so, goal);
        a[k - 1] = old;
    }

第四题

公园票价为5角。假设每位游客只持有两种币值的货币:5角、1元。
再假设持有5角的有m人,持有1元的有n人。
由于特殊情况,开始的时候,售票员没有零钱可找。
我们想知道这m+n名游客以什么样的顺序购票则可以顺利完成购票过程。
显然,m < n的时候,无论如何都不能完成;
m>=n的时候,有些情况也不行。比如,第一个购票的乘客就持有1元。
请计算出这m+n名游客所有可能顺利完成购票的不同情况的组合数目。
注意:只关心5角和1元交替出现的次序的不同排列,持有同样币值的两名游客交换位置并不算做一种新的情况来计数。


这里笔者想引入另一道题来做同类分析,这是笔者在做蓝桥杯大赛练习系统中的练习题时遇见的题目:《未名湖畔的烦恼》
每年冬天,北大未名湖上都是滑冰的好地方。
北大体育组准备了许多冰鞋,可是人太多了,每天下午收工后,常常一双冰鞋都不剩。
每天早上,租鞋窗口都会排起长龙,假设有还鞋的m个,有需要租鞋的n个。
现在的问题是,这些人有多少种排法,可以避免出现体育组没有冰鞋可租的尴尬场面。
(两个同样需求的人(比如都是租鞋或都是还鞋)交换位置是同一种排法)


解法:
   可以发现,这两道题目描述的问题抽象出来都是一样的,将输入类型分为两种,在做排列时,A类必须比B类人数更多,且第一个必须是A类。然后再做排列组合,而在算法方面,可以把这个过程看作是对二叉树的一次DFS。
  这里需要提到一点,笔者刚开始思考这类问题的递归方法时,认为每一次递归是从队首开始一个一个人地确定下去,但实则不然。如果是基于迭代的解法,确实是一个一个确定当前位置排的是那类人;但对于递归来说则不同,笔者这里谈谈自己的理解,如果有错误还请一定及时批评!递归是从大问题逐步化小到小问题,然后小问题求解之后得出结果再一步一步得出大问题的结果。
  笔者认为这道题的顺序是一步一步决定队尾的人是哪类人,然后再一步一步缩小排队范围,逐渐接近决策点(队头),意思就是,上层做问题规划,底层做问题决策,这是递归决策的核心思想!!!!!!!!

/**
 * @param m 五角人数
 * @param n 一元人数
 * @param memo Memoization默记法存储单元
 *
 * @return
 */
public static int parkGiveChangeProblem(int m, int n, int[][] memo) {
    /*这部分是main方法
    Scanner scanner = new Scanner(new BufferedInputStream(System.in));

    int     m    = 2;
    int     n    = 2;
    int[][] memo = new int[m + 1][n + 1];

    for (int i = 0; i < memo.length; i++) {
        Arrays.fill(memo[i], -1);
    }

    System.out.println(parkGiveChangeProblem(m, n, memo));
     */

    if (m < n) return 0;//无法继续完成排队
    /*
        当可能完成的时候,设立递归出口
        当只剩一种人的时候,只有1种排法
     */
    if (n == 0) return 1;


    if (memo[m][n] == -1) {
        int a = parkGiveChangeProblem(m - 1, n, memo);
        int b = parkGiveChangeProblem(m, n - 1, memo);
        memo[m][n] = a + b;
    }

    return memo[m][n];
}
    上述解法运用到一个思想,就是笔者最近接触的Dynamic Programming动态规划(DP)思想中的Memoization默记法。
    我们在C语言课堂上第一次学习递归的时候,一般都会学习Fibonacci数列,但我们发现一个怪现象
    ——当求解到fib(9)或者是fib(12)时,我们就可以观察到从运行开始到Print出结果已经有了明显的延迟
    这是为什么呢?

    这里可以举一个栗子,我们来求解一下fib(5):
        fib(5) = fib(4) + fib(3);
        fib(5) = fib(3) + fib(2) + fib(2) + fib(1);
        fib(5) = fib(2)  + fib(1) + fib(2) + fib(2) + fib(1);
    可以发现,做了很多重复操作,对不对?

    我们可以利用一个数组,将每次计算出来的结果保留下来,并在下次计算需要时直接提取出来,将可以节省大部分的时间。
    比方说我在计算f(3)的时候把fib(3)保存下来,在计算fib(4),fib(5)时遇到fib(3)就可以直接取出而不用再向下计算fib(3)=fib(2) + fib(1);

第五题

小明参加了学校的趣味运动会,其中的一个项目是:跳格子。
地上画着一些格子,每个格子里写一个字,如下所示:(也可参见下图)

从我做起振
我做起振兴
做起振兴中
起振兴中华

比赛时,先站在左上角的写着“从”字的格子里,可以横向或纵向跳到相邻的格子里,但不能跳到对角的格子或其它位置。
一直要跳到“华”字结束。
要求跳过的路线刚好构成“从我做起振兴中华”这句话。
请你帮助小明算一算他一共有多少种可能的跳跃路线呢?


解法:
  其实过程和字根本没关系,就是从左上角到右下角不管正向逆向能走到的路径就满足条件

public static int rejuvenateChinaStartingFromMe(int row, int column, int[][] memo) {
    /*这部分是main方法
    int row    = 4;
    int column = 5;

    int[][] memo = new int[row + 1][column + 1];

    for (int i = 0; i < memo.length; i++) {
        Arrays.fill(memo[i], -1);
    }

    System.out.println(rejuvenateChinaStartingFromMe(row, column, memo));
     */
    if (row == 1 && column == 1) return 1;//当走到左上角时,递归出口

    if (row == 1) {//如果已经走到最左边,则只能向上走
        if (memo[row][column] == -1) {
            memo[row][column] = rejuvenateChinaStartingFromMe(row, column - 1, memo);
        }
        return memo[row][column];
    }
    if (column == 1) {//如果已经走到最上面,则只能向左走
        if (memo[row][column] == -1) {
            memo[row][column] = rejuvenateChinaStartingFromMe(row - 1, column, memo);
        }
        return memo[row][column];
    }

    if (memo[row][column] == -1) {
        //常规操作,当前状态的结果等于向左走的结果+向上走的结果
        memo[row][column] = rejuvenateChinaStartingFromMe(row - 1, column, memo) + rejuvenateChinaStartingFromMe(row, column - 1, memo);
    }
    return memo[row][column];
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值