【算法與數據結構】—— 動態規劃之走格子問題

動態規劃之走格子問題

問題一:
給一個由數字組成的n*m的矩陣,初始在左上角,要求每次只能向下或向右移動,路徑和就是經過的數字全部加起來,求最小路徑和。

分析:
這類題大家會很自然地想到用暴力搜索法,可是很顯然的是,暴力法在數據範圍過大的情況下是難以勝任的。出現這樣的原因和斐波那契數列中的遞歸法一樣,都是因爲出現了大量的重複工作
比如對於某個數字矩陣:
1 3 5 7
8 6 4 2
5 0 1 3
4 8 7 2
假設從位置(1,1)到位置(3,3),這中間會有如下路徑:
矩陣路徑和
試想,我們在利用暴力法搜索從座標(1,1)到座標(3,3)處,再到最右下角位置(4,4)時,我們都需要嘗試這圖中的每種走法,因此這樣的算法是極其低效的。
而優化的方法也正是通過生成一個和原矩陣大小相同的二維表,來將從起點到每個位置的最小路徑和記錄下來,進而以直接取值來替代多次的重複遞歸,達到優化目的

而這樣的方法就是動態規劃,下面就來介紹一下這個方法
首先我們需要來填寫一張和題目中給出的矩陣大小相同的表格,如下:
填表格
第一步:初始化,由於在邊界位置(即最上方一行和最左邊一列),其路徑只有一條(最上方的每個格子只能來自左方,最左邊的每個格子只能來自上方),我們別無選擇,因此可以直接填寫,如下:
填表格
第二步,確定某個位置的路徑和
比如就拿位置座標爲(2,2)的這個點來說,由於走到這一點只能來自其上方或左方,因此爲了使得走到這一點時路徑和最小,那麼我們就需要從其上方或左方中選出值較小的那個點過來
於是我們在填寫位置座標爲(2,2)這個點的路徑和時,我們的公式爲:
dp[2,2]= min(dp[2,1],dp[1,2]) + value[2,2] = min(9,4)+6 = 4+6 =10
其中dp[x,y]表示位置座標爲(x,y)的最小路徑和,value[x,y]表示位置座標爲(x,y)的數字
於是,現在的表格內容如下:
填表格

接下來繼續這個過程,直到最後填寫完該表格,那麼最終dp[n,m]中的內容即爲我們所需的答案
我們回看上面,不難得出這其中的動態轉換方程爲:
dp[ i ][ j ] = min( dp[ i ][ j-1 ],dp[ i-1 ][ j ] ) + value[ i ][ j ]
將上述過程的完整代碼給出,如下:

#include<iostream>
using namespace std;

const int N=10000;
int dp[N][N],value[N][N];
int n,m;

int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)				//錄入初始數字矩陣 
		for(int j=1;j<=m;j++)
			cin>>value[i][j];
	for(int i=1;i<=n;i++)				//初始化最上方第一行 
		dp[1][i]=dp[1][i-1]+value[1][i];
	for(int i=1;i<=n;i++)				//初始化最左邊第一列 
		dp[i][1]=dp[i-1][1]+value[i][1]; 
	for(int i=2;i<=n;i++)
		for(int j=2;j<=m;j++)			//開始動態轉移,填寫dp表 
			dp[i][j]=min(dp[i][j-1],dp[i-1][j])+value[i][j];
	cout<<dp[n][m]<<endl; 
	return 0;
}

上面的程序中用到了兩個二維數組(dp[N][N]和value[N][N])
如果我們的N=10 0000,這個程序在編譯的時候就會報錯:內存不足!
這是上面程序的一個缺陷
我們來試着優化一下,首先很容易看出的是,我們沒有必要用value[N][N]數組
因爲我們可以直接將原數字矩陣放進dp[N][N]中,然後把初始化和動態轉移操作都在dp[N][N]數組上進行
這樣,我們就把動態轉移方程變成了:dp[ i ][ j ] = min( dp[ i ][ j-1 ]+dp[ i-1 ][ j ] )+dp[ i ][ j ]
改進後的代碼如下:

#include<iostream>
using namespace std;

const int N=10000;
int dp[N][N];
int n,m;

int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)				//錄入初始數字矩陣 
		for(int j=1;j<=m;j++)
			cin>>dp[i][j];
	for(int i=1;i<=n;i++)				//初始化最上方第一行 
		dp[1][i]=dp[1][i-1]+dp[1][i];
	for(int i=1;i<=n;i++)				//初始化最左邊第一列 
		dp[i][1]=dp[i-1][1]+dp[i][1]; 
	for(int i=2;i<=n;i++)
		for(int j=2;j<=m;j++)			//開始動態轉移,填寫dp表 
			dp[i][j]=min(dp[i][j-1],dp[i-1][j])+dp[i][j];
	cout<<dp[n][m]<<endl; 
	return 0;
}

但是這樣的改進實際上沒有任何作用,因爲整個程序在N=10 0000時,依然無法運行
我們應該要關注的是,怎麼把dp[N][N]數組的大小降低
這就需要回到我們的動態轉移方程上:dp[ i ][ j ]=min( dp[ i ][ j-1 ]+dp[ i-1 ][ j ] )+dp[ i ][ j ]
我們發現,該方程在尋找最小路徑和的過程中,實際上只關心兩處:該位置的上方,該位置的左方
那麼我們在優化的時候就應該從這裏入手,現在我們還是來進行填表,如下:
填表
這裏依然對樣例:
1 3 5 7
8 6 4 2
5 0 1 3
4 8 7 2
進行填寫,並假設現在填寫的新的數組爲fp[N]
第一步:初始化第一行,由於最上面那一行的行走方式只有一種(來自左方),因此我們可以直接填寫,於是得到fp[N]={1,4,9,16},如下:
填表格
你可以視當前數組中的內容爲dp[1][1] - dp[1][4]中的路徑和
第二步:開始動態轉移,本步驟是一個迭代過程,主要有如下兩步
① 填寫第一個格子(注意:當前位置爲(2,1)),由於最左邊的那一列的行走方式只有一種(來自上方),因此可以直接填寫,於是得到:
填表格
② 填寫其餘格子。
比如現在填寫第2個格子(注意:當前位置爲(2,2)),由於當前在第1個格子裏存放的是位置(2,1)的路徑和(即fp[1]=dp[2][1]),第2個格子裏存放的是位置(1,2)的路徑和(即當前fp[2]=dp[1][2])
那麼根據公式dp[ i ][ j ] = min( dp[ i ][ j-1 ]+dp[ i-1 ][ j ] )+dp[ i ][ j ]
我們將其轉換到數組fp[N]中則得到:fp[ i ] = min( fp[ i-1 ],fp[ i ] ) + valueNow
其中,valueNow表示當前輸入的第dp[ i ][ j ]個數字,即當前值

然後不斷地重複整個第二步,最終得到的fp[m]即爲所求
下面將改進後的程序代碼給出:

#include<iostream>
using namespace std;

const int N=100000;
int dp[N];
int n,m;

int main()
{
	int n,m,temp;
	cin>>n>>m;
	for(int i=1;i<=m;i++){		//第一步:初始化 
		cin>>temp;
		dp[i]=dp[i-1]+temp;
	}
	for(int i=2;i<=n;i++)		//第二步
	{
		cin>>temp;				//得到當前值 
		dp[1]+=temp;			//第二步中的第①步:求第一個值 
		for(int j=2;j<=m;j++){	//第二步中的第②步:求其餘值 
			cin>>temp;
			dp[j]=min(dp[j-1],dp[j])+temp;
		}
	}
	cout<<dp[m]<<endl;			//輸出最終結果 
	return 0;
}

可以發現,在改進的算法中,即使N取值到了10 0000依然能夠正常運行
現在,這個算法在時間未變的前提下,將空間複雜度優化到了O(m)



問題二:
給一個由數字組成的矩陣,初始在左上角,要求每次只能向下或向右移動,路徑和就是經過的數字全部加起來,求可能的最大路徑和。
解決辦法也很簡單,直接將動態轉移方程變爲:dp[ i ] = max( dp[ i-1 ],dp[ i ] ) + valueNow 即可



問題三:
給一個規格爲n*m的矩陣,初始在左上角,要求每次只能向下或向右移動,求到右下角的方法數(值會很大,通常要求對100000007取模)。
要知道對於某個格子而言,到達該點的方法數=到該點左邊的方法數+到該點上邊的方法數
即,動態轉移方程變爲:dp[ i ][ j ] = dp[ i ][ j-1 ]+dp[ i-1 ][ j ]

下面給出該問題的完整代碼:

#include<iostream>
using namespace std;
const int N=100;
const int MOD=100000007;
int dp[N][N];
int n,m;
void DP()
{
	for(int i=1;i<=m;i++) dp[1][i]=1;
	for(int i=1;i<=n;i++) dp[i][1]=1;
	for(int i=2;i<=n;i++)
		for(int j=2;j<=m;j++)
			dp[i][j]=(dp[i][j-1]+dp[i-1][j])%MOD;
}
int main()
{
	cin>>n>>m;
	DP();
	cout<<dp[n][m]<<endl;
	return 0;
}

同樣地,本題也能直接使用一維數組,即將動態轉移方程變爲:dp[ j ] = dp[ j-1 ]+dp[ j ]
改進後的代碼如下:

#include<iostream>
using namespace std;
const int N=105;
const int MOD=100000007;
int dp[N];
int n,m;
void DP()
{
	dp[1]=1;
	for(int i=1;i<=n;i++)
	for(int j=2;j<=m;j++)
		dp[j]=(dp[j-1]+dp[j])%MOD;		
}
int main()
{
	cin>>n>>m;
	DP();
	cout<<dp[m]<<endl;
	return 0;
}



接下來看兩道道非常經典的例題:
藍橋杯 算法訓練 數字三角形
藍橋杯 歷屆試題 格子刷油漆



本文章參考自 [一隻大白兔兔兔兔兔] 的這篇博客,感謝博主!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章