算法学习动态规划-3

前言

动态规划下面继续开始算法的学习,算法是一种需要长期练习,增强灵感~~~

一、最小路径累加和

在这里插入图片描述

题目:

 * 给定一个二维数组matrix,一个人必须从左上角出发,最后到达右下角
 * 沿途只可以向下或者向右走,沿途的数字都累加就是距离累加和
 * 返回最小距离累加和

分析:

还是从尝试入手,从(00)出发到达右下角的目标
递归函数的含义定义为:从哪个位置出发,到达右下角沿途最小累加和
递归函数中,分三种情况讨论
1、最后一行的时候,只能向右
2、最后一列的时候,只能向下
3、其他普通情况,分别求出向右和向下的累加和,取最小
再通过递归函数改动态规划。

递归代码:

 public static int minPathSum(int[][] m) {
        if (m == null || m.length == 0) {
            return 0;
        }

        return process(m, 0, 0);
    }


    /**
     * 返回从(row,col)位置出发到右下角沿途累加和最小值
     *
     * @param m
     * @param row
     * @param col
     * @return
     */
    public static int process(int[][] m, int row, int col) {
        int rowLength = m.length;
        int colLength = m[0].length;
        int lastRow = rowLength - 1;
        int lastCol = colLength - 1;

        //base case
        if (row == lastRow && col == lastCol) {
            return m[lastRow][lastCol];
        }

        //最后一行,只能向右
        if (row == lastRow) {
            return m[row][col] + process(m, row, col + 1);
        }

        //最后一列,只能向下
        if (col == lastCol) {
            return m[row][col] + process(m, row + 1, col);
        }

        //普通情况
        int down = m[row][col] + process(m, row + 1, col);
        int right = m[row][col] + process(m, row, col + 1);

        return Math.min(down, right);

    }

动态规划代码;

    public static int minPathSum(int[][] m) {
        if (m == null || m.length == 0) {
            return 0;
        }
        int rowSize = m.length;
        int colSize = m[0].length;
        int lastRow = rowSize - 1;
        int lastCol = colSize - 1;
        int[][] dp = new int[rowSize][colSize];
        dp[lastRow][lastRow] = m[lastRow][lastCol];

        //最后一行,只能向右
        for (int i = lastCol - 1; i >= 0; i--) {
            dp[lastRow][i] = m[lastRow][i] + dp[lastRow][i + 1];
        }

        //最后一列,只能向下
        for (int i = lastRow - 1; i >= 0; i--) {
            dp[i][lastCol] = m[i][lastCol] + dp[i + 1][lastCol];
        }

        for (int row = lastRow - 1; row >= 0; row--) {
            for (int col = lastCol - 1; col >= 0; col--) {
                //普通情况
                int down = m[row][col] + dp[row + 1][col];
                int right = m[row][col] + dp[row][col + 1];

                dp[row][col] = Math.min(down, right);
            }
        }

        return dp[0][0];
    }

思考:

这里有办法再优化吗?
我们可以看到,动态规划版本,我们需要再新增和m数组等规模的二维数组,但是我们可以改成一维数组,因为从dp动态规划版本中,我们知道,中间任一格子只依赖自己下一个格子和右边的格子

空间压缩的代码:

    public static int minPathSum(int[][] m) {
        if (m == null || m.length == 0) {
            return 0;
        }
        int rowSize = m.length;
        int colSize = m[0].length;
        int lastRow = rowSize - 1;
        int lastCol = colSize - 1;
        int[] dp = new int[colSize];
        dp[lastCol] = m[lastRow][lastCol];

        //最后一行,只能向右
        for (int i = lastCol - 1; i >= 0; i--) {
            dp[i] = m[lastRow][i] + dp[i + 1];
        }

        for (int row = lastRow - 1; row >= 0; row--) {
            dp[lastCol] = m[row][lastCol] + dp[lastCol];
            for (int col = lastCol - 1; col >= 0; col--) {
                int p1 = dp[col + 1] + m[row][col];
                int p2 = dp[col] + m[row][col];
                dp[col] = Math.min(p1, p2);
            }
        }
        return dp[0];
    }

二、Bob求生概率

题目:

 * 给定5个参数,NM,row,col,k
 * 表示在N*M的区域上,醉汉Bob初始在(row,col)位置
 * Bob一共要迈出k步,且每步都会等概率向上下左右四个方向走一个单位
 * 任何时候Bob只要离开N*M的区域,就直接死亡
 * 返回k步之后,Bob还在N*M的区域的概率

分析:

题型和象棋马走日的题目相似,通过递归求出Bob走k步还在区域内的所有的可能数量,然后在除以4的k次方。
因为Bob每次有4种选择,并且可以走k次,所以Bob所有可以走的次数就是4的k次方。
在通过递归函数改写动态规划

递归代码:

 public static double livePosibility(int row, int col, int k, int N, int M) {
        return process(row, col, k, N, M) / Math.pow(4, k);
    }

    /**
     * 返回走rest步数后,还在N*M区域内的路径数量
     *
     * @param row
     * @param col
     * @param rest
     * @param N
     * @param M
     * @return
     */
    public static long process(int row, int col, int rest, int N, int M) {
        //越界判断
        if (row < 0 || col < 0 || row >= N || col >= M) {
            return 0;
        }

        //base case
        if (rest == 0) {
            return 1;
        }

        long up = process(row + 1, col, rest - 1, N, M);
        long down = process(row - 1, col, rest - 1, N, M);
        long left = process(row, col - 1, rest - 1, N, M);
        long right = process(row, col + 1, rest - 1, N, M);

        return up + down + left + right;
    }

动态规划代码:

 public static double livePosibility2(int row, int col, int k, int N, int M) {
        long[][][] dp = new long[N + 1][M + 1][k + 1];
        for (int r = 0; r <= N; r++) {
            for (int c = 0; c <= M; c++) {
                dp[r][c][0] = 1;
            }
        }

        for (int rest = 1; rest <= k; rest++) {
            for (int r = 0; r <= N; r++) {
                for (int c = 0; c <= M; c++) {
                    long up = pick(dp, r + 1, c, rest - 1, N, M);
                    long down = pick(dp, r - 1, c, rest - 1, N, M);
                    long left = pick(dp, r, c - 1, rest - 1, N, M);
                    long right = pick(dp, r, c + 1, rest - 1, N, M);

                    dp[r][c][rest] = up + down + left + right;
                }
            }
        }

        return dp[row][col][k] / Math.pow(4, k);
    }

    public static long pick(long[][][] dp, int row, int col, int rest, int N, int M) {
        //越界判断
        if (row < 0 || col < 0 || row >= N || col >= M) {
            return 0;
        }
        return dp[row][col][rest];
    }

三、不同货币组成目标面值

题目:

 * arr是货币数组,其中的值都是正数。再给定一个正数aim。
 * 每个值都认为是一张货币,
 * 即便是值相同的货币也认为每一张都是不同的,
 * 返回组成aim的方法数
 * 例如:arr = {1,1,1},aim = 2
 *0个和第1个能组成2,第1个和第2个能组成2,第0个和第2个能组成2
 * 一共就3种方法,所以返回3

分析:

递归函数中,我们要讨论当前值要和不要两种,然后将两种情况求和

递归代码:

 public static int coinWays(int[] arr, int aim) {
        if (arr == null || arr.length == 0 || aim < 0) {
            return 0;
        }
        return process(arr, 0, aim);
    }

    public static int process(int[] arr, int index, int rest) {
        //base case
        if (index == arr.length) {
            return rest == 0 ? 1 : 0;
        }

        //当前位置不选择
        int p1 = process(arr, index + 1, rest);
        //选择当前位置
        int p2 = process(arr, index + 1, rest - arr[index]);

        return p1 + p2;
    }

动态规划代码:

    public static int dp(int[] arr, int aim) {
        if (arr == null || arr.length == 0 || aim < 0) {
            return 0;
        }
        int N = arr.length;
        int[][] dp = new int[N + 1][aim + 1];
        dp[N][0] = 1;

        for (int index = N - 1; index >= 0; index--) {
            for (int rest = 0; rest <= aim; rest++) {
                int p1 = dp[index + 1][rest];
                int p2 = 0;
                if (rest - arr[index] >= 0) {
                    p2 = dp[index + 1][rest - arr[index]];
                }
                dp[index][rest] = p1 + p2;
            }
        }

        return dp[0][aim];
    }

四、货币不限张数组成目标面值

题目:

 * arr是面值数组,其中的值都是正数且没有重复。再给定一个正数aim。
 * 每个值都认为是一种面值,且认为张数是无限的。
 * 返回组成aim的方法数
 * 例如:arr = {1,2},aim = 4
 * 方法如下:1+1+1+11+1+22+2
 * 一共就3种方法,所以返回3

分析:

递归函数中,需要考虑张数,讨论张数从0到小于等于目标面值的情况去计算

递归代码:

 public static int coinsWay(int[] arr, int aim) {
        if (arr == null || arr.length == 0 || aim < 0) {
            return 0;
        }

        return process(arr, 0, aim);
    }

    public static int process(int[] arr, int index, int rest) {
        //base case
        if (index == arr.length) {
            return rest == 0 ? 1 : 0;
        }

        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 dp(int[] arr, int aim) {
        if (arr == null || arr.length == 0 || aim < 0) {
            return 0;
        }

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

        for (int index = N - 1; index >= 0; index--) {
            for (int rest = 0; rest <= aim; rest++) {
                int ways = 0;
                for (int zhang = 0; zhang * arr[index] <= rest; zhang++) {
                    ways += dp[index + 1][rest - zhang * arr[index]];
                }

                dp[index][rest] = ways;
            }
        }

        return dp[0][aim];
    }

思考:

通过动态规划的代码,居然出现三个for循环,时间复杂度升高了,要想办法优化,这里通过学习,我们可以通过观察动态规划的位置依赖的方法,来看下优化的可能性。通过观察我们知道,中间的格子依赖自己下一行以及往左的格子,进一步观察,可以知道距离自己左边-arr[index]位置的格子也是依赖下一行的格子,并且和我们重复了。所以存在优化的空间

动态规划优化代码:

    public static int dp(int[] arr, int aim) {
        if (arr == null || arr.length == 0 || aim < 0) {
            return 0;
        }

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

        for (int index = N - 1; index >= 0; index--) {
            for (int rest = 0; rest <= aim; rest++) {
                dp[index][rest] = dp[index + 1][rest];
                //当自己左边的格子存在
                if (rest - arr[index] >= 0) {
                    //这一步相当于将上个代码的for循环给简化
                    dp[index][rest] += dp[index][rest - arr[index]];
                }
            }
        }

        return dp[0][aim];
    }

五、货币限定张数组成目标面值

题目:

 * arr是货币数组,其中的值都是正数。再给定一个正数aim。
 * 每个值都认为是一张货币,
 * 认为值相同的货币没有任何不同,
 * 返回组成aim的方法数
 * 例如:arr = {1,2,1,1,2,1,2},aim = 4
 * 方法:1+1+1+11+1+22+2
 * 一共就3种方法,所以返回3

分析:

和第4题差不多,只是在递归尝试的时候,张数需要加限定条件

递归代码:

 public static class Info {
        private int[] coins;
        private int[] zhangs;

        public Info(int[] coins, int[] zhangs) {
            this.coins = coins;
            this.zhangs = zhangs;
        }
    }

    public static Info getInfo(int[] arr) {
        HashMap<Integer, Integer> map = new HashMap<>();

        for (int i = 0; i < arr.length; i++) {
            if (map.containsKey(arr[i])) {
                map.put(arr[i], 1 + map.get(arr[i]));
            } else {
                map.put(arr[i], 1);
            }
        }

        Set<Integer> keys = map.keySet();
        int[] coins = new int[map.size()];
        int[] zhangs = new int[map.size()];
        int index = 0;
        for (Integer key : keys) {
            coins[index] = key;
            zhangs[index++] = map.get(key);
        }
        Info info = new Info(coins, zhangs);
        return info;
    }

    public static int coinsWay(int[] arr, int aim) {
        if (arr == null || arr.length == 0 || aim < 0) {
            return 0;
        }

        Info info = getInfo(arr);

        return process(info.coins, info.zhangs, 0, aim);
    }

    /**
     * 返回在限定张数和面值的情况下组成目标面额的所有方法
     *
     * @param coins  每个货币的面值
     * @param zhangs 每种货币张数
     * @param index  计算到哪种货币的下标
     * @param rest   剩余目标数
     * @return
     */
    public static int process(int[] coins, int[] zhangs, int index, int rest) {
        //base case
        if (rest == 0) {
            return 1;
        }

        //越界情况
        if (index == coins.length) {
            return 0;
        }

        int ways = 0;
        for (int zhang = 0; zhang <= zhangs[index] && rest - zhang * coins[index] >= 0; zhang++) {
            ways += process(coins, zhangs, index + 1, rest - zhang * coins[index]);
        }

        return ways;
    }

递归代码:

    public static int dp(int[] arr, int aim) {
        if (arr == null || arr.length == 0 || aim < 0) {
            return 0;
        }

        Info info = getInfo(arr);
        int[] coins = info.coins;
        int[] zhangs = info.zhangs;
        int N = coins.length;
        int[][] dp = new int[N + 1][aim + 1];

        //第一列都是1
        for (int index = 0; index <= N; index++) {
            dp[index][0] = 1;
        }

        //最后一行,除第一列,其余都是0

        //普遍情况
        for (int index = N - 1; index >= 0; index--) {
            for (int rest = 1; rest <= aim; rest++) {
                int ways = 0;
                for (int zhang = 0; zhang <= zhangs[index] && rest - (zhang * coins[index]) >= 0; zhang++) {
                    ways += dp[index + 1][rest - zhang * coins[index]];
                }
                dp[index][rest] = ways;
            }
        }

        return dp[0][aim];
    }

同理这里需要进行优化

优化后的动态规划代码:

    public static int dp(int[] arr, int aim) {
        if (arr == null || arr.length == 0 || aim < 0) {
            return 0;
        }

        Info info = getInfo(arr);
        int[] coins = info.coins;
        int[] zhangs = info.zhangs;
        int N = coins.length;
        int[][] dp = new int[N + 1][aim + 1];

        //第一列都是1
        for (int index = 0; index <= N; index++) {
            dp[index][0] = 1;
        }

        //最后一行,除第一列,其余都是0

        //普遍情况
        for (int index = N - 1; index >= 0; index--) {
            for (int rest = 1; rest <= aim; rest++) {
                dp[index][rest] = dp[index + 1][rest];
                if (rest - coins[index] >= 0) {
                    dp[index][rest] += dp[index][rest - coins[index]];
                }
                if (rest - coins[index] * (zhangs[index] + 1) >= 0) {
                    dp[index][rest] -= dp[index + 1][rest - coins[index] * (zhangs[index] + 1)];
                }
            }
        }

        return dp[0][aim];
    }

总结

1、向第一道题目,后面刷题的过程中,如果遇到数组情况,并且存在可以压缩的情况,尽量进行优化,做人要对自己有要求才能不断提高。
2、像第4和第5题,动态规划中居然出现三个for循环,这个时候,我们需要去观察动态规划位置依赖,如果存在重复计算的格子,那么我们就可以进行优化
算法之路继续前行,感谢网上大佬提供的学习资料

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值