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鄭博軒
mathdp
- 350