DQ 與 DP

By 建國中學 賴昭勳

分治法

(Divide and Conquer)

把問題切好幾塊

  1. 分成子問題
  2. 解決子問題
  3. 合併子問題

合併排序(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最經典的題目(之一)了!

給你一個數列,問有多少組

符合

a_i, a_j
i < j \ \ and \ \ a_i > a_j
n \leq 10^5, a_i \leq 10^9

可以這樣想問題

對於每個東西,問你右邊有幾個東西比他小,把那個數量加起來。

O(n^2) Naive 解...

怎麼樣更快

如果 a < b, b < c, 那麼 a < c

廢話><....嗎?

更好的複雜度

代表一定有不必要的資訊

沒錯!就是Merge Sort

  • 合併時順帶紀錄答案
  • 對於每個左邊的東西,看有幾個右邊的元素已經先放進排序好的序列(比他小)
  • 把 i > j 這個維度壓掉不用考慮

實作 time!

把剛剛merge sort 的程式做一些修改,寫出你的逆序數對吧!(好中二

 

可以丟這:https://tioj.ck.tp.edu.tw/problems/1080​

 

Btw, 這題也可以

值域壓縮+BIT喔

以後資結課再講

分治例題

有沒有發現這樣我就不用講很多dp 了XDD

Q0. 太陽軍團 (從資芽偷起來)

有一個 n列m行 的正整數矩陣,要問你每ㄧ列的最大值,但你不知道矩陣長什麼樣子,只能詢問某一個位置的數值。保證每一列的最大值位置嚴格遞增。

n \leq m \leq 10^6, Query \leq 10^8

先問中間的,就可以讓上下的搜尋範圍縮小!

直線上有 n < 10^5 個點,第 i 個點有位置pi,依速度 vi 做等速運動,令 d(i, j) 為第 i 個點跟第 j 個點在以後無限時間最短的距離,

\sum_{1\leq i < j \leq n} d(i, j)

P.S. 這題以後學資料結構再做也可以喔!

Q2. 平面最近點對

平面上有 n 個點,求任兩點之間最短距離。

n \leq 10^5

假設分成左右兩塊的都算好答案了...

答案會是 min(左, 右, 左右之間)

d

​演算法

  1. 按 x 座標排序,分成左右兩半遞迴求解。
  2. 令左右找到最小的距離為 d,以左邊點的最右界來看,找到左右邊離那條線距離 < d 的點。
  3. 對於每個左邊的點,找到 y 比他小的右側兩點,和 y 比他大的上側兩點更新答案。

複雜度:

O(nlogn)

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] 代表考慮了前    個東西,總重量       時的最大價值

這樣的話答案必為

 

怎麼轉移?

i
\geq j

如果多加了一個東西,就可以更新到他現在考慮的那個東西的那個重量!

dp[i][j] = max(dp[i][j],\ dp[i - 1][j - a[i]] + p[i])
dp[n - 1][m]

複雜度?

O(nm)

注意:這理論上不是真的多項式複雜度喔!(有值域項)

0-1 背包的兄弟:無限背包

有 n 種東西,每個東西有重量 wi 和價值 pi,且每種東西都有無限供應

 

你有一個耐重 m 的背包,問在不超過耐重的前提下價值最多可以多少?

其實跟0-1背包一樣,只是同一個東西可以一直加上去

0-1

背包

無限

背包

再來一個:分數背包!

有 n 個東西,每個東西有重量 wi 和價值 pi,且每種東西都可以切成任意大小塊

 

你有一個耐重 m 的背包,問在不超過耐重的前提下價值最多可以多少?

哈哈是Greedy啦

現在來講LIS 吧

給你一個正整數序列,問你最多可以從裡面選出幾個元素(不改變順序),使得這些元素嚴格遞增。

 

n \leq 10^5

例如:7, 1, 2, 2, 5, 3, 4 的LIS 是

1, 2, 3, 4

思路

假設第   項要選的話,我們一定希望那項之前選的個數越多越好。

i

如果讓         表示取了        為最後一個數字的LIS呢?

要怎麼轉移?

dp[i]
a[i]

這要怎麼維護才不會O(n^2)?

(如果不用值域壓縮+BIT的話)

換個定義好了

               代表長度為       的時候最小的數字可以是多少 (注意他一定嚴格遞增)

dp[i]
i

轉移:找到使     最大,且比

         小的           ,去更新

dp[i]
i
a[i]
dp[i+1]

實作

可以運用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;
}

最大連續和

有一個整數數列,問你選一段連續區間最大的和是多少。

n \leq 2 * 10^6

前綴 and 紀錄min

原來這也是DP

DP 經典題還有太多太多了...

之後再慢慢講吧!

寫DP 時要注意的問題

  • 轉移方式:Top-Down or Bottom-up

  • 複雜度=狀態數*轉移時間

  • 轉移順序

DP 例題

0. 池塘裡的青蛙

池塘中有一隻青蛙在四塊石頭A、B、C、D之中跳來跳去。今青蛙由A起跳,每次跳到另一塊石頭,青蛙跳了n次後停在A的方法數有多少呢?

註:原題範圍很小,可以出到 

並將答案模 1000000007

n \leq 10^6

1. 池塘裡的青蛙

池塘中有 n 顆排成一列的石頭,每顆石頭上有一隻青蛙和一個數字 a[i]。

每一分鐘,所有在第 i 顆石頭上的青蛙會往右跳 a[i] 格到第 i + a[i]個石頭(一顆石頭上可能有多隻青蛙),如果a[i] + i >= n青蛙就會跳進池塘裡。

請問在 k 分鐘後有幾隻青蛙會在池塘裡?

 

 

From PCCA 2020

n, k \leq 10^6

倒回來做!

dp[i] 表示從 i 開始走幾步才會到池塘

 

dp[i] = dp[i + a[i]] + 1 \\ if \ \ i + a[i] \geq n, dp[i] = 1

Ans:

由下到上有n 個平台,FHVirus 從第 1 個開始,每次可以往上跳 1~2個平台。給你每個平台的水平位置,請問跳到第 n 個平台最少的水平移動是多少?

n \leq 10^6

3. Gas Pipeline

你要在直線道路上架管線,管線由管線本身(?)和支柱組成。每段管線高度可為1或2(首尾高度皆必須為1),且有一個 01組成的序列,如果第 i 項為 1 代表第 i 公尺高度一定是2。管線每公尺要花 a 元,支柱每公尺 b 元,請問最小花費是多少? (n <= 10^5)

這題同時有Greedy 跟DP解喔!

Hint:

給你 n*m 的矩陣,每個元素代表一單位空間,其中有些位置有障礙物。你必須選取一個沒有障礙物的正方形區塊。請問這個區塊最大的面積是多少?

n, m \leq 5000

本題有多筆測資!!!別像我當年一樣WA十遍 ;-;

有 r 對紅色棍子,g 對綠色,b 對藍色(不同對棍子長度不同)

每次可以取兩對顏色不同的棍子組成長方形,且一對棍子只能用一次。請問組完若干個長方形的最大面積總和是多少?

 

r, g, b \leq 200

Hint:排序不等式

a_i < b_i, a_j < b_j \\ a_i * a_j + b_i * b_j \geq a_i * b_j + b_i * a_j

Hint

先排序再dp

這樣可以把可能的狀態變成幾種?

dp[i][j][k]

代表紅色取     個,綠色取     個,

藍色取      個的最大面積

i
j
k
dp[i][j][k] = max(dp[i - 1][j - 1][k] + a[i] * a[j], \\ dp[i][j - 1][k - 1] + a[j] * a[k], dp[i - 1][j][k - 1] + a[i] * a[k])

Ans:

6. A 遊戲

8e7 和 Jass 在玩一個遊戲:有一個正整數數列,兩人輪流把最左邊或最右邊的數字拿走,假設 8e7 先手,他跟 Jass 都使用最佳策略下分別可以拿幾分?

n \leq 1000

以後會教的DP

好難好難

  • 矩陣快速冪
  • 位元DP
  • 有向圖DP
  • 樹DP
  • 線段樹上DP
  • 單調隊列優化
  • 斜率優化
  • 四邊形優化
  • Aliens

DQ 與 DP (資讀)

By justinlai2003