将整数 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]; }