问题描述:求将正整数 n 无序拆分成最大数为 k 的拆分方案个数,要求所有的拆分方案不重复。
例如 n=5, k=5时, 对应的拆分方案如下:
5 = 5
5 = 4 +1
5 = 3 + 2
5 = 3 + 1 + 1
5 = 2 + 2 + 1
5 = 2 + 1 + 1 + 1
5 = 1 + 1 + 1 + 1 + 1
dp求解问题的特点:最优性原理,无后效性,重叠子问题。
dp求解的核心就是得到状态转移方程,并注意对边界情况的处理。
定义f(n, k)为正整数n拆分成最大数为k的拆分方案个数,下面根据 n, k 不同取值情况写出对应的状态转移方程。
(1)当处于边界情况 n=1 或 k=1 时,明显 f(n, k)=1
(2)n<k 时,f(n, k)=(n, n)
(3)n=k 时,n要么拆分为最大数n的一个方案,或者最大数为n-1的方案,对应f(n, k)=f(n, k-1)+1
(4)n>k 时,拆分方案中分为含 k 和不含 k 两种情况:
- 如果包含k,那么剩下数之和为n-k,n-k仍有可能大于k,所以对应的状态方程 f(n-k, k)。
- 如果不包含,那么最大的拆分数变为k-1,对应 f(n, k-1)。
综合以上,当 n>k 时,状态转移方程 f(n, k)=f(n-k, k)+f(n, k-1)。
1. 数组标记法动态规划(自下而上)
每次计算中都是用到之前的记录,这样就可以先从小到大计算出程序,使得计算较大数的时候调用已经计算出的较小的记录,程序直接是用循环就可以完成任务,避免了重复计算和空间栈的开销。
public class Solution {
public static int dp(int n, int k) {
int[][] dp = new int[n + 1][k + 1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= k; j++) {
if (i == 1 || j == 1) {
dp[i][j] = 1;
} else if (i < j) {
dp[i][j] = dp[i][i];
} else if (i == j) {
dp[i][j] = dp[i][j - 1] + 1;
} else {
dp[i][j] = dp[i - j][j] + dp[i][j - 1];
}
}
}
return dp[n][k];
}
public static void main(String[] args) {
System.out.println(dp(5, 5)); // 输出7
}
}
2.递归+记录数组(自上而下)
该问题本身属于递归的也可采用递归实现,但递归中存在很多相同的子问题,为避免重复计算可增加一个记录数组dp,用dp[n][k]记录f(n, k)的值,初始dp数组全为0,这样如果某个位置不为0便可以直接返回避免计算。这种通过记录数组避免重复计算的方法叫做备忘录方法。
public class Solution {
public static int dp(int n, int k) {
int[][] dp = new int[n + 1][k + 1];
recursion(n, k, dp);
return dp[n][k];
}
private static int recursion(int n, int k, int[][] dp) {
if (dp[n][k] != 0) {
return dp[n][k];
}
if (n == 1 || k == 1) {
dp[n][k] = 1;
} else if (n < k) {
dp[n][k] = recursion(n, n, dp);
} else if (n == k) {
dp[n][k] = recursion(n, k - 1, dp) + 1;
} else {
dp[n][k] = recursion(n - k, k, dp) + recursion(n, k - 1, dp);
}
return dp[n][k];
}
public static void main(String[] args) {
System.out.println(dp(5, 5));
}
}