揹包 學習筆記
甚至到退役都沒有系統地學習過這個東西,唉,草臺班子SDZX。
01揹包
到高中畢業也只會這一種。。
不過狀態轉移方程還是很好寫,注意如果要滾掉一維,直接倒序列舉容量即可。
例題 P1048
for(int i=1;i<=n;++i)
{
for(int w=m;w>=0;--w)
{
if(w-a[i]>=0)dp[w]=max(dp[w],dp[w-a[i]]+val[i]);
}
}
完全揹包
每個物品可以無限選,那麼比較naive的想法就是再在最裡面套一層列舉選取個數的迴圈,但奈何這種時間複雜度較高,想一想有沒有更加優秀的做法。
為什麼01揹包不能正序列舉?因為可能會出現 \(m\) 容量時,我已經用 當前物品做了一次轉移,也就是已經被用了,但是後面列舉 \(m+w_i\) 這個容量的時候,可能又會從 \(m\) 這個狀態 再裝一個當前物品進行轉移,這樣從物理意義上來說,就是用了兩次同一個物品,在 01揹包的意義下明顯是不合法的。
但是這種不合法恰恰就正好是完全揹包的要求所在啊!!!所以我們正序列舉就行。
for(int i=1;i<=n;++i)
{
for(int j=w[i];j<=m;++j)
{
dp[j]=max(dp[j],dp[j-w[i]]+val[i]);
}
}
多重揹包
不是無限個,可以選多次。
暴力地話就是完全揹包的naive做法。複雜度是 \(O(nmk)\) 的。
for(int i=1;i<=n;++i)
{
for(int j=m;j>=0;--j)
{
for(int cnt=0;cnt<=num[i]&&j-cnt*w[i]>=0;++cnt)
{
dp[j]=max(dp[j],dp[j-cnt*w[i]]+val[i]*cnt);
}
}
}
二進位制拆分可以減少無意義選取的次數,注意這裡要求拆分的方式能夠組合出所有數:複雜度 \(O(m\sum k_i)\)
for(int i=1,tval,tw,tnum;i<=n;++i)
{
re(tval),re(tw),re(tnum);
for(int j=1;j<=tnum;j<<=1)
{
w[++cnt]=tw*j,val[cnt]=tval*j;
tnum-=j;
}
if(tnum)w[++cnt]=tw*tnum,val[cnt]=tval*tnum;
}
for(int i=1;i<=cnt;++i)
{
for(int j=m;j>=w[i];--j)
{
dp[j]=max(dp[j],dp[j-w[i]]+val[i]);
}
}
還有一種比較優秀的單調佇列最佳化,但是今晚害得做一做 cf ,挖個坑。
//TODO
例題 P1776
分組揹包
每組裡面只能選取一件,直接列舉組數->列舉容量->列舉每個子物品就可以了。
for(int i=1;i<=t;++i)
{
for(int j=m;j>=0;--j)
{
for(int k=1;k<=a[i].cnt;++k)
{
if(j-a[i].w[k]>=0)dp[j]=max(dp[j],dp[j-a[i].w[k]]+a[i].val[k]);
}
}
}
費用揹包
多了一維費用,實際上就是兩維的揹包。
for(int i=1;i<=n;++i)
{
cin>>w[i]>>t[i];
for(int j=m;j>=w[i];--j)
{
for(int k=T;k>=t[i];--k)
{
dp[j][k]=max(dp[j][k],dp[j-w[i]][k-t[i]]+1);
}
}
}
例題 1855
依賴揹包
言下之意就是存在某些物品 \(B\) ,如果要選它 ,那麼必須先選另外一個指定的物品 \(A\) 。
發現這樣的話無非就要麼只買主件,要麼就同時買其中若干個附件,但是由於這些情況又不能夠同時成立,所以就轉化成了一個分組揹包的模型。
例題 P 1064