1. 程式人生 > 實用技巧 >DP學習記錄Ⅰ

DP學習記錄Ⅰ

DP學習記錄Ⅱ

前言

狀態定義,轉移方程,邊界處理,這三部分想好了,就問題不大了。重點在狀態定義,轉移方程是基於狀態定義的,邊界處理是方便轉移方程的開始的。因此最好先在紙上寫出自己狀態的意義,越詳細越好(如至少/恰好,包含/不包含XXX)

DP題通常碼量不大,但是非常考驗碼力,因為細節非常多,比如邊界包含不包含0/n?轉移順序是正著轉移還是倒著轉移?

通常情況下,邊界設為 0~n 最為保險,但是要保證不出負數,並且保證0/n+1的狀態合法(inf OR -inf OR 0) 等這麼寫完後發現會越界再改也可以。

至於轉移順序,就要具體分析了。主要看我們想要不想要之前已經轉移過來的狀態再多次轉移 (如01揹包就不能一件物品選多次,因此要乾淨的

之前狀態;恰好 -> 至少 的常用方法就要用一種類似前/字尾和的思想,就要不乾淨的/包含所有之前資訊的狀態來轉移。

Continued...


線性dp

通常體現為序列上的dp。應該算dp的基礎部分了(儘管也有難題)

P3646 [APIO2015]巴厘島的雕塑

主要考查的是按位貪心的想法,以及可行性dp轉化為最優性dp的方法。妙處在於設定“模板”來確定轉移的合法性。坑點在 define int long long 並不能改 \(1 << i\)\(1ll << i\),可能爆負數。

可行性dp -> 最優性dp

當我們的一開始想出的狀態為 \(f[i][j][k]\)

表示前 \(i\) 箇中某特徵為 \(j\),某特徵為 \(k\) 的狀態是否合法的時候,如果 \(k\) 越大越好(或者越小越好之類的),可以轉化為 \(f[i][j]\) 表示前 \(i\) 箇中某特徵為 \(j\)最大的合法的 \(k\) 是多少


揹包

常見揹包

\(f[i]\) 表示花費 \(i\) 的代價所能獲得的最大收益。

  • 01揹包
  • 完全揹包
  • 多重揹包(二進位制拆分,單調佇列)

特殊揹包

  • 多限制揹包

如除了體積外,還有大小,花費等限制。

\(f[i][x][y][z][...]\) 表示考慮前 \(i\) 個物品,體積為 \(x\),大小為 \(y\),花費為 \(z\)

...的最大收益。第一位可以壓掉。

  • 大容量小价值揹包

體積的規模巨大,但是能保證價值之和足夠小。

\(f[i][v]\) 表示考慮前 \(i\) 件物品,獲得 \(v\) 的價值最小要用多大容量的揹包。第一位可以壓掉,答案可以二分。

揹包合併

\(O(n^2)\) 的做法:

把一個揹包看作 \(n\) 件物品,即 \(f[w] = v\) 看作一個體積為 \(w\),價值為 \(v\) 的物品。並且要求只能選擇一件物品。這就要求我們在更新 \(F[W]\) 之前,不能更新 \(F[W - w]\)。(具體間轉移方程)

轉移方程:(\(f\) 合併到 \(F\) 裡)

\[MAX(F[W], F[W-w] + f[w]) \]

需要保證 \(F[W-w]\) 是乾淨的,不帶任何有關所有 \(f[w]\) 的。因此需要把 \(W\) 放在外層列舉,且倒序; \(w\) 放內層,順序任意

for (i = K -> 0)
	for (j =  0 -> i)
		MAX(F[i], F[i - j] + f[j]);

揹包與自然數的劃分

\(n\) 劃分為若干不同的正整數的方案數:可以看做對\(1\) ~ \(n\)\(n\) 個物品做01揹包。

\(n\) 劃分為若干可重的正整數的方案數:可以看做對\(1\) ~ \(n\)\(n\) 個物品做多重揹包。


樹形dp

樹形dp做多少題也不算多

  • 最大獨立集

  • 最小點覆蓋

  • 最小支配(點或鄰點被選,謂之“支配”)(三種狀態)

  • 最大匹配

  • 換根dp

  • 基環樹dp

  • 樹形揹包

換根dp

首先\(O(n)\) 的時間內搞出以1為根的資訊;再嘗試把根換為相鄰點,需要 \(O(1)\) (或 \(O(logn)\))的時間內換完,然後更新答案,遞迴;回溯時還原資訊。

樹形揹包

樹上轉移式子為 \(f[fa][i + j] = f[fa][i] + f[son][j]\) 的dp題。通常可以通過限制dp上界,保證複雜度不超過 \(O(n^2)\)

這部分細節比較多,對分類討論能力要求較高。一定要細心啊!!

P3354 [IOI2005]Riv 河流

非常考察費用提前計算思想(沒想到就無法dp)。

\(f[i][j][k]\) 表示 \(i\) 的伐木場建在 \(j\),且 \(i\) 及其子樹內共建立了 \(k\) 個伐木場的最小費用。

子樹向父親轉移類似兩揹包合併。

基本轉移方程:

\[MIN(f[cur][anc][k],f[to][anc][k']+f[cur][anc][k-k']) \]

\[f[cur][anc][k] += (dep[]-dep[]) * w[] \]

小技巧1:分類與合併

我們發現 \(i\) 點建與不建伐木場是有一些區別的。因為如果單純用 \(f[i][i][ * ]\) 來表示在 \(i\) 點建立伐木場的話,那麼 \(i\) 的祖先將無法統計上 \(i\) 點。

那麼我們需要一個 \(f[i][anc][ * ]\) 來表示 \(i\) 已經建了伐木場了,但是它還是想要在 \(anc\) 這一祖先上建伐木場。這樣 \(anc\) 才能識別並拾取 \(i\) 點資訊。

因此,我們需要新開一個狀態 \(g[cur][anc][ * ]\) 表示 \(cur\) 點建了伐木場且希望在 \(anc\) 處再建一個伐木場。對 \(g\) 差別對待,最後再把它合併到 \(f\) 裡頭。

小技巧2:無腦賦值初始化

不知道怎麼初始化怎麼辦?直接拿一絕對合法的轉移做初始化即可。

小技巧3:費用最後一塊算

如果不願意在轉移裡面寫那麼多 \(+w[cur]\) 之類的東西還怕出錯算重的話,可以嘗試在最後同一加上 \(w[cur]\)

P3267 [JLOI2016/SHOI2016]偵察守衛

消防局的設立的超級加強版。

\(f[cur][d]\) 表示從 \(cur\) 開始(含)向下 \(d\)需要被覆蓋(有可能並不需要,或參差不齊,但是保證d層往下絕對不需要)的狀態,的最小代價。

\(g[cur][d]\) 表示 \(cur\) 可以向上(不含)覆蓋 \(d\) 層的狀態(當然也包含可以向上蓋d + XX層的情況),的最小代價。

轉移方程:

將新子樹的資訊逐個加入至原資訊中

一、g的轉移。

  1. 初始化(一進dfs,未加入子樹時就進行):\(g[cur][d(<= ~ D)] = w[cur]\)\((f[cur][0] =) g[cur][0] = w[cur](if ~ cur ~ is ~ important) ~ OR ~ 0(else)\)

  2. 子蓋外:\(g[cur][d] <- f[cur][d + 1] + g[to][d + 1]\)

  3. 外(它子)蓋子:\(g[cur][d] <- g[cur][d] + f[to][d]\)

  4. 維護“可以”的性質(字尾和):\(g[cur][d] <- g[cur][d + 1]\)

二、f的轉移。

  1. 初始化(未加入子樹時進行):\(f[cur][d] = 0\)\(f[cur][0] = w[cur]~ (if ~ cur ~ is ~ important)\)

  2. 需要子樹全部被蓋:\(f[cur][0] = g[cur][0]\)

  3. 父子同時等待被蓋:\(f[cur][d] += f[to][d-1]\)

  4. 維護“d層內隨意”性質(字首和):\(f[cur][d] <- f[cur][d - 1]\)

想清楚所有情況後,轉移順序之類的小細節就有了依據。因此要碼這種噁心的DP題,最好先想好所有的狀態及轉移。

T135128 樹

給定 \(n\) (<=1e4) 個點的點帶權樹,要求選擇一些點,使得其兩兩之間距離大於 \(K\) (<=1e2),最大化點權和。

收到上一道題的毒害,我這道題還是想\(f\),\(g\),然後寫了五六個式子,最後連定義都弄不清了,發現 \(f\)\(g\) 有重疊的地方,也就是說,我可以根據 \(f\) 來推匯出 \(g\)。到這裡我基本可以確定我又一次掉坑裡了。

還是不要思維固化啊

實際上這道題還是聽簡單的,就只用設計一個狀態就好了。畢竟不是覆蓋問題。

\(f[cur][j]\) 表示處理好了 \(cur\) 及其子樹(目前的),並且子樹內(含cur)距離 \(cur\) 最近的那個點的距離不小於 \(j\) 的...

然後轉移方程什麼的就很顯然了:

\[f[cur][0] = val[cur] \]

\[f[cur][min(j, k + 1)] <- f[cur][j] + f[to][k],(j+k+1>K) \]

\[f[cur][j] <- f[cur][j + 1] \]

P6223 [COCI 2009] PODJELA

樹形揹包。

考查狀態的靈活改變。使用 \(f[cur][i]\) 表示處理完 \(cur\) 節點的子樹,使用 \(i\) 次交換機會的最大 val[cur] 值(貪心)

這裡有一個有關dp順序的小技巧:如果實在不知道怎麼安排dp順序,可以新開一個數組 \(g\),儲存 \(f\) 的值,然後再清空 \(f\),再用 \(g\) 去更新 \(f\),就能保證 \(g\) 全部是乾淨的。

P3177 [HAOI2015]樹上染色

樹形揹包。

考查狀態的靈活改變。使用 \(f[cur][i]\) 表示處理完 \(cur\) 節點的子樹,使用了 \(i\) 個黑點,子樹中的邊對答案的貢獻的最大值。

注意到,如果存的是子樹中的答案的最大值的話,沒有“最優子結構”的性質(區域性最優 \(\not=\) 全域性最優).但是如果考慮邊的貢獻的話,搞完子樹裡面的邊後,只要確定子樹中有 \(i\) 個黑點,就和外面的邊的貢獻沒關係了。

剩餘的和上一道題類似。

其實這種把“兩兩之間的距離之和”轉化為一條條邊的貢獻還是很常見的一種套路。

P4037 [JSOI2008]魔獸地圖

狀態不是很好想: \(f[cur][j][c]\) 表示 \(cur\) 子樹中,有恰好 \(j\)\(cur\) 用來上貢,使用恰好 \(c\) 個金幣所能獲得的最大剩餘價值。

然後先算出 \(j\)\(cur\),恰好花 \(c\) 個金幣的最大剩餘價值,這是個樹形揹包:

\[g[cur][j][c+c'] <- f[cur][j][c] + f[to][j * need[to]][c'] \]

然後再把 \(g\) 處理成 \(f\)

\[f[cur][j][c] <- f[cur][j'][c] + (j'-j) * val[cur] \]

葉子的 \(f\) 可以直接特判掉:

\[f[cur][j][j * w[cur]] <- (j'-j) * val[cur] \]

看似複雜度達到 \(1e10\),但是是可以跑過的。

然後細節較多,各種邊界,dp順序之類的東西要格外注意。

(然而最終竟然掛在了陣列大小上...)

P4201 [NOI2008]設計路線

題目要求樹上選擇一些鏈,並且要求葉子節點到根節點的“輕邊”的數量的最大值最小,還要求方案數。

如果只求最小值,那麼這題可以開到 1e6,我們直接貪心DP取最小值即可。但是由於要求方案數,一些“區域性不優”的方案可能會被一些不得不做的“更劣解”所“掩蓋”掉,使其合法化,因此記錄子樹最大輕邊數並做一些看似不必要的轉移顯得十分必要。

幸運的是,根據樹鏈剖分的知識可知,最大輕邊數是 \(log\) 級別的。因此直接計入狀態DP即可。

我們可以設 \(f[cur][k][0/1/2]\) 表示 \(cur\) 子樹裡輕邊數都小於等於 \(k\) 的方案數。

為了方便DP,我們可以設 \(g[cur][k][0/1]\) 表示 \(cur\) 的父邊為輕/重邊,對父親關於 \(k\) 的轉移的貢獻

然後分類討論:

\[g[cur][k][0]<-f[cur][k-1][0/1/2] \]

\[g[cur][k][1]<-f[cur][k][0/1] \]

\[f[cur][k][0]<-f[cur][k][0]* g[to][k][0] \]

\[f[cur][k][1]<-f[cur][k][0]* g[to][k][1]+f[cur][k][1]* g[to][k][0] \]

\[f[cur][k][2]<-f[cur][k][1]* g[to][k][1]+f[cur][k][2]* g[to][k][0] \]

然後隨便DP即可。


區間DP

通常是給定一個區間以及某些特徵後,該區間內的最優價值/代價就可以通過兩個子區間來確定,那麼可以考慮通過區間DP來做。

常見的轉移方程形式主要為:單點擴充套件;列舉劃分點。

CF1312E Array Shrinking

板子題,不多說。狀態:\(f[l][r]\) 表示合併 \([l, r]\) 後的那個數是什麼。

P3205 [HNOI2010]合唱隊

倒過來模擬,發現原字首體現為一段區間。然後記錄 \(f[l][r][0/1]\) 表示 \([l,r]\) (最後加在左/右邊)的方案數。

注意,如果 \(f[i][i][0] = f[i][i][1] = 1\) 的話會算重。可以去掉一個,或者直接將長度為二的狀態作為邊界。

P1864 [NOI2009]二叉查詢樹

由於key值一定,可以確定中序序列。

發現區間DP類似BST的構建。一個區間類似一個子樹。

如果我們設 \(f[l][r][rt]\) 的話,將無法體現 \(rt\) 的權值是什麼,因為可能已經被修改了。因此,可以設 \(f[l][r][m]\) 表示區間的根的權值不低於 \(m\)。這樣,就可以分兩種情況(改 OR 不改)討論轉移。

由於權值可以取實數,避免了很多討論。


狀壓DP

有時我們為了完整地表示出狀態,不得不用一堆數來表示一個狀態。狀態壓縮是一種常用的方法。

一般狀態為01串最方便。如果狀態為 \(k\) 進位制數的話,最好從0開始數數,並且手寫幾個函式,來支援一系列操作,這樣會方便很多。

旅行商(哈密頓路徑)

過於經典的狀壓例題。

逐行轉移 & 逐格轉移(輪廓線DP)

例題:互不侵犯,炮兵陣地,玉米田

卡逐行轉移的例題:P2435 染色

設上一行的狀態為 \(S\),列舉這一行的狀態為 \(T\)。判斷 \(S\) 能否轉移到 \(T\) 即可。複雜度: \(O(nm2^{2m})\)(一般達不到這個上限,因為有一些不合法的狀態;並且那個 \(m\) 是位運算複雜度,可以認為是略小於 \(O(1)\) 的)

如果 \(m\) 比較大,比如 15 或者 20,怎麼辦?

逐格轉移(輪廓線DP)!

(似乎感受到了插頭DP的氣息)

\(f[state]\)輪廓線上的狀態。這樣,在轉移第 \(i\) 行第 \(j\) 列的格子轉移的時候,只用判斷輪廓線上與那個格子相鄰的幾個格子是否合法即可。

有的時候需要在一行的開頭和結尾做一些特判。

複雜度:\(O(nm2^m)\)

//P2435
for (register int i = 1; i <= n; ++i)
	for (register int j = 0; j < m; ++j) {
	memcpy(g, f, sizeof(f));
	memset(f, 0, sizeof(f));
		for (register int s = 0; s < All; ++s) {
			if (!g[s])	continue;
			int l = j ? Find(s, j - 1) : -1;
			int u = Find(s, j);
			for (register int t = 0; t < k; ++t)
				if (l != t && u != t)
					MOD_ADD(f[Add(Del(s, j), j, t)], g[s]);
		}
	}

【經典問題】骨牌覆蓋問題:初級 高階 終極

即:給定 \(n\)\(m\) 列的棋盤,要求使用 1 × 2 的骨牌恰好完全覆蓋整個棋盤。求方案數。

感覺hihoCoder上面講得非常棒

Part1 : n = 2, m <= 1e9

狀態 \(f[n]\) 表示到 \(n\) 列且恰好鋪滿的方案數。

發現只有最後橫鋪兩個或者豎著鋪一個這兩種轉移。即:\(f[n] = f[n - 1] + f[n - 2]\)。矩陣加速求斐波那契數列的第 \(m\) 項即可。

Part2 : n <= 7, m <= 1e9

考慮“按行轉移”,關鍵在查詢哪些狀態的轉移是合法的。不難發現,當前行確定後,上一行的可行狀態唯一。

如果當前行橫鋪一個,那麼要求上一行對應的那兩列為1.

如果當前行豎著來一個,那麼要求上一行對應列為0.

如果當前行空著一個格子,那麼要求上一行對應列為1.

DFS判定即可。

得到矩陣後,以此作為轉移矩陣,隨便矩乘幾下即可。

複雜度:\(O((2^n)^3logm)\)

Part3 : n,m <= 20

陣列完全存不下。但是發現每一行對應的只有一種狀態,直接存那種狀態即可。

複雜度: \(O(2^nm)\)

Part4 : 不要求恰好完全覆蓋,n,m <= 16

可以考慮按格轉移。維護“一行”輪廓線。然後分三種情況轉移即可。

Part5 : 不要求恰好完全覆蓋,n <= 8, m <= 1e9

這時一種狀態就可能會轉移出多種狀態了。需要 \(O(2^{2n})\) 列舉,\(O(n)\) 逐一判斷。

據說可以“去掉⽆⽤的狀態,即可通過”,然而並不知道哪些狀態“無用”。

所以我這種方法只能通過 n <= 7 的點, n = 8 的時候計算量達到了 5e8。不過矩乘常數不大,或許可過? 可能需要更寬的時限。

P3959 寶藏

由於 \(n\) 非常小,因此可以考慮狀壓。

又因為代價由兩個引數決定,如果控制住了其中一個,另一個就可以貪心解決了。

發現 \(L\) 沒法控制,只好控制 \(K\)。即:一層一層地DP。

\[f[i][S] <= f[i - 1][s] + i * trans[s][S ~ xor ~ s] \]

需要列舉 \(S\) 和其子集 \(s\)

一個常識是:列舉一個集合 \(S\) 的所有子集的所有子集的複雜度是 \(O(3^{|S|})\)。這個可以用每一個數在各集合中的三種不同狀態來證明。同理可證,列舉一個集合 \(S\) 的所有子集的所有子集的所有子集的複雜度是 \(O(4^{|S|})\).

然後預處理 \(trans[s][s']\) 後按照層數 DP 即可。預處理 \(trans[][]\) 可以貪心。思想與斯坦納樹類似。

複雜度: \(O(n^23^n)\)

P4297 [NOI2006]網路收費

DP綜合題:需要用到樹形DP,揹包,狀壓DP,以及“做承諾”(費用提前計算)思想。

\(f[cur][k][s]\) 表示DP到了\(cur\),子樹中共有 \(k\)\(B\) 型點,且從 \(fa[cur]\) 到根的點的狀態為 \(s\) 的最優代價。

葉子的所有狀態可以直接處理出來。這裡包含了所有代價,剩下我們要做的只是把子樹拼一拼,看看合不合法。

根據 \(A\) 型點的子樹 \(A\) 嚴格少於 \(B\) 型點,我們控制列舉的 \(k\) 的範圍。對於“拼子樹”,做樹形揹包即可。

然後發現需要 \(O(2^{3n})\) 的空間複雜度。考慮到深度變淺一點, \(|s|\) 會變小一點,\(k\) 會增大一點。這樣,我們直接把後兩維壓成一維即可做到空間 \(O(2^{2n})\)

細節非常多。我謹慎小心地寫程式碼,還出了六個錯。還是太菜了。