服務端的同步方式和非同步方式
揹包問題
我們可以從集合的角度來看待這個問題,主要是如何表示狀態以及進行狀態間的轉移(決策)。
通常可以考慮兩個角度
-
狀態表示
- 集合:當前這個狀態代表的具體的含義
- 屬性:它的特點,如最大最小等
-
狀態計算
狀態計算本質是對集合的劃分,再進一步想其實是對於當前這個集合怎麼劃分才能不重不漏(不一定不重,但一定不能遺漏)以便於後面進行集合的決策轉移,也就是劃分成更小的子集使這些子集都可以被計算出來。
優化的根本是有一個東西代表一類東西,而且大部分是在DAG上做遞推,我當前這個狀態如果從所有能夠到他的狀態都列舉決策過,那麼當前這個狀態一定是最優的(最優子結構),因此對於下一個狀態就不用再從頭開始轉移了,他只會用到一些最優子結構來形成一個新的最優子結構。一般列舉狀態的時候要滿足拓撲序
01揹包
在一些限制下,每個物品最多選一次。
狀態表示 :f(i,j) :表示從前i個裡面選體積不超過j的最大價值
狀態計算:可以通過第i個物品選於不選將集合劃分成兩個子集,且不重不漏
轉移:$f(i, j) = max(f(i-1,j),f(i-1,j-v[i]+w[i]))$。
發現當前這個狀態是用到了第$i- 1$層的狀態,如果我們想用滾動陣列將$f$優化到一維,我們可以倒敘列舉$j$,因為只會從他前面更小的$j$,因為是倒敘列舉的所以前面更小的$j$對應的還是第$i-1$層的狀態。
轉移:$f(j) = max(f(j),f(j-v[i] + w[i]))$。
完全揹包
在一些限制下每個物品可以選無數次。
狀態表示:f(i,j): 表示從前i個裡面選體積不超過j的最大價值
狀態計算:可以通過將第i個物品選多少個將集合劃分,且補充不漏
對於第i個物品體積不超過j則有:$f(i,j)=max(f(i-1,j),f(i-1,j-v)+w,f(i-1,j-2v)+2w,...,f(i-1,j-kv)+kw)$
對於第i個物品體積不超過j-1則有:$f(i, j-v)= max(\ \ \ \ f(i-1,j-v)+w,f(i-1,j-2v)+2w,...,f(i-1,j-kv)+kw)$
將下面帶入上面的發現$f(i,j)=max(f(i-1,j),f(i, j - v)+w)$
轉移:$f(i,j)=max(f(i-1,j),f(i, j - v)+w)$
發現當前狀態轉移過來用到的是第$i$層的狀態,因此滾動陣列優化只需要正序列舉體積即可。
轉移:$f(j)=max(f(j),f(j-v)+w)$
多重揹包
在一些限制下每個物品可以選一定的次數。
思路一(暴力)
發現我們多加一維來表示第i個選多少個即可,或者假設某個物品可以選k次那就把它拆成k個一樣的物品即可,時間複雜度為$O(nm)$
核心程式碼
for (int i = 1; i <= n; i ++ )
for (int j = 0; j <= m; j ++ )
for (int k = 0; k <= s[i] && k * v[i] <= j; k ++ )
f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);
思路二(二進位制優化)
考慮優化暴力裡的第二個方法,不把k拆成k個1,我們把k轉化成二進位制形式假設為1011,則有$1011=0111+100$,我們可以把它拆成$1,2,4,4$這樣,這樣是可以組合出$0-11$裡的任何一個數的,因為$111$每一位選與不選可以組合出$0-7$中任何一個數,然後對於$100$這個數選與不選相當於讓$0- 7$往後平移$4$,所以可以湊出任何一個數。複雜度降到了$O(mlogn)$
核心轉化程式碼
for (int i = 1; i <= n; i ++ ) {
int a, b, s;
cin >> a >> b >> s;
int k = 1;
while (k <= s) {
cnt ++;
v[cnt] = k * a;
w[cnt] = k * b;
s -= k;
k <<= 1;
}
if (s > 0) {
cnt ++;
v[cnt] = s * a;
w[cnt] = s * b;
}
}
思路三 (單調佇列優化)
分組揹包
在一定限制下每組物品最多選一個。
狀態表示:f(i,j):從前i組裡選,且體積是j的最大價值
狀態計算:我們可以多加一維迴圈來列舉第i組裡選哪個,這樣就變成了01揹包
轉移:$f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);$
可以像01揹包一樣倒著列舉體積優化掉第一維
轉移:$ f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);$
有依賴的揹包問題
對於一顆樹選擇某個結點就必須選擇他的父節點。
狀態表示:f(i,j):以i為根的子樹必須選根節點,且體積不超過j的最大價值
狀態計算:對於每個結點$u$來說,先遞迴處理它的子節點當子結點資訊正確後再向父節點進行轉移,將集合劃分成當前結點的這個子節點的體積是多少,可以將每一棵子樹看成一個物品組,就變成了一個分組揹包問題。
核心程式碼:
int dfs(int u) {
for (int i = v[u]; i <= m; i ++ ) f[u][i] = w[u]; // 因為u必選所以給成立的加上一個u的價值
for (int i = h[u]; ~i; i = ne[i] ) {
int v = e[i]; dfs(v);
for (int j = m; j >= v[u]; j -- )
for (int k = 0; k <= j - v[u]; k ++ ) // 留給選根結點的體積,這樣轉移的過程中任意一個f(i,j)中均含有w[u]這個價值,因為向他轉移的那個f(u,j-k)中一個含有w[u],所以就滿足了依賴性
f[u][j] = max(f[u][j], f[u][j - k] + f[v][k]);
}
}