動態規劃-跳一跳(構造法)-算法課-精華版

(3)M排N列的木樁,從第一排開始跳到最後一排。每次跳到下一排的同一列、前一列或後一列木樁。

     設R[i,j]爲跳到i排j列木樁的獎金。求從第一排跳到最後一排獲得的獎金總量的最大值,給出遞推方程。

(4)上述問題,如果允許在同一排向左或向右跳,且橫跳總次數不超過H,給出求獎金總量的最大值的遞推方程

(5)如果每個木樁只有第一次跳上去的時候有獎金,如何求最大值
第4題相對簡單,所以放在文章的最後面。
下面是第五題的答案:
所要注意的是,在狀態轉移方程哪裏,我進行了很多代碼簡化,要不然代碼有點長。
思路:
首先這道題要考慮你是不是重複走到同一個點。
首先自然想到對自己走過的點打標記,但是這樣一張表是不夠的,而且不好記錄,以爲你是一行一行跑的,所以你對某一個狀態的路徑打了標記,會影響到其他狀態(其他狀態可能沒有經過這些點),狀態之間會有衝突,不能用一張表記錄。
solution 1:
首先,自然想到對每一個狀態獨一的用一張表記錄,但是如果一行一行跑的(所有狀態並行的跑的)話,要用分廠多的表記錄。
所以我們先把一個狀態跑完,然後在跑其他狀態,一個一個串行,這樣空間上只需要一張表了。
我們採用自頂向下帶備忘錄的形式泄動態規劃。
但是記錄狀態結果是要注意,要想數位DP一樣,無約束時才記錄(數位DP時前面的state == ture),這裏只有第一次到達該行的結果採用數組記錄下來。
這個方法還是時間複雜度有點高,後面有更好的方法,不過需要一點創造力。
下面看這種解法的代碼。

#include<bits/stdc++.h>
#define per(i,a,b) for(int i = (a);i <= (b); ++i)
using namespace std;
const int maxn=1e3;
int n = 0,m = 0,h = 0;
int r[maxn+10][maxn+10];
int dp[100][100][100];
int vis[maxn+10][maxn+10];
int dfs(int x,int y,int hn,bool sta){//到達x,y,橫着走了hn次,sta是否是第一次到達該行 
	if(x == m && hn == h){//遞歸終止條件 
		return (vis[x][y] == 0) ? r[x][y] : 0;
	}
	if(sta &&dp[x][y][hn] != 0){//sta表示第一次到達這一行,那麼可以返回原來計算的結果,這有點類似數位DP 
		return dp[x][y][hn];
	}
	int bonus = (vis[x][y] == 0 ) ? r[x][y] : 0;//只有第一次碰到,纔有獎金,所以vis是一個整數數組,bool數組不夠用 
	++vis[x][y];
	int res = 0;
	if(hn + 1 <= h){
		if(y-1 >= 1){
			res = dfs(x,y-1,hn+1,false);	
		}
		if(y+1 <= n){
			res = max(res,dfs(x,y+1,hn+1,false));	
		}
	}
	if(x != m){
		if(y - 1 >= 1){
			res = max(res,dfs(x+1,y-1,hn,true));	
		}
		res = max(res,dfs(x+1,y,hn,true));
		if(y+1 <= n){
			res = max(res,dfs(x+1,y+1,hn,true));
		}
	}
	if(sta){//類似於數位DP,如果是第一次到達該行的值,那麼就記錄下來,其他的會有衝突 
		dp[x][y][hn] = max(res + bonus,dp[x][y][hn]);
	}
	--vis[x][y];
	return res + bonus;
}
int main(){
	//std::ios::sync_with_stdio(false);
	//cin.tie(0);cout.tie(0);
	while(~scanf("%d %d %d",&m,&n,&h)){
		per(i,1,m){
			per(j,1,n){
				scanf("%d",&r[i][j]); 
			}
		}
		memset(dp,0,sizeof(dp)); memset(vis,0,sizeof(vis));
		int maxv = -1,loc = 0;
		per(i,1,n){
			int ans = dfs(1,i,0,true);
			if(ans > maxv){
				loc = i;
				maxv = ans;
			}
		}
		printf("The starting position is %d, The maximum bonus you can get is %d\n",loc,maxv);
	}
	return 0;
}

下面是一種更好的解法:構造法。
首先畫出走重複路的情況:

			  		-> ->
<-	<- <- <- <- <-|	
-> ->		


這種情況下只有中間那一行的箭頭是有效的,兩邊的重複的可以看做無效的。
首先我們的難題是他在一行的橫跳的方向是不確定的,我們能否把他編程方向固定的呢?
帶着這樣的思考,中間的那一行只能有一種方向,向左或向右,我們兩種情況都枚舉。
那麼兩邊重複的怎麼辦?
既然他們是重複的,那麼可不可以看做獎金是0的點,所以兩邊可以構造成全是0的點。
所以在原來的圖上面,兩個行之間新構造0 0 0 0 。。。行,用來過渡。
所以問題就轉化爲原來的郵箱航只能有一種方向橫跳,就不用考慮重複問題了。
這裏的奇數行(有效行)和偶數行(0行)要分類討論。
狀態轉移方程:
dp[x][y][h][dic]表示第x行第y列橫跳了不超過h次,這行(第x行)的方向是dic(0:往左跳,1:往右跳)的最大獎金。
r[i][j]表示相應的獎金。
奇數行:
dp[x][y][h][0] = max(dp[x-1][y][h][0],dp[x-1][y][h][1],dp[x][y+1][h-1][0]) + r[i][j];
dp[x][y][h][1] = max(dp[x-1][y][h][0],dp[x-1][y][h][1],dp[x][y-1][h+1][1]) + r[i][j];
偶數行:
dp[x][y[[h][0] = max(dp[x-1][y-1][h][0和1],dp[x-1][y][h][0和1],dp[x-1][y+1][h][0和1] dp[x][y+1][h-1][0]) + r[i][j];(共7個)
dp[x][y[[h][1] = max(dp[x-1][y-1][h][0和1],dp[x-1][y][h][0和1],dp[x-1][y+1][h][0和1] dp[x][y-1][h-1][1]) + r[i][j];(共7個)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<vector>
#include<set>
#include<stack>
#include<queue>
#include<map>
#include<cstring>
#include<string>
#include<cmath>

using namespace std;

typedef long long LL;

#define INF 0x3f3f3f3f
#define PI acos(-1.0)
#define pii pair<int,int>
#define all(x) x.begin(),x.end()
#define mem(a,b) memset(a,b,sizeof(a))
#define per(i,a,b) for(int i = a;i <= b;++i)
#define rep(i,a,b) for(int i = a;i >= b;--i)
const int maxn = 30;
int n = 0,m = 0,h = 0;
int dp[maxn*2][maxn][maxn][2];
int r[maxn*2][maxn];
void max_bonus(){
	map<int,int> mp;
	mp[0] = 1; mp[1] = -1;
	per(i,1,2*m-1){
		per(k,0,h){//h循環必須放在j外面,因爲由i&1==1的狀態轉移方程可知,
		//dp[x][y][h][0] = max(...,dp[x][y+1][h-1][0]),
		//y要調用y+1的結果,所以這個時候發現h調用的是h-1,所以h>y,h-1的時候計算出所有的y 
			per(dic,0,1){//這層循環可以放在這裏,也可以放在j裏面 
				per(j,1,n){
					if(i & 1){
						dp[i][j][k][dic] = max(dp[i-1][j][k][0],max(dp[i-1][j][k][1],(k > 0 ? dp[i][j+mp[dic]][k-1][dic] : 0))) + r[i][j];
						//dp[i][j][k][0] = max(dp[i-1][j][k][0],max(dp[i-1][j][k][1],(k > 0 ? dp[i][j+1][k-1][0] : 0)) + r[i][j];
						//dp[i][j][k][1] = max(dp[i-1][j][k][0],max(dp[i-1][j][k][1],(k > 0 ? dp[i][j+1][k-1][1] : 0)) + r[i][j];	
					}else{
						per(p,0,1){
							dp[i][j][k][dic] = max(dp[i][j][k][dic],max(dp[i-1][j-1][k][p],max(dp[i-1][j][k][p],dp[i-1][j+1][k][p])) + r[i][j]) ;
						}
						dp[i][j][k][dic] = max(dp[i][j][k][dic],(k > 0 ? dp[i][j+mp[dic]][k-1][dic]: 0) + r[i][j]) ;
					}
				}	
			}	
		}
	}
	per(i,1,m){
		per(j,1,n){
			int maxl = 0,maxr = 0;
			per(k,0,h){
				maxl = max(maxl,dp[i][j][k][0]);
				maxr = max(maxr,dp[i][j][k][1]);
			}
			printf("%d(%d) ",maxl,maxr);
		}
		printf("\n");
	}
	int maxv = -1,loc = 0;
	per(j,1,n){
		//per(k,0,h){//最大值一定是在k == h處取得 
			per(dic,0,1){
				if(dp[2*m-1][j][h][dic] >= maxv){
					maxv = dp[2*m-1][j][h][dic];
					loc = j;
				}
			}
		//}
	}
	printf("%d %d\n",loc,maxv);
}
int main(){
	while(~scanf("%d %d %d",&m,&n,&h)){
		memset(dp,0,sizeof(dp));  memset(r,0,sizeof(r));
		per(i,1,m){
			per(j,1,n){
				scanf("%d",&r[i*2-1][j]);
			}
		}
		max_bonus();
	}
	
	return 0;
}
/*
test:
3 3 3
1 2 3 
4 5 6
9 8 7

answer:3 38

1 3 4
1 2 3

6
*/ 

第4題代碼:

#include<bits/stdc++.h> 
using namespace std;
#define per(i,a,b) for(int i=(a);i<=(b);i++)
int n = 0,m = 0,h = 0;
int r[10][10];
int dp[10][10][10];
void solve(){
	memset(dp,0,sizeof(dp));
	per(i,1,m){
		per(k,0,h){//h循環要放在列循環前面,因爲列循環每一次都要用到第k-1的結果 
			per(j,1,n){
				dp[i][j][k] = max(dp[i-1][j-1][k],max(dp[i-1][j][k],dp[i-1][j+1][k])) + r[i][j];
				if(k == 0){
					continue;
				}
				dp[i][j][k] = max(dp[i][j][k],max(dp[i][j-1][k-1],dp[i][j+1][k-1]) + r[i][j]);
			}	
		}
	}
	int maxv = -1,loc = 0,times = 0;
	per(i,1,n){
		if(dp[m][i][h] > maxv){
			maxv = dp[m][i][h];
			loc = i;
		}
	}
	printf("The starting position is %d, The maximum bonus you can get is %d\n",loc,maxv);
}
int main(){
	//std::ios::sync_with_stdio(false);
	//cin.tie(0);cout.tie(0);
	while(~scanf("%d %d %d",&m,&n,&h)){
		per(i,1,m){
			per(j,1,n){
				scanf("%d",&r[i][j]);
			}
		}
		solve();
	}
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章