DQ 與 DP
By 建國中學 賴昭勳
分治法
(Divide and Conquer)
把問題切好幾塊
- 分成子問題
- 解決子問題
- 合併子問題
合併排序(Merge Sort)
1. 分成子問題
把序列切一半
2. 解決子問題
遞迴處理(先假設做得到)
3. 合併子問題
假設有兩個已排序好的序列,要怎麼有效率地將它們合併成一個?
1 | 3 | 5 | 8 | 9 |
---|---|---|---|---|
1 | 2 | 2 | 7 |
1
1
2
2
3
5
7
8
9
聽懂了ㄇ?(這張講師做好久ww
剛剛那個的時間複雜度?
O(n)
2 1 4 4,7 8 3 6 4,7
1,2 4,4,7 3,8 4,6,7
7 4 4 7
1,2,4,4,7 3,4,6,7,8
1,2,3,4,4,4,6,7,8
這樣有 O(logn)層
每層都是O(n)
所以就能O(nlogn)排序了!!
分治的時間複雜度分析
aka 主定理 Master's Theorem
(講師也不會的東西)
Code It!
#include <iostream>
using namespace std;
void merge_sort(int a[], int l, int r) { //[l, r)
if (r - l <= 1) return;
int mid = (l + r) / 2;
merge_sort(a, l, mid);
merge_sort(a, mid, r); //thus, [l, mid) and [mid, r) are sorted
int sorted[r - l];
int li = l, ind = 0;
for (int ri = mid;ri < r;ri++) { //two pointers
while (li < mid && a[li] <= a[ri]) {
sorted[ind] = a[li];
ind++, li++;
}
sorted[ind] = a[ri];
ind++;
}
while (li < mid) { //insert remaining elements
sorted[ind] = a[li];
ind++, li++;
}
for (int i = 0;i < r - l;i++) a[i + l] = sorted[i];
}
int main() {
int n;
cin >> n;
int a[n];
for (int i = 0;i < n;i++) {
cin >> a[i];
}
merge_sort(a, 0, n);
for (int i = 0;i < n;i++) cout << a[i] << " ";
cout << endl;
}
這很容易有bug喔!有問題可以看這份!
分治可以幹嘛w
Q: 逆序數對
可以說是全TIOJ最經典的題目(之一)了!
給你一個數列,問有多少組
符合
可以這樣想問題
對於每個東西,問你右邊有幾個東西比他小,把那個數量加起來。
O(n^2) Naive 解...
怎麼樣更快
如果 a < b, b < c, 那麼 a < c
廢話><....嗎?
更好的複雜度
代表一定有不必要的資訊
沒錯!就是Merge Sort
- 合併時順帶紀錄答案
- 對於每個左邊的東西,看有幾個右邊的元素已經先放進排序好的序列(比他小)
- 把 i > j 這個維度壓掉不用考慮
實作 time!
Btw, 這題也可以
值域壓縮+BIT喔
以後資結課再講
分治例題
有沒有發現這樣我就不用講很多dp 了XDD
Q0. 太陽軍團 (從資芽偷起來)
有一個 n列m行 的正整數矩陣,要問你每ㄧ列的最大值,但你不知道矩陣長什麼樣子,只能詢問某一個位置的數值。保證每一列的最大值位置嚴格遞增。
先問中間的,就可以讓上下的搜尋範圍縮小!
Q1. CF 1311F
直線上有 n < 10^5 個點,第 i 個點有位置pi,依速度 vi 做等速運動,令 d(i, j) 為第 i 個點跟第 j 個點在以後無限時間最短的距離,
求
P.S. 這題以後學資料結構再做也可以喔!
Q2. 平面最近點對
平面上有 n 個點,求任兩點之間最短距離。
假設分成左右兩塊的都算好答案了...
答案會是 min(左, 右, 左右之間)
d
演算法
- 按 x 座標排序,分成左右兩半遞迴求解。
- 令左右找到最小的距離為 d,以左邊點的最右界來看,找到左右邊離那條線距離 < d 的點。
- 對於每個左邊的點,找到 y 比他小的右側兩點,和 y 比他大的上側兩點更新答案。
複雜度:
Q3. FFT (x)
分治的應用
- FFT(噁
- 線段樹, BIT
- 倍增法(Doubling):LCA, Sparse Table
- 重心剖分(噁
- CDQ 分治(噁
- 高維偏序 (噁
動態規劃<3
遞迴的威力
有個 n 階的樓梯,每次可以往上走1, 2, 3階,共有幾種走完n階的方法?
寫遞迴式
int solve(int n) {
if (n < 0) return 0;
else if (n <= 1) return 1;
else return solve(n - 1) + solve(n - 2) + solve(n - 3);
}
這樣的複雜度?
遞迴時把重複的存起來
int solve(int n) {
int ans[n + 1];
ans[0] = 0;
for (int i = 1;i <= n;i++) {
ans[i] = ans[i - 1];
if (i > 1) ans[i] += ans[i - 2];
if (i > 2) ans[i] += ans[i - 3];
}
return ans[n];
}
動態規劃的精髓
可以dp 的問題符合兩個條件:
-
重複子問題
-
可分治性
dp 式的三要素
0.定義
1.轉移方式 (aka 遞迴式)
2.邊界條件
來看個例題
0. 定義
dp[i][j] 表示走到第 i 橫排第 j 格的時候可能的最大值。
答案:max(dp[n - 1][ j ])
1. 轉移式
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1]) + a[i][j]
2. 邊界條件
dp[0][0] = a[0][0]
注意 j == 0 時不能從 dp[i - 1][j - 1] 轉移!
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int n;
cin >> n;
int a[n][n], dp[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j<= i; j++) {
cin >> a[i][j];
}
}
for (int i = n - 1; i >= 0; i--) {
for (int j = 0; j<= i; j++) {
if (i < n - 1) {
dp[i][j] = a[i][j] + \
max(dp[i + 1][j], dp[i + 1][j + 1]);
} else {
dp[i][j] = a[i][j];
}
}
}
cout << dp[0][0] << endl;
return 0;
}
參考程式
#include <cstdio>
inline short read() {
short num = 0;
char c = getchar_unlocked();
while (c < '0' || c > '9') {
c=getchar_unlocked();
}
while (c>='0' && c<='9') {
num *= 10;
num += c - '0';
c = getchar_unlocked();
}
return num;
}
void putint(short a) {
int d = 0;
char c[5];
while (a) {
c[d++] = '0' + a % 10;
a /= 10;
}
for (int i = d - 1;i >= 0;i--) putchar_unlocked(c[i]);
}
short dp[200];
int main() {
short n = read();
short ans = 0;
for (short i = 0; i < n; i++) {
for (int j = 0;j <= i;++j) {
dp[100 + j] = (dp[j] > (j ? dp[j - 1] : -1) ? dp[j] : dp[j - 1]) + read();
}
for (int j = 0;j <= i;++j) {
dp[j] = dp[100 + j];
ans = dp[j] > ans ? dp[j] : ans;
}
}
putint(ans);
return 0;
}
接下來就是一些經典題目了!
DP 經典題
背包問題
aka 比FFT難的東西
有 n 個東西,每個東西有重量 wi 和價值 pi,你有一個耐重 m 的背包,問在不超過耐重的前提下價值最多可以多少?
想法
單純窮舉每個東西放與不放的話,複雜度是O(2^n)。
有沒有辦法減少可能的狀態數?
最後答案可以怎麼取得?
一種取法可以怎麼表示?
有沒有辦法減少可能的狀態數?有w
最後答案可以怎麼取得?
看每一種重量下最多能拿多少取max
一種取法可以怎麼表示?
有總重量和總價值兩個數值
dp[i][j] 代表考慮了前 個東西,總重量 時的最大價值。
這樣的話答案必為
怎麼轉移?
如果多加了一個東西,就可以更新到他現在考慮的那個東西的那個重量!
複雜度?
注意:這理論上不是真的多項式複雜度喔!(有值域項)
0-1 背包的兄弟:無限背包
有 n 種東西,每個東西有重量 wi 和價值 pi,且每種東西都有無限供應。
你有一個耐重 m 的背包,問在不超過耐重的前提下價值最多可以多少?
其實跟0-1背包一樣,只是同一個東西可以一直加上去
0-1
背包
無限
背包
再來一個:分數背包!
有 n 個東西,每個東西有重量 wi 和價值 pi,且每種東西都可以切成任意大小塊。
你有一個耐重 m 的背包,問在不超過耐重的前提下價值最多可以多少?
哈哈是Greedy啦
現在來講LIS 吧
給你一個正整數序列,問你最多可以從裡面選出幾個元素(不改變順序),使得這些元素嚴格遞增。
例如:7, 1, 2, 2, 5, 3, 4 的LIS 是
1, 2, 3, 4
思路
假設第 項要選的話,我們一定希望那項之前選的個數越多越好。
如果讓 表示取了 為最後一個數字的LIS呢?
要怎麼轉移?
這要怎麼維護才不會O(n^2)?
(如果不用值域壓縮+BIT的話)
換個定義好了
代表長度為 的時候最小的數字可以是多少 (注意他一定嚴格遞增)
轉移:找到使 最大,且比
小的 ,去更新
實作
可以運用vector+lower_bound
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main() {
int n;
cin >> n;
int a[n];
for (int i = 0;i < n;i++) cin >> a[i];
vector<int> lis;
for (int i = 0;i < n;i++) {
int ind = lower_bound(lis.begin(), lis.end(), a[i]) - lis.begin();
//lower bound: the iterator of the first element >= a[i]
if (ind == lis.size()) {
lis.push_back(a[i]);
} else {
lis[ind] = a[i];
}
}
cout << lis.size() << endl;
return 0;
}
最大連續和
有一個整數數列,問你選一段連續區間最大的和是多少。
前綴 and 紀錄min
原來這也是DP
DP 經典題還有太多太多了...
之後再慢慢講吧!
寫DP 時要注意的問題
-
轉移方式:Top-Down or Bottom-up
-
複雜度=狀態數*轉移時間
-
轉移順序
DP 例題
0. 池塘裡的青蛙
池塘中有一隻青蛙在四塊石頭A、B、C、D之中跳來跳去。今青蛙由A起跳,每次跳到另一塊石頭,青蛙跳了n次後停在A的方法數有多少呢?
註:原題範圍很小,可以出到
並將答案模 1000000007
1. 池塘裡的青蛙
池塘中有 n 顆排成一列的石頭,每顆石頭上有一隻青蛙和一個數字 a[i]。
每一分鐘,所有在第 i 顆石頭上的青蛙會往右跳 a[i] 格到第 i + a[i]個石頭(一顆石頭上可能有多隻青蛙),如果a[i] + i >= n青蛙就會跳進池塘裡。
請問在 k 分鐘後有幾隻青蛙會在池塘裡?
From PCCA 2020
倒回來做!
dp[i] 表示從 i 開始走幾步才會到池塘
Ans:
2. Jumping Up
由下到上有n 個平台,FHVirus 從第 1 個開始,每次可以往上跳 1~2個平台。給你每個平台的水平位置,請問跳到第 n 個平台最少的水平移動是多少?
3. Gas Pipeline
你要在直線道路上架管線,管線由管線本身(?)和支柱組成。每段管線高度可為1或2(首尾高度皆必須為1),且有一個 01組成的序列,如果第 i 項為 1 代表第 i 公尺高度一定是2。管線每公尺要花 a 元,支柱每公尺 b 元,請問最小花費是多少? (n <= 10^5)
這題同時有Greedy 跟DP解喔!
Hint:
給你 n*m 的矩陣,每個元素代表一單位空間,其中有些位置有障礙物。你必須選取一個沒有障礙物的正方形區塊。請問這個區塊最大的面積是多少?
本題有多筆測資!!!別像我當年一樣WA十遍 ;-;
5. Colored Rectangles
有 r 對紅色棍子,g 對綠色,b 對藍色(不同對棍子長度不同)
每次可以取兩對顏色不同的棍子組成長方形,且一對棍子只能用一次。請問組完若干個長方形的最大面積總和是多少?
Hint:排序不等式
若
Hint
先排序再dp
這樣可以把可能的狀態變成幾種?
代表紅色取 個,綠色取 個,
藍色取 個的最大面積
Ans:
6. A 遊戲
8e7 和 Jass 在玩一個遊戲:有一個正整數數列,兩人輪流把最左邊或最右邊的數字拿走,假設 8e7 先手,他跟 Jass 都使用最佳策略下分別可以拿幾分?
以後會教的DP
好難好難
- 矩陣快速冪
- 位元DP
- 有向圖DP
- 樹DP
- 線段樹上DP
- 單調隊列優化
- 斜率優化
- 四邊形優化
- Aliens
DQ 與 DP (資讀)
By justinlai2003
DQ 與 DP (資讀)
- 2,598