演算法 - 基礎DP(2)
Slides: Lemon
Lecturer: AaW
LIS
- 全名:Longest Increasing Subsequence
- 中文:最長遞增子序列
那是什麼意思 (?
遞增
遞增有兩種:
- 嚴格遞增 (後項 > 前項)
- 非嚴格遞增 (後項 >= 前項)
i.e.
[1, 2, 2, 3] 非嚴格遞增,但不嚴格遞增w
子序列
從原序列隨便挑幾個元素
並且讓其排列順序和原序列順序一樣
=> 子序列(Subsequence)
e.g.
[1, 3, 2]
[1], [3], [2], [1, 3], [1, 2], [3, 2], [1, 3, 2]
就是它的子序列
做法!
蔗糖課要教ㄉ DP
由於LIS遞增
我們會希望序列結尾的數字愈小愈好
而DP要記錄的東東
是在特定長度下LIS,結尾最小ㄉ
做法!
做法!
你會覺得LIS的DP非常適合用vector來做
因為DP會跟著LIS的長度提升而變長
我們把數字一個個放進去DP
如果比目前DP的結尾更大
LIS的長度就變長ㄌ,同時延長DP
否則就取代第一個比它大的DP
DP的長度就是LIS的長度!
而我們會使用二分搜來找到第一個比它大的DP
複雜度: \(O(nlogn)\)
CODE
#include <iostream>
#include <vector>
#include <algorithm> //for lower_bound()
using namespace std;
int main() {
int n; //數字個數
cout << "請輸入數字個數:\n";
cin >> n;
vector<int> LIS; //LIS的DP
cout << "請輸入數列:\n";
for(int i = 0; i < n; ++i) {
int buf;
cin >> buf; //輸入數字
if(LIS.empty() || buf > LIS.back()) { //如果LIS為空 or 新數字比LIS的結尾更大
LIS.push_back(buf);
}
else {
auto p = lower_bound(LIS.begin(), LIS.end(), buf); //找到第一個大於等於新數字的DP
*p = buf; //取代
}
cout << "輸入第" << i + 1 << "個數字後的DP:\n";
for(auto it = LIS.begin(); it != LIS.end(); ++it) {
cout << *it << ' ';
}
cout << '\n';
}
return 0;
}
這是嚴格遞增的CODE
可以自己試試看非嚴格遞增ㄉw
例題
TIOJ 1175(裸題)
ckefgiscoj 4468(想一下ㄅ)
原本OJ爛掉暫時的新連結
Knapsack
聽說比FFT還難(?
我們這裡所說的背包(Knapsack)問題
指的是 0/1背包問題
意思是:
對於\(n\)個物品價值\(p_i\)、重量\(w_i\)
只能選擇拿 or 不拿
一個指定的背包大小\(m\)
我們想知道最多能拿到多少價值的物品
可能的想法
Greedy!
一直選擇CP值最高的物品放進背包
你會發現
這顯然是個唬爛作法
考慮一下
想想這個狀況:
背包大小 \(m = 6\),有\(n = 3\)個物品如下
\(p_1 = 5, w_1 = 4\)
\(p_2 = 3, w_2 = 3\)
\(p_3 = 3, w_3 = 3\)
顯然選擇第2、3個物品會是更好的選擇
更好的方法
DP!
我們紀錄\(dp[n][m]\)為考慮到第\(n\)個物品
且背包大小為\(m\)時能拿到的最大價值
我們可以很快ㄉ知道
\(dp[0][0], dp[0][1], ..., dp[0][m] = 0\)
想一下轉移式:
\(dp[n][m] = max(dp[n-1][m], dp[n-1][m - w_n] + p_n)\)
解釋la
\(dp[0][0], dp[0][1], ..., dp[0][m] = 0\)
\(dp[n][m] = max(dp[n-1][m], dp[n-1][m - w_n] + p_n)\)
首先上面那行應該很好理解
就是如果甚麼東西都沒有的話,價值一定是0
而下面那行
\(dp[n-1][m]\):代表我不把第\(n\)個東西放進背包
\(dp[n-1][m - w_n] + p_n\):代表我把第\(n\)個東西放進背包
CODE
#include <iostream>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
int dp[n+1][m+1]; // 記得多開一個
// 初始化
for(int i = 0; i <= m; ++i) {
dp[0][i] = 0;
}
// 開始DP
for(int i = 1; i <= n; ++i) {
int w, p;
cin >> w >> p; // 輸入重量、價值
for(int j = 0; j < m; ++j) {
if (j < w[i]) {
dp[i][j] = dp[i-1][j]; //放不下所以只能選擇不放
}
else {
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w] + p);
}
}
}
cout << dp[n][m] << '\n';
}
參考一下w
例題
EXTRA:滾動DP
仔細觀察DP式
\(dp[n][m] = max(dp[n-1][m], dp[n-1][m - w_n] + p_n)\)
你會發現我們只會用到\(dp[n-1]\)的項
稍微改變一下DP的順序
使\(m\)從大到小
我們就剛好會找到我們需要的\(dp[n-1]\)
CODE
#include <iostream>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
int dp[m+1]; // 少開一個維度
// 初始化
for(int i = 0; i <= m; ++i) {
dp[i] = 0;
}
// 開始DP
for(int i = 1; i <= n; ++i) {
int w, p;
cin >> w >> p; // 輸入重量、價值
for(int j = m; j >= w; --j) { // 記得從w開始
dp[j] = max(dp[j], dp[j-w] + p);
}
}
cout << dp[m] << '\n';
}
背包問題再補充
- 01背包問題:剛剛講的
- 有限背包問題:每個物品有k個
- 無限背包問題:每個物品有無限個
- 硬幣問題:怎麼找錢用最少硬幣?聽起來很greedy但其實是背包
區間DP
給個飯粒:Deque
玉艮和冰沙在玩一個遊戲,
玉艮的分數為X、冰沙的分數為Y。
玉艮希望能讓X - Y愈大愈好;
而冰沙則希望讓X - Y愈小愈好。
遊戲規則是:
玉艮先手,冰沙後手。
給一個數列S
每人輪流選擇數列開頭 or 結尾
把數字拿掉,並把該數字加在自己的分數
區間DP
栗子:
[1, 2, 3, 4, 5]
玉艮拿5 => [1, 2, 3, 4]
冰沙拿4 => [1, 2, 3]
玉艮拿3 => [1, 2]
冰沙拿2 => [1]
玉艮拿1 => []
X = 9, Y = 6
區間DP
若先手的分數為X,後手為Y
狀態:
\(dp[l][r]\):在\([l, r]\)範圍內最佳狀況ㄉ X - Y
轉移:
\(dp[l][r] = max(s[l] - dp[l+1][r], s[r] - dp[l][r-1])\)
CODE
#include <iostream>
using namespace std;
const int MAXN = 3005;
long long int dp[MAXN][MAXN];
int s[MAXN];
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;
for(int i = 0; i < n; ++i) {
cin >> s[i];
dp[i][i] = s[i];
}
for(int i = 1; i < n; ++i) {
for(int j = 0; i+j < n; ++j) {
dp[j][i+j] = max(s[j] - dp[j+1][i+j], s[i+j] - dp[j][i+j-1]);
}
}
cout << dp[0][n-1] << '\n';
return 0;
}
例題
這裡的題目都比較難QAQ