900. 整数划分
题目描述
一个正整数n可以表示成若干个正整数之和,形如:n=n1+n2+…+nk,其中n1≥n2≥…≥nk,k≥1。
我们将这样的一种表示称为正整数n的一种划分。(无序、可重复)
现在给定一个正整数n,请你求出n共有多少种不同的划分方法。
输入格式
共一行,包含一个整数n。
输出格式
共一行,包含一个整数,表示总划分数量。
由于答案可能很大,输出结果请对109+7取模。
数据范围
1≤n≤1000
输入样例:
5
输出样例:
7
思路一:完全背包思想
思路: 把整数1,2,3, … n分别看做n个物体的体积,这n个物体都没有使用次数限制,问恰好能装满总体积为n的背包的总方案数(完全背包问题变形)
状态表示: f[i][j]
表示前i个整数(1,2…,i)恰好凑成整数j的方案数
求方案数:把集合选0个i,1个i,2个i,…,s个i全部加起来,其中s*i<=j,(s+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 - s*i] ;
f[i][j - i] = f[i - 1][j - i] + f[i - 1][j - 2*i] + ...+ f[i-1][j - s*i];
因此有优化后的状态转移方程:
f
[
i
]
[
j
]
=
f
[
i
−
1
]
[
j
]
+
f
[
i
]
[
j
−
i
]
;
f[i][j]=f[i−1][j]+f[i][j−i];
f[i][j]=f[i−1][j]+f[i][j−i];
初始化问题:
由于i-1、j-i的存在,可能涉及第0行、第0列。
且我们从数字1,即第一行开始递推,就要用到第0行的初始化:
f[0][j]
,即第0行,不选择一个数选择凑到数字j,显然不可能,所以除了f[0][0]
,都初始化为0,什么都不选也可以凑到数字0,所以f[0][0]=1
。
f[i][0]
当给i个数字凑数字0时,那么什么都不选就可以了,所以f[i][0]=1
。
而我们只要初始化第0行就可以了,这样下面计算的时候让j从0开始循环,就可以让每一行的f[i][0]=1
了。
#include <iostream>
using namespace std;
const int N = 1e3 + 7, mod = 1e9 + 7;
int f[N][N];
int main() {
int n;
cin >> n;
f[0][0] = 1; "容量为0时,前 i 个物品全不选也是一种方案,凑够数字0,将第0行的f[0][0]初始化为0"
for (int i = 1; i <= n; i ++) {
for (int j = 0; j <= n; j ++) { "j从0开始"
f[i][j] = f[i - 1][j]; "同时处理j=0的列"
if (j >= i) f[i][j] = (f[i - 1][j] + f[i][j - i]) % mod;
}
}
cout << f[n][n] << endl;
return 0;
}
一维数组优化:
观察
f
[
i
]
[
j
]
=
f
[
i
−
1
]
[
j
]
+
f
[
i
]
[
j
−
i
]
;
f[i][j]=f[i−1][j]+f[i][j−i];
f[i][j]=f[i−1][j]+f[i][j−i];
当按行更新时,即先循环行,再循环列,求前i个数字下凑各种数字j的各种方案数,那么就可以只用到第i行和第i-1行,就可以优化为一维数组存储。
按照正序遍历,因为前面更新过的要被后面的用到。
算法实现
#include <iostream>
#define read(x) scanf("%d",&x)
using namespace std;
const int N=1010,mod=1e9+7;
int dp[N];
int main() {
int n;
read(n);
"初始化第0行"
dp[0]=1;
"每次更新一行,先循环i,再循环j,保证可以得到需要的数据"
for (int i=1;i<=n;i++)
for (int j=i;j<=n;j++) "正序遍历"
dp[j]=(dp[j]+dp[j-i])%mod;
printf("%d",dp[n]);
return 0;
}
思路二:人为划分集合
f[i][j]
表示i的j划分,即总和为i,数字i被用j个正数表示的方案数。表示的数字范围从1到n。
人为的将j个数的集合划分成两部分:是否包含数字1?如果包含,可以把1踢出去:如果不包含,可以让集合整体减1:
动态转移方程:f[i][j]=f[i-1][j-1]+f[i-j][j];
最终的结果:即数字n被j个数表示的方案总和,j=1,2,……,n,所以要求f[n][1]
到f[n][n]
的累加和。
初始化问题:
f[0][j]
,j个正数表示数字0; f[i][0]
,0个正数表示数字i。
第一行、第一列除了f[0][0]
外都初始化为0,f[0][0]=1
,因为0个正数可以表示数字0。
对于二维表f,主对角线处f[i][i]
指i个数表示数字i,必然全为1,只有一种情况,所以f[i][i]=1
。
第一列f[i][1]
,数字i用1个数表示,这个是肯定是i,所以f[i][1]=1
。
此外,这个最终的二维表f是个下三角矩阵,因为对于数字i,由j个数相加而成,j的最大值就是i。 i个1相加。
不能优化成一位数组:
- 按行更新时,求第i行要用到第i-1行和第i-j行,不是用到了两行,不可。
- 按列更新时,求第j列时,虽然只会用到j列和第j-1列,但我们最终需要二维表的第n行,按列更新时最终只会记录第j列,得不到我们要的最终结果。
算法实现
#include <iostream>
#define read(x) scanf("%d",&x)
using namespace std;
const int N=1010,mod=1e9+7;
int dp[N][N];
int main()
{
int n;
read(n);
//初始化
dp[0][0]=1;
for (int i=1;i<=n;i++)
for (int j=1;j<=i;j++)
dp[i][j]=(dp[i-1][j-1]+dp[i-j][j])%mod;
int res=0;
for (int i=1;i<=n;i++) res=(res+dp[n][i])%mod;
printf("%d",res);
return 0;
}
思路三:分治
状态表示:
f[n][m]
表示将数字n最多分成m份,最多m个数相加的n。
LL solve(int n , int m ){
"如果n == 1 显然只能分成一组 "
"如果m == 1 显然只能分成一组 "
if( n == 1 || m == 1) return 1 ;
"如果n > m "
"分成m份:那么给集合中每个数减去1,此时份数不变,等价于=> solve(n-m,m)"
"不分成m份: 递归求分成m-1份=>solve(n,m-1)"
if( n > m ) return (solve(n-m,m)+solve(n,m-1))%mod;
"如果n == m "
"情况同上,n > m,只不过分成n份时结果已知为1,就不用再去减1了, 不分成m份时=>solve(n,m-1)"
if(n == m ) return (1+solve(n,m-1))%mod ;
"如果n < m , solve(n,m) == solve(n,n)"
if(n < m ) return solve(n,n)%mod ;
}
C++代码
#include <iostream>
using namespace std;
const int N = 1010,mod = 1e9+7;
int f[N][N];
int main()
{
int n;
cin >> n;
for(int i = 1 ; i <= n ; i++) f[1][i] = f[i][1] = 1; "第一行第一列才都初始化为1"
for(int i = 2 ; i <= n ; i++ ){
for(int j = 2; j <= n ; j++ ){ "没有限制j<=i,所以上面第一行第一列才都初始化为1"
if(i == j) f[i][j] = (1+f[i][j-1])%mod ;
else if(i > j) f[i][j] = (f[i-j][j]+f[i][j-1])%mod ;
else f[i][j] = f[i][i]%mod;
}
}
cout << f[n][n] << endl;
return 0;
}