LOJ#6089 小 Y 的背包计数问题 - DP精题

2022-10-16,,,

题面

题解

(本篇文章深度剖析,若想尽快做出题的看官可以参考知名博主某C202044zxy的这篇题解:https://blog.csdn.net/C202044zxy/article/details/109141757)

在起码了解了背包DP后,我们来做这道题

既然每种物品最大可占据 i*i 的空间,设 

那么  和  两种情况肯定是不同的 ,而且应该可以分开处理

由于  时 i 的范围很小,可以在一个处理到一半的背包上(即处理了[sq+1,n]的背包dp数组)继续跑(背包大小 * 物品数),所以我们倒着处理背包,先做  的背包

[sq+1 , n]的处理

我原先是这么想的,

对于[sq+1 , n]中的任意一个 i ,肯定都有 i * i > n ,也就是说,每种物品最多也就能装下 i - 1 个,

那不妨就设 [sq+1 , n] 每种物品无穷多个,肯定不影响答案,于是后面部分就可以做完全背包了 ,成功省掉个数限制

但是这样只能过一半的分。

于是我来到NDSC看了 好几 篇题解, 终于  明白怎么做的了,

由于我们一般的背包DP本质上是一个二维的状态 dp[i][j] 表示进行到前 i 个物品,总大小为 j 的~~~,只是一般有用的都是最终的dp[n][j],所以第一维都滚动或者直接扔掉了

而这个部分的背包我们如果还是这么想的话,那就还是有 (n-sq) * n 种状态,

但是我们可以这样定义:dp[i][j] 为总共装了 i 件物品后,总大小为 j 的方案数,

因为这里每个物品大小起码都是 sq+1 ,所以最多装 sq 件物品,还不到,状态数就只有 sq * n ,大大减小

怎么转移呢, 大多数 题解上说:“1. 新加入一个大小为 sq+1 的数,2. 将之前 i 个数每个都加1”、“可以通过两种操作从 i - 1 的序列得到,即新加一个 √n + 1,或整体 + 1”、……(很明显第二对引号中是错的,因为整体加 1 后 i 不变,所以说网上题解真实性堪忧啊,唉)

大概的意思是 “  ”,怎么理解呢?

咱们得从 dp[i][j] 定义说起

dp[i][j] 其实呢,说它是一个状态,不过是许多最基本的状态的状态集,每个最基本的状态就是选了哪些物品的物品集(可重集),比如 dp[2][20] 就含有 {10,10}、{9,11}等状态

而 dp[i][j] 的状态转移,实际上是把一类的所有最基本状态,刚好地全部转移为另一类状态,

让我们看看这个状态转移是不是这样的:

首先初始状态是什么都没有装:(empty)

然后任何一个目标状态排个序:sq+a1、sq+a2、sq+a3 ……

可以证明,初始状态可以通过一条唯一路径转移到任意合法的目标状态,只用把状态反向转移回去:

    如果数集(也就是此题的物品集)中最小的数是 sq+1 (a1 == 1),则把它删了,物品数量 -1,再判断一次
    否则,把所有数 -1 ,最小的数就变为 sq+a1-1,重复这个过程,最小的数迟早变成 sq+1,然后进行转移 1

于是,物品数量会持续减,直到减为零,反向转移到初始状态!(唔,真的巧妙)

知道怎么转移后,便可做了,j 从前往后循环,然后把所有 j 相同的 dp[i][j] 加到 f[j] 里,再跑下面的背包

[1 , sq]的处理

按照背包的思路,可以这样转移(压掉第一维 i )

然后用多重背包的常规优化,用倍增减小每种物品的量,

但是  过不了

题解上说是前缀和优化,

我就纳闷了,前缀和不是每次sum[ j ] += sum[ j - 1 ]?可是这题是隔了一段 i 的!

难不成你每次sum[ j ] += sum[ j - i ]? i 一变化你还得重新跑!唉,离谱……

……

(好像真没问题)

i 只会变化 sq 次(显而易见),每次把[1,n]算一个隔段的前缀和,同上,然后O(1)转移

呜呼!然后输出 f[n] 就完了!

CODE

详见代码吧,不是很长(也就头有点长而已)

#include<map>
#include<queue>
#include<cmath>
#include<vector>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
#define MAXN 100005
#define LL long long
#define DB double
#define ENDL putchar('\n')
#define lowbit(x) ((-x)&(x))
LL read() {
LL f = 1,x = 0;char s = getchar();
while(s < '0' || s > '9') {if(s=='-')f=-f;s = getchar();}
while(s >= '0' && s <= '9') {x=x*10+(s-'0');s=getchar();}
return f*x;
}
const int MOD = 23333333;
int n,m,i,j,s,o,k;
int dp[325][MAXN],sq;
int f[MAXN],sum[MAXN];
int main() {
n = read();
sq = (int)sqrt((DB)n);
dp[0][0] = 1;
f[0] = 1;
for(int i = 1;i <= sq;i ++) {
for(int j = (sq+1)*i;j <= n;j ++) {
dp[i][j] = (dp[i-1][j-sq-1] + dp[i][j-i]) % MOD;
(f[j] += dp[i][j]) %= MOD;
}
}
for(int i = 1;i <= sq;i ++) {
for(int j = 0;j <= n;j ++) {
sum[j] = f[j];
if(j >= i) (sum[j] += sum[j-i]) %= MOD;
}
for(int j = i;j <= n;j ++) {//转移每次就加 sum[j-i]( - sum[j-i*i-i])
int pre = j - i*i - i; //注意必须要是关于 i 同余的相减
int k = (pre < 0 ? 0:sum[pre]);
(f[j] += ((LL)sum[j-i] +MOD- k) % MOD) %= MOD;
}
}
printf("%d\n",f[n]);
return 0;
}

精髓

这道题的精髓就在于,dp[i][j] 的设计,你得找到合适的归类方法,同时满足状态数足够少,转移复杂度足够小,这对于每道题是有很高的自由度的

这也许就是DP题的魅力所在吧

LOJ#6089 小 Y 的背包计数问题 - DP精题的相关教程结束。

《LOJ#6089 小 Y 的背包计数问题 - DP精题.doc》

下载本文的Word格式文档,以方便收藏与打印。