9/22 校隊培訓
FHVirus
這堂課的內容主要是解題的想法、一般性的概念(尤其是DP),
相關的題目變化多端,請多多做題練習。
國小生都寫的出來(不是嘴砲)
有 \(N\) 個線段 \([l_i, r_i]\),
求最多能選幾個線段
使得他們互不相交
有 \(N \le 10^5\) 個人站成一排,每個人左右手各拿一張牌 \(a_i, b_i\),
每個人的分數為「前面所有人的 \(a\) 乘積除以自己的 \(b\)」。
已知第一個人固定且不計分,
求一種排序,使得所有人分數最大值最小。
你有 \(10^9\) 單位的時間以及 \(N \le 10^5\) 份工作。
每一份工作有一個截止時間 \(D_i \le 10^9\)和一個報酬 \(P_i\)。
每一單位時間你可以做一份工作,求最大獲利。
某國硬幣有 \(N\) 種面額 \(a_1, a_2, \cdots, a_N\)
現在給你要用這些幣值湊出 \(X\) 元,
求最少需要幾個硬幣。
我懶的放題敘
我懶的放題敘
當子問題重複,可以紀錄子問題答案,避免重複計算
時間複雜度也會變好!
(不會重複的話就 DQ 就好)
DP 的問題形式,也就是 DP 狀態
要先定好狀態(想清楚要問什麼)才能找答案
例如費氏數列,狀態 \(dp_i\) 代表第 \(i\) 項
這步驟通常是最難的(知道要問什麼問題)
有點玄學有點通靈
多做題也許有用
DP 的子問題,也就是 DP 轉移來源
代表「現在這個問題的答案可能是從哪個子問題來的」
在最佳化問題中,來源不一定單一
例如費氏數列,狀態 \(dp_i\) 的來源為 \(dp_{i-1}, dp_{i-2}\)
要特別小心不能再分下去的狀態,也就是邊界狀態
例如費氏數列的邊界狀態就是 \(dp_0 = dp_1 = 1\)
DP 的子問題合併,也稱為 進行轉移
代表「從子問題得到現在狀態的答案」
在最佳化問題中,來源不一定單一
例如費氏數列,轉移就是 \(dp_i = dp_{i-1} +dp_{i-2}\)
轉移通常可以寫成式子表示
DP 優化也多從式子出發
強烈建議想題的時候把式子寫出來
DP 的問題答案紀錄,也會被稱為 DP 表格
通常開個陣列紀錄之類的
例如費氏數列要求到第 \(N\) 項就開長度為 \(N\) 的陣列
也有人會用 DFS + unordered_map
但效率奇差,不建議使用
除非你是要驗題或是唬爛
你爽就好,但某些題目不同作法難度會不同
Bottom-Up
(Push)
Bottom-Up
(Pull)
Top-Down
(DFS style)
常數稍大
當 DP 長的像這樣:\(dp_{i, [\cdots]} = f(dp_{i-1, [\cdots]})\)
也就是所有的 \(i\) 都只會從 \(i-1\) 轉移
那在算完 \(i\) 之後 \(i-1\) 就可以丟掉了(省空間!)
例如費氏數列,如果只要求第 \(N\) 項可以這樣寫:
long long a = 1, b = 1, c, N = 49;
// a = f(i-2), b = f(i-1), c = f(i)
for(int i = 2; i <= N; ++i){
c = a + b;
a = b;
b = c;
}
如果要滾的是一整個陣列,也可以用指標或 vector 來做
swap(vector<T>, vector<T>) 是 \(O(1)\) 的喔!
例題:\(N \times M\) 方格,有多少種方式從左上到右下
// sc for source
vector<int> dp(M + 1, 0), sc(M + 1, 0); // 記得大小要開對!
sc[0] = 1; // base case
for(int i = 1; i <= N; ++i){
for(int j = 1; j <= M; ++j){
// do things
}
swap(dp, sc);
}
cout << sc[M] << '\n'; // 最後會多 swap 一次!
滾動好處多多!
當 Greedy 不對就代表
「這個問題不能只由這個子問題轉移」
換句話說,Greedy 就是可以證明只有一個轉移來源的 DP
reference: baluteshih @2021TOI 1! 簡報
強烈建議各位去翻翻看