C++[5][1]

DP[1]

and math

and DP[2]

哈哈講師沒空間放更多簡報了

knapsack

背包問題

背包

給定 N 個物件,各自帶有各自的價值與重量

給定一個有承重上限的背包

問此背包最多能裝多少價值的物品

(每個物品只能拿一次

(當然也有 N 次與無限次的版本

(但先不要

(這太難了

\(N \leq 100\)

窮舉 \(O(2^ N)\)哈哈

怎麼做?

二維DP

考慮建一個二維陣列大小為 \((N + 1) \times (W + 1)\)

\(dp[i][j]\)為考慮到第 i 種物品時,最大負重為 j 的背包,能夠拿取的最大價值。

想一下或著猜一下怎麼轉移

\(dp[i][j] = max(dp[i - 1][j], dp[i - 1][j – w[i]] + v[i])\)

???講人話

dp[i - 1][j]: 我考慮不拿第 i 個物品,所以總價值保持原樣

dp[i - 1][j – w[i]] + v[i]): 我拿第 i 個物品,如果能拿的話,目前的總價值就會 += v[i]

如果拿不了的話,j – w[i]就會小於零,所以SIG fault要把他判掉

空間複雜度

\(O(N \times W), N \times W = 10 ^ 7\)

沒爆炸

但很大

所以不能二維

注意到

注意到在處理第 i 行時,第 i - 1 行的數值不受影響

所以可以直接覆蓋

注意到 \(dp[i - 1][j - w[i]] + v[i]\) 中的 \([j - w[i]]\) 會需要用到前面的資料

所以迴圈要從後往前跑

空間複雜度 \(O(W)\)

這個概念就是滾動

之後學其他的 DP 會時常看到他

這是一個常見壓縮空間複雜度的技巧

AC CODE

#include <bits/stdc++.h>
using namespace std;
int main()
{
    int n,weight;
    cin>>n>>weight;
    long long int v[n],w[n];
    for(int i=0;i<n;i++){
        cin>>w[i]>>v[i];
    }
    long long int dp[weight+1]={0};
    for(int i=0;i<n;i++){
        for(int j=weight;j>=0;j--){
            dp[j]=max(dp[j],(w[i]>j?0:dp[j-w[i]]+v[i]));
        }
    }
    cout<<dp[weight];
    return 0;
}

\( W \leq 10 ^ 9\)

爆炸

怎麼做

作法相似

原本是看重量夠不夠,然後加上價值

現在是看價值夠不夠,看看加上重量

建一個一維陣列,大小為 \(\sum_{i = 0}^{N}{v[i]} +1\)

轉移式為 \(dp[j] = min(dp[j], dp[j - v[i]] + w[i])\)

輸出為,此陣列中從尾到頭首個重量小於題目需求的位置

AC CODE

#include <bits/stdc++.h>
using namespace std;
int main()
{
	int n,weight;
	cin >> n >> weight;
	long long v[n], w[n];
	int sum = 0;
	for (int i = 0 ; i < n ; i++) {
		cin >> w[i] >> v[i];
		sum += v[i];
	}
	long long int dp[sum + 1];
	fill(dp, dp + sum + 1, INT_MAX);
	dp[0] = 0;
	for (int i = 0 ; i < n ; i++) {
		for (int j = sum; j >= v[i] ; j--) {
			dp[j] = min(dp[j], dp[j - v[i]] + w[i]);
		}
	}
	for (int i = sum ; i > 0 ; i--) {
	    if(dp[i] <= weight){
	        cout << i;
	        break;
	    }
	}
	return 0;
}

LIS

最長遞增子序列

甚麼是遞增甚麼是子序列甚麼是最長

給定一個陣列,如果每一項都比前一項大的話,此陣列即為嚴格遞增

如果是大於等於的話,即為非嚴格遞增

甚麼是遞增甚麼是子序列甚麼是最長

2 4 5 6 114514

這是一個陣列

我隨便抓幾個數字

然後照原陣列的順序排出來

4 6 114514

此陣列即為原陣列的子陣列

最長遞增子序列

所以最長遞增子序列就是:

對於一個陣列的所有子陣列,為嚴格遞增(或非嚴格遞增)中長度最長的一個

怎麼做

別暴力了

\(N \leq 10^5\)

 

對於一個嚴格遞增的子陣列,如果他想再塞更多值進去,此陣列最後一個值應該要越小越好

想想怎麼做

有點通靈

好吧不是有點

作法

開一個 vector

把原陣列的數字丟進去

如果 vector 的最後一個值比較小,就把原陣列的數字 push_back 到 vector 裡

如果 vector 的最後一個值比較大,找到 vector 中第一個大於等於原陣列數值的位置,把他丟進去(lower_bound)

這會好?

痾對

然後你會覺得很奇怪

為甚麼這樣子 replace 會好

為甚麼要丟到中間

4 2 3 1 5 6 7 8

如果我只看尾端,vector的尾端太小就push_back,太大就取代,會發生甚麼?

2 1 5 6 7 8

明顯爛了,那如果我去找到中間來取代呢?

1 5 6 7 8

神奇

複雜度?

找 N 次,每次要二分搜

所以是 \(O(N \times log(N))\)

AC code

#include <bits/stdc++.h>
using namespace std;
int main()
{
    cin.sync_with_stdio(0);
    cin.tie(0);
    int n,c;
    cin >> n;
    vector<int>a;
    while (n--) {
        cin >> c;
        if (!a.empty() && c <= a[a.size() - 1]){
            a[lower_bound(a.begin(), a.end(), c) - a.begin()] = c;
        }
        else a.push_back(c);
    }
    cout << a.size();
    return 0;
}

LCS

最長共同子序列

給定兩個陣列,考慮其所有的子陣列

兩陣列的相同子陣列,最長是多少

(此題目問的是子陣列內容

怎麼做

開個 \(N \times M \) 的陣列

N 為 A 陣列長度,M 為 B 陣列長度

DP[i][j]為 A 的前 i 個字元與 B 的前 j 個字元的 LCS

怎麼轉移?

如果 A[i] == b[j] 則我可以從 LCS(A[i - 1], B[j - 1])中多接一個字元

如果 A[i] != b[j] 則我就看看 DP[i - 1][j] 與 DP[i][j - 1] 哪個較長

not ac code

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, m;
    cin >> n >> m;
    
    string a,b;
    cin >> a >> b;
    
    vector<vector<int>> dp(n+1, vector<int>(m+1));
    
    for (int i=0;i<=n;i++) {
        dp[i][0] = 0;
    }
    
    for (int i=0;i<=m;i++) {
        dp[0][i] = 0;
    }
    
    for (int i=0;i<n;i++) {
        for (int j=0;j<m;j++) {
            if (a[i]==b[j]) {
                dp[i+1][j+1] = dp[i][j] + 1;  
            }
            else {
                dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j]);  
            }
        }
    }
    
    cout << dp[n][m];
}

\(O(N \times M)\)

輸出解

從右下角開始

如果 A[i] == b[j],則把字元丟出來, i--, j--

如果 dp[i - 1][j] > dp[i][j - 1],則 i--

否則 j--

最後把丟出來字元的順序 reverse 然後輸出

輸出解

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    string s, t;
    cin >> s >> t;
    int dp[s.length() + 1][t.length() + 1] = {0};
    for (int i = 0 ; i < t.length() ; i++) dp[0][i] = 0;
    for (int i = 1 ; i <= s.length() ; i++) {
        for (int j = 1 ; j <= t.length() ; j++) {
        dp[i][j] = max(max(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1] + (s[i - 1] == t[j - 1]));
        } 
    }
    int i = s.length(), j = t.length();
    vector<char>ans;
    while(i * j > 0){
        if(s[i - 1] == t[j - 1]) {
            ans.push_back(s[i - 1]);
            i--;
            j--;
        }
        else if(dp[i - 1][j] > dp[i][j - 1])i--;
        else j--;
    }
    for(int i = ans.size() - 1 ; i >= 0 ; i--) cout << ans[i];
    return 0;
}

補充

對於兩個元素無重複的陣列

可以用 LCS 轉 LIS 在 \(O(n log n + m)\)內輸出 LCS 長度

矩陣快速冪

快速冪

複習一下快速冪

矩陣乘法

5 2
3 4
1 3
3 4

矩陣A

矩陣B

14 14
27 22

\(A \times B\)

1*5+3*3 = 14

1*2+3*4 = 14

3*5+4*3 = 27

3*2+4*4 = 22

\(N \leq 10^{18}\)

WHAT

怎麼做

注意到這個矩陣乘法

發現左上角與左下角的數字

每進行一次矩陣乘法,就會變成下一個費式數

1 1
1 0
1 1
1 0
2 1
1 1
2 1
1 1
1 1
1 0
3 1
2 1
3 1
2 1
1 1
1 0
5 1
3 1
5 1
3 1
1 1
1 0
8 1
5 1

同時矩陣乘法有結合律

所以能用快速冪

這樣就能在 \(log(N)\) 時間複雜度算出第 N 個費式數

 

AC code

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define mod 1000000007
void mul(vector<vector<int>>&a,vector<vector<int>>b){
    vector<vector<int>>c(2,vector<int>(2));
    c[0][0]=((a[0][0]*b[0][0])%mod+(a[0][1]*b[1][0])%mod)%mod;
    c[0][1]=((a[0][0]*b[0][1])%mod+(a[0][1]*b[1][1])%mod)%mod;
    c[1][0]=((a[1][0]*b[0][0])%mod+(a[1][1]*b[1][0])%mod)%mod;
    c[1][1]=((a[1][0]*b[0][1])%mod+(a[1][1]*b[1][1])%mod)%mod;
    a[0][0]=c[0][0];
    a[0][1]=c[0][1];
    a[1][0]=c[1][0];
    a[1][1]=c[1][1];
    return;
}
main()
{
    int n;
    cin>>n;
    if(n==0){
        cout<<0;
        return 0;
    }
    if(n==1){
        cout<<1;
        return 0;
    }
    if(n==2){
        cout<<1;
        return 0;
    }
    n-=2;
    int b=0,p=0;
    vector<vector<int>>t(2,vector<int>(2,1)),ans(2,vector<int>(2,1));
    t[1][1]=0;
    ans[1][1]=0;
    for(int i=1;i<=ceil(log2(n))+1;i++){
        b=(n>>(i-1))&1;
        if(b){
            for(int j=p;j<i-1;j++){
                mul(t,t);
            }
            mul(ans,t);
            p=i-1;
        }
    }
    cout<<ans[0][0];
    return 0;
}

bitmask dp

比特面具

bitmask

bitmask 核心概念

就是把狀態做成二進位

然後有效率的窮舉

例如說有二十個物品

取或不取的狀態用 0 or 1 來定

搭電梯

給定一個承重上限為 x 的電梯

有 n 個人

各重 w[i]

問全部搭上去至少要幾趟

(n <= 20)

狀態

過程中要窮舉每個人要不要上去電梯

w = [1, 2, 4, 7];
s = 0b0001 // sum = 7
s = 0b0010 // sum = 4
s = 0b0011 // sum = 11
s = 0b0100 // sum = 2
s = 0b0101 // sum = 9
s = 0b0110 // sum = 6
s = 0b0111 // sum = 13
......

狀態

接下來對於一個集合,看看第 i 個人有沒有搭上去,有的話就從沒搭上去的狀態轉移過來

也就當第 i 來搭電梯時,哪種狀態下能轉移出最好的結果

好的定義是:趟次少的最好,趟次相同時,負重少的最好

狀態

x = 7
w = [1, 2, 4, 7];
s = 0b1101 
s1 = 0b1100 // a = 1, w = 3
s2 = 0b1001 // a = 2, w = 1
s3 = 0b0101 // a = 2, w = 2

(a 是趟次,w 是第 a 趟時的負重)

s1 轉移至 s: a = 2, w = 7

s2 轉移至 s: a = 2, w = 5

s3 轉移至 s: a = 2, w = 6

發現 s2 比較好,所以 DP[s]設為{2, 5}(最好的狀態)

const int MXN = 2e5+5,MOD = 1e9+7;
pp dp[(1 << 20)];
void solve(){
	int n,x;
	cin >> n >> x;
	vector<int> w(n);
	cin >> w;
    //初始化: 最多搭 n 次,所以開 n+1 當 inf
	for (int s = 0; s < (1 << n) ; ++s) dp[s].first = n + 1;
	dp[0].second = x, dp[0].first = 0; //沒人上電梯 讓第零班電梯目前負重是 x 這樣下一個人上電梯一定會搭到第一班
	for(int s = 1; s < (1 << n) ; ++s){ // 窮舉全部狀態
		for(int i = 0; i < n ; ++i){ // 從「去掉第 i 個人」的狀態轉移過來
			if(!(s & (1 << i))) continue; // 如果第 i 個沒搭過,跳過
			int pv = s ^ (1 << i); // i 沒搭的狀態
			auto [a, b] = dp[pv];
			if(b + w[i] > x) b = w[i], ++a; // i 搭不上去前一班,多一班
			else b += w[i]; // 搭的上去
			if(a < dp[s].first) dp[s] = pp{a, b}; // 次數少先取
			else if(a == dp[s].first) dp[s].second = min(dp[s].second, b); // 次數一樣,取最小的負重
		}
	}
	cout << dp[(1 << n) - 1].first << '\n';
}

\(O(n \times 2^n)\)

題目

斜率優化 DP

又DP又數學的(

定狀態

dp[i] = 打掉第 i 隻小怪時所需的最小時間

f[i] = 當下玩家的 f

然後定轉移式

\(dp[i] = \forall j < i, min(f[j] \times s[i] + dp[j])\)

然後 \(O(n^2)\)原地爆炸

我們需要一個更有效率找最小值的方法

斜截式

\(dp[i] = f[j] \times s[i] + dp[j]\)

注意到它看起來很像\(y = ax + b\)

然後你的 \(f[j]\) 是非嚴格遞減(題目規定)

同時 s[i] 是非嚴格遞增(題目規定)

所以越往後的線,斜率越小,一旦後面的線比前面的線更好(\(s[i]\)代進\(x\)後的值更小),前面的線不可能反超後面的線,所以就可以丟掉。

實作

用 deque 存

依順序把 \(f[i]\) 跟 \(dp[i]\) 丟進去當斜率跟截距

所以 \(dq[0]\) 是最優的線

當 \(dq[1]\) 比 \(dq[0]\) 好的時候,把 \(dq[0]\) 踢掉,因為 \(dq[0]\) 在後面也一定會比 \(dq[1]\) 差

bug

dq = [L1, L2, L3]

然而在黃色區間中,L1 比 L2 好,所以 L1 不會被踢掉

然而 L3 才是最優解

為甚麼會有這種情況?

因為 L2 在這個區間裡面一次被兩個線幹掉

怎麼辦?

先把 L2 踢掉

所以要判斷 L2 會不會一次被兩條線幹掉。

如果 L3 與 L2 的交點 比 L1 與 L2 的交點更前面,L2 就會被兩條線一起幹掉。

怎麼求交點?

$$ax+b = cx+d, x = \frac{d - b}{a - c}$$

所以你會了

其他東西

如果 L1 L2 的斜率相同,那 L1 一定更好

因為截距 (\(dp[i]\)) 是非嚴格遞增

所以 L2 會更爛

然後 deque 要先丟一個還沒開始打怪時的線

#include <bits/stdc++.h>
using namespace std;
#define int long long
signed main()
{
    int n, x;
    cin >> n >> x;
    int s[n], f[n], dp[n];
    for(int i = 0 ; i < n ; i++) {
        cin >> s[i];
    }
    for(int i = 0 ; i < n ; i++) {
        cin >> f[i];
    }
    deque<pair<int, int>>line;
    line.push_front({x, 0});
    for(int i = 0 ; i < n ; i++){
        while(line.size() >= 2 && line[0].first * s[i] + line[0].second > line[1].first * s[i] + line[1].second){ // 舊的線被超過了
            line.pop_front();
        }
        dp[i] = line[0].first * s[i] + line[0].second;
        pair<int, int>nline = {f[i], dp[i]};
        if(line[line.size() - 1].first == nline.first) continue; // 斜率相同,不考慮
        while(line.size() >= 2 && (nline.second - line[line.size() - 1].second)/(line[line.size() - 1].first - nline.first) < (line[line.size() - 1].second - line[line.size() - 2].second)/(line[line.size() - 2].first - line[line.size() - 1].first)){
            line.pop_back(); // L2 同時被 L1 跟 L3 幹掉
        }
        line.push_back(nline);
    }
    cout << dp[n - 1];
    return 0;
}

?李超線段樹

如果現在的斜率沒有遞減了怎麼辦

李超線段樹

阿講師不會所以這裡就不教了

上機考也不會考

矩陣優化 DP

又DP又數學的(

給予一個圖,圖上的邊權全都都是一

問兩點之間路徑長為 \(k\) 的路徑數

一條邊可以被重複走多次

k=1e9

對於一個存圖的鄰接陣列,你可以把它當成矩陣

這個矩陣乘自己 n 之後,G[i, j] 的數字就是 i 到 j 路徑長為 n 的路徑數

然後矩陣快速冪

好欸

mathdp

By ck11300768鄭博軒