C++[5]

DP[1]

and math

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

聽說要多點人才會好玩

Made with Slides.com