1. 程式人生 > >第九章 動態規劃相關知識點總結

第九章 動態規劃相關知識點總結

一、動態規劃

動態規劃的核心是 狀態 和 狀態轉移方程。

解決動態規劃的方法一般有兩種 

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);
}