前言
馬上csp-s考試了,卻發現自己dp太菜了,打算惡補dp
線性dp理解
遞推/記憶化搜尋,有很多種理解方式
遞迴重疊子問題的記憶化搜尋:
像這裡例如 \(f[3]\) 可以透過一次計算得到,儲存答案,下一次直接呼叫即可,省去很多複雜度
我們從此引出dp第一個性質:最優子結構
大問題的最優解包含小問題的最優解,並且小問題的最優解可以推匯出大問題的最優解
遞推:
我們不管dp[i-1]是多少,但可以從dp[i-1]推導得出dp[i]
dp第二個性質:無後效性
未來與過去無關
dp實現方法
自頂而下:大問題拆解成小問題求解
常用遞迴+記憶化實現
自底而上:小問題組合成大問題求解
常用製表遞推實現
這是最常見的dp方法
揹包dp
0/1揹包
狀態設計
\(dp[i][j]\) 表示只裝前i個物品,體積為j時的最大價值
轉移方程
\(c[i],v[i]\)表示第i個物品的體積和價值
方式1:
方式2:
區別:
方式一是透過上一維來推導這一維,方式二是透過這一維推導下一維
滾動陣列
因為看到 \(dp[i][]\) 這一維只與 \(dp[i-1][]\) 這一維有關,所以可以壓掉一維,最佳化空間
交替滾動
用 \(dp[1][]\) 和 \(dp[0][]\) 交替滾動,邏輯清晰,建議初學者食用
int dp[2][N];
int now=0,old=1;
for(int i=1;i<=n;i++){
swap(now,old);
for(int j=0;j<=C;j++){
if(j>=w[i]) dp[now][j]=max(dp[old][j],dp[old][j-w[i]]+c[i]);
else dp[now][j]=dp[old][j];
}
}
自我滾動
int dp[N];
for(int i=1;i<=n;i++){
for(int j=C;j>=w[i];j--){
dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
}
}
注:j應該反過來迴圈,因為 要保證 \(dp[j-w[i]]\) 這一位還是商議維的答案,沒有被覆蓋掉
規律:只要這一位(i這一位)修改的地方已經用過了,不會再用了,就對答案無影響
分組揹包
把物品分為n組,每組只能選一個
只要每組列舉選哪個,0/1揹包哪組選或不選就完了
多重揹包
規定每種物品有 \(m_i\) 個
暴力解法
把每個物品看成一個獨立的物品進行0/1揹包
二進位制最佳化多重揹包
將 \(m_i\) 按2的倍數從小到大拆,最後是一個小於或等於最大倍數的餘數,相當於1個k,2個k,4個k...這些物品進行0/1揹包,便可組合出選 \(0~m_i\) 個k的情況,為什麼呢,我們看一組例子
在這組例子中,相當於用 \(1k,2k,4k,3k\) 的組合方案來代替 \(0~10k\) 的組合方案,複雜度 \(O(C\sum_{i=1}^n\log_2m_i)\)
單調佇列最佳化多重揹包
複雜度更優,待填坑
最長公共子序列
設 \(dp[i][j]\) 表示序列 \(X_{0~i}\) 和序列 \(Y_{0~j}\) 的最長公共子序列長度
當 \(x_i==y_j\) 時:\(dp[i][j]=dp[i-1][j-1]+1\)
當 \(x_i!=y_j\) 時:\(dp[i][j]=max(dp[i-1][j],dp[i][j-1])\)
複雜度 \(O(N^2)\),不是最優解
最長上升子序列
設 \(dp[i]\) 為長度為i的最長上升子序列,最後一個數的大小
對於每一個 \(a[i]\) 找到最大一個 \(dp[j]<a[i]\) 使 \(dp[j+1]=min(dp[j+1],a[i])\)
這個過程可以用二分加速,複雜度 \(O(N\log_2N)\)
P2758 編輯距離
子問題:將 \(X_{1...i}\) 轉換成 \(Y_{1...j}\) 的最小操作次數
狀態設計: \(dp[i][j]\) 為將 \(X_{1...i}\) 轉換成 \(Y_{1...j}\) 的最小操作次數
狀轉方程:
當 \(X_i==Y_i\) 時:$$dp[i][j]=dp[i-1][j-1]+1$$
當 \(X_i!=Y_i\) 時:$$dp[i][j]=max(dp[i-1][j-1],dp[i-1][j],dp[i][j-1])+1$$
\(dp[i-1][j-1]\) :替換 \(X_i\) 為 \(Y_i\)
\(dp[i][j-1]\) :刪除 \(X_i\)
\(dp[i-1][j]\) :在i位置插入 \(Y_i\)
最小劃分
給一個正整陣列,把它分為s1,s2兩部分,然後求最小的 \(|s1-s2|\)
考慮一部分劃分越接近 \(sum/2\) 越優,然後0/1揹包做就可以了
樹形dp
樹上做dp非常常見,因為樹本身有子結構性質(樹和子樹)
一般解題思路:先把樹轉化為有根樹(如果不連通的樹,就加一個虛擬根,它連線所有孤立的樹),然後在做dfs,遞迴到葉子節點,再一層層返回資訊,就在這一步做dfs
P2015 二叉蘋果樹
定義狀態 \(dp[u][j]\) 表示以節點u為根的子樹上留j條邊時
換根dp
寫的比較好的部落格
P3478 STA-Station
板子題,比較適合練手
二叉樹做法
考慮左右節點的邊的總數是一定的,所以我們列舉一個子樹的邊就可以了
void dfs(int u){
for(int i=0;i<=num;i++){//num代表子樹內邊的數量
dp[u][i]=max(dp[u][i],dp[lson][i],dp[rson][num-i]);
}
}
多叉樹做法
我們挨個子節點列舉,然後再列舉當前子節點v選的邊數,剩下的就是1~v-1 的子節點保留的分叉數
void dfs(int u){
for(auto v:b[u]){//v是u的子節點
for(int i=num;i>=1;i--){//列舉要割幾條邊
for(int j=0;j<=i;j++){
dp[u][j]=max(dp[u][j],dp[u][i-j-1]+dp[v][j]+w[u]);//i-j-1因為連向v也算一條邊
//實際上是dp[u][v][j]=max(dp[u][v][j],dp[u][v-1][i-j-1]+dp[v][j]+w[u]壓掉v一維所以列舉i時要倒敘列舉
}
}
}
}
P1352 沒有上司的舞會
典題,不多贅述
P2014 選課
樹上揹包板子題,我們設 \(dp[u][t]\) 在u子樹內選t個點所得到的最大值
所以我們的我們的針對一個子樹內進行操作,列舉所有的子節點,再列舉要在這個子樹內選取的點的個數 \(t\),再列舉一個在這個子節點的子樹內列舉的點的個數 \(j\)
然後最後答案統計就是 \(dp[u][t]=dp[v][j]+dp[u][t-j]\)
狀壓dp
應用背景以集合為狀態,集合一般可以用二進位制表示,用二進位制的位運算處理
集合問題一般是指數複雜度的,例如:1.子集問題,設n個元素沒有先後關係,那麼一共有 \(2^n\) 個子集;2.排列問題,對所有n個元素進行全排列,共有 \(n!\) 個排列
狀態壓縮:主要就是dp的一種狀態,與dp轉移關係不大
位運算:\(a\&(a-1)\) 把a的最後一個1去掉
P10447 最短 Hamilton 路徑
典題,每個點只能經過一次,所以只需要設 \(dp[s][j]\) ,s為哪些點已經訪問過了狀壓的狀態,j為現在在哪個點,狀態轉移方程顯然
區間dp
先在小區間上進行dp得到最優解,然後再合併小區間的最優解求得大區間的最優解,解題時,先解決小區間的問題,再將小區間合併為大區間,合併操作一般是將兩個相鄰區間合併
注:合併順序從小區間到大區間,因該先從小到大列舉區間的長度,遞推出j在哪裡
ybt題解
揹包問題
T2:
考慮首先b中的元素a中一定有,其次b中的元素不能被a中的元素拼成
所以我們先把b陣列排個序,然後多重揹包轉移此數能否被之前書數拼成即可
T3:
多重揹包二進位制最佳化板子題
T4:
多重揹包,但是狀態只有0/1之分
所以我們考慮可以用一種新奇的方式轉移
我們用 \(f[i-a[i]]\) 來遞推 \(f[i]\) ,若 \(f[i-a[i]]\) 是用了一些 \(a[i]\) 硬幣轉移過來的,所以它轉移到 \(f[i]\) 是又用了一枚硬幣才轉移過來的,因為硬幣個數有限制,所以我們不能無限制的轉移下去
存一個 \(cnt[j]\) 表示轉移到 \(j\) 共用了多少枚 \(a[i]\) 硬幣,判斷是否超出範圍即可
考慮多重揹包為什麼不能這樣做呢?
猜想:可能是因為多重揹包的狀態維護的最大價值並不具有轉移性質,可能有一個較大的不是用\(a[i]\)來轉移的也有可能,反正我不好說
問題暫留,等待各位大佬解答