1. 程式人生 > 其它 >基礎動態規劃題單

基礎動態規劃題單

「進行中...」一些關於 DP 的經典題目

[Accepted]Luogu4302. [SCOI2003]字串摺疊

\(f_{l,r}\) 表示將 \([l,r]\) 中的字串摺疊得到的最短長度,那麼有兩種情況:

  • \([l,r]\) 由一個字串重複多次得到,那麼我們嘗試列舉這個長度 \(x\),每次判斷之間用雜湊判斷分成的 \(\frac n x\) 個串,每個長度判斷的時間複雜度是 \(O(\frac n x)\),取最終結果最小值,對於這一段的時間複雜度為 \(O(\sum_{d|n} \frac n d)=O(n\log n)\)
  • 其他的字串的摺疊情況一定可以通過將原串分為兩部分拼接而成,列舉斷點合併即可,時間複雜度為 \(O(n)\)

於是最終答案是 \(f_{1,n}\),總體時間複雜度為 \(O(n^3\log n)\)

[Accepted]LOJ2124. [HAOI2015]樹上染色

由於塗黑的點數有限制,所以自然地設計狀態為 \(f_{i,j}\) 表示在以 \(i\) 為根的子樹中選了 \(j\) 個為黑點,得到的子樹中的最大價值,我們考慮一條邊 \(i\) 如何貢獻,假設它所連向以 \(x\) 為根的子樹,\(x\) 的子樹中有 \(j\) 個黑點,那麼不論外面怎麼選,\(i\) 上面一定有 \(k-j\) 個黑點,於是貢獻就是 \(w_i\cdot(k-j)\cdot j\),白點經過這條邊的代價類似,然後類似樹上揹包合併即可。

看起來時間複雜度是 \(O(n^3)\) 的,其實不然。即設現在要計算 \(x\) 的子樹的價值,稱 \(x\) 的子樹為大子樹,當前考慮的 \(x\) 的某個兒子 \(y\) 的子樹為小子樹。由於計算貢獻時,我們揹包的大小是子樹大小,相當於給小子樹內的每個點一個編號,給小子樹外、大子樹的點一個編號,每次分別列舉一個子樹內的點和一個子樹外的點配對(將列舉的大小看作編號),於是每個點作為小子樹內的點配對時,最多與其他 \(n-1\) 個點各進行配對一次,於是總體時間複雜度為 \(O(n^2)\),注意這個時間複雜度成立的前提就是我們合併時列舉的揹包大小必須是子樹大小。

[Acceptted]LOJ2559 [HNOI2003]消防局的設立

雖然說是 DP,但更容易想到的還是貪心,顯然對於當前所有未被覆蓋的點,我們一定是選擇覆蓋其中深度最深的節點,而且我們一定是將其放在他的祖父位置放置那麼我們考慮如果要覆蓋這個點,那麼我們一定是在它的祖父位置(如果存在)安置最優(不存在就放在父親),這樣可以覆蓋到的點一定最多。

考慮如何確定一個點是否被覆蓋,最樸素的想法便是記錄到達離它最近的消防站距離它的距離 d[],考慮如何更新一個點的距離:

  • 將某個點的祖父選中時,修改其父親和其祖父的 d[]
  • 處理到某個點時,用其父親和祖父的 d[] 嘗試更新自己(來自子孫的如果有一定已經被更新);

然後做就是了。

[Acceptted]Luogu1131 [ZJOI2004]時態同步

注意到 \(x\) 的子樹內的葉節點的時間一定被同步到其中最初到的最晚的時間,只需要在 \(x\) 下面不滿足的子樹到 \(x\) 的路徑上進行對應的修改即可。

於是 \(f_{i}\) 表示將 \(i\) 的子樹內的時態同步所需的最小操作次數,按照上面的想法進行轉移即可。

[Acceptted]Luogu1220 關路燈

考慮在任意時刻關閉的燈一定是一段連續的區間,於是我們可以設狀態 \(f_{l,r}\) 表示將區間 \([l,r]\) 內的燈全部關閉時已消耗的最少電功,發現每次關燈一定是從之前區間的 \([l,r]\) 的邊界中移動到 \(l-1\)\(r+1\) 關燈,於是完善狀態為:

\(f_{l,r,0/1}\) 表示將區間 \([l,r]\) 內的燈全部關閉時,最後關閉的燈是 \(l\) 或是 \(r\),已消耗的最少電功,

轉移就考慮列舉一個區間 \([l,r]\),然後從 \([l+1,r]\)\([l,r-1]\) 分別進行轉移即可。

[Solved]Luogu4766 [CERC2014]Outer space invaders

不難發現如果我們可以把每一個外星人看作一個時間軸上的區間,對於所有完全包含於某個區間 \([l,r]\) 內的所有外星人,必然有一次的殺傷距離是其中距離最遠的外星人所處的位置,然後再考慮消滅該外星人所選取的時間 \(t\),那麼顯然與 \(t\) 有交集的所有外星人都會被消滅,那麼剩下的一定是完全包含於 \([l,t-1]\)\([t+1, r]\) 中的外星人。

根據上面的想法,我們可以設計如下的狀態:\(f_{l, r}\) 表示將被完全包含在 \([l,r]\) 中的外星人全部消滅所需的最小代價,於是應當有轉移:

\[f_{l,r}=\min_{l_x\leq t\leq r_x}\{d_x+f_{l,t-1}+f_{t+1, r}\}, \]

其中 \(x\) 為該區間內距離最遠的外星人的編號。顯然我們需要將時間序列離散化。

[Solved]Luogu1864 [NOI2009]二叉查詢樹

本題的一大難點就是觀察出這棵二叉查詢樹是 Treap,改變權值會使得其旋轉, 但中序遍歷得到的資料值的序列不會變化,於是我們可以在這個序列上進行操作。

同時,雖然題目要求每個節點的權值不同,但是可以是實數,所以我們可以讓某個點的權值無限逼近某個值,於是原題中權值不同的要求就消失了。

考慮對於一段中序遍歷,它同時也是一個 dfn 序列,由於 Treap 的形態由各個節點的權值決定,我們可以考慮用這個 dfn 序列來還原出一棵子樹,同樣考慮到由於 Treap 的權值是滿足堆性質,於是我們只需要確定出根節點的權值,剩下兩棵子樹的權值的下界也就確定了,而剩下的兩棵子樹怎樣形成顯然是另一個子問題,於是可以動態規劃。

\(f_{l,r,x}\) 表示將 dfn 序列中的 \([l,r]\) 劃分為一棵子樹,子樹中的點的權值不小於 \(x\) 的最小代價,然後我們考慮要不要修改當前根的價值,如果不改(要求 \(v_i\geq x\)),那麼一定具有轉移:

\[\begin{aligned} f_{l,r,x}&=\min_{l\leq i\leq r}\left\{f_{l,i-1,v_i}+f_{i+1,r,v_i}+sum_{l,r}\right\},\\ \end{aligned} \]

否則我們要改,那麼一定是貪心地將該點的權值修改為 \(x\),這樣剩下的子樹的限制會更小,於是有轉移:

\[\begin{aligned} f_{l,r,x}&=\min_{l\leq i\leq r}\left\{f_{l,i-1,x}+f_{i+1,r,x}+sum_{l,r}+k\right\},\\ \end{aligned} \]

其中,\(sum_{l,r}\) 表示 \([l,r]\) 的訪問頻度的和,這樣做是將訪問頻度的代價拆分為了每層上的一部分一部分的代價和;

注意我們需要把所有的權值進行離散化到區間 \([1,n]\) 中,這樣我們最後的答案就是 \(f_{1,n,1}\).

Luogu6478 [NOI Online #2 提高組]遊戲

[Solved]Luogu2157 [SDOI2009]學校食堂

首先不難看出代價就是 \(a\text{ xor } b\)

對於狀態,一個很自然的想法是 \(f_i\) 表示 \(i\) 及之前的完事了所花費的最小時間,但是注意到這樣顯然不能合理地區分狀態,因為選擇可以是不連續的,但是同樣注意到對於 \(i\) 來說向後的擴充套件不可能超過 \(7\) 個人,於是我們考慮記錄這 \(7\) 個人的狀態,也就是將狀態補充為 \(f_{i,S}\),表示 \([1,i-1]\) 中的人都已經打完了飯,第 \(i\) 個人以及他後面的 \(7\) 個人的狀態是 \(S\),用一個 \(8\) 位二進位制數表示,再考慮到轉移時同樣需要上一個打飯的是誰,於是在狀態中還需要增添一維,要表示上一個打飯的人的位置,由於第 \(i-1\) 個人已經打了飯,顯然上一個打飯的人不可能比 \(i-8\) 還要考前,所以用 \([-8,7]\) 表示上一個打飯的是 \(i\) 向後第幾個人(用陣列存需要 \(+8\)),於是,我們就能用 \(f_{i,S,j}\) 完整地表示一個狀態,即:\([1,i-1]\) 中的所有人都已經打了飯,\([i,i+7]\) 中是否已打飯的情況是 \(S\),上一個打飯的是第 \(i+j\) 個人。

設計好了狀態,下面來考慮如何轉移。

注意到,對於狀態 \(f_{i,S,j}\) 如果 \(S\) 代表 \(i\) 的一位已經為 \(1\),那麼它應當和狀態 \(f_{i+1,S',j-1}\) 等價,其中 \(S'\) 為將 \(S\) 中代表 \(i\) 的一位去掉,加上代表 \(i+8\) 的狀態(為 \(0\))的一位,這一點是顯然的,這一步的轉移是不需要代價的。

之後,我們應當嘗試從 \([i,i+7]\) 中尋找一位未打飯的給他打飯,轉移到對應狀態,注意給這個人打飯需要保證在其之前的所有未打飯的人都能接受他先打飯,為此我們需要維護一個位置為可行的能打飯的最遠位置,這個位置是前面所有人可接受位置的最小值,我們只需要順序列舉,在轉移 \(f\) 的同時維護即可,注意到如果一個人不能滿足了,那麼他後面的一定也不行。

粗略的估計整體的時間複雜度是 \(O(n\cdot2^8\cdot15\cdot7)\),其中包含了很多不可達的狀態。

[Accepted]Luogu5005 中國象棋 - 擺上馬

考慮直接設計狀態為 \(f_{i,S_1,S_2}\),表示考慮到第 \(i\) 行,擺放狀態為 \(S_i\),第 \(i-1\) 行的擺放狀態為 \(S_2\)。直接預處理出對於單行,上一行不能存在的位置有哪些,然後再對於兩行 \(S_1,S_2\)\(S_1\) 在下,\(S_2\) 在上),上面一行 \(S_3\) 不能存在的位置有哪些,容易發現這恰好也即是 \(S_1\) 在上,\(S_2\) 在下時,下面一行 \(S_3\) 不能存在的位置,然後可以直接做。

[Accepted]Luogu3977 [TJOI2015]棋盤

容易發現關鍵點一定在中間的一行,於是可以直接處理出當前行每種狀態上下的影響範圍,設狀態為 \(f_{i,S}\),為到第 \(i\) 行的狀態為 \(S\),可行的情況數,時間複雜度為 \(O(n2^{2m})\),顯然不可過,於是考慮優化。

注意到對於一個狀態 \(S\),可行的轉移來源都是確定的,也就是

\[f_{i,S}=\sum_{S'}c_{S,S'}\cdot f_{i-1,S'}, \]

其中 \(c_{S,S'}\) 為當前行狀態為 \(S\),上一行是否可以為 \(S'\)\(c_{S,S'}\) 顯然都是確定的,於是考慮用矩陣快速冪優化。顯然狀態數不會超過 \(2^6=64\),於是構建一個 \(64\times64\) 的轉移矩陣 \(T\)\(T_{i,j}\) 為第 \(j\) 個狀態是否可以從(上一行狀態為)狀態 \(i\) 轉移,\(F_i\) 為一個 \(64\) 元行向量,表示第 \(i\) 行每種狀態的可行方案數,顯然有

\[F_i=F_{i-1}T\Rightarrow F_n=F_1(T)^{n-1}, \]

然後直接矩陣快速冪即可,時間複雜度為 \(O(2^{3m}\log n)\).

[Solved]LOJ2318 [NOIP2017]寶藏

較為重要的一點是看出我們最終一定是得到一棵原圖的生成樹,然後對於一個樹上的點,它的貢獻就是它連向原本生成樹的邊權乘上它所在的深度,於是我們設計狀態 \(F_{S,i}\) 為當前已經加入生成樹的點集為 \(S\),生成樹的最大深度為 \(i\) 時的最小代價。

那麼考慮轉移,我們列舉形成 \(S\) 的子集 \(S'\)\(S\)\(S'\) 向外連邊得到,顯然應當連線 \(\complement_SS'\) 中的點向 \(S'\) 中所花費最少的邊,於是應當是這些邊權求和乘上最大深度,顯然這樣算出的答案不一定是正確的,但是假設 \(\complement_SS'\) 中有的點不是連到 \(S'\) 中最大深度的點上,那麼一定存在另一個集合 \(S''\) 包含這些點,且最大深度與 \(S'\) 相同,顯然這個集合得到的答案不會比 \(S'\) 得到的更大,於是更新到 \(S\) 上時得到的答案一定是正確的答案。

總體時間複雜度為 \(O(3^n\cdot n^2)\),考慮到不合法的狀態,\(3^n\)\(n^2\) 都不容易跑滿,於是能過。

[Solved]Luogu5933 [清華集訓2012]串珠子

我們首先直接考慮設 \(f_S\) 為點集 \(S\) 中的點全部連通的方案數,直接計算髮現不好計算,正難則反,設 \(g_S\) 為點集 \(S\) 中的點所有的連邊的方案數,\(h_S\) 為點集 \(S\) 中的點不連通的方案數,顯然有

\[f_S=g_S-h_S, \]

\(g_s\) 的計算比較顯然,就是存在的點兩兩之間連邊的方案數的乘積,可以預處理。

再來考慮 \(h_S\),發現我們可以指定 \(S\) 的一個子集 \(S'\) 連通,剩下的部分連通性隨意,這兩部分之間不相連,這樣的不同的方案的數量就是 \(h_S\),我們考慮如何在不算重的情況下計算 \(h_S\),對於我們指定連通的子集 \(S'\),我們可以強制要求其中包含點 \(x\)\(x\in S\)),得到的 \(\complement_S S'\) 自然一定不含 \(x\),這樣,將 \(S\) 分為兩個部分,\(x\) 所處的部分一定都不同,於是得到的結果一定是不重複的。

Luogu4363 [九省聯考2018]一雙木棋chess

Luogu3943 星空

Luogu2150 [NOI2015] 壽司晚宴

AGC016F Games on DAG

CF1523F Favorite Game