有限背包计数问题
题意:
有一个大小为 \(n\) 的背包,你有 \(n\) 种物品,第 \(i\) 种物品的大小为 \(i\) ,且有 \(i\) 个,求装满这个背包的方案数有多少?
两种方案不同,当且仅当存在至少一个数 \(i\) 满足第 \(i\) 种物品使用的数量不同。
Solution:
首先考虑一个朴素的背包,设 \(f[i][j]\) 为考虑前 \(i\) 种物品,大小和为 \(j\) 的方案数,可以很快写出转移方程。
得到了一个 \(O(n ^ 2 \log n)\) 的做法,可以用前缀和做到 \(O(n ^ 2)\)。
发现当物品的编号大于 \(\sqrt n\) 时,这个物品的个数其实时不收限制的,因为不可能取超过 \(\sqrt n\) 个,这启发我们根号分治。
对于编号小于等于 \(\sqrt n\) 的物品,我们采用朴素的 01 背包做法,利用前缀和优化做到 \(O(n \sqrt n)\)。
对于编号大于 \(\sqrt n\) 的物品,我们对这一部分采用完全背包的做法,也可以做到 \(O(n \sqrt n)\)。
回顾完全背包的做法,设 \(f[i][j]\) 为选了 \(i\) 个物品,大小和为 \(j\) 的方案数。
它的转移运用到了一种思想:一个序列 \(a\),可以通过每次增加一个 \(1\),或者全体加上 \(1\) 来得到,这里相当于序列 \(a\) 种最小的数是 \(\sqrt n + 1\),所以每次添加的数是 \(\sqrt n + 1\)。
得到方程 ; \(f[i][j] = f[i][j - i] + f[i - 1][j - \sqrt n - 1]\)。
期中 \(f[i][j - i]\) 的含义是全体加上 1,\(f[i - 1][j - \sqrt n - 1]\) 的含义是新增一个最小的数。
最后设 \(f[i]\) 为只用编号小于等于 \(\sqrt n\) 的方案,\(g[i]\) 表示只用编号大于 \(\sqrt n\) 的方案。
答案即为 \(\sum f[i] \times g[n - i]\)。
cin >> n;
B = sqrt(n);
dp[0][0] = 1;
for(int i = 1; i <= B; i ++) {
for(int j = 0; j < i; j ++) s[j] = 0;
for(int j = 0; j <= n; j ++) {
int t = j % i;
s[t] = s[t] + dp[i - 1][j];
dp[i][j] = s[t];
if(j >= i * i) s[t] = s[t] - dp[i - 1][j - i * i];
}
}
for(int i = 0; i <= n; i ++) f[i] = dp[B][i];
memset(dp, 0, sizeof dp);
dp[0][0] = 1;
for(int i = 1; i <= B; i ++)
for(int j = 0; j <= n; j ++) {
if(j >= i) dp[i][j] = dp[i][j] + dp[i][j - i];
if(j >= B + 1) dp[i][j] = dp[i][j] + dp[i - 1][j - B - 1];
g[j] = g[j] + dp[i][j];
}
g[0] = 1;
Z ans = 0;
for(int i = 0; i <= n; i ++)
ans = ans + f[i] * g[n - i];
cout << ans << '\n';

浙公网安备 33010602011771号