dp一遍通+ybt題解

daydreamer_zcxnb發表於2024-11-12

前言

馬上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:

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

方式2:

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

區別:

方式一是透過上一維來推導這一維,方式二是透過這一維推導下一維

滾動陣列

因為看到 \(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]\)來轉移的也有可能,反正我不好說

問題暫留,等待各位大佬解答

相關文章