题目描述:
对于一个整数n 来说可以表示为n = n1 + n2 +
n3 + … + nk 其中n1 >= n2 >= n2 … >= 1
求解n 有多少种划分方法
1、递归
如果n 是4, 可以分为:
4 = 4
4 = 3 + 1
4 = 2 + 2
4 = 2 + 1 + 1
4 = 1 + 1 + 1 + 1
总共5中划分方法,观察上面的等式,可以发现每次都有一个最大的ni 比如4 = 3 + 1, 最大的ni = 3
所以可以定义一个函数f(n, m) 表示最大的ni 不超过m 的划分方法个数
那么最后的答案就是f(n, n)
不是一般性考虑对于任意的f(n, m) 如何转化
1· 如果m > n 由于ni 不会大于n 所以 f(n, m) = f(n, n)
2· 如果n == m, 当ni = n 的时候划分集合只有{n} 这时候可以将ni 单独拿出,f(n, n) = 1 + f(n, n - 1)
3· 当m < n 时, 若ni = m,可以划分为{m, {n1, n2 … nk}} 这么一种集合,sum{n1, n2 … nk} = n - m这时候f(n, m) = (n - m, m)
若ni != m,可以划分为{n1, n2, … nk} 集合,这时候f(n, m) = f(n, m - 1)
4· 当n == 1 || m == 1 只有一种方案 {1, 1, 1,…,1}
所以代码为:
int f(int n, int m) {
if(n <= 0 || m <= 0) return 0;
if(n == 1 || m == 1) return 1;
if(m > n) return f(n, n);
if(m == n) return f(n, m - 1) + 1;
return f(n, m - 1) + f(n - m, m);
}
2、完全背包
观察划分的方式可以发现,这是一种特殊的组合问题,从1 ~ n 中选任意的数使得总和等于n 的方案个数
这样就抽像为经典的完全背包问题
物品为1 ~ n 中间的数,体积为i, 价值为i
定义f(i, j) 表示考虑[1, i] 之间的数,使得总和为j 的方案个数
状态转移:f(i, j) = f(i - 1, j) + f(i - 1, j - i) + f(i - 1, j - 2 * i) + … + f(i - 1, j - k * i)
最后答案就是f(n, n), 初始化f(0, 0) = 1
int get_back(int n) {
int[][] f = new int[n + 1][n + 1];
f[0][0] = 1; // 初始化
for(int i = 1; i <= n; ++i)
for(int j = 1;j <= n; ++j )
for(int k = 0;k * i <= j; ++k)
f[i][j] += f[i - 1][j - k * i];
return f[n][n];
}
时间复杂度:O(n3) ,可以优化为O(n2)这里就不介绍了
3、dp
除了用背包的思想还可以将划分的集合分类,用dp
- 划分集合的最小值是1
- 划分集合最小值不是1
可以定义dp[i][j] 表示 考虑正好有i 个数使得和为j 的方案数
那么如果最小值为1,可以去掉最后一个1,dp[i][j] = dp[i - 1][j - 1]
如果最小值不是1 可以将集合所有元素都减去1, 总共可以减去i 个1 所以dp[i][j] = dp[i][j - i]
状态转移:dp[i][j] = dp[i - 1][j - 1] + dp[i][j - i]
因此代码为:
int get_dp(int n) {
int[][] dp = new int[n + 1][n + 1];
// 初始化 dp[1][i] 集合只有一种可能 {i}
for(int i = 1;i <= n; ++i ) dp[1][i] = 1;
for(int i = 2;i <= n; ++i )
for(int j = i;j <= n; ++j)
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j - i];
// 最后统计所有的dp[i][n] 就可以
int res = 0;
for(int i = 1;i <= n; ++i) res += dp[i][n];
return res;
}
时间复杂度:O(n2)