将正整数m分裂为几块的和,每一块不下降,有几种分裂方法?

正整数m的分裂方法数,块与块间保证不下降

提示:读懂题目要做什么,理解什么是斜率优化
暴力递归到动态规划的四种尝试方案:
动态规划有四种尝试模型:尝试暴力递归代码,最后改为傻缓存或者动态规划DP代码;
DP1:从左往右的尝试模型【分析i从0–N如何如何】
DP2:从L–R范围上的尝试模型【分析范围上以i开头或者以i结尾如何如何】
DP3:样本位置对应模型【2个串,或2个数组这种,分析对应0–i,0–j上以i,或j结尾的子组、子串、子序列如何如何】
DP4:业务限制类模型【限定几种业务情况如何如何】


题目

一个正整数m,可以将其分裂为几块数字的和,要求每一种分裂的数字块不能出现下降,请问有多少种分裂方法数?


一、审题

示例:
– 1:分裂为1,就1种方案;
– 2:1+1,2;2种方案
– 3:有这些分裂方案:1+1+1,1+2,2+1,3,但是2+1中,21显然是一个下降的方案,所以不符合要求,只有3种方案;
– 4:1111,【前面是1,后面是3,跟上面一样,那就是3种方案】,4,则就是1+3+1种,5种方案。


二、解题

这个题呢,看样子还可以递推呢?一会试试

先来看本题的的解题,左神的方案。
要求后面的数,不能降序,也就是至少,前面分为啥数pre,后面剩下的数rest还不能小于前面的数:即pre<=rest;
定义这么一个暴力递归的函数f(pre, rest):表示前面一块为pre的情况下,还剩rest要去分裂,当分裂成功之后,方案数时多少?
主函数调用f(1,n),即:至少第一块要分为1,现在让你分裂n,请问有多少种分裂放法数?

(1)base case:当rest=0时,说明,n已经被分裂完成,return 1;即成功了一种方法;
(2)跳过base case(1),则说明rest>=1,还需要继续分裂rest
但是如果此时发现
pre>rest
,这是不行的,违规了,return 0;
(3)如果上面12都跳过了,说明还要找分裂rest的放法数,怎么分裂呢?把i从pre到rest枚举一遍,依次尝试前一块为i,后一块为rest-i的这种分裂方式 形成的方法数。
看图:
图1

暴力递归的代码:

public static int ways1(int n){
        if (n < 1) return -1;//n必须是正数

        return process(1, n);//前面那个1,保证第一个pre是不能小于1的数
    }

    public static int process(int pre, int rest){
        if (rest == 0) return 1;//当啥时候你分完了,就是一个合理的方案,后面的rest是大于pre的

        if (pre > rest) return 0;//一旦pre>rest就不行了

        int ans = 0;
        //如果还可以分裂,保证,至少我后续是从pre开始分裂的,最大是rest
        for (int i = pre; i <= rest; i++) {
            //这样就能保证分裂后续是升序而不降
            ans += process(i, rest - i);//俩的和是rest
        }

        return ans;
    }

显然,一看暴力递归,就是速度很慢的,还需要改为动态规划,本质就是填表
目前俩变量,pre和rest,分别将其命名为i,j,填dp一个二维表,i表示dp的行,j表示dp的列;
dp[i][j]代表将数n分裂为前面一个i,后面还要继续分为j的方案数,是多少?
本质这样也就是一个样本变量做行i,一个样本变量做列j的样本位置对应模型。
暴力递归怎么写的代码,咱们填表就怎么填

i从1–n枚举一遍,所以n+1长度,i=0咱不用它
j从0–n取值,因为i+j是恒定的,所以也是n+1长度
故我们需要填下面这个dp表:根据递归调用主函数,我们要打星那个格子的值,dp[1][n]
图2
暴力递归中几个条件:
i=0全部不需要填写
(1)base case:当rest=0(j)时,return 1;显然,不管pre(i)等于多少,j=0,则dp[i][j]=1,因而dp表中第0列,全为1;
(2)跳过base case(1),则说明rest>=1,还需要继续分裂rest
但是如果此时发现
pre>rest
,这是不行的,违规了,return 0;也就是说i>j那些格子,主对角线下方那些,全部是0,本身dp就是0,不管了;
(3)如果上面12都跳过了,说明还要找分裂rest的放法数,怎么分裂呢?把i从pre到rest枚举一遍,依次尝试前一块为i,后一块为rest-i的这种分裂方式 形成的方法数。这里的话,暴力递归怎么枚举,咱就怎么填dp表;
由于咱需要dp[1][n] ,所以肯定是从表格最后一行往上填,每次,从左往右填,这样才能求出dp[1][n];
看图,就知道填表的顺序:
图3
暴力递归修改为动态规划的代码:

public static int waysDP(int n){
        if (n < 1) return 0;

        int[][] dp = new int[n + 1][n + 1];
        //base case,rest==0时,全1,其他不管
        for (int pre = 1; pre <= n; pre++) {
            dp[pre][0] = 1;
        }
        //剩下的,pre>rest时,左下角非0列的其他格子全是0,默认就是了
        //然后,简单分析,既然是要dp[1][n],自然是先从右下方格子开始,从下往上推,然后从左往右推
        //或者,递归中i==pre,i<=rest,显然一个格子pre,rest,我ij依赖的是pre急下面的格子,rest,及左边的格子,所以应该从最右下角开始填写
        //看笔记就行

        for (int pre = n; pre >= 1; pre--) {
            for (int rest = pre; rest <= n; rest++) {
                //递归copy即可
                int ans = 0;
                for (int i = pre; i <= rest; i++) {
                    ans += dp[i][rest - i];
                }
                dp[pre][rest] = ans;//把表格填了,它依赖好多呢
            }
        }

        return dp[1][n];
    }

再观察,动态规划的代码中,仍然有三个for循环,显然是o(n**3)的时间复杂度,仍然太慢了;

这就是本文要讲的重点了:省去枚举行为的斜率优化方法,这是大厂面试中的重要考点,尤其是字节,每次必考的算法题。
而,要讲枚举行为省去,必然要找一个更为简单的dp[i][j]的表达式,怎么做呢?
一定是要简单举例,只有举例才能迅速观察出来;
比如,求dp[1][7],你必然枚举从i=1–7的所有格子
即这些的和:
dp[1][6],dp[2][5],dp[3][4],dp[4][3],dp[5][2],dp[6][1],dp[7][0]
再看dp[2][7],也就是dp[1][7]的下方那个格子,它是这些的和:
dp[2][5],dp[3][4],dp[4][3],dp[5][2],dp[6][1],dp[7][0]
发现了没,是上面除了dp[1][6]之外的别的那一串,咱能不能用来替换呢
也就是让dp[1][7]=dp[1][6]+dp[2][7]
看图:所以dp[i][j] = dp[i + 1][j] + dp[i][j - i] ,即下方的格子,加左边j-i那个格子,这个以依赖就非常简单了

好处是啥呢?
降低一个量级的时间复杂度:时间复杂度,2个for循环,显然o(n2)**
图4
看代码:

public static int waysDPNoRepeat(int n){
        if (n < 1) return 0;

        int[][] dp = new int[n + 1][n + 1];
        for (int pre = 1; pre <= n; pre++) {
            dp[pre][0] = 1;
        }

        //最后一行最后一个对角线的格子,得手动填写
        dp[n][n] = 1;//就一种分裂方式
        for (int pre = n - 1; pre >= 1; pre--) {
            for (int rest = pre; rest <= n; rest++) {
                if (pre + 1 <= n) dp[pre][rest] = dp[pre + 1][rest] + dp[pre][rest - pre];
                //dp[pre][rest - pre]能代表我枚举的一大堆值
            }
        }

        return dp[1][n];
    }

测试:

public static void test(){
        int n = 4;
        System.out.println(ways1(n));
        System.out.println(waysDP(n));
        System.out.println(waysDPNoRepeat(n));
    }

    public static void main(String[] args) {
        test();
    }

总结

提示:重要经验:

1)理解斜率优化的概念,通过观察dpij依赖周围的某几个格子来省去枚举行为
2)知道暴力递归的尝试到写出动态规划的转移方程来

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值