<style> .reveal .slides { text-align: left; font-size:28px; } </style> # DP and DP-optimize Introduction to Competitive Programming 5/3 --- ## 上課內容 - 單調隊列 * 單調隊列介紹 * 多重背包優化 - 區間DP - 數位DP --- ## 單調隊列優化 [例題](https://zerojudge.tw/ShowProblem?problemid=c528): 給定一整數陣列,求取出數字的總和最大,且滿足相鄰兩數距離不能超過 $k$ ---- 定義狀態: $$dp[i]=\mbox{從頭開始選到 $i$ 且有選 $i$ 的最大值}$$ 狀態轉移: $$dp[i] = arr[i] + max\{dp[i-1], dp[i-2], \dots, dp[i-k], 0\}$$ ---- ### 方法一: $max\{dp[i-k], \dots, dp[i-3], dp[i-2], dp[i-1]\}$ 相當於求區間最大值,砸線段樹! 複雜度 $O(N\log N)$ ---- ### 方法二: $max\{dp[i-k], \dots, dp[i-3], dp[i-2], dp[i-1]\}$ 如果 $a < b$ 且 $dp[a] < dp[b]$,那 $dp[a]$ 就再也用不到了 我們將 $(dp[x], x)$ 存入 deque $k = 7$ ,維護 $dp[4] \sim dp[10]$ $[(8, 4), (3, 5), (5, 6), (7, 7), (6, 8), (1, 9), (2, 10)]$ 把沒用的拿掉會變成 $[(8, 4), (7, 7), (6, 8), (2, 10)]$ 讓 deque 保持單調遞減 ---- 現在要計算 $dp[11]$ $dp[11] = 8 + arr[i] = 5$ (假設) 從右邊塞入 $(dp[11], 11)$ 把比 $dp[11]$ 小的 pop 掉 $[(8, 4), (7, 7), (6, 8), (2, 10)]$ -> $[(8, 4), (7, 7), (6, 8), (5, 11)]$ 在求 $dp[12]$ 之前我們檢查 deque 的最左邊的元素離 $12$ 有沒有超過 $k$ 小於 $12 - k$ 的話要 pop 掉 -> $[(7, 7), (6, 8), (5, 11)]$ ---- 複雜度: 轉移只需要 $O(1)$ 取出 deque 的頭部 每個元素最多只會被 push 一次 pop 一次 所以總複雜度是 $O(N)$ ---- 範例 code (std::deque 很肥最好用其它東西代替) ```cpp= vector<pair<int, int>> deq(n + 5); int l = 0, r = 0; deq[l] = {0, 0}; for (int i = 1; i <= n; i++) { while (deq[l].second < i - k) l++; dp[i] = deq[l].first + arr[i]; while (l <= r and deq[r].first <= dp[i]) r--; deq[++r] = {dp[i], i} } ``` ---- ## 單調隊列優化背包問題 有 $N$ 種物品,價值、重量、個數分別是 $v_i, w_i, t_i$ ,和負重 $M$ 的背包,求最大值。 ---- ### 方法一: 把 $t_i$ 個相同物品當成 $t_i$ 不同物品作 $0/1$ 背包 複雜度:$O(M \sum t_i)$ ---- ### 方法二: 其實不需要 $t_i$ 個物品那麼多,假設 $t_i=15=1111_{(2)}$,把 $t_i$個物品分成 四組分別有 $1, 2, 2^2, 2^3$ 個物品,這些物品可以組合成 $0 \sim 15$ 。如果 $t_i$ 不是 $2^p - 1$ , 我們可以先取 $q$ 個,讓 $t_i - q = 2^p - 1$ ,把 $t_i 分解成 {1, 2, \dots, 2^{p-1}, q}$ 複雜度:$O(M \sum \log t_i)$ ---- ### 方法三: 觀察轉移式 $$dp[i][j] = \max_{k=0\sim t_i}\{ dp[i-1][j-k*w[i]]+k*v[i] \}$$ $$ = \max_{k=0\sim t_i}\{ dp[i-1][j-k*w[i]]-\lfloor\frac{j-k*w[i]}{w[i]}\rfloor*v[i]\}+\lfloor\frac{j}{w[i]}\rfloor * v[i]$$ 我們令 $F[t] = dp[i-1][t] + \lfloor \frac{t}{w[i]} \rfloor*v[i]$,轉移式就可以寫成 $$ dp[i][j] = \max_{k=j, j-w[i],\dots j-w[i]*t[i]}\{F[k]\}+\lfloor \frac{j}{w[i]} \rfloor * v[i] $$ 這樣就可以把 $(F[i], i)$ 丟進 deque 維護單調性 ---- 對每個物品 $i$ ,分別對所有除 $w[i]$ 有相同餘數的 $index$ 做單調隊列優化 DP ,一共做 $w[i]$ 次,複雜度是 $O(MN)$ 。 --- ## 區間DP - 區間 $dp$ 就是定義 $dp[l][r]=$ $l\sim r$ 的最優解。 - 問題會有合併這個動作,所以子結構會被分成 $l\sim k$ 和 $k+1\sim r$。 - 通常會是 $O(n^2)$ 的狀態加上 $O(n)$ 轉移 常見的範圍:$n \le 200, 300, 500$ ---- 矩陣鏈乘積: $r\times h$ 的矩陣乘 $h \times c$ 的矩陣需要 $r\times c\times h$ 次運算。現在有 $n$ 個矩陣 $A_1A_2\cdots A_n$ 要相乘,求最少需要幾次運算 - $A_i$ 是 $r_i \times c_i$ 的矩陣,且 $c_i=r_{i+1},i=1\sim n-1$ $((A_1(A_2A_3))A_4)$ $((A_1A_2)(A_3A_4))$ $(((A_1A_2)A_3)A_4)$ $\cdots$ ---- $dp[l][r]=$ 計算 $A_lA_{l+1}...A_{r-1}A_{r}$ 的最少運算次數 $dp[1][5]$ 的子問題: $(A_1)(A_2A_3A_4A_5) \implies (dp[1][1])(dp[2][5])$ $(A_1A_2)(A_3A_4A_5) \implies (dp[1][2])(dp[3][5])$ $(A_1A_2A_3)(A_4A_5) \implies (dp[1][3])(dp[4][5])$ $(A_1A_2A_3A_4)(A_5) \implies (dp[1][4])(dp[5][5])$ ---- 轉移式:$$dp[l][r] = \min_{k=l\sim r-1}\{dp[l][k]+n_im_km_r+dp[k+1][r]\}$$ 狀態有 $O(n^2) 個$ ,轉移需要 $O(n)$ ,複雜度是 $O(n^3)$ 按照區間長度由小到大計算 $dp$ ---- code ``` for (int i = 1; i <= n; i++) dp[i][i] = 0 // 區間大小為 1 為 base case for (int len = 2; len <= n; len++) // 由小區間往大開始轉移 for (int l = 1, r = len; r <= n; l++, r++) { dp[l][r] = INF; for (int k = l; k < r; k++) dp[l][r] = min(dp[l][r], dp[l][k] + dp[k+1][r] + n[i]*m[k]*n[r]); } return dp[1][n]; ``` ---- [P1880 [NOI1995] 石子合并:](https://www.luogu.com.cn/problem/P1880) 有 $n$ 堆石頭擺成 $\textbf{一圈}$,石頭的數量分別是 $a_1 \sim a_n$ ,每次可以合併相鄰的兩堆石頭,獲得的分數是這堆石頭的個數,問把所有石頭變成一堆的最大得分和最小得分 - $1 \le n \le 100, 0 \le a_i \le 20$ ---- 先考慮更簡單的問題,$n$ 堆石頭擺成 "一排" 的情況 $dp[l][r] =$ 把 $l\sim r$ 合併成一堆的最小分數 在 $l\sim r$ 合併成一堆之前他原本是兩堆,這兩堆一定分別是 $l\sim k$ 堆出來的和 $k+1\sim r$ 堆出來的,所以轉移式是 $$dp[l][r] = \min_{k=l\sim r-1}\{dp[l][k]+dp[k+1][r]+sum[l][k]+sum[k+1][r]\}$$ $$= \min_{k=l\sim r-1}\{dp[l][k]+dp[k+1][r]\}+sum[l][r]$$ 複雜度 $O(n^3)$ ---- 現在考慮原問題,在把一圈石頭合併成一堆之前他是兩堆石頭,這兩堆石頭是怎麼來的? 以 $n=7$ 為例 如果最優解是由 $1\sim4$ 和 $5\sim7$ 兩堆而來的,那 $dp[1][7]$ 可以計算到它 ![](https://i.imgur.com/6n7hqxd.png) ---- 可是如果是這樣的話 $dp[1][7]$ 就算不到它了 ![](https://i.imgur.com/kQSiRsg.png) 但是我們可以把原序列 $a_1, a_2, a_3,\cdots ,a_7$,透過旋轉擺成 $a_3, a_4, \cdots a_7, a_1, a_2$,再去算 $dp$ ,這時候 $dp[1][7]$ 就可以從由這兩堆轉移而來 ---- 因為我們不知道最優解長怎樣,所以要把 $a_1, a_2, a_3, a_4, a_5, a_6, a_7$, $a_7, a_1, a_2, a_3, a_4, a_5, a_6$, $a_6, a_7, a_1, a_2, a_3, a_4, a_5$, $\cdots$ 都算一遍然後取最優的那個 $dp[1][7]$, 因為要做 $n$ 次 $dp$ 所以複雜度是 $O(n^4)$ ---- 我們不需要每次旋轉一格然後從算,直接把原序列複製一遍接在後面變成 $a_1, a_2, a_3, a_4, a_5, a_6, a_7,$ $a_1, a_2, a_3, a_4, a_5, a_6, a_7$ 再去算長度為 $2n$ 的 $dp$,最後取 $dp[1][7], dp[2][8], dp[3][9], \cdots, dp[7][13]$ 的最小值,複雜度還是 $O(n^3)$ ---- [CF1132F Clear the String](https://codeforces.com/problemset/problem/1132/F) 給一個長度為 $n$ 由 $a\sim z$ 組成的字串 $s$,每次操作可以刪除 $s$ 中有連續相同字元的區間,問最少要幾次操作才能把 $s$ 全部消掉 - $1 \le n=|s| \le 500$ ---- 這種消掉一個區間然後再合併的區間消除問題很常見,它有兩個很重要的性質。 1. 連續一樣的東西一定是一起消掉,沒理由分開消 以這題來說,顯然 2. 最左邊或是最右邊的連通塊一定可以留到最後消 以最左邊為例,先消掉最左邊不會影響它右邊的部分,因為它的左邊沒有東西跟右邊合併,所以把最左邊留下先把右邊的部分消完也不影響答案 ---- - 由 1. 我們可以把原字串連續一樣的部分直接變成 $($什麼字元, 有幾個$)$ - 由 2. 我們可以定義 $dp[l][r]=$ 把 $s[l:r]$ , 消到剩下(不知道幾個) $s[l]$ 需要多少次操作 ---- 轉移: $s[l:r]$ 消成一堆 $s[l]$ 可以 - 直接把 $s[l+1:r]$ 消掉留下 $s[l]$ $dp[l][r] = dp[l+1][r] + 1$ - $s[l]$ 最後跟 $s[k]$ 合併 $s[l]=s[k]$ 要把 $s[l, k-1]$ 消成 $s[l]$,$s[k:r]$ 消成 $s[k]$ $dp[l][r] = dp[l][k-1] + dp[k][r]$ - $s[l]$ 最後跟 $s[k], s[k_1], s[k_2], \cdots$ 合併? 不用考慮那麼多,如果跟 $s[k_1], s[k_2]\cdots$ 併會比較好的話,會在把 $s[k:r]$ 變成一堆 $s[k]$ 的時候合併,$dp[k][r]$ 已經幫我們處理好了。 ---- $$dp[l][r]=min(dp[l+1][r]+1, \min_{k=l+1\sim r}\{dp[l][k-1]+dp[k][r]\})$$ 答案是 $dp[1][n]+1$ 因為要再花一次操作把 $s[1]$ 消掉。 複雜度:$O(n^3)$ ---- code ``` for (int i = 1; i <= n; i++) dp[i][i] = 0; for (int len = 2; len <= n; len++) for (int l = 1, r = len; r <= n; l++, r++) { dp[l][r] = 1 + dp[l+1][r]; for (int k = l+1; k <= r; k++) if (S[l] == S[k]) dp[l][r] = min(dp[l][r], dp[l][k-1] + dp[k][r]); } return dp[1][n] + 1; ``` ---- [2020 ICPC 台灣站 pE](https://codeforces.com/gym/102835/problem/E) 給一個由 R, G, B, C, M, Y, K 七種字元組成的字串 $s$ ,每次可以把長度 $\ge m$ 的相同字元的子字串消掉,問能不能把 $s$ 全部消掉。 - $0 < n, m, \le 500$ ---- - 由 1. 先把 $s$ 連續相同的部分合併,變成 $($什麼字元, 有幾個$)$ - 由 2. 定義 $dp[l][r]=$ 能把 $s[l:r]$ 消成 "最多幾個" $s[l]$,或是沒辦法 $0$ 如果能把 $s[l:r]$ 全部消掉的話,把最左邊那塊留到最後,等於 $s[l]$ 的這塊最後可能直接消掉或是跟別人合併,不管是哪種一定是越大越好。 ---- 轉移: $s[l:r]$ 消成一堆 $s[l]$ 可以 - 直接把 $s[l+1:r]$ 消掉留下 $s[l]$ 要判斷 $s[l+1:r]$ 能不能被消掉,也就是 $dp[l+1][r] \ge m$ $dp[l][r] = dp[l][l]$ - $s[l]$ 最後跟 $s[k]$ 合併 $s[l]=s[k]$ 要判斷能不能把 $s[l:k-1]$ 消成 $s[l]$,和 $s[k:r]$ 消成 $s[k]$ 也就是 $dp[l][k-1] > 0\ \mbox{and}\ dp[k][r] > 0$ $dp[l][r] = dp[l][k-1] + dp[k][r]$ - $s[l]$ 最後跟 $s[k], s[k_1], s[k_2], \cdots$ 合併? 不用考慮那麼多... ---- 跟上一題差不多,直接放code ``` for (int i = 1; i <= n; i++) dp[i][i] = cnt[i]; for (int len = 2; len <= n; len++) for (int l = 1, r = len; r <= n; l++, r++) { if (dp[l+1][r] >= m) dp[l][r] = dp[l][l]; for (int k = l+1; k <= r; k++) if (s[l] == s[k] and dp[l][k-1] and dp[k][r]) dp[l][r] = max(dp[l][r], dp[l][k-1] + dp[k][r]); } return dp[1][n] >= m; ``` --- ## 數位 DP 數位是指個、十、百、千等等一位一位地拆開,通常是拆十進位數$(0\sim 9)$或二進位數$(0,1)$ 問在 $[L, R]$ 之間滿足某些條件的數有幾個,$L,R$ 的範圍通常可以到 $10^{18}$ 或是 $2000 \sim 5000$ 位數,條件會跟位數或是數值有關。 ---- [例題1](https://cses.fi/problemset/task/2220): 給你 $a, b$ 兩個數字,問 $a\sim b$ 沒有連續兩位相同的數有幾個 - $0 \le a \le b \le 10^{18}$ ---- 問題的答案是 $ans_{[L,R]}$,我們可以拆成 $ans_{[0,R]}-ans_{[0,L-1]}$,問題變成求 $0\sim S$ 有幾個滿足條件的數 記 $S[i]$ 表示 $S$ 從左往右看的第 $i$ 位,可以把 $S$ 想像成字串 從最高位開始放數字,現在要放第 $i$ 位,要滿足兩個條件 - 不能跟前一位 $S[i-1]$ 相同 - 不能超過 $S$ 如果前面已經有一位 $j$ 滿足 $x[j] < S[j]$ 後面不管怎麼放都不會超過 $K$,如果前面個位數都跟 $S$ 一樣,那目前要放的這一位不能超過 $S[i]$ ---- 我們需要知道兩個資訊,前面個位數是不是貼著 $S$ 或已經小於 $S$,和前一位數是什麼,所以我們定義 $dp[i][j][0]=$ 第 $i$ 位是 $j$,且前 $i$ 位小於/等於(0/1) $S$ 的前 $i$ 位,且滿足條件的數有幾個 最後的答案是 $\sum dp[|S|-1][0\sim 9][0/1]$ ---- 用 push 的方式轉移 - $dp[i][j][0]$ 已經不用關心大小了,只要和 $j$ 不一樣就好 for $k=0\sim 9 \land k \ne j$ $dp[i+1][k][0] \mathrel{+}= dp[i][j][0]$ - $dp[i][j][1]$ 除了不能跟 $j$ 一樣還不能超過 $S[i+1]$ if $S[i+1] \ne j$ $dp[i+1][S[i+1]][1] \mathrel{+}= dp[i][j][1]$ for $k = 0\sim S[i+1]-1 \land k \ne j$ $dp[i+1][k][0] \mathrel{+}= dp[i][j][1]$ ---- 狀態數: $O(|S|\times 10 \times 2)$ 轉移數:$O(10)$ 複雜度 $O(10\times 10\times 2\times |S|)=O(位數)$ ---- code ``` long long l, r; cin >> l >> r; auto DP = [&](long long x) -> long long { if (x == -1) return 0; vector<int> s; for (char c : to_string(x)) s.emplace_back(c - '0'); const int n = s.size(); memset(dp, 0, sizeof(dp)); for (int i = 1; i <= s[0]; i++) dp[0][i][i == s[0]] = 1; for (int i = 0; i+1 < n; i++) { dp[i][0][0]++; for (int j = 0; j < 10; j++) { for (int k = 0; k < 10; k++) { if (k == j) continue; if (k < s[i+1]) dp[i+1][k][0] += dp[i][j][1]; if (k == s[i+1]) dp[i+1][k][1] += dp[i][j][1]; dp[i+1][k][0] += dp[i][j][0]; } } } long long ans = 1; for (int i = 0; i < 10; i++) ans += dp[n-1][i][0] + dp[n-1][i][1]; return ans; }; cout << DP(r) - DP(l-1) << '\n'; ``` ---- [例題2:](https://atcoder.jp/contests/dp/tasks/dp_s) 計算 $1\sim K$ 中有幾個數滿足以下條件,輸出 $\mbox{mod } 10^9+7$ 後的結果 - 所有位數的總和是 $D$ 的倍數 - $1 \le K < 10^{10000}$ - $1 \le D \le 100$ ---- 這次我們不需要紀錄上一位放什麼,但是要記錄目前的位數和是多少,我們最後只關心他是不是 $D$ 的倍數,所以定義狀態為 $dp[i][j][0] =$ 前 $i$ 位的位數和 $\% D$ 為 $j$,且前 $i$ 位小於/等於(0/1) K 的前 $i$ 位的數有幾個 答案是 $dp[|K|-1][0][0]+dp[|K|-1][0][1]$ ---- 轉移: 這裡一樣用 push 的 - $dp[i][j][0]$ 後面可以放任合數 for $k=1\sim 9:$ $dp[i+1][(j+k)\%D][0] \mathrel{+}= dp[i][j][0]$ - $dp[i][j][1]$ 後面不能放超過 $K[i+1]$ 的數 for $k=1\sim K[i+1]$ $dp[i+1][(j+k)\%D][k==K[i+1]] \mathrel{+}= dp[i][j][1]$ 複雜度:$O(Dlog_{10}K)$ ---- code ``` for (int i = 0; i <= s[0]; i++) dp[0][i%d][i == s[0]] += 1; for (int i = 0; i + 1 < n; i++) { for (int j = 0; j < d; j++) for (int k = 0; k < 10; k++) { if (k <= s[i+1]) (dp[i+1][(j+k)%d][k == s[i+1]] += dp[i][j][1]) %= mod; (dp[i+1][(j+k)%d][0] += dp[i][j][0]) %= mod; } } cout << (dp[n-1][0][0] + dp[n-1][0][1] + mod - 1) % mod << '\n'; ``` --- ### Homework and Practice deadline: 5/13 link: https://vjudge.net/contest/556099
{"metaMigratedAt":"2023-06-18T01:11:40.326Z","metaMigratedFrom":"YAML","title":"DP and DP-optimize","breaks":true,"slideOptions":"{\"transition\":\"fade;\"}","contributors":"[{\"id\":\"5c232982-829c-49dc-9b99-412c8d927dbc\",\"add\":11939,\"del\":1965},{\"id\":\"19f09ccf-6b99-452f-971f-955cfc1657f3\",\"add\":299,\"del\":100},{\"id\":\"e4a3af8b-860c-46a0-96f1-558667fdcf41\",\"add\":316,\"del\":23}]"}
    1132 views
   owned this note