暴力递归到动态规划

本文介绍了如何使用动态规划和记忆搜索来解决一系列组合问题,包括机器人达到指定位置的方法数、换钱的最少货币数、纸牌博弈、象棋中马的跳法以及可重复使用面值凑目标钱数。通过逐步优化递归过程,转化为记忆搜索和严格表结构的动态规划,减少重复计算,提高算法效率。示例代码详细展示了各种问题的解决方案和优化过程。
摘要由CSDN通过智能技术生成

动态规划就是暴力尝试减少重复计算的技巧

1)找到什么可变参数可以代表一个递归状态,也就是哪些参数一旦确定,返回值就确定了 
2)把可变参数的所有组合映射成一张表,有 1 个可变参数就是一维表,2 个可变参数就 是二维表,......
3)最终答案要的是表中的哪个位置,在表中标出 
4)根据递归过程的 base case,把这张表的最简单、不需要依赖其他位置的那些位置填好 值
5)根据递归过程非base case的部分,也就是分析表中的普遍位置需要怎么计算得到,那 么这张表的填写顺序也就确定了
6)填好表,返回最终答案在表中位置的值

暴力递归尝试(从左至右\范围)
->
转化成记忆搜索
->
转化成严格表结构的DP
->
观察表结构进行优化

在这里插入图片描述

题目一:机器人达到指定位置方法数

题目:
假设有排成一行的 N 个位置,记为 1~N,N 一定大于或等于 2
开始时机器人在其中的 M 位 置上(M 一定是 1~N 中的一个),机器人可以往左走或者往右走
如果机器人来到 1 位置, 那 么下一步只能往右来到 2 位置;如果机器人来到 N 位置,那么下一步只能往左来到 N-1 位置。
规定机器人必须走 K 步,最终能来到 P 位置(P 也一定是 1~N 中的一个)的方法有多少种
给 定四个参数 N、M、K、P,返回方法数
=============================================================================================
举例:
N=5,M=2,K=3,P=3 
上面的参数代表所有位置为 1 2 3 4 5。机器人最开始在 2 位置上,必须经过 3 步,最后到 达 3 位置。
走的方法只有如下 3 种: 
1)从2到1,从1到2,从2到3
2)从2到3,从3到2,从2到3 
3)从2到3,从3到4,从4到3 所以返回方法数 3。
N=3,M=1,K=3,P=3 
上面的参数代表所有位置为 1 2 3。
机器人最开始在 1 位置上,必须经过 3 步,最后到达 3 位置。怎么走也不可能,所以返回方法数 0。
package class08;

/**
 * @title: RobotWalk
 * @Descriptor: 总共N个位置,从M点出发,还剩K步,返回最终能到达P的方法数
 * @Author DD
 * @Date: 2022/6/8 11:01
 * @Version 1.0
 */

public class RobotWalk {

    public static int way1(int N, int M, int K, int P) {
        if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
            return 0;
        }
        return walk(N, M, K, P);
    }

    public static int walk(int N, int cur, int rest, int P) {
        if (rest == 0) {
            return cur == P ? 1 : 0;
        }
        //  rest > 0 还可以继续走
        if (cur == 1) {
            return walk(N, cur + 1, rest - 1, P);
        }
        if (cur == N) {
            return walk(N, cur - 1, rest - 1, P);
        }
        return walk(N, cur + 1, rest - 1, P) + walk(N, cur - 1, rest - 1, P);
    }

    public static int way2(int N, int M, int K, int P) {
        //  记忆搜索

        if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
            return 0;
        }
        int[][] dp = new int[K + 1][N + 1];
        for (int i = 0; i <= K; i++) {
            for (int j = 0; j <= N; j++) {
                dp[i][j] = -1;
            }
        }
        return walk2(N, M, K, P, dp);
    }

    public static int walk2(int N, int cur, int rest, int P, int[][] dp) {

        if (dp[rest][cur] != -1) {//  当前状态之前已经计算过
            return dp[rest][cur];
        }
        //  之前没计算过该状态
        if (rest == 0) {
            dp[rest][cur] = cur == P ? 1 : 0;
            return dp[rest][cur];
        }
        //  rest > 0 还可以继续走
        if (cur == 1) {
            dp[rest][cur] = walk(N, cur + 1, rest - 1, P);
            return dp[rest][cur];
        } else if (cur == N) {
            dp[rest][cur] = walk(N, cur - 1, rest - 1, P);
            return dp[rest][cur];
        } else {
            dp[rest][cur] = walk(N, cur + 1, rest - 1, P) + walk(N, cur - 1, rest - 1, P);
        }
        return dp[rest][cur];
    }

    public static int way3(int N, int M, int K, int P) {
        //  严格表结构的动态规划
        if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
            return 0;
        }
        int[][] dp = new int[K + 1][N + 1];
        return walk3(N,M,K,P,dp);
    }

    public static int walk3(int N,int cur,int rest,int P,int[][] dp){

        // 参数无效直接返回0
        if (N < 2 || rest < 1 || cur < 1 || cur> N || P < 1 || P > N) {
            return 0;
        }
        dp[0][P] = 1;
        for (int i = 1; i <= rest; i++) {
            for (int j = 1; j <= N; j++) {
                if (j == 1) {
                    dp[i][j] = dp[i - 1][2];
                } else if (j == N) {
                    dp[i][j] = dp[i - 1][N - 1];
                } else {
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j + 1];
                }
            }
        }
        return dp[rest][cur];
    }

    public static int way4(int N, int M, int K, int P) {
        //  严格表结构的动态规划 -- 省空间版
        if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
            return 0;
        }
        return walk4(N, M, K, P);
    }

    public static int walk4(int N, int M, int K, int P) {

        // 参数无效直接返回0
        if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
            return 0;
        }
        int[] dp = new int[N + 1];
        dp[P] = 1;
        for (int i = 1; i <= K; i++) {
            int leftUp = dp[1];// 左上角的值
            for (int j = 1; j <= N; j++) {
                int tmp = dp[j];
                if (j == 1) {
                    dp[j] = dp[j + 1];
                } else if (j == N) {
                    dp[j] = leftUp;
                } else {
                    dp[j] = leftUp + dp[j + 1];
                }
                leftUp = tmp;
            }
        }
        return dp[M];
    }

    public static void main(String[] args) {
        System.out.println(way2(5, 2, 4, 4));
        System.out.println(way3(5,2,4,4));
        System.out.println(way4(5, 2, 4, 4));
    }
}

表结构

题目二:换钱的最少货币数

【题目】 
给定数组 arr,arr 中所有的值都为正数且不重复。
每个值代表一种面值的货币,每种面值 的货币可以使用任意张,
再给定一个整数 aim,代表要找的钱数,求组成 aim 的最少货币数。
【举例】
arr=[5,2,3],aim=20  4张5元可以组成 20 元,其他的找钱方案都要使用更多张的货币,所以返回4。 
arr=[5,2,3],aim=0   不用任何货币就可以组成 0 元,返回 0。
arr=[3,5],aim=2     根本无法组成 2 元,钱不能找开的情况下默认返回-1
package class08;

/**
 * @title: minCoins
 * @Descriptor: 换钱的最少货币数
 * @Author DD
 * @Date: 2022/6/19 16:42
 * @Version 1.0
 */

public class minCoins {

    //  暴力递归版本

    public static int minCoins1(int[] arr, int aim) {
        if (aim == 0) {
            return 0;
        }
        return process1(arr, 0, aim);
    }

    public static int process1(int[] arr, int i, int rest) {
        // 从arr[i....] 中组出rest这么多钱,所用的最少货币数
        if (rest < 0) {
            //  之前的组合导致钱数大于aim 失败
            return -1;
        }
        if (rest == 0) {
            //  之前的组合刚好凑出了aim 无需使用之后的货币了
            return 0;
        }
        //  rest>0
        if (i == arr.length) {
            //  之前的组合把钱用完了 没有可供凑数的
            return -1;
        }
        //  rest>0 并且 还有可供凑数的钱
        int p1 = process1(arr, i + 1, rest); // 没有选择i位置上的钱
        int p2Next = process1(arr, i + 1, rest - arr[i]); // 选了i位置上的钱
        if (p1 == -1 && p2Next == -1) { //   选不选都不能满足要求
            return -1;
        } else {
            if (p1 == -1) {
                return p2Next + 1;
            }
            if (p2Next == -1) {
                return p1;
            }
            return Math.min(p1, p2Next + 1);
        }
    }

    public static int minCoins2(int[] arr, int aim) {
        //  记忆搜索版本
        if (aim == 0) {
            return 0;
        }
        int[][] dp = new int[arr.length + 1][aim + 1];
        for (int i = 0; i < arr.length + 1; i++) {
            for (int j = 0; j < aim + 1; j++) {
                dp[i][j] = -2;  //  初始化记忆数组 -2表示未访问过
            }

        }
        return process2(arr, 0, aim, dp);
    }

    public static int process2(int[] arr, int i, int rest, int[][] dp) {

        if (rest < 0) {
            //  之前的组合导致钱数大于aim 失败
            return -1;
        }
        if (rest == 0) {
            //  之前的组合刚好凑出了aim 无需使用之后的货币了
            dp[i][rest] = 0;
        } else if (i == arr.length) {//  rest>0
            //  之前的组合把钱用完了 没有可供凑数的
            dp[i][rest] = -1;
        } else {
            //  rest>0 并且 还有可供凑数的钱
            int p1 = process1(arr, i + 1, rest); // 没有选择i位置上的钱
            int p2Next = process1(arr, i + 1, rest - arr[i]); // 选了i位置上的钱
            if (p1 == -1 && p2Next == -1) { //   选不选都不能满足要求
                dp[i][rest] = -1;
            } else {
                if (p1 == -1) {
                    dp[i][rest] = p2Next + 1;
                } else if (p2Next == -1) {
                    dp[i][rest] = p1;
                } else {
                    dp[i][rest] = Math.min(p2Next + 1, p1);
                }
            }
        }
        return dp[i][rest];
    }

    public static int minCoins3(int[] arr, int aim) {

        //  严格表结构的动态规划

        int N = arr.length;
        int[][] dp = new int[N + 1][aim + 1];

        for (int col = 1; col < aim + 1; col++) {
            dp[N][col] = -1;
        }

//        for (int i = 0; i < N + 1; i++) {
//            for (int j = 0; j < aim + 1; j++) {
//                System.out.print(dp[i][j]);
//                if (j == aim) {
//                    System.out.println();
//                }
//            }
//        }
//        System.out.println("=============");

        for (int i = N - 1; i >= 0; i--) {
            for (int rest = 1; rest <= aim; rest++) {

                //  rest>0 并且 还有可供凑数的钱
//                int p1 = process1(arr, i + 1, rest); // 没有选择i位置上的钱
                int p1 = dp[i + 1][rest];
//                int p2Next = process1(arr, i + 1, rest - arr[i]); // 选了i位置上的钱
                int p2Next = -1;
                if (rest - arr[i] >= 0) {
                    p2Next = dp[i + 1][rest - arr[i]];
                }

                if (p1 == -1 && p2Next == -1) { //   选不选都不能满足要求
                    dp[i][rest] = -1;
                } else {
                    if (p1 == -1) {
                        dp[i][rest] = p2Next + 1;
                    } else if (p2Next == -1) {
                        dp[i][rest] = p1;
                    } else {
                        dp[i][rest] = Math.min(p1, p2Next + 1);
                    }
                }
            }
        }
//        for (int i = 0; i < N + 1; i++) {
//            for (int j = 0; j < aim + 1; j++) {
//                System.out.print(dp[i][j]);
//                if (j == aim) {
//                    System.out.println();
//                }
//            }
//        }
        return dp[0][aim];

    }

    public static void main(String[] args) {
        int[] money = {2, 3, 5, 7, 2};
        System.out.println(minCoins1(money, 2));
        System.out.println(minCoins1(money, 5));
        System.out.println(minCoins1(money, 150));
        System.out.println(minCoins1(money, 10));
        System.out.println("=======================");
        System.out.println(minCoins2(money, 2));
        System.out.println(minCoins2(money, 5));
        System.out.println(minCoins2(money, 150));
        System.out.println(minCoins2(money, 10));
        System.out.println("=======================");
//        minCoins3(money, 10);
        System.out.println(minCoins3(money, 2));
        System.out.println(minCoins3(money, 5));
        System.out.println(minCoins3(money, 150));
        System.out.println(minCoins3(money, 10));
    }
}

题目三:排成一条线的纸牌博弈问题

给定一个整型数组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拿走4,则排列变为[1,2,100],接下来玩家B可以拿走1或100,然后继续轮到玩家A... 
玩家A作为绝顶聪明的人不会先拿4,因为拿4之后,玩家B将拿走100。
所以玩家A会先拿1,让排列变为[2,100,4],接下来玩家B不管怎么选,100都会被玩家 A拿走。	玩家A会获胜,
分数为101。所以返回101.

表结构图

package class08;

/**
 * @title: cardInLine
 * @Descriptor: 给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸牌,
 *              规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家B都绝顶聪明。请返回最后获胜者的分数。
 * @Author DD
 * @Date: 2022/5/30 17:07
 * @Version 1.0
 */

public class cardInLine {

    public static int win1(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));
    }

    //  先手 在arr的i~j范围内所能得到的最大分数
    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));
    }

    //  后手 在arr的i~j范围内所能得到的最大分数
    public static int s(int[] arr, int i, int j) {
        if (i == j) {   //  牌组中仅剩一张牌
            return 0;   //  因为是后手 所以得不到这张牌
        }
        int p1 = f(arr, i + 1, j);  //  对手选了arr[i]后最终所能达到的最大分数
        int p2 = f(arr, i, j - 1);  //  对手选了arr[j]后最终所能达到的最大分数
        //  让对手所得到的分数尽可能的小
        return Math.min(p1, p2);
    }

    public static int win2(int[] arr) {

        //  范围递归转DP 先画图

        if (arr == null || arr.length == 0) {
            return 0;
        }
        int[][] f = new int[arr.length][arr.length];
        int[][] s = new int[arr.length][arr.length];
        for (int j = 0; j < arr.length; j++) {
            f[j][j] = arr[j];
            for (int i = j - 1; i >= 0; i--) {
                f[i][j] = Math.max(arr[i] + s[i + 1][j], arr[j] + s[i][j - 1]);
                s[i][j] = Math.min(f[i + 1][j], f[i][j - 1]);
            }
        }
        return Math.max(f[0][arr.length - 1], s[0][arr.length - 1]);
    }

    public static void main(String[] args) {
        int[] arr = {1, 9, 1};
        System.out.println(win1(arr));
        System.out.println(win2(arr));
//        System.out.println(win3(arr));
    }
}

题目四:想起中马的跳法(三维尝试改DP)

【题目】 
请同学们自行搜索或者想象一个象棋的棋盘,然后把整个棋盘放入第一象限,棋盘的最左下 角是(0,0)位置。
那么整个棋盘就是横坐标上9条线、纵坐标上10条线的一个区域。
给你三个 参数,x,y,k,
返回如果“马”从(0,0)位置出发,必须走k步,最后落在(x,y)上的方法数有多少种

要第Step步跳到目标位置,只需找(Step-1)步跳到目标周围(下一步跳一下就可以到目标)
在这里插入图片描述抽象成三维表结构
在这里插入图片描述

package class08;

/**
 * @title: horseJump
 * @Descriptor: 象棋中马的跳法
 * @Author DD
 * @Date: 2022/6/20 14:57
 * @Version 1.0
 */

public class horseJump {
    public static int getWays(int x, int y, int step) {
        return process(x, y, step);
    }

    public static int process(int x, int y, int step) {
        if (x < 0 || x > 8 || y < 0 || y > 9) {
            return 0;
        }
        if (step == 0) {
            return (x == 0 && y == 0) ? 1 : 0;
        }
        return process(x - 1, y + 2, step - 1)
                + process(x + 1, y + 2, step - 1)
                + process(x + 2, y + 1, step - 1)
                + process(x + 2, y - 1, step - 1)
                + process(x + 1, y - 2, step - 1)
                + process(x - 1, y - 2, step - 1)
                + process(x - 2, y - 1, step - 1)
                + process(x - 2, y + 1, step - 1);
    }

    public static int dpWays(int x, int y, int step) {
        if (x < 0 || x > 8 || y < 0 || y > 9 || step < 0) {
            return 0;
        }
        int[][][] dp = new int[9][10][step + 1];
        dp[0][0][0] = 1;
        for (int h = 1; h <= step; h++) {
            for (int r = 0; r < 9; r++) {
                for (int c = 0; c < 10; c++) {
                    dp[r][c][h] += getValue(dp, r - 1, c + 2, h - 1);
                    dp[r][c][h] += getValue(dp, r + 1, c + 2, h - 1);
                    dp[r][c][h] += getValue(dp, r + 2, c + 1, h - 1);
                    dp[r][c][h] += getValue(dp, r + 2, c - 1, h - 1);
                    dp[r][c][h] += getValue(dp, r + 1, c - 2, h - 1);
                    dp[r][c][h] += getValue(dp, r - 1, c - 2, h - 1);
                    dp[r][c][h] += getValue(dp, r - 2, c - 1, h - 1);
                    dp[r][c][h] += getValue(dp, r - 2, c + 1, h - 1);
                }
            }
        }
        return dp[x][y][step];
    }

    public static int getValue(int[][][] dp, int row, int col, int step) {
        if (row < 0 || row > 8 || col < 0 || col > 9) {
            return 0;
        }
        return dp[row][col][step];
    }

    public static void main(String[] args) {
        int x = 7;
        int y = 7;
        int step = 10;
        System.out.println(getWays(x, y, step));
        System.out.println(dpWays(x, y, step));
    }
}

题目五:可重复使用面值凑目标钱数

【题目】 
arr内都是正数,没有重复值,每一个值代表一种面额的货币,每一种都可以重复使用
最终要找零钱数是aim
返回不同组合的方法数
==================================
[举例]
arr = [2,3,5] 希望凑成10元
case1: 2,2,2,2,2
case2: 2,3,5
case3: 2,2,3,3
case4: 5,5
package class08;

/**
 * @title: coinsWays
 * @Descriptor: 使用数组中提供的面值(可以重复使用)组合成目标面值 返回组合数
 * @Author DD
 * @Date: 2022/6/20 18:56
 * @Version 1.0
 */

public class coinsWays {

    //  arr内都是正数,没有重复值,每一个值代表一种面额的货币,每一种都可以重复使用
    //  最终要找零钱数是aim
    //  返回不同组合的方法数

    public static int way1(int[] arr, int aim) {
        return process(arr, 0, aim);
    }

    //  使用arr[index...]中的所有面值,凑成rest,返回所有凑法 -- 暴力递归
    public static int process(int[] arr, int index, int rest) {
        if (index == arr.length) {  // 所有面值都已尝试结束
            return rest == 0 ? 1 : 0;
        }
        //  使用 x张 arr[index]面值的货币 需要x*arr[index]<rest ==> 不越界
        int ways = 0;
        for (int zhang = 0; zhang * arr[index] <= rest; zhang++) {
            ways += process(arr, index + 1, rest - zhang * arr[index]);
        }
        return ways;
    }

    //  记忆搜索
    public static int way2(int[] arr, int aim) {
        int N = arr.length;
        int[][] dp = new int[N + 1][aim + 1];
        return process2(arr, 0, aim, dp);
    }

    private static int process2(int[] arr, int index, int rest, int[][] dp) {
        if (index == arr.length) {
            dp[index][rest] = rest == 0 ? 1 : 0;
            return dp[index][rest];
        }
        int ways = 0;
        for (int zhang = 0; zhang * arr[index] <= rest; zhang++) {
            ways += process2(arr, index + 1, rest - zhang * arr[index], dp);
        }
        dp[index][rest] = ways;
        return dp[index][rest];
    }

    //  严格表结构的动态规划
    public static int way3(int[] arr, int aim) {
        return process3(arr, 0, aim);
    }

    public static int process3(int[] arr, int index, int aim) {
        int N = arr.length;
        int[][] dp = new int[N + 1][aim + 1];
        dp[N][0] = 1;
        for (int i = N - 1; i >= 0; i--) {
            for (int j = 0; j <= aim; j++) {
                int ways = 0;
                for (int zhang = 0; zhang * arr[index] <= aim; zhang++) {
                    ways += process2(arr, index + 1, aim - zhang * arr[index], dp);
                }
                dp[i][j] = ways;
            }
        }
        return dp[0][aim];
    }

    //  表结构的优化
    //  当前数据依赖 arr[index+1][rest] 和 arr[index][rest - arr[index]](优化的点就在于避免了重复计算该项)
    public static int way4(int[] arr, int aim) {
        int N = arr.length;
        int[][] dp = new int[N + 1][aim + 1];
        dp[N][0] = 1;
        for (int i = N - 1; i >= 0; i--) {
            for (int j = 0; j <= aim; j++) {
                dp[i][j] = dp[i + 1][j];
                if (j - arr[i] >= 0) {
                    dp[i][j] += dp[i][j - arr[i]];
                }
            }
        }
        return dp[0][aim];
    }

    public static void main(String[] args) {
        int[] money = {2, 3, 5};
//        System.out.println(way1(money, 1));
        System.out.println(way3(money, 10));
        System.out.println(way3(money, 2));
        System.out.println(way4(money, 10));
        System.out.println(way4(money, 2));

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值