动态规划-跳一跳(构造法)-算法课-精华版

(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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值