DP[1]
and math
背包問題
給定 N 個物件,各自帶有各自的價值與重量
給定一個有承重上限的背包
問此背包最多能裝多少價值的物品
(每個物品只能拿一次
(當然也有 N 次與無限次的版本
(但先不要
(這太難了
\(N \leq 100\)
窮舉 \(O(2^ N)\)哈哈
怎麼做?
考慮建一個二維陣列大小為 \((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 會時常看到他
這是一個常見壓縮空間複雜度的技巧
#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])\)
輸出為,此陣列中從尾到頭首個重量小於題目需求的位置
#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;
}最長遞增子序列
給定一個陣列,如果每一項都比前一項大的話,此陣列即為嚴格遞增
如果是大於等於的話,即為非嚴格遞增
| 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))\)
#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;
}最長共同子序列
給定兩個陣列,考慮其所有的子陣列
兩陣列的相同子陣列,最長是多少
(此題目問的是子陣列內容
開個 \(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] 哪個較長
#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 個費式數
#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;
}聽說要多點人才會好玩