题目描述
长度为n的线段有多少种切割方法,切割时两个为一组,一组中第一个数字必须为4,两个数字都必须为正整数,例如n=10有两种:
| 4 | 6 |
| 4 | 1 | 4 | 1 |
注意,在所有可能的结构中,消息的长度不可以为 0。也就是说,不能出现这样的消息:| 4 | 0 |。
当然,答案可能会很大,所以你需要给出答案模 998244353 之后的结果。
示例1
输入:10
返回值:2
题目分析
直观的解法可以模拟切割过程,遍历所有可能的切割情况,根据题意可知,切割的每个部分(一组两个数)至少为5,最大可以到线段的长度,当剩余的线段长度小于5时,不能再进行切割,只有剩余的线段长度刚好等于0,才算是一种方法;
解题思路
方法1:模拟压缩过程,深度遍历所有可能的切割情况
dfs的遍历过程:
int res = 0; dfs(n){ if(n == 0){ // 因为最初线段长度一定大于0,所以在后面切割过程中n等于0,刚好切割完成 res++; } // 剩余的线段不足以维持一个组,返回 if(n<5) return 0; for(int i=5;i<=n;i++){ // 先分出一部分从5开始到n,遍历剩余n-i的部分 dfs(n-i); } }
直接dfs的过程,需要遍历很多重复的数字,每一层都要遍历n,但随着n的减少,每层遍历的次数也减少,时间复杂度约为,很容易超时,可以使用记忆化递归,将之前已经计算过的数字存储下来,减少遍历的次数。
方法2:找到状态转移方程,使用动态规划
1.定义 dp[i] 是线段长度为 i 时的切割方法数,初始化dp[0]为1;
2.根据方法1中的递归思路,可知:
①当 i 大于等于5时,dp[i] = dp[i-5] + dp[i-6] + ... dp[i-i](dp[0]) 等式①,
②当 i 大于5时, dp[i-1] = dp[i-6] + dp[i-7]...dp[0] 等式②;
将等式①减去等式②可得:dp[i] - dp[i-1] = dp[i-5];
最后得到状态转移方程: dp[i] = dp[i-1] + dp[i-5],只需要将i从6遍历到n,计算dp[n]的方法数。
代码实现
方法1:记忆化深度遍历所有可能的切割情况
int mod = 998244353; HashMap<Integer, Integer> map; public int messageCount (int N) { map = new HashMap<>(); return dfs(N); } public int dfs(int N){ // 记忆化存储 if(map.containsKey(N)){ return map.get(N); } // 线段分割完成,直接完成 if(N == 0) return 1; // 剩余线段长度小于5,不能分割 if(N < 5){ return 0; } // 记录长度为N的线段可以分割的方法种数 int res = 0; // 从N中分出 长度为j (最少为5)的线段,剩余长度为 1 ~ N-j for(int j=5;j<=N;j++){ // 继续递归 N-j 的部分 if(!map.containsKey(N-j)){ int add = dfs(N-j); map.put(N-j, add); } res = (res + map.get(N-j))%mod; } return res; }
时间复杂度:,使用记忆化后,每个数字只需要遍历一次,在外层的循环中,可以对之前遍历的数据直接返回,遍历的深度最多为 n/5 (每次都要减5),总次数 n^2/5,当数据量过大时,使用该方***报栈溢出错误;
空间复杂度:,递归的深度最多为 n/5,且需要 n 的空间来记忆化存储数据,所以空间复杂度为。
方法2:动态规划
public int messageCount (int N) { if(N < 5) return 0; int[] dp = new int[N+1]; int mod = 998244353; // 初始化 dp[5] = 1; for(int i=6;i<=N;i++){ // 状态转移方程 dp[i] = dp[i-1] + dp[i-5] dp[i] = (dp[i-1] + dp[i-5]) % mod; } return dp[N]; }
时间复杂度:,需要从6开始遍历到N,遍历的次数为 N-5,时间复杂度为;
空间复杂度:,需要借助 N+1 空间的数组,空间复杂度为。