揹包

blind5883發表於2024-04-26

一般程式碼只是例子,具體使用依據題目來, 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方案最大價值的方案數,
有兩種大方向

  1. 利用不超過直接求解,因此f初始化都為0,對於每種情況f[i][j]最少有1種方案,然後轉移時如果可以更新就直接更替cnt,相等就加上cnt,最後直接輸出cnt[n][m]即可
  2. 利用恰好,把不超過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題庫
上面是較好的一道,也是較難的一道。
下面的較為簡單適合入入門。

求具體方案

這種會問你最後選擇的方案是是什麼,而不是方案數,可能有限制,如選擇字典序最小的最優選擇方案是什麼。這種有兩種思路。

  1. 直接儲存方案,利用string,或者vector,在轉移過程中儲存方案,透過比較方案來選擇最小字典序,這種一般較慢,但很穩定。
  2. 先求出最優方案,再利用最優方案,從大列舉,或從小列舉,推出選擇方案。這種較快,但邊界和列舉設計需要考慮清楚。
    AcWing 12. 揹包問題求具體方案 - AcWing
    具體程式碼如上

關於狀態轉移的奇技淫巧

  1. 有時候我們不能直接轉移狀態,而要合成狀態,像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)

相關文章