1. 程式人生 > 其它 >[筆記]動態規劃入門

[筆記]動態規劃入門

以前存在本地的東西,發上來方便找(x)


動態規劃

目錄

模型

揹包

01揹包\(f[i][j]=\max(f[i-1][j],f[i-1][j-w_i]+v_i)\)

完全揹包(每個物品次數不限),那就用\(f[i][j]=\max(f[i-1][j],f[i][j-w_i]+v_i)\),滾動陣列的時候對於體積直接從小到大列舉就好。

多重揹包(第\(i\)種物品有\(k_i\)個),大暴力是\(O(nm\sum k_i)\),二進位制拆分\(k_i=1+2+\dots+2^t+(k_i-(1+2+\dots,2^t))\),複雜度\(O(nm\sum\log k_i)\)

int x;scanf("%d",&x);
int c=1;
while(x>c){
    x-=c;
    w[++n]=W[i]*c;
    v[n]=V[i]*c
    c<<=1;
}
if(x)w[++n]=W[i]*x,v[n]=V[i]*x;

單調佇列優化多重揹包

暴力dp:\(f[i][j]=\max_{k=0}^{k_i}(f[i-1][j-k*w[i]]+k*v[i])\),這樣對每一個\(i\)轉移的代價是\(O(m\sum k_i)\)的。接著我們注意到,對於\(0\leq j<w[i]\)\(f[i-1][j],f[i-1][j+w[i]],f[i-1][j+2w[i]],\dots\)

這樣一系列位置會對\(f[i][j+kw[i]]\)造成影響,我們考慮列舉\(j=0,\dots,w[i]-1\),再去列舉\(k\),用單調佇列來優化求\(f[i]\)的過程,這樣總複雜度就是\(O(n\sum w_i\frac{W}{w_i})=O(nW)\),其中\(W\)是揹包的大小。

二維費用揹包:和普通揹包類似,多記錄一維代價。

分組揹包:組內的物品互相沖突不能同時選擇,改成先列舉組,然後是體積,最後才是組內的所有物品:

for(int k=1;k<=K;k++)for(int j=V;j>=0;j--)for(int i=1;i<=size[k];i++)...

最外面的一個k類似於普通揹包的第幾個物品,最裡面一層列舉組內的物品,就保證了每組最多選一個(因為是從先從大到小列舉體積\(j\)

,那麼對於\(f[j],f[j-c_i]\)這些只能由上一組的狀態轉移過來)

泛化物品:每個物品可能沒有固定的價值/體積,價值\(V\)是關於體積的函式\(f(W)\)

數位DP

ll dfs(int x,int pre,int st,……,int lead,int limit){
	if(!x) return st;
	if((dp[x][pre][st]…[]!=-1&&!limit&&!lead))return dp[x][pre][st]…[];
	ll ret=0;
	int mx=limit?a[x]:9;
	rep(i,0,mx){
        //有前導0並且當前位也是前導0
		if(!i&&lead) ret+=dfs(……,……,……,i==mx&&limit);
        //有前導0但當前位不是前導0,當前位就是最高位
		else if(i&&lead) ret+=dfs(……,……,……,i==mx&&limit); 
		else if(根據題意而定的判斷) ret+=dfs(……,……,……,i==mx&&limit);
	}
	if(!limit&&!lead) dp[x][pre][st]……[……]=ret;
	return ret;
}
ll solve(ll x){
	len=0;
	while(x) a[++len]=x%10,x/=10;
	memset(dp,-1,sizeof dp);//初始化-1(因為有可能某些情況下的方案數是0)
	return dfs(……,……,……,……);//進入記搜
}
int main(){
	scanf("%d",&T);
	while(T--){
		scanf("%lld%lld",&l,&r);
	    if(l) printf("%lld",solve(r)-solve(l-1));//[l,r](l!=0)
	    else printf("%lld",solve(r)-solve(l));//從0開始要特判,按情況看看要不要加別的
	}
	return 0;
}

線性

http://poj.org/problem?id=2279

$ N$個學生合影,站成左端對齊的 \(k\)排,每排分別有\(N1,N2,\dots,N_k\)個人。 \((N1≥N2≥…≥Nk)\)

第1排站在最後邊,第 \(k\) 排站在最前邊。 學生的身高互不相同,把他們從高到底依次標記為\(1,2,…,N\)

在合影時要求每一排從左到右身高遞減,每一列從後到前身高也遞減。問一共有多少種安排合影位置的方案?(\(k\leq 5,\sum N_i\leq 30\))

線性dp容易給出一個,以及注意一種\(O(30^5)\)的做法,但是這題會炸空間。

這個背景是和整數拆分有關的【楊氏矩陣】,記\(n=\sum N_i\),每個位置\((i,j)\)所有它右邊和下方(如果有)的格子個數(包括自己)稱為這個“鉤子”的長度,這題對應的答案就是\(n!/\Pi\)鉤子長度,證明不會。

https://www.acwing.com/problem/content/274/

最長上升公共子序列,LIS是\(f[i]\)記錄以\(i\)為結尾的LIS,LCS則是記錄\(1-i\)\(1-j\)的,這裡就考慮綜合起來,\(f[i][j]\)表示以\(s[1,\dots,i]\),以\(t[j]\)為結尾的LCIS的長度,類似LCS,如果\(a[i]\neq b[j]\),直接\(f[i][j]=f[i-1][j]\),否則如果\(a[i]=b[j]\),就有轉移\(\begin{aligned}f[i][j]=\max_{k=1,\dots,j-1,b[k]<b[j]}(f[i-1][k])+1\end{aligned}\),又\(b[j]=a[i]\),下面改成\(b[k]<a[i]\),原本\(O(n)\)的轉移就可以優化了:列舉\(i\)之後對固定的\(i\),列舉\(j\)的同時更新這個\(max\),整體就能做到\(O(n^2)\)

https://www.acwing.com/problem/content/275/

\(a[]\),需要構造一個單調不減或者單調不增的\(b[]\),最小化\(\sum |a_i-b_i|,n\leq 2000\)

容易想到先考慮單調不增或者單調不減的其中一種情況,另一種把\(a[]\)翻轉過來再做一遍就行。就以考慮單調不減的序列為例,用\(f[i][j]\)表示前\(i\)個,以\(j\)作為\(b[]\)的結尾的最小答案,這樣就有\(\begin{aligned}f[i][j]=\min_{k=1,\dots,j}f[i-1][j]+|a_i-j|\end{aligned}\),不過這樣值域有點大,注意到任何一個\(b[j]\)的取值,一定是取\(a[]\)中的某個項,不然就浪費了,所以可以離散化一下,複雜度\(O(n^3)\),進一步,這題其實和上題類似,對於一個固定的\(i\),決策集合只增不減,\(O(n)\)的轉移可以和列舉\(j\)一起進行,時間複雜度\(O(n^2)\)

關於\(b[j]\)一定取\(a[]\)中的某個值可以考慮一個數學歸納:假設對\(1,\dots,k-1\)成立,對於\(i=k\)來說,如果\(b[k-1]\leq a[k]\),直接構造\(b[k]=a[k]\)就做到了最優的;否則考慮讓\(b[k]=b[k-1]\),根據歸納我們能夠說明\(b[k-1]\)取的是\(a[1,\dots,k-1]\),但這還不夠,我們需要證明我們取\(b[k]=b[k-1]\)這種決策能夠達到最優,想一下也顯然,考慮從\(i=k\)往前的一段區間\([j,k]\),這一段的\(b[i]\)都取得相同的\(v\),那這就變成給一個\(a[]\),最小化\(\sum |a_i-v|\)的問題了,這個問題很經典,顯然最優情況下的\(v\)能夠取得到\(a[]\)中的值。

https://www.acwing.com/problem/content/276/

\(L\)個點的有嚮往全圖,每條邊有邊權(即對應\(u\to v\)的代價),有3個人一開始分別在1,2,3處,\(1-n\)個時刻,每個時刻需要有一個人在\(p_i\)這個點,同一個時刻只有同一個時刻一個點不能有多個人,問滿足所有需求需要的最小代價。\(L\leq 200,n\leq 1000\)

考慮DP還是先想怎麼記錄狀態,依然先考慮最暴力的,三個人的位置肯定要能夠被記錄進去,同時還要記錄處理到了哪個時刻,於是就有了\(O(L^3n)\)級別的狀態,太大了沒法跑。

很套路地想怎麼去掉一些狀態,任意一個時刻一定會有一個人處於\(p_i\)的位置,於是就可以去掉一個\(L\),比如\(f[i][a][b]\)表示到時刻\(i\),一個人在\(p[i]\),另外兩個人分別在\(a,b\)這兩個位置,轉移也很好想,即\(p_i/a/b\to p_{i+1}\)三種轉移方式,實現的時候判定一下狀態合法即可。

揹包

https://codeforces.com/problemset/problem/19/B

Bob拿著\(n\)件商品在收銀臺付款,掃描第\(i\)件商品需要\(t_i\)的時間,第\(i\)件的價格為\(c_i\),在掃描的時候可以選擇偷走一些商品,偷走一個商品需要1個單位的時間,問最少花多少錢能獲得所有商品。\(n\leq 2000,t_i\leq 2000\)

相當於把商品劃分成兩個集合\(S,T\),滿足\(|T|\leq \sum_{S}t_i\),使得\(\sum_{S}c_i\)最小,左邊的式子稍微變形會得到\(\sum_{T}t_i+|T|=\sum_{T}(t_i+1)\leq \sum t_i\),這就很像揹包了:反著考慮獲得哪些物品不要花錢,總容量為\(\sum t_i\),選擇一件物品不花錢得到的代價是\(t_i+1\),這樣一個01揹包問題,但是複雜度會達到\(O(nt^2)=O(n^3)\),接受不了。

不過順著這個揹包的思路繼續想,選擇花錢買一件物品相當於多獲得一個\(t_i+1\)的體積,對應的付出\(c_i\)的代價,最終目標是獲得所有物品,即\(\sum_{S}t_i+1\geq n\),於是又是一個揹包:選擇花錢購買一些物品,使得這些物品的體積之和超過\(n\),求最小的代價。同樣的問題又來了,這樣做揹包的體積上界是多少?如果還是\(O(n^2)\)級別的話這個優化就沒什麼用了:仔細想一下上界不會很大,在一系列決策之後如果當前的\(\sum t_i+\)已經超過了\(n\),那後面的一定不會繼續選擇購買,所以最大的情況一定是從一個小於\(n\)的體積跨越到一個大於\(n\)的體積,對應的上界就是\(n-1+(v_{max}+1)=n+v_{max}\)了。

https://www.luogu.com.cn/problem/P4141

揹包問題變形,\(n\)個物品,需要回答如果沒有第\(i\)個物品的時候,恰好裝滿容量為\(x=1,\dots m\)的揹包需要多少代價?\(n,m\leq 2000\)

原問題是\(dp[i][j]=dp[i-1][j]+dp[i-1][j-v[i]]\)\(dp[0][0]=1\),暴力做是直接\(O(n^2m)\)的,先處理處沒有第一個物品的答案,然後考慮如何給dp刪除一個物品和新增物品。

按照滾動陣列得到的dp陣列考慮,注意到物品順序不影響答案,假設當前得到的是\(f[0,\dots,m]\),刪去物品\(i\)之後的答案是\(g[1,\dots,m]\),則有當\(j<v[i]\)時,\(f[j]=g[j]\),當\(j\geq v[i]\)\(f[j]=g[j]+g[j-v[i]]\),於是就能從小到大反推出\(g[]\),這樣每一次只需要\(O(m)\)的代價計算刪除和加入一個物品的答案。

rep(i,2,n){ 
    rep(j,0,w[i]-1)g[j]=f[j];
    rep(j,w[i],m)g[j]=(f[j]-g[j-w[i]]+10)%10;

    rep(j,0,m)f[j]=g[j];
    for(int j=m;j>=w[i-1];j--)upd(f[j],f[j-w[i-1]]);
    rep(j,1,m)printf("%c",f[j]+'0');
    puts("");
}

https://www.luogu.com.cn/problem/P1877

01揹包變形,\(dp[i][j]\)\(dp[i-1][j-c[i]]\)\(dp[i-1][j+c[i]]\)兩個轉移。

https://www.luogu.com.cn/problem/P1509

二維揹包變形,兩個代價(rmb和rp)以及兩個收益(在泡到最多MM的前提下時間最小),可以考慮兩個dp陣列,在MM最多的前提下再比較第二維,或者像我在實現的時候直接開個struct來存dp,過載一個比較函式。

https://www.luogu.com.cn/problem/P3985

二維揹包變形,一開始理解錯題意,以為是DP的時候多帶一個極差\(\leq3\)的限制,後面發現原來是給的資料保證極差\(\leq 3\),那就好做了,考慮最後選擇的物品的集合是\(S\),最後要\(\sum_{i\in S} v_i\leq W\),取\(X=\min_i(v_i)\),式子變成\(X|S|+\sum_{i\in S}(v_i-X)\leq W\),考慮個雙重限制的揹包:一個是\(v_i\in{0,1,2,3}\),和第二維代價:每選一個物品代價是1,先對這個二維揹包進行DP,然後再統計符合條件的答案。

https://www.luogu.com.cn/problem/P1455

並查集維護連通塊,然後直接對每一個聯通塊01揹包

https://www.luogu.com.cn/problem/P1858

\(K\)個人,每個人有一個揹包,容量都是\(V\)\(N\)件物品,現在要每個人都能恰好裝滿揹包,並且任意兩個人選的物品不完全相同,所有人價值之和的最大\(K\leq 50,V\leq 5000,N\leq 200\)

發現相當於在求一個01揹包要求完全裝滿的前\(K\)大值,前\(K\)大的處理一般是把普通的DP式子轉化成一個單調佇列來維護,轉移變成\(O(k)\)地歸併狀態。

rep(i,1,n)per(j,V,v[i]){
	tmp.clear();
	for(auto itr:f[j])tmp.pb(itr);
	f[j].clear();

    int p=0,q=0;
    while((p<tmp.size()||q<f[j-v[i]].size())&&(p+q<=k)){
        int sp=tmp.size(),sq=f[j-v[i]].size();
        if(q>=sq||(p<sp&&q<sq&&tmp[p]>w[i]+f[j-v[i]][q]))f[j].pb(tmp[p++]);
        else f[j].pb(w[i]+f[j-v[i]][q++]);
    }
}

https://www.luogu.com.cn/problem/P1776

完全揹包問題,帶log的和單調佇列優化的都能過,暴力的還沒試過(

https://www.luogu.com.cn/problem/P5322

(一道評價還不錯的dp題)手上的士兵很像揹包的容量,而對於每座城堡來說又可以看成一個類似於“物品”的東西,就轉換成了一個類似01揹包的問題,但是因為有多個玩家,我們對每座城堡所有玩家的\(a_i\)升序排列,然後在從大到小列舉體積\(j\)的時候直接f[j]=max(f[j],f[j-2*a[i]-1])+i 就行(因為排序之後就保證了不會在同一個\(i\)內部重複計算。

https://www.luogu.com.cn/problem/P1782

多重揹包+泛化揹包,不過這個泛化揹包的處理也很暴力…以及這題有點卡常

單調棧/單調佇列

https://www.luogu.com.cn/problem/P2254

給你一張地圖,一些地方不能走,輸入初始位置,\(K\)段時間,每段時間內要麼只能往指定的方向走,要麼不走,問最遠能走多長的路徑。\(n,m,k\leq 200\)

\(f[i][j][k]\)表示第\(k\)段時間走完之後在\((i,j)\)處的答案,暴力轉移\(\begin{aligned}f[i][j][k]=\max_{t=0}^{ed_k-st_k+1}\{f[i-t\Delta x][j-t\Delta y][k-1]+t\}\end{aligned}\),這樣就得到了一個四次方的DP,但是應該是資料太水(以及可能因為是十幾年前的題了,當時測評機沒這麼快)…這麼個大暴力就過了這題:

rep(k,1,K)rep(i,1,n)rep(j,1,m)if(avl[i][j]){
    int mx=-INF;
    rep(t,0,ed[k]-st[k]+1){
        int cx=i-t*di[d[k]],cy=j-t*dj[d[k]];
        if(!check(cx,cy))break;
        if(f[cx][cy][k-1]==-INF)continue;
        mx=max(mx,f[cx][cy][k-1]+t);
    }
    f[i][j][k]=mx;
}

不過還是來優化轉移過程,其實\(\Delta x,\Delta y\)裡面會有一個是0,假設\(j\)定下來,\(i\)是這次動的方向,我們對於每個確定的\(k,j\),其實可以\(O(n)\)

\(f[i][j][k]=\max_{t=0}^{len}\{f[i-t\Delta x][j][k-1]+t\}\)進行轉移:假設這個\(d[k]\)意味著\(i\)只能增大,那我就從1列舉\(i\),用單調佇列維護\(f[i-t\Delta x][j][k-1]-t\),每次再用\(Q.front()+t\)來更新答案(注意前面單調佇列裡維護的是\(-t\),因為原來dp式子裡的\(t\)的含義其實是兩個位置的距離,相當於是\(t_{當前}-t_{從哪轉移來}\)

數位

http://acm.hdu.edu.cn/showproblem.php?pid=2089

數位上不能有連續的62,以及不能有4。

https://www.luogu.com.cn/problem/P2602

統計\([l,r]\)內所有數碼出現的次數,一位一位數碼考慮,要算數位出現的次數,答案也丟到dfs裡面來跑,狀態多記幾個不會爆的…

ll dfs(int x,int cnt,int D,bool limit,bool lead)
{
	if(~f[x][cnt][limit][lead])return f[x][cnt][limit][lead];
	if(!x)return f[x][cnt][limit][lead]=cnt;
	int mx=limit?digit[x]:9;
	ll ret=0;
	rep(i,0,mx)
	{
		int cnt2=cnt;
		if(i)cnt2+=(i==D);
		else cnt2+=(!lead&&i==D);
		ret+=dfs(x-1,cnt2,D,limit&&i==digit[x],lead&&!i);
	}
	return f[x][cnt][limit][lead]=ret;
}

https://codeforces.com/problemset/problem/276/D

考慮\(a,b\in [l,r]\),問\(a\oplus b\)的最大值,\(l,r\leq 10^{18}\)

異或想到考慮二進位制,但是不太會考慮,那就大力列舉,數位dp不就是幹這種事情嘛?

\(f[x][l1][r1][l2][r2]\)表示從高往低考慮到第\(x\)位,\(l1,r1\)表示對\(a\)的約束,同樣\(l2,r2\)是對\(b\)的約束。

if(!x)return 0;
if(~f[x][l1][r1][l2][r2])return f[x][l1][r1][l2][r2];
ll ans=0;
//l1,r1 means first number's upper and lower bound 
rep(i,l1?L[x]:0,r1?R[x]:1)
    rep(j,l2?L[x]:0,r2?R[x]:1){
        ll t=i^j;t<<=(x-1);
        t+=dfs(x-1,l1&&i==L[x],r1&&i==R[x],l2&&j==L[x],r2&&j==R[x]);
        ans=max(ans,t);
    }
return f[x][l1][r1][l2][r2]=ans;

樹形

https://www.luogu.com.cn/problem/P1352

入門題

http://acm.hdu.edu.cn/showproblem.php?pid=2196

對樹上每個點求它到哪個點距離最遠,一個\(O(n\log n)\)的做法是先\(O(n)\)求出直徑\(r_1,r_2\),那麼每個點\(x\)的答案就是\(max\{dis(r_1.x).dis(r_2.x)\}\),注意這題有多組資料(以及一開始以為沒清空乾淨陣列,瘋狂MLE/TLE)。