将整数 n 分成 k 份,且每份不能为空,任意两个方案不能相同(不考虑顺序)。
例如: n=7,k=3 ,下面三种分法被认为是相同的。
问有多少种不同的分法, 答案对 109 + 7 取模。
数据范围:
进阶:空间复杂度 ,时间复杂度
public static int divideNumber(int n, int k) { final int MOD = (int) (Math.pow(10, 9) + 7); // 定义模数 // 构建二维dp数组 // dp[i][j] 表示将i拆分成j份的方案数 int[][] dp = new int[n + 1][k + 1]; // 初始化dp数组 for (int i = 0; i <= n; i++) { dp[i][0] = 0; // 不能将i拆分成0份 } for (int j = 0; j <= k; j++) { dp[0][j] = 0; // 不能将0拆分成j份 } dp[0][0] = 1; // 将0拆分成0份只有1种方案 // 动态规划填充dp数组 for (int i = 1; i <= n; i++) { for (int j = 1; j <= k; j++) { if (i >= j) { dp[i][j] = (dp[i - 1][j - 1] + dp[i - j][j]) % MOD; } else { dp[i][j] = 0; } } } return dp[n][k]; }
public class Solution { private static final int MOD = 1_000_000_007; private int[][] memo; public int divideNumber(int n, int k) { this.memo = new int[n + 1][k + 1]; return dfs(n, k); } private int dfs(int n, int k) { if (n == 0 || n < k) { return 0; } if (k == 1 || k == n) { return 1; } if (memo[n][k] > 0) { return memo[n][k]; } return memo[n][k] = (dfs(n - 1, k - 1) + dfs(n - k, k)) % MOD; } }
展示一下本平民对这道题的思考过程:暴力递归->动态规划->优化动态规划
import java.util.*; public class Solution { /** * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 * * * @param n int 被划分的数 * @param k int 化成k份 * @return int */ public int divideNumber (int n, int k) { // write code here return recurrent(1, n, k); } /** * 分区调度,对模型按照复杂度划分为partition个工作量相近的分区 * @param prev 前一个划分值 * @param rest 目标数剩余部分 * @param k 剩余划分数 * @return 返回方法数 */ private int recurrent(int prev, int rest, int k) { if(rest == 0 && k == 0) { return 1; // 剩余要凑的数没有了,形成一种方案 } if(rest < prev || k < 0) { return 0; // 剩余数为负数,或者没有划分操作次数了,当前划分方案失效 } // 尝试当前可以划分出来的值,为了保持单调不减,需以从前一个数为下界 int ways = 0; for(int i = prev; i <= rest; i++){ ways += recurrent(i, rest - i, k - 1); } return ways; } }暴力递归仅用来形成一个思路,实际跑的话是会超时的,但我们根据递归的思路可以改出动态规划的版本。
import java.util.*; public class Solution { /** * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 * * * @param n int 被划分的数 * @param k int 化成k份 * @return int */ public int divideNumber (int n, int K) { // write code here int[][][] dp = new int[n + 1][n + 1][K + 1]; // 只要rest和k归零,就形成一种方案 for(int prev = 1; prev <= n; prev++){ dp[prev][0][0] = 1; } for(int rest = 1; rest <= n; rest++){ for(int prev = 1; prev <= rest; prev++){ for(int k = 1; k <= K; k++){ for(int i = prev; i <= rest; i++){ dp[prev][rest][k] += dp[i][rest - i][k - 1]; } } } } return dp[1][n][K]; } }考虑到题中要求的时间和空间复杂度,再看看能不能删掉一些参数,从时间复杂度O(nk)来看,显然是不能删去k这个参数的。那么再想想rest和prev哪个可以删掉?考虑到某个数n拆分成k份,dp[n][k]会有两种情况:
import java.util.*; public class Solution { /** * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 * * * @param n int 被划分的数 * @param k int 化成k份 * @return int */ public int divideNumber (int n, int K) { // write code here int[][] dp = new int[n + 1][K + 1]; dp[0][0] = 1; for(int rest = 1; rest <= n; rest++){ for(int k = 1; k <= K && k <= rest; k++){ dp[rest][k] = (dp[rest - 1][k - 1] + dp[rest - k][k]) % 1000000007; } } return dp[n][K]; } }
/** * dp[i][j]将数i分为j分的分法 * 拆分的结果不包含1的情况:如果不包含1,我们把n拆分成k块时可以看做先将每一块加上个1, * 则n还剩余n-k,即f(n-k,k) * 拆分的结果包含1的情况:那么就直接选一个1,即f(n-1,k-1)。 * dp[i][j]=dp[i-j][j]+dp[i-1][j-1] * @param n * @param k * @return */ public int divideNumber (int n, int k) { // write code here int[][] dp = new int[n+1][k+1]; dp[0][0]=1; for (int i=1;i<=n;i++){ for (int j=1;j<=k&&j<=i;j++){ dp[i][j]=dp[i-j][j]+dp[i-1][j-1]; } } return dp[n][k]; }