第九章 動態規劃相關知識點總結
一、動態規劃
動態規劃的核心是 狀態 和 狀態轉移方程。
解決動態規劃的方法一般有兩種
1、遞推計算
遞推計算的關鍵是邊界和計算順序
2、記憶化搜尋
記憶化搜尋不用事先確定計算順序,所謂的記憶化搜尋,就是給每個狀態設定一個標誌,當這個狀態已經被計算過,通過標誌判斷不再重複計算。
3、DAG上的動態規劃
一般的問題可以轉化為有向無環圖的動態規劃,一般分為不知道起點和終點的最長路,已知起點和終點的最長路和最短路。
(1)在未知起點的最長路中,我們可以寫出狀態轉移方程
d(i)=max{d[j]+1 | (i,j)∈E}
程式碼可以寫為:
如果有多解,又要保證字典序最小,遞迴輸出。那麼:int dp(int i) { int& ans = d[i]; if(ans>0) return ans; ans = 1; for(int j=1;j<=n;j++) if(G[i][j]) ans = max(ans,dp[j]+1); return ans; }
void print_ans(int i)
{
printf("%d",i);
for (int j=1;j<=n;j++) if(G[i][j]&&d[i]==d[j]+1)
{
printf_ans(j);
break;
}
}
(2)固定終點的最長路和最短路(以硬幣問題為例)
需要考慮終點不能到達的情況。
使用一個極小的數(特殊值)來表示終點不能到達程式碼:
int dp(int S)//S為還剩下的價值 { int& ans =d[S]; //硬幣數目 if(ans!=-1) return ans; ans = -(1<<30); for (int i=1;j<=n;i++) if(S>=V[i]) ans = max(ans,dp(S-V[i])+1); return ans; }
使用vis[]來表示是否已經訪問,程式碼:
int dp(int S)//S為還剩下的價值
{
if(vis[S]) return d[S];
vis[S]=1;
int& ans =d[S]; //硬幣數目
ans = -(1<<30);
for (int i=1;j<=n;i++) if(S>=V[i]) ans = max(ans,dp(S-V[i])+1);
return ans;
}
如果狀態複雜,可以使用map來記錄狀態值,通過if(d.count(S))可以來判斷狀態是否計算過。
如果既要求最短路,又要求最長路,使用記憶化搜尋需要需要寫兩個,這時可以採用遞推的方法。
程式碼:
minv[0]=maxv[0]=0;
for(int i=1;i<=S;i++)
{
minv[i]=INF;
maxv[i]=-INF;
}
for(int i=1;i<=S;i++)
for(int j=1;j<=n;j++)
if(i>V[j])
{
maxv[i]= max(maxv[i],maxv[i-V[j]]+1);
minv[i]= min(minv[i],minv[i-V[j]]+1);
}
printf("%d %d\n",minv[S],maxv[S]);
//print ans
void print_ans(int *d ,int S)
{
for(int i =1;i<=n;i++)
if(S>=V[i] && d[S]==d[S-V[i]]+1)
{
printf("%d ",i);
print_ans(d,S-V[i]);
break;
}
}
實際上,無論我們使用遞推計算還是記憶化搜尋進行計算,計算的順序都是從小的狀態到大的狀態,而大的狀態可以根據小的狀態的值進行求解。
傳統的遞推法可以表示為”對於每個狀態i,計算f(i)“,這需要對於每個狀態i,找到計算f(i)以來的所有狀態,而另外一種方法是"對於每個狀態i,更新f(i)所影響的狀態",稱為“刷表法”。
0-1揹包問題
for(int i=n;i>=1;i++)//n種物品
for(int j=0;j<=C;j++)//j表示的為揹包的剩餘重量
{
d[i][j] = (i==n?0:d[i+1][j]);
if(j>=V[i]) //表示了揹包剩餘重量的所有可能,對這個進行了列舉
d[i][j]=max(d[i][j],d[i+1][j-V[i]]+W[i]);
//V[i]為第i個物品的體積,W[i]為第i個物品的重量
//d[i][j]表示不選物品i, d[i+1][j-V[i]]+W[i]表示現在了物品i。
}
採用滾動陣列求解
memset(f,0,sizeof(f));
for(int i=1;i<=n;i++)
{
scanf("%d%d",&V,&W);
for (int j = C;j>=0;j--)
if(j>=V) f[j]=max(f[j],f[j-V]+W);
}
經典動態規劃模型
1、最長上升子序列問題
2、最長公共子序列問題
3、最優矩陣鏈乘
4、最優三角剖分
樹上的動態規劃
樹上的動態規劃,總體而言,就是計算順序從葉子節點到根,那麼需要進行DFS到葉子節點計算狀態後返回。適合遞迴操作。
1、樹的最大獨立集
2、樹的重心(質心)
3、樹的最長路徑(最遠點對)
複雜狀態的動態規劃
1、最優配對問題
最優配對問題的狀態定義為d(i,S)表示在前i個點,位於集合S的元素兩兩配對的最小距離和。狀態轉移方程為:
d(i,S)=min{ |PiPj| + d(i-1,S-{i}-{j)| j∈S}
首先通過子集二進位制方式表示下表,進行列舉子集,程式碼如下:
//列舉i
for(int i = 0;i<n;i++)
//列舉子集
for(int S=0;S<(1<<n);S++){
d[i][S]=INF;
for(int j=0;j<i;j++) if(S&(1<<j))
d[i][S]=min(d[i][S],dist(i,j)+d[i-1][S^(1<<i)^(1<<j)]);
}
2、TSP問題
TSP問題是想尋找一條道路,從起點出發,最終回到起點,最終道路的總長度最短。可以設起點和終點都為0.
可以設定狀態為d(i,S)為當前在城市i, 還需訪問集合S中的城市各一次後回到城市0的最短長度,則
d(i,S) = min (d(j,S-{j})+dist(i,j) | j∈S)
邊界為d(i,{})= dist(0,i);
3、圖的色數
圖的色數問題是在一個無向圖中,把圖中的節點染成儘量少的顏色,使得相鄰結點顏色不同。
d(S) 表示把結點S染色,所需要的顏色數的最小值,則
d(S)=d(S-S‘)+1 其中S‘是S的子集,並且內部沒有邊(即不存在S‘內的兩個結點u和v使得u和v相鄰),換句話說S‘是一個“可以染成同一種顏色”的結點集。
程式碼如下:
d[0]=0; for(int S=1;S< (1<<n);S++) { d[S]=INF; for(int S0=S;S0;S0 = (S0-1)&S) if(no_edge_inside[S0]) d[S]=min(d[S],d[S-S0]+1); }