演算法 - 基礎DP(2)

Slides: Lemon
Lecturer: AaW

講師來不及備課

直接借用去年學長的啦

source

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

例題

Atcoder-DPcontest DE

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';
}

背包問題再補充

  1. 01背包問題:剛剛講的
  2. 有限背包問題:每個物品有k個
  3. 無限背包問題:每個物品有無限個
  4. 硬幣問題:怎麼找錢用最少硬幣?聽起來很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

寒訓報名倒數兩天啊啊啊啊

DP愉快w