【C++】競程筆記(多維 DP、LCS 最長共同子序列 習題練習)
【C++】競程筆記(多維 DP、LCS 最長共同子序列 習題練習)
練習題目參考自:NTUCPC Guide,此筆記僅為個人學習用途。
823 . RGB
Problem Source:https://oj.ntucpc.org/problems/823
先定義三種顏色的代號以及對分數的貢獻(權重):
- 紅色($j=0$):權重為 $-v_i$(題目要求扣掉紅色數字總和)。
- 綠色($j=1$):權重為 $+v_i$(題目要求加上綠色數字總和)。
- 藍色($j=2$):權重為 $0$(藍色不影響分數)。
最後再來定義狀態:定義 $dp[i][j]$ 為考慮到第 $i$ 個數字,且將第 $i$ 個數字塗上顏色 $j$ 時,目前所能獲得的最大價值。
- $i : 1 \le i \le N$
- $j : 0 \le j \le 2$ (分別代表紅、綠、藍)
求 DP 轉移式:
題目限制「相鄰的數字必須標上不同的顏色」,要計算第 $i$ 個數字塗某個顏色時,第 $i-1$ 個數字必須是另外兩種顏色之一,最終解答是從中選取較大者,再加上當前顏色的權重。
對第 $i$ 個數字 ($i > 1$):
- 若第 $i$ 個塗紅色($j=0$):前一個必須是綠色或藍色。$$dp[i][0] = \max(dp[i-1][1], dp[i-1][2]) - v_i$$
- 若第 $i$ 個塗綠色($j=1$):前一個必須是紅色或藍色。$$dp[i][1] = \max(dp[i-1][0], dp[i-1][2]) + v_i$$
- 若第 $i$ 個塗藍色($j=2$):前一個必須是紅色或綠色。$$dp[i][2] = \max(dp[i-1][0], dp[i-1][1])$$
初始狀態定義:
當考慮序列中第 1 個數字($i=1$)時,沒有前一個數字的限制,直接根據顏色計算價值。
- $dp[1][0] = -v_1$
- $dp[1][1] = +v_1$
- $dp[1][2] = 0$
最終答案:在塗完最後一個數字 $N$ 後,最後一個數字可能是紅、綠、藍任一種,取三者中的最大值即為答案:$$Answer = \max(dp[N][0], dp[N][1], dp[N][2])$$
時間複雜度: $O(N)$
空間複雜度: $O(N)$
記得 DP 陣列要是 long long,題目測資有點大。
範例程式碼:
1 |
|
Grid 1
Problem Source:https://atcoder.jp/contests/dp/tasks/dp_h
- 狀態定義:$dp[i][j]$ 為從起點 $(1, 1)$ 走到座標 $(i, j)$ 的路徑總數。
- $i$(row),範圍 $1 \sim H$。
- $j$(column),範圍 $1 \sim W$。
- 轉移式定義:
題目要求只能「向右」或「向下」移動。
要到達 $(i, j)$,只可能從上方 $(i-1, j)$ 或左方 $(i, j-1)$ 走過來。
對於每格 $(i, j)$,判斷規則如下:
- $(i, j)$ 是障礙物(
#):無法到達該格,路徑數為 0。$$dp[i][j] = 0$$ - $(i, j)$ 是空地(
.):路徑數等於「從上方來的路徑數」加上「從左方來的路徑數」。$$dp[i][j] = dp[i-1][j] + dp[i][j-1]$$
- 初始狀態定義: $dp[1][1] = 1$ ,因為一開始就在 $(1, 1)$,算 1 種方法(停在原地)。
需要注意的是,當跑雙層迴圈,i == 1 且 j == 1 的情況要被跳過,否則後續計算會錯誤,結果會是 0。
另外在讀取 m 的字元時,當 j = W,會讀到字串結尾 \0,透過 j - 1 可以避開。
或是可以在輸入的時候做 padding(填充),就可以直接寫 m[i][j]:
1 | for (int i = 1; i <= H; ++i) { |
範例程式碼:
1 |
|
P2386 放蘋果
Problem Source:https://www.luogu.com.cn/problem/P2386
狀態定義: $dp[i][j]$ 是將 $i$ 顆蘋果放入 $j$ 個盤子的方法數。
轉移式定義:
- 至少一個盤子為空:既然盤子是相同的,空哪個都沒差。可先將一個盤子移走,不用它。將 $i$ 個蘋果放入 $j-1$ 個盤子。 $$dp[i][j - 1]$$
- 所有盤子都有放蘋果(無空盤):
- 每個盤子至少要有一個蘋果。
- 可先在每個盤子上各放 $1$ 個蘋果(消耗掉 $j$ 個蘋果)。
- 剩下的蘋果數量為 $i - j$ 個。
- 問題轉化為「將剩下的 $i-j$ 個蘋果,放入 $j$ 個盤子(可隨意放)」。$$dp[i-j][j], i \ge j$$
最終得到 $$dp[i][j] = dp[i][j-1] + dp[i-j][j]$$
當 $i < j$ 時,因為不可能每個盤子都放蘋果,第二項為 0,即 $dp[i][j] = dp[i][j-1]$
- 初始狀態定義:
- 沒有蘋果($i=0$):$$dp[0][j] = 1$$
- 只有 1 個盤子($j=1$):$$dp[i][1] = 1$$
範例程式碼:
1 |
|
N 箱 M 球
Problem Source:https://tioj.ck.tp.edu.tw/problems/1291
- 狀態定義:$dp[i][j]$ 為將 $i$ 個不同的球,放入 $j$ 個相同的箱子,且沒有空箱的方法數。
- $i$ 代表當前處理到的球數($1 \le i \le m$)
- $j$ 代表當前使用的箱子數($1 \le j \le n$)
- 轉移式定義:
- 考慮第 $i$ 顆球放入時的情況,此時已用 $j$ 個箱子,第 $i$ 顆球有兩種選擇:
- 自己獨佔一個新箱子:代表前 $i-1$ 顆球已佔用了 $j-1$ 個箱子,第 $i$ 顆球放入第 $j$ 個箱子。方法數為:$$dp[i-1][j-1]$$
- 放入已經有球的箱子中:代表前 $i-1$ 顆球已佔用了 $j$ 個箱子,第 $i$ 顆球可以放入這 $j$ 個箱子中的任一個。因為這 $j$ 個箱子裡面已有不同的球了,所以這 $j$ 個箱子被視為不同的箱子。方法數為:$$j \times dp[i-1][j]$$
- 考慮第 $i$ 顆球放入時的情況,此時已用 $j$ 個箱子,第 $i$ 顆球有兩種選擇:
最終得到:$$dp[i][j] = (dp[i-1][j-1] + j \times dp[i-1][j]) \pmod{1000000}$$
初始狀態定義:
- $dp[0][0] = 1$:0 個球放入 0 個箱子,視為一種方法(空集合)。
最終答案:$$ans = \sum_{k=1}^{n} dp[m][k] \pmod{1000000}$$
因為題目這邊要求的是使用 1 個箱子到使用 $n$ 個箱子的所有情況總和。
範例程式碼:
1 |
|
幸運表格
Problem Source:https://oj.ntucpc.org/problems/790
- 狀態定義:$dp[i][j]$ 為從座標 $(i, j)$ 出發,依照規則走到離開表格為止,所能獲得的最大幸運指數總和。
- 轉移式定義:為了讓總和最大,選「右」、「下」兩方向中值較大的那一個。
- 向右走:
- 如果右邊還有格子($j+1 < M$),得到$$dp[i][j+1]$$
- 如果右邊是邊界(離開表格),得到$$0$$
- 向下走:
- 如果下面還有格子($i+1 < N$),得到 $$dp[i+1][j]$$
- 如果下面就是邊界(離開表格),得到 $$0$$
- 向右走:
最終得到:$$dp[i][j] = A[i][j] + \max(\text{向右獲利}, \text{向下獲利})$$
當中 $A[i][j]$ 是幸運表格的數字。
- 初始狀態定義:最右下角的格子 $(N-1, M-1)$ 因為右邊和下面都是界外,所以 $dp[N-1][M-1] = A[N-1][M-1] + \max(0, 0)$。
在程式設計上,由於計算 $(i, j)$ 需要用到 $(i+1, j)$ 和 $(i, j+1)$ 的值,須由右下角往左上角計算。
範例程式碼:
1 |
|
APCS 2020/10 P3 勇者修煉
Problem Source:https://oj.ntucpc.org/problems/789
- 狀態定義:
定義兩個輔助狀態陣列,及一個最終狀態陣列(假設目前處理到第 $i$ 列):
- $L[j]$ :表示到達位置 $(i, j)$,且最後一步是由上方 $(i-1, j)$ 或是左方 $(i, j-1)$ 走過來的最大經驗值。(向右掃描)
- $R[j]$ :表示到達位置 $(i, j)$,且最後一步是由上方 $(i-1, j)$ 或是右方 $(i, j+1)$ 走過來的最大經驗值。(向左掃描)
- $dp[i][j]$:表示從起點到達 $(i, j)$ 的最終最大經驗值。
在同一列中,一條路徑不可能同時「先向左再向右」或「先向右再向左」,這樣會經過同一格兩次,所以到達 $(i, j)$ 的最佳路徑,必定是 $L[j]$ 和 $R[j]$ 兩者取其大。
- 轉移式定義:
假設已算完第 $i-1$ 列的結果,記為 $prev_dp$,現在要計算第 $i$ 列的值,該格的經驗值為 $cost[i][j]$。
對於第 $i$ 列的每一個位置 $j$:
- 向右掃描 $L[j]$ :考慮從「上一列直接下來」比較好,還是從「這一列的左邊走過來」比較好。$$L[j] = cost[i][j] + \max(prev_dp[j], L[j-1])$$
- 邊界條件:若是第一欄 $j=0$,則只能從上面下來,不能從左邊來。
- 向左掃描 $R[j]$ :考慮從「上一列直接下來」比較好,還是從「這一列的右邊走過來」比較好。$$R[j] = cost[i][j] + \max(prev_dp[j], R[j+1])$$
- 邊界條件:若是最後一欄 $j=m-1$,則只能從上面下來,不能從右邊來。
- 最後 $dp[i][j]$ 取兩者為最大值:$$dp[i][j] = \max(L[j], R[j])$$
- 初始狀態定義:所有上面要用的陣列大小為 m,都設 0。
這邊採用的是滾動 DP 的方式解題,能節省空間。
範例程式碼:
1 |
|
連鎖店 (Chains)
Problem Source:https://tioj.ck.tp.edu.tw/problems/1938
題目中所謂每家都要開在前一家的東北方,意思就是$N$ 個點的 $X$ 與 $Y$ 座標都必須是嚴格遞增。
- 狀態定義:$dp[k][y]$ 為在考慮當前的 $X$ 座標時,已選取了 $k$ 家連鎖店,且第 $k$ 家店位於 $Y$ 座標為 $y$ 的位置時,所有連鎖店累積的最大顧客數總和。
- 轉移式定義:
當遍歷到第 $x$ 列時,嘗試在此列放置第 $k$ 家店。
假設要在 $(x, y)$ 放置第 $k$ 家店,根據題目要求,第 $k-1$ 家店必須位於 $(x’, y’)$,其中 $x’ < x$ 且 $y’ < y$。
因為是由 $x = 0$ 到 $M-1$ 依序計算,當處理到 $x$ 時,$dp$ 表中儲存的正是 $x’ < x$ 的最佳解,因此只要注意 $y$ 的限制($y’ < y$)即可。
可得轉移式:$$dp[k][y] = \max(dp[k][y], \quad \text{profit}(k, x, y) + \max_{0 \le y’ < y} { dp[k-1][y’] })$$
- $\text{profit}(k, x, y) = (a \cdot (k-1) + b \cdot x + c \cdot y) \mod d$
- $\max_{0 \le y’ < y} { dp[k-1][y’] }$ 代表在前幾列中,選了 $k-1$ 家店且 $Y$ 座標小於目前 $y$ 的最大值。
為了避免用到同一列 $x$ 更新過的資料($x$ 不能相同),在實作時,內層迴圈計算 $k$ 時應由大到小($N$ 到 $1$),或者使用暫存變數,確保所讀取的 $dp[k-1]$ 是來自之前的 $x$ 列。
- 初始狀態定義:所有 $dp[k][y]$ 初始化為一個極小值。
範例程式碼:
1 |
|
Edit Distance
Problem Source:https://cses.fi/problemset/task/1639
- 狀態定義:
令輸入的兩個字串分別為 $S$(長度 $n$)和 $T$(長度 $m$)。
$dp[i][j]$ 為將字串 $S$ 的前 $i$ 個字元轉換成字串 $T$ 的前 $j$ 個字元,所需的最少操作次數。
- $S[1 \dots i]$ 表示 $S$ 的前 $i$ 個字元(index 1 based)。
- $T[1 \dots j]$ 表示 $T$ 的前 $j$ 個字元。
- 轉移式定義:
對於 $dp[i][j]$,考慮 $S$ 的第 $i$ 個字元($S[i]$)和 $T$ 的第 $j$ 個字元($T[j]$)的關係。
可從三個方向轉移,分別對應三種操作:
- 插入:假設 $S[1 \dots i]$ 已轉成 $T[1 \dots j-1]$,只要在 $S$ 的尾端插入一個字元 $T[j]$ 即可變成 $T[1 \dots j]$。$$dp[i][j-1] + 1$$
- 刪除:假設 $S[1 \dots i-1]$ 已轉成 $T[1 \dots j]$,只要把 $S$ 原本多出來的第 $i$ 個字元刪除。$$dp[i-1][j] + 1$$
- 替換:
- 如果 $S[i] == T[j]$ ,則不需要任何操作,直接繼承 $dp[i-1][j-1]$ 的結果。$$dp[i-1][j-1]$$
- 如果 $S[i] \neq T[j]$ ,把 $S$ 的第 $i$ 個字元替換成 $T$ 的第 $j$ 個字元。$$dp[i-1][j-1] + 1$$
最後得到轉移式:$$dp[i][j] = \min \begin{cases}
dp[i][j-1] + 1 & \text{(插入)} \
dp[i-1][j] + 1 & \text{(刪除)} \
dp[i-1][j-1] + cost & \text{(替換)}
\end{cases}$$
其中 $cost$ 的定義為若 $S[i] == T[j]$ 則 $cost = 0$,否則 $cost = 1$。
- 初始狀態定義
- $dp[i][0] = i$:將 $S$ 的前 $i$ 個字元轉換成空字串,需要刪除 $i$ 次。
- $dp[0][j] = j$:將空字串轉換成 $T$ 的前 $j$ 個字元,需要插入 $j$ 次。
範例程式碼:
1 |
|




