一般程式碼只是例子,具體使用依據題目來, DP是一種思想,程式碼都以屬性為最大值等等為例子
01揹包
最基本的揹包
簡單說就是有n個物品和容量為m的包,求其max/min/方案數等等即屬性
一般轉移方程為f[i][j]
意思為在前i個裡容量為j的情況下的要求的屬性
(可忽略)一般這裡的轉移是在f[i][j]
,第i個數取與不取
時間複雜度O(n*m)
一般程式碼
for (int i = 1; i <= n; i ++ ) // 列舉當前幾個物品
{
int v, w;
cin >> v >> w;
for (int j = 1; j <= m; j ++ )
{
f[i][j] = f[i - 1][j];
if (j >= v) f[i][j] = max(f[i][j], f[i - 1][j - v] + w);
}
}
一維最佳化
for (int i = 1; i <= n; i ++ ) // 列舉當前幾個物品
{
int v, w;
cin >> v >> w;
for (int j = m; j >= v; j -- ) // 體積
f[j] = max(f[j], f[j - v] + w);
}
必須倒序列舉體積,我們的f[j - v]用的實際是f[i - 1][j - v], 而正序列舉我們可能會先更新了
f[j - v]使得後面用到f[j - v]時實際用的是f[i][j - v]從而錯誤, 只要倒序就可以解決此問題
完全揹包
即有n種物品,每種無限個
一般的轉移方程為 f[i][j] = max(f[i - 1][j], f[i][j - v]);
最佳化程式碼
O(n*m)
for (int i = 1; i <= n; i ++ )
{
int w, v;
cin >> v >> w;
for (int j = 0; j <= m; j ++ )
{
f[i][j] = f[i - 1][j];
if (j >= v) f[i][j] = max(f[i][j], f[i][j - v] + w);
}
}
樸素版
O(n*m*k)
for (int i = 1; i <= n; i ++ )
{
int v, w;
cin >> v >> w;
for (int j = 1; j <= m; j ++ )
for (int k = 0; k <= j / v; k ++ )
f[i][j] = max(f[i][j], f[i - 1][j - v * k] + w * k);
}
方程推導
正常的思路:
f[i][j] = max(f[i - 1][j], f[i - 1][j - v] + w, f[i - 1][j - 2v] + 2w,...,f[i][j - kv] + kw)
k∈(0, j/v);
這種複雜度過高,一般接受不了
一般的方程f[i][j] = max(f[i - 1][j], f[i][j - v]);
f[i][j - v]
如何得來? (注意max中的對應)
f[i][j] = max(f[i - 1][j], f[i - 1][j - v], f[i - 1][j - 2v],...,f[i][j - kv])
########f[i][j - v] = max(f[i - 1][j - v], f[i - 1][j - 2v],...,f[i][j - kv])
k是相同的
可得f[i][j] = max(f[i - 1][j], f[i][j - v]);
加上價值就是f[i][j] = max(f[i - 1][j], f[i][j - v] + w);
一維最佳化
for (int i = 1; i <= n; i ++ )
{
int v, w;
cin >> v >> w;
for (int j = v; j <= m; j ++ )
f[j] = max(f[j], f[j - v] + w);
}
這裡只能正序列舉,和01揹包相反,因為我們轉移的實際上是f[i][j - v]
,正序可以提前更新f[j - v]
使他實際上成為f[i][j - v]
符合要求倒序則為f[i - 1][j - v]
不合要求
轉移的注意
對於完全揹包,一定要轉移完全,即在從k = 0的時候也要轉移進去,不能終斷,否則會出現錯誤
如P1941 [NOIP2014 提高組] 飛揚的小鳥 - 洛谷 | 電腦科學教育新生態 (luogu.com.cn)如果直接做即完全揹包的上升和01揹包的下降同時轉移,那麼祝賀你,會得到70分的程式碼.
多重揹包
有n種物品但是每種物品有數量限制
樸素程式碼
for (int i = 1; i <= n; i ++ )
{
int w, v, s;
cin >> v >> w >> s;
for (int j = 0; j <= m; j ++ )
for (int k = 0; k <= min(s, j / v); k ++ )
f[i][j] = max(f[i][j], f[i - 1][j - k * v] + k * w);
}
二進位制最佳化
把物品的個數S拆成一堆數,形成一個單獨的物品,最後採用01揹包去做
一般採用二進位制的方法
因為一堆二進位制數加起來可以加成任何數
如0001,0010,0100,1000這四個二進位制數相加,可以組成任何小於等於1111的數(正數)
for (int i = 1; i <= n; i ++ )
{
int v, w, s, k = 1;
cin >> v >> w >> s;
while (k <= s)
{
V[ ++ cnt] = v * k;
W[cnt] = w * k;
s -= k; // 這樣拆是logn個比較高效
k <<= 1;
}
if (s > 0) // 我們減到最後可能會有剩下的,也加上
{
V[ ++ cnt] = v * s;
W[cnt] = w * s;
}
}
for (int i = 1; i <= cnt; i ++ )
{
int v = V[i], w = W[i];
for (int j = m; j >= v; j -- )
f[j] = max(f[j], f[j - v] + w);
}
一般夠用
單調佇列最佳化
口訣:比你小還比你強,你就出列了~~~
時間複雜度為O(nm)
因為一般m都比較大所以常用[[DP的通用最佳化#滾動陣列]]
程式碼
for (int i = 1; i <= n; i ++ )
{
int w, s, v;
cin >> v >> w >> s;
memcpy(g, f, sizeof f);
for (int j = 0; j < v; j ++ )
{
int hh = 0, tt = -1;
for (int k = j; k <= m; k += v)
{
if (hh <= tt && q[hh] < k - s * v) hh ++ ;
while (hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) tt -- ;
q[ ++ tt] = k;
f[k] = max(f[k], g[q[hh]] + (k - q[hh]) / v * w);
}
}
}
思路
首先你要知道在s個物品限制下轉移方程為(一般情況s >= m / v
)
這裡推薦看我以前的筆記
f[i][j] = max(f[i - 1][j], f[i - 1][j - v] + w, f[i - 1][j - 2v] + 2w, ... , f[i - 1][j - sv] + sw);
f[i][j - v] = max(f[i - 1][j - v], f[i - 1][j - 2v] + 1w, ... , f[i - 1][j - sv] + (s - 1)w, f[i - 1][j - (s + 1)v] + sw);
f[i][j - 2v] = ...
f[i][j - 3v] = ...
...
r = j % v;
f[i][r + v] = max(f[i - 1][r + v], f[i - 1][r] + w)
f[i][r] = f[i - 1][r]
經典的圖啊
根據完全揹包最佳化的思路我們可以得到上面的圖(也可以看我過去筆記的圖片)
我們能發現一個事情,因為有s的限制,在一般情況下,我們沒法直接用f[i][j - v]來轉移
but我們可以從它的餘數出發, f[i][r], 這個是值固定的,往上f[i][r + v]看圖
越往上我們會發現,在他們的max內的第一個都在增加v(r + v), 且當s > j / v時它就開始滑動(像f[i][j], f[i][j - v], max的數量一樣,但是f[i][j]
向前進了1個v, 且w的值都加了1w, 除了新進來的)
這就想到了單調佇列,可以靠它維護這個區間最大值,最佳化的點,保持佇列隊頭最大,利用這個最大,最佳化計算量;並且可以計算滑動視窗內的最大值
看看程式碼,多想想
因為過於複雜還是看y的課比較好。
以前的筆記
如果你還有記憶的話應該會懂得
一般比二進位制最佳化快
二維費用揹包
很簡單就是有兩個費用,多開一維,記錄費用即可非常簡單
狀態一般為 f[i][j][k]
在前i個數內,在兩個費用都不超過j和k的情況下的屬性
可以和上面三種揹包結合,相當於字首
一般程式碼(二維費用01揹包)
for (int i = 1; i <= n; i ++ )
{
int w, v, t;
cin >> v >> t >> w;
for (int j = m; j >= v; j -- )
for (int k = p; k >= t; k -- )
{
f[j][k] = max(f[j][k], f[j - v][k - t] + w);
}
}
不少於問題
之前沒整理到這個
簡單說就是對於費用不少於的問題,這也可以和上面三種揹包連用,相當於字首
對於這個直接DP分析即可,關於不少於,直接設f[i][j]
為在前i個裡面,費用不少於j的情況
那麼第i個可選可不選,不選就是f[i - 1][j]
,選的話是f[i - 1][j - v]
這時候可能會有j < v的情況,這時候也要轉移,因為它符合,費用不少於j(大於了當然可以),寫成這個就行f[i - 1][max(0, j - v)]
。從相當於從0直接到j。
這時候回頭想想,好像之前的當j < v的時候就不轉移了(可看看上面程式碼),這是因為之前狀態是費用最大為j,這就是本質的差距。
恰好問題
這是一類小問題,狀態設計要改變為,恰好,轉移沒什麼變化,但初始化有變動,一定要先設定為不可能情況,因為對於一種狀態
像有許多石頭,價值和重量無關,求恰好重量為j時的最大價值,石頭有(前面是價值,後面是重量){1, 1},{2, 1},{2, 2}
當f[2][3]轉移的時候,f[1][2]這種情況是不可能的,所以f[2][3] = max(f[1][2] + 2)
整個轉移是轉移不了的,但是價值不大於j時是可以的。這就是它的本質
求方案數
這類其實有點技巧,最穩妥的方法是,把題目改為恰好,這裡用一個例題來說明。
求最優方案數
AcWing 11. 揹包問題求方案數 - AcWing
題中要求不超過j方案最大價值的方案數,
有兩種大方向
- 利用不超過直接求解,因此f初始化都為0,對於每種情況
f[i][j]
最少有1種方案,然後轉移時如果可以更新就直接更替cnt,相等就加上cnt,最後直接輸出cnt[n][m]
即可 - 利用恰好,把不超過j,改為恰好為j,轉移和上面一樣,然後在
f[n][j]
中記錄,最大值,並把所有的最大值相等的方案加上。
如上程式碼在上面連結中
因此對於一個問題,除了特殊記錄以外,就可以直接轉移或利用恰好湊出方案
而轉移時如果可以更新就直接更替cnt,相等就加上cnt,這是所有方案問題的最基本的思想,不限於動態規劃
關於方案的儲存
問題在儲存選的方案,像這個AcWing 1013. 機器分配 - AcWing比較典型,除了求解答案,還要輸出每個工廠選擇的機器數,可以dfs,可以像我這樣原路推回去(實際上更多使用這種),
沒有固定的方式,這裡只說明有此類問題。
列舉的狀態
正常揹包的轉移為f[i][j] = max(f[i][j], f[i - 1][j - v] + w)
我們依賴於第i - 1個物品進行轉移,實際上還可以依賴體積,來轉移。
思路為
{不選}
{選擇體積最大為k的子集,並加上這個點} k <= j
對於正常的揹包,這是沒有用的,但對於樹形等物件間有特殊關係的,有奇效.
AcWing 10. 有依賴的揹包問題 - AcWing這題就需要對於每個子樹分配體積,來計算
否則複雜度將爆炸
有依賴的問題
分為很多種,例如可能依賴是樹形的,可能是線性的,等等。
中心思想就是轉移好子集,不能言傳。
AcWing 10. 有依賴的揹包問題 - AcWing
487. 金明的預算方案 - AcWing題庫
上面是較好的一道,也是較難的一道。
下面的較為簡單適合入入門。
求具體方案
這種會問你最後選擇的方案是是什麼,而不是方案數,可能有限制,如選擇字典序最小的最優選擇方案是什麼。這種有兩種思路。
- 直接儲存方案,利用string,或者vector,在轉移過程中儲存方案,透過比較方案來選擇最小字典序,這種一般較慢,但很穩定。
- 先求出最優方案,再利用最優方案,從大列舉,或從小列舉,推出選擇方案。這種較快,但邊界和列舉設計需要考慮清楚。
AcWing 12. 揹包問題求具體方案 - AcWing
具體程式碼如上
關於狀態轉移的奇技淫巧
- 有時候我們不能直接轉移狀態,而要合成狀態,像
f[i + 1][j + v] = ...
這時候可能會有“卡殼”情況,即我們無法直接判斷情況,這時候不妨從0出發把for (int i = 1; i <= n; i ++ )
變為for (int i = 0; i < n; i ++ )
這樣“退位”,在某些時候有奇效如這題P1156 垃圾陷阱而這就是刷表法,[[刷表法和填表法]]
變化的價值/體積(泛化物品的揹包)
對於一類題,它的價值和體積會隨著你的選擇順序不同而改變,這時候需要考慮優先順序,
常用方法
先設x,y為兩個相鄰物品,然後列出(如價值)的不等式,像如此價值的情況
先x後y w = (p + c[x]) * b[x] + (p + c[x] + c[y]) * b[y] (1)
先y後x w = (p + c[y]) * b[y] + (p + c[y] + c[x]) * b[x] (2)
我們如果要讓x在前的話,那麼(1) > (2)
可以解得 c[x] * b[y] > b[x] * c[y]
,然後我們利用這個性質排序,再進行揹包處理
做了那麼多題,對於揹包,正確的物品排序確實很重要。
給出對應的例題試試吧P1417 烹調方案 - 洛谷 | 電腦科學教育新生態 (luogu.com.cn)
在最值情況下的最值
這類題題意如求在最大價值1的情況下,最小的價值2為多少,即在最大價值1下的最小价值2,
這種算是比較好做,只要在轉移出最大價值1的同時,記錄最小价值2,更新時替換,相等時對比求最小。注意,前提是最大價值1,所以要對它Dp
例題P1509 找啊找啊找GF - 洛谷 | 電腦科學教育新生態 (luogu.com.cn)