【算法学习笔记】暴力递归

暴力递归

暴力递归就是尝试:

  1. 把问题转化为规模缩小了的同类问题的子问题(递归思路)
  2. 有明确的不需要继续进行递归的条件(base case——基线条件)
  3. 有当得到了子问题的结果之后的决策过程
  4. 不记录每一个子问题的解

尝试的方法和起点是关键。

经典例题

汉诺塔问题

题目说明
有三根竖直的滑杆(左、中、右),有若干个大小不一的中空圆盘,初始时所有圆盘按上小下大的顺序放在左滑杆上。现在要把所有圆盘移动到右滑杆上,且最终保持上小下大的顺序,请输出操作顺序。
递归思路
假设某个中间状态有n个圆盘需要移动,初始滑杆为 from ,目标滑杆为 to ,剩下的那个滑杆为 other ,那么我们需要做的就是:把 1 ~ n-1 的圆盘从 from 移到 other (递归),然后把第n个圆盘从 from 移到 to ,最后把 1 ~ n-1 的圆盘从other 移到 to (递归)最终即可把所有圆盘按要求摆放好。
代码实现

public static void hanota(int n, String from, String other, String to) {
        if (n == 1) {
            System.out.println("Move plane " + n + " from " + from + " to " + to);
            return;
        }
        hanota(n-1, from, to, other);
        System.out.println("Move plane " + n + " from " + from + " to " + to);
        hanota(n-1, other, from, to);
    }

打印字符串的子序列

题目说明
打印一个字符串的全部子序列,包括空字符串。(子序列无需在父串中连续)
递归思路
对字符串中的每一个字符,都有保留和不保留两种选择,实际上变成了一颗递归二叉树。
代码实现

public static void printAllSubsquence(String str) {
    char[] chs = str.toCharArray();
    process(chs, 0);
}

public static void process(char[] chs, int i) {
    if (i == chs.length) {
        // 选完一个子串,打印
        System.out.println(String.valueOf(chs));
        return;
    }
    // 保留当前字符,到下一个字符
    process(chs, i + 1);
    char tmp = chs[i];
    // ascii码0代表空字符
    chs[i] = 0;
    // 不保留当前字符,到下一个字符
    process(chs, i + 1);
    // 还原
    chs[i] = tmp;
}

打印字符串的全部排列

题目说明
打印一个字符串中字符的全部排列,且不出现重复的排列。(字符串中均为小写字母)
递归思路
对于字符串中的每一个位置,都尝试下还未尝试过的字符(分支限界,不合要求的分支直接剪掉),最终得到的结果就是去重的所有排列。
代码实现

public static List<String> printAllStr(String str) {
    ArrayList<String> res = new ArrayList<>();
    if (str == null || str.length() == 0) {
        return res;
    }
    char[] chs = str.toCharArray();
    process(chs, 0, res);
    return res
}

public static void process(char[] chs, int i, ArrayList<String> res) {
    if (i == chs.length) {
        // 得到一种排列
        res.add(String.valueOf(chs));
        return;
    }
    // 去重,字符串仅小写字母时可以用该方法
    boolean[] vis = new boolean[26];
    for (int j = i; j < chs.length; ++j) {
        if (!vis[str[j] - 'a']) {
            // 把当前字符转为下标,只有当前字符未试过时才进行下一步
            // 例如 ...c... 已经试过了,那就不再尝试
            vis[str[j] - 'a'] = true;
            swap(chs, i, j);
            // i 位置尝试之后的所有字符
            process(chs, i + 1, res);
            swap(chs, i, j);
        }
        
    }
}

public static void swap(char[] chs, int i, int j) {
    char tmp = chs[i];
    chs[i] = chs[j];
    chs[j] = tmp;
}

卡牌游戏

题目说明
给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张指派,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家B都绝顶聪明。请返回最后获胜者的分数。
【举例】
arr=[1,2,100,4]
开始时,玩家A只能拿走1或4。如果开始时玩家A拿走1,则排列变为[2,100,4],接下来玩家B可以拿走2或4,然后继续轮到玩家A…
显然,玩家A会拿1,接下来不管B拿2还是4,A都可以把100拿到,所以A获胜,总分是101。
递归思路
把拿第一张和最后一张的情况分别递归获得分数,取最大值。
代码实现

public static int win(int[] arr) {
    if (arr == null || arr.length == 0) {
        return 0;
    }
    // 先手和后手中较大值(胜者)
    return Math.max(f(arr, 0, arr.length - 1), s(arr, 0, arr.length - 1));
}

// 先手函数
public static int f(int[] arr, int i, int j) {
    if (i == j) {
        // 只剩一张牌可抽,直接返回
        return arr[i];
    }
    // 自己抽第一张牌的结果和抽最后一张牌的结果取最大值
    return Math.max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1));
}

// 后手函数
public static int s(int[] arr, int i, int j) {
    if (i == j) {
        // 只剩一张牌可抽(被对方抽走),直接返回0
        return 0;
    }
    // 对方抽第一张牌的结果和抽最后一张牌的结果取最小值(对方一定选对我不利的结果)
    return Math.min(f(arr, i + 1, j), f(arr, i, j - 1));
}

逆序栈

题目说明
给你一个栈,请你逆序这个栈,不能申请额外的数据结构,只能使用递归函数。
递归思路
每次取栈底元素,用递归内的空间存储,取完后依次下压,即可逆序。关键是取栈底函数的实现。
代码实现

// 用递归逆序栈
public static void reverse(Stack<Integer> stack) {
    if (stack.isEmpty()) {
        return;
    }
    // 取栈底
    int i = f(stack);
    // 翻转剩余的栈
    reverse(stack);
    // 栈底变栈顶
    stack.push(i);
}

// 取栈底元素的函数
public static int f(Stack<Integer> stack) {
    int result = stack.pop();
    if (stack.isEmpty()) {
        // 栈为空说明当前即是栈底元素,返回
        return result;
    } else {
        // 不是则继续递归获取栈底元素
        int last = f(stack);
        // 将之前弹出的元素压回
        stack.push(result);
        // 返回栈底元素
        return last;
    }
}

数字字符串转化

题目说明
规定 1 和 A 对应、2 和 B 对应、 3 和 C 对应…
那么一个数字字符串比如 “111”,就可以转化为 “AAA”、“KA”、“AK”。
给定一个数字字符串str,返回有多少种转化结果。
递归思路
还是老思路,从左往右尝试可能的转化。假设当前来到位置i,注意几种情况:i是0,则当前尝试作废(没有可供转化的字母),返回0;i是1,有单独使用和与后一位数字一起使用两种尝试;i是2,如果后一位数字不大于6,也有两种,大于6则只有一种尝试;i是3-9,只有单独使用一种尝试。
代码实现

// 转化数字字符串
public static int revertNumStr(String str) {
    if (str == null || str.length() == 0) {
        return 0;
    }
    char[] chs = str.toCharArray();
    return process(chs, 0);
}

public static int process(char[] chs, int i) {
    if (i == str.length) {
        // 尝试出一种转化,返回
        return 1;
    }
    if (chs[i] == '0') {
        // 无效尝试,返回0
        return 0;
    }
    if (chs[i] == '1') {
        // 单独使用
        int res = process(chs, i + 1);
        if (i + 1 < chs.length) {
            // 联合后一位使用
            res += process(chs, i + 2);
        }
        return res;
    }
    if (chs[i] == '2') {
        // 单独使用
        int res = process(chs, i + 1);
        if (i + 1 < chs.length && chs[i + 1] >= '0' && chs[i + 1] <= '6') {
            // 联合后一位使用
            res += process(chs, i + 2);
        }
        return res;
    }
    // 3-9 直接单独使用
    return process(chs, i + 1);
}

拿取最大总价值的物品

题目说明
给定两个长度都为N的数组weights和values,weights[i]和values[i]分别代表i号物品的重量和价值。给定一个正整数bag,表示一个载重bag的袋子,你装入袋中的物品不能超过这个重量。返回你能装下的物品的最大价值是多少?
递归思路
依然是从左往右尝试,每个货物都尝试要和不要,即遍历所有方式,得到最大结果。
代码实现

// 拿取最大总价值的物品
public static int maxValue(int[] weights, int[] values, int bag) {
    if (weights.length == 0 || bag <= 0) {
        return 0;
    }
    return process(weights, values, bag, 0, 0);
}

public static int process(int[] weights, int[] values, int bag, int i, int alreadyweight) {
    if (alreadyweight > bag || i == weights.length) {
        // 已经超重或已经选完
        return 0;
    }
    // 当前拿和不拿两种情况取最大值
    return Math.max(process(weights, values, bag, i + 1, alreadyweight), values[i] + process(weights, values, bag, i + 1, alreadyweight + weights[i]));
}

N皇后问题

见另一篇文章:N皇后问题

总结

暴力递归的关键点在于尝试的思路,思路对了,尝试自然就能成功。而且优秀的尝试,或者说递归思路是之后用动态规划优化递归的前置条件,递归思路越优秀——可变参数形式越简单、数量越少,越容易用动态规划来优化。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值