【算法&数据结构体系篇class22】:暴力递归到动态规划

目录

一、砍死怪兽的概率问题

二、组成aim的最少货币数

三、求N值的裂开方法数


一、砍死怪兽的概率问题

给定3个参数,NMK

怪兽有N滴血,等着英雄来砍自己

英雄每一次打击,都会让怪兽流失[0~M]的血量

到底流失多少?每一次在[0~M]上等概率的获得一个值

K次打击之后,英雄把怪兽砍死的概率

 

package class22;

/**
 * 给定3个参数,N,M,K
 * 怪兽有N滴血,等着英雄来砍自己
 * 英雄每一次打击,都会让怪兽流失[0~M]的血量
 * 到底流失多少?每一次在[0~M]上等概率的获得一个值
 * 求K次打击之后,英雄把怪兽砍死的概率
 */
public class KillMonster {

    //方法一: 暴力递归
    public static double right(int N, int M, int K) {
        //边界的判断 小于1都是异常入参 直接返回0
        if(N < 1 || M < 1 || K < 1) return 0;

        //求得是砍死怪兽得概率,我们可以先分析总共得砍法数
        //每次打击时 0-M得等概率一个值 也就是有 M+1种 ,共K次 每次就是M+1种 那么总方法数就是 (M+1)^ k次方
        long all = (long) Math.pow(M+1,K);

        //递归函数  砍K次  每次砍掉 0-M的等概率血量  血量为N  返回砍死的方法次数
        long kill = process(K,M,N);

        //返回砍死方法数/ 全部的方法数  就得到了一个砍死概率
        return (double)kill/ (double) all;
    }

    /**
     * 递归 剩余rest 次砍, 每次砍0-m血量, 剩余血量hp 返回砍死的方法数
     * @param rest
     * @param m
     * @param hp
     * @return
     */
    public static long process(int rest, int m, int hp){
        //base case: 剩余打击次数为0时 看看当前的血量是否是小于等于0 是则表示已经砍死怪兽 返回1种方法 否则就是0种
        if(rest == 0){
            return hp <=0 ? 1 : 0;
        }

        //base case: 剩余打击次数还有剩余,如果当前血量已经是小于等于0了 表示已经砍死怪兽了 后续的都是砍死的方法数
        //加入剩余次数rest = 2 那么1次砍 会有0-m的血量流失 有m+1种  第2次砍 也会有0-m的血量流失 有m+1种 所以方法数就是 (m+1)^2 种
        if(hp <= 0){
            return (long) Math.pow(m+1,rest);
        }

        //接着开始分析常规情况  定义变量保存结果方法数
        long ways = 0;

        //剩下rest次打击, 每次都是 0-m的血量流失 遍历m+1种
        for(int i = 0; i <= m; i++){
            //累加方法数 步数-1 , 流失m的血量固定值  ,  血量剩下 hp-i
            ways += process(rest-1,m,hp-i);
        }
        //最后返回全部方法数
        return ways;
    }


    //方法二:动态规划
    public static double dp1(int N, int M, int K){
        //边界的判断 小于1都是异常入参 直接返回0
        if(N < 1 || M < 1 || K < 1) return 0;

        //求得是砍死怪兽得概率,我们可以先分析总共得砍法数
        //每次打击时 0-M得等概率一个值 也就是有 M+1种 ,共K次 每次就是M+1种 那么总方法数就是 (M+1)^ k次方
        long all = (long) Math.pow(M+1,K);

        //分析递归的变量 有K 剩余打击数 范围0-K  N 剩余血量 范围是 负数 N  负数范围可不考虑 则为0 N
        long[][] dp = new long[K+1][N+1]; // 行对应 打击数   列对应 血量

        //base case: 剩余打击次数为0时 看看当前的血量是否是小于等于0 是则表示已经砍死怪兽 返回1种方法 否则就是0种
        dp[0][0] = 1;     //第0行已经处理好

        //rest次数 依赖 rest-1  也就是 第1行依赖第0行  第2行依赖第1行.. 下一行依赖上一行 目前已经完善第0行 接着往下填充赋值
        //剩下rest次打击, 1-m的血量流失情况
        //血量为0 且打击数大于0 的情况 前面base case已经分析完善 所以血量从1开始
        for(int rest = 1; rest <= K; rest++){
            //base case: 剩余打击次数还有剩余,如果当前血量已经是小于等于0了 表示已经砍死怪兽了 后续的都是砍死的方法数
            //加入剩余次数rest = 2 那么1次砍 会有0-m的血量流失 有m+1种  第2次砍 也会有0-m的血量流失 有m+1种 所以方法数就是 (m+1)^2 种
            dp[rest][0] = (long)Math.pow(M+1,rest);

            for(int hp = 1; hp <= N; hp++){
                //从第一行 每行的第一行开始 跳过了第0列 第0行 base case已经处理
                long ways = 0;
                for(int i = 0; i <= M;i++){
                    //填充每个格子的方法数 需要枚举循环 0-m的血量流失的情况累加
                    if((hp - i) >= 0){
                        //注意这里  列值 也就是剩余血量 可能会存在被流失到负数 而我们dp数组边界是从0 开始 如果是大于等于0 则不越界 那么累加就按依赖进行累加
                        ways += dp[rest-1][hp-i];
                    }else{
                        //如果 hp-i 剩余血量 减到了负数 比如 当前血量是 2  剩余打击数 1  m假设比较大5 那么可能会是5的情况 2-5= -3 就是负数情况
                        //我们可以转换考虑,如果是负数的血量  那么后续的打击次数 肯定都是符合情况的 砍死情况 那么剩下的打击数 就是rest -1 次  后续的方法数就是 (m+1)^ rest-1 种
                        ways += (long) Math.pow(M+1,rest-1);
                    }
                }
                //枚举情况累加完之后 就把值赋给对应dp的位置
                dp[rest][hp] =  ways;
            }
        }
        //递归调用是从 K次打击数 N血量开始 所以对应就返回dp位置的方法数 除以 总的方法数得到砍死概率
        return (double)dp[K][N]/ (double) all;
    }


    //方法三:动态规划  优化枚举的时间复杂度 转成成常数时间处理每个单元格的方法数
    public static double dp2(int N, int M, int K){
        //边界的判断 小于1都是异常入参 直接返回0
        if(N < 1 || M < 1 || K < 1) return 0;

        //求得是砍死怪兽得概率,我们可以先分析总共得砍法数
        //每次打击时 0-M得等概率一个值 也就是有 M+1种 ,共K次 每次就是M+1种 那么总方法数就是 (M+1)^ k次方
        long all = (long) Math.pow(M+1,K);

        //分析递归的变量 有K 剩余打击数 范围0-K  N 剩余血量 范围是 负数 N  负数范围可不考虑 则为0 N
        long[][] dp = new long[K+1][N+1]; // 行对应 打击数   列对应 血量

        //base case: 剩余打击次数为0时 看看当前的血量是否是小于等于0 是则表示已经砍死怪兽 返回1种方法 否则就是0种
        dp[0][0] = 1;     //第0行已经处理好

        //rest次数 依赖 rest-1  也就是 第1行依赖第0行  第2行依赖第1行.. 下一行依赖上一行 目前已经完善第0行 接着往下填充赋值
        //剩下rest次打击, 1-m的血量流失情况
        //血量为0 且打击数大于0 的情况 前面base case已经分析完善 所以血量从1开始
        for(int rest = 1; rest <= K; rest++){
            //base case: 剩余打击次数还有剩余,如果当前血量已经是小于等于0了 表示已经砍死怪兽了 后续的都是砍死的方法数
            //加入剩余次数rest = 2 那么1次砍 会有0-m的血量流失 有m+1种  第2次砍 也会有0-m的血量流失 有m+1种 所以方法数就是 (m+1)^2 种
            dp[rest][0] = (long) Math.pow(M+1,rest);

            for(int hp = 1; hp <= N; hp++){
                //从第一行 每行的第一行开始 跳过了第0列 第0行 base case已经处理
                //根据前面动态规划的推算。 假设dp[3][4] 剩余3次打击 4血量 M=3
                //dp[3][4] 依赖于m+1中丢失血量0,1,2,3 四种情况累加
                //dp[3][4] = dp[2][4] + dp[2][3] + dp[2][2] + dp[2][1]

                //同理 判断dp[3][5] 依赖于m+1中丢失血量0,1,2,3 四种情况累加
                //dp[3][5] =dp[2][5] + dp[2][4] + dp[2][3] + dp[2][2]

                //得到dp[3][5] 其实可以通过dp[3][4] 常数个值来转换得到 比dp[3][4]少了 dp[2][1] 多了dp[2][5] 那么少了就- 多了就+
                //dp[3][5] = dp[3][4] - dp[2][1] + dp[2][5]     而这里注意了dp[2][1] 是有可能越界的 如果M比较大,那么多的这个值就不能直接减去 否则会越界异常 越界说明剩下小于0的血量了 都是符合砍死的方法数 直接返回(m+1)^剩余次数-1

                //我们可以先赋值左位置的依赖以及加上该位置下位置[rest-1][hp]。 目前是不会越界的 rest-1>=0 hp-1>=0
                dp[rest][hp] = dp[rest][hp-1] + dp[rest-1][hp];

                //而要减去左位置的值dp[rest][hp-1] 多了最后一个值 dp[rest-1][hp-1-M] 可能存在越界
                // dp[2][1] 就是等于dp[2][5 - 1- 3 ] = dp[2][1] 这么转换来的
                if((hp - 1 - M)>=0){
                    //不越界 就说明在dp数组中直接减
                    dp[rest][hp] -= dp[rest-1][hp-1-M];
                }else{
                    //越界了 就说明血量小于0  依赖的是rest-1次打击 每次 0-m血量丢失  m+1种 所以方法数就是(m+1)^rest-1
                    dp[rest][hp] -= Math.pow(M+1,rest-1);
                }
            }
        }
        //递归调用是从 K次打击数 N血量开始 所以对应就返回dp位置的方法数 除以 总的方法数得到砍死概率
        return (double)dp[K][N]/ (double) all;
    }

    public static void main(String[] args) {
        int NMax = 10;
        int MMax = 10;
        int KMax = 10;
        int testTime = 200;
        System.out.println("测试开始");
        for (int i = 0; i < testTime; i++) {
            int N = (int) (Math.random() * NMax);
            int M = (int) (Math.random() * MMax);
            int K = (int) (Math.random() * KMax);
            double ans1 = right(N, M, K);
            double ans2 = dp1(N, M, K);
            double ans3 = dp2(N, M, K);
            if (ans1 != ans2 || ans1 != ans3) {
                System.out.println("Oops!");
                break;
            }
        }
        System.out.println("测试结束");
    }
}

 

二、组成aim的最少货币数

arr是面值数组,其中的值都是正数且没有重复。再给定一个正数aim

每个值都认为是一种面值,且认为张数是无限的。

返回组成aim的最少货币数

 

package class22;

/**
 * arr是面值数组,其中的值都是正数且没有重复。再给定一个正数aim。
 * 每个值都认为是一种面值,且认为张数是无限的。
 * 返回组成aim的最少货币数
 */
public class MinCoinsNoLimit {

    //方法一: 暴力递归
    public static int minCoins(int[] arr, int aim) {
        //调用递归 arr面值数组  从第一个面值开始 0索引  组成aim  返回最少货币数 如果没有就返回整形最大值
        return process(arr, 0, aim);
    }

    /**
     * 返回 当前面值数组 arr[index...] 组成剩余目标值rest的最少货币数
     *
     * @param arr
     * @param index
     * @param rest
     * @return
     */
    public static int process(int[] arr, int index, int rest) {
        //base case: 当面值越界 没有面值的时候 如果rest所剩余是0 说明不需要货币数了返回0 否则就是返回最大值
        if (index == arr.length) {
            return rest == 0 ? 0 : Integer.MAX_VALUE;
        }

        //定义一个辅助遍历 保存每个面值的各种选择 取较小值  初始化为最大值
        int ways = Integer.MAX_VALUE;

        //每个面值都是无限张 从0...开始多种尝试 张数*面值要小于等于目标值rest
        for (int count = 0; count * arr[index] <= rest; count++) {
            //当前arr[i] 取0,1,2...张对应的下个面值arr[i+1]的各种情况的较小组成张数
            //下个面值来到index+1位置  剩余目标值是 rest - 张数*当前面值
            int next = process(arr, index + 1, rest - count * arr[index]);
            if (next != Integer.MAX_VALUE) {
                //需要先判断 后面的面值最小张数是否存在 存在的话就不是整形最大值,那么就进行比较
                //当前结果是ways 跟 next的最小张数 加上 当前index选择的张数 count 就是当前新的最少张数
                ways = Math.min(ways, next + count);
            }
        }
        return ways;
    }


    //方法二: 动态规划
    public static int dp1(int[] arr, int aim) {
        //递归分析 可变参数 index 索引 0,arr.length  aim目标值 0-aim
        int n = arr.length;
        int[][] dp = new int[n + 1][aim + 1];

        //base case: 当面值越界 没有面值的时候 如果rest所剩余是0 说明不需要货币数了返回0 否则就是返回最大值
        for (int rest = 1; rest <= aim; rest++) {
            dp[n][rest] = Integer.MAX_VALUE;   //越界index等于n 没有面值 如果剩余目标值还大于0 就是返回最大值
        }

        //分析常规情况 递归依赖 index  依赖于 index+1  最后一行n已经填充好  从n-1行往上填充每一行
        for (int index = n - 1; index >= 0; index--) {
            //每一行从左往右遍历到aim最后一列
            for (int rest = 0; rest <= aim; rest++) {
                //定义一个辅助遍历 保存每个面值的各种选择 取较小值  初始化为最大值
                int ways = Integer.MAX_VALUE;

                //每个面值都是无限张 从0...开始多种尝试 张数*面值要小于等于目标值rest
                for (int count = 0; count * arr[index] <= rest; count++) {
                    //当前arr[i] 取0,1,2...张对应的下个面值arr[i+1]的各种情况的较小组成张数
                    //下个面值来到index+1位置  剩余目标值是 rest - 张数*当前面值
                    int next = dp[index + 1][rest - count * arr[index]];
                    if (next != Integer.MAX_VALUE) {
                        //需要先判断 后面的面值最小张数是否存在 存在的话就不是整形最大值,那么就进行比较
                        //当前结果是ways 跟 next的最小张数 加上 当前index选择的张数 count 就是当前新的最少张数
                        ways = Math.min(ways, next + count);
                    }
                }
                dp[index][rest] = ways;
            }
        }
        return dp[0][aim];   //根据递归函数 从第0个面值开始 aim目标值 返回对应位置
    }

    //方法三: 动态规划 优化其中每个位置枚举行为 用相邻位置替换法 处理成常数位置求值
    public static int dp2(int[] arr, int aim) {
        //递归分析 可变参数 index 索引 0,arr.length  aim目标值 0-aim
        int n = arr.length;
        int[][] dp = new int[n + 1][aim + 1];

        //base case: 当面值越界 没有面值的时候 如果rest所剩余是0 说明不需要货币数了返回0 否则就是返回最大值
        for (int rest = 1; rest <= aim; rest++) {
            dp[n][rest] = Integer.MAX_VALUE;   //越界index等于n 没有面值 如果剩余目标值还大于0 就是返回最大值
        }

        //分析常规情况 递归依赖 index  依赖于 index+1  最后一行n已经填充好  从n-1行往上填充每一行
        for (int index = n - 1; index >= 0; index--) {
            //每一行从左往右遍历到aim最后一列
            for (int rest = 0; rest <= aim; rest++) {
               //画图找位置。 递归中 可以得到是[index][rest] 依赖下一行的多个值。同理[index][rest - arr[index]] 也是依赖下一行多个值
                //dp[3][14]  arr[3]面值假设是3 目标值14 依赖
                //0张  dp[3+1][14 - 0* arr[3]]= dp[4][14]
                //1张  dp[4][11]
                //2张  dp[4][8]
                //3张  dp[4][5]
                //4张  dp[4][2] ..再往下就越界了   0-4张 依赖这5个位置的最小值表示最少张数

                //对于dp[3][11]  依赖
                //0张   dp[4][11]
                //1张   dp[4][8]
                //2张   dp[4][5]
                //3张   dp[4][2] ..再往下就越界了 0-3张  依赖着4个位置的最小值 表示最少张数

                //这里注意转换时候张数问题 dp[3][14] 想用到 dp[3][11] 对应 11 8  5  2 位置都一致 但张数都比dp[3][14]少1张 所以需要+1 少了dp[4][14] 要再进行比较大小
                //dp[3][14] = 依赖每个位置的最小值  而dp[3][11]的值 需要+1张 就对应上dp[3][14]的公共位置张数 再跟下位置比较最小
                //先赋值下位置的值 待会再跟左位置的比较大小
                dp[index][rest] = dp[index+1][rest];
                if((rest-arr[index])>=0 && dp[index][rest - arr[index]] != Integer.MAX_VALUE){
                    //左边列rest-arr[index]需要不越界 并且这个左边位置值应该不是整形最小值  才加入与 下位置进行比较大小, 注意要加上1
                    // 前面的分析结论 就是该位置与当前位置的公共依赖是少了1张的  否则就没必要比较了
                    dp[index][rest] = Math.min(dp[index][rest],dp[index][rest-arr[index]]+1);
                }
            }
        }
        return dp[0][aim];   //根据递归函数 从第0个面值开始 aim目标值 返回对应位置
    }



    // 为了测试
    public static int[] randomArray(int maxLen, int maxValue) {
        int N = (int) (Math.random() * maxLen);
        int[] arr = new int[N];
        boolean[] has = new boolean[maxValue + 1];
        for (int i = 0; i < N; i++) {
            do {
                arr[i] = (int) (Math.random() * maxValue) + 1;
            } while (has[arr[i]]);
            has[arr[i]] = true;
        }
        return arr;
    }

    // 为了测试
    public static void printArray(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }

    // 为了测试
    public static void main(String[] args) {
        int maxLen = 20;
        int maxValue = 30;
        int testTime = 300000;
        System.out.println("功能测试开始");
        for (int i = 0; i < testTime; i++) {
            int N = (int) (Math.random() * maxLen);
            int[] arr = randomArray(N, maxValue);
            int aim = (int) (Math.random() * maxValue);
            int ans1 = minCoins(arr, aim);
            int ans2 = dp1(arr, aim);
            int ans3 = dp2(arr, aim);
            if (ans1 != ans2 || ans1 != ans3) {
                System.out.println("Oops!");
                printArray(arr);
                System.out.println(aim);
                System.out.println(ans1);
                System.out.println(ans2);
                break;
            }
        }
        System.out.println("功能测试结束");
    }


}

三、求N值的裂开方法数

给定一个正数n,求n的裂开方法数,

规定:后面的数不能比前面的数小

比如4的裂开方法有:

1+1+1+11+1+21+32+24

5种,所以返回5

package class22;

/**
 * 给定一个正数n,求n的裂开方法数,
 * 规定:后面的数不能比前面的数小
 * 比如4的裂开方法有:
 * 1+1+1+1、1+1+2、1+3、2+2、4
 * 5种,所以返回5
 */
public class SplitNumber {

    //方法一: 暴力递归
    // n为正数
    public static int ways(int n) {
        //边界判断  小于0 返回0 无效
        if(n < 0) return 0;
        //等于1时 拆分就只能是自己  返回1种
        if(n == 1) return 1;

        //调用递归函数 根据题意 后面值大于等于前面 所以函数定义一个 前置值 目标值n
        return process(1, n);
    }

    //递归函数 前面已经选择了pre 目前剩下rest需要拆分 返回裂开方法数
    public static int process(int pre, int rest){
        if(rest == 0) return 1;   //剩余需拆分为0 说明前面都已经拆分好了返回1
        //base case: 如果前置数 大于 剩余的需要拆分值 那么肯定该方法是无效的 不符合题意 后面大于等于前面
        if(pre > rest) return 0;


        //分析常规情况 pre < rest时
        //rest可以如何拆分: 确保后面大于等于前面 所以rest拆分的最小值就是从pre开始 到 rest
        int ways= 0;
        for(int i = pre; i <= rest;i++){
            ways += process(i,rest-i);
        }
        return ways;
    }

    //方法二: 动态规划
    public static int dp1(int n){
        //边界判断  小于0 返回0 无效
        if(n < 0) return 0;
        //等于1时 拆分就只能是自己  返回1种
        if(n == 1) return 1;

        //分析递归可变参数 pre前置值 范围是1-n   剩余后续需拆分值rest 范围0-n
        int[][] dp = new int[n+1][n+1];  //pre前置表示行,0行不涉及不需要处理  目标值rest表示列

        for(int i = 1; i <= n; i++){
            dp[i][0] = 1;  //base case 剩余需拆分为0 说明前面都已经拆分好了返回1 第一列为0

            //对角线base case  dp[i][i]依赖 dp[i][rest-i] 此时rest=i 得到依赖dp[i][0] 也就是前面赋值得第一列得值 所以直接初始化1
            dp[i][i] = 1;
        }
        //base case: 如果前置数 大于 剩余的需要拆分值 那么肯定该方法是无效的 不符合题意 后面大于等于前面
        //也就是 dp[pre][rest] pre>rest 数组的下三角区 位置都是0  默认值就是0 无需处理

        //分析依赖 rest  依赖 rest-pre  也就是右边列依赖左边列
        //比如 dp[3][6]位置  前置拆分3  剩余6  依赖哪些位置: 6拆 1-5  2-4 3-3 4-2 5-1 6-0 其中前置3比1 2 大 所以不符合
        // 所以依赖 dp[3][3] dp[4][2] dp[5][1] dp[6][0]  仔细发现这个规律 是dp[3][6]左边 减去前置值3的列数位置dp[3][3]开始往左下位置直到不能拆分的位置
        //由于前面的初始化 我们已经填充好了矩阵的下三角区  根据某个位置的依赖情况 依赖于下一行 左边列  那么我们就可以从最底部的位置开始出发 这些依赖都是有填充好值的了
        //目前是dp[n-1][n]右下角位置 开始填充。所以从n-1行往上填充 每一行都已经填充到了dp[pre][pre]对角线位置 所以列就是从pre+1开始填充
        for(int pre = n-1; pre >=0; pre--){   //从n-1行往上填充
            for(int rest = pre+1;rest <= n; rest++){ //列就是从pre+1开始填充
                //列 第一列开始
                //分析常规情况 pre < rest时
                //rest可以如何拆分: 确保后面大于等于前面 所以rest拆分的最小值就是从pre开始 到 rest
                int ways= 0;
                for(int i = pre; i <= rest;i++){
                    ways += dp[i][rest-i];
                }
                dp[pre][rest] = ways;
            }
        }
        return dp[1][n];  //递归主程序调用 是前置1 目标数n 对于返回数组位置
    }

    //方法三: 动态规划 优化单个位置的枚举行为 通过相邻位置法 找到可以替换的具体有限个数
    public static int dp2(int n){
        //边界判断  小于0 返回0 无效
        if(n < 0) return 0;
        //等于1时 拆分就只能是自己  返回1种
        if(n == 1) return 1;

        //分析递归可变参数 pre前置值 范围是1-n   剩余后续需拆分值rest 范围0-n
        int[][] dp = new int[n+1][n+1];  //pre前置表示行,0行不涉及不需要处理  目标值rest表示列

        for(int i = 1; i <= n; i++){
            dp[i][0] = 1;  //base case 剩余需拆分为0 说明前面都已经拆分好了返回1 第一列为0

            //对角线base case  dp[i][i]依赖 dp[i][rest-i] 此时rest=i 得到依赖dp[i][0] 也就是前面赋值得第一列得值 所以直接初始化1
            dp[i][i] = 1;
        }
        //base case: 如果前置数 大于 剩余的需要拆分值 那么肯定该方法是无效的 不符合题意 后面大于等于前面
        //也就是 dp[pre][rest] pre>rest 数组的下三角区 位置都是0  默认值就是0 无需处理

        //分析依赖 rest  依赖 rest-pre  也就是右边列依赖左边列
        //比如 dp[3][6]位置  前置拆分3  剩余6  依赖哪些位置: 6拆 1-5  2-4 3-3 4-2 5-1 6-0 其中前置3比1 2 大 所以不符合
        // 所以依赖 dp[3][3] dp[4][2] dp[5][1] dp[6][0]  仔细发现这个规律 是dp[3][6]左边 减去前置值3的列数位置dp[3][3]开始往左下位置直到不能拆分的位置
        //由于前面的初始化 我们已经填充好了矩阵的下三角区  根据某个位置的依赖情况 依赖于下一行 左边列  那么我们就可以从最底部的位置开始出发 这些依赖都是有填充好值的了
        //目前是dp[n-1][n]右下角位置 开始填充。所以从n-1行往上填充 每一行都已经填充到了dp[pre][pre]对角线位置 所以列就是从pre+1开始填充
        for(int pre = n-1; pre >=0; pre--){   //从n-1行往上填充
            for(int rest = pre+1;rest <= n; rest++){ //列就是从pre+1开始填充
                //已知前面dp[3][6] 依赖累加的位置是 dp[3][3] dp[4][2] dp[5][1] dp[6][0]
                //通过尝试邻近位置发现在下位置 dp[4][6] 依赖值有公共位置 累加依赖位置 dp[4][2] dp[5][1] dp[6][0]  比dp[3][6]少一个dp[3][3]
                // 左边 拆分与前置值等大数 dp[3][6] 6拆分下个值是从3 开始 所以就是 6-3 来到的左边该位置 +  下位置的值
                dp[pre][rest] = dp[pre][rest-pre] + dp[pre+1][rest];
            }
        }
        return dp[1][n];  //递归主程序调用 是前置1 目标数n 对于返回数组位置
    }


    public static void main(String[] args) {
        int test = 39;
        System.out.println(ways(test));
        System.out.println(dp1(test));
        System.out.println(dp2(test));
    }
}

 

你可能感兴趣的:(算法,数据结构,动态规划,java)