HDU 1176 免费馅饼(DP详细的分析)

hdu 1176原题
Problem Description

都说天上不会掉馅饼,但有一天gameboy正走在回家的小径上,忽然天上掉下大把大把的馅饼。说来gameboy的人品实在是太好了,这馅饼别处都不掉,就掉落在他身旁的10米范围内。馅饼如果掉在了地上当然就不能吃了,所以gameboy马上卸下身上的背包去接。但由于小径两侧都不能站人,所以他只能在小径上接。由于gameboy平时老呆在房间里玩游戏,虽然在游戏中是个身手敏捷的高手,但在现实中运动神经特别迟钝,每秒种只有在移动不超过一米的范围内接住坠落的馅饼。现在给这条小径如图标上坐标:

0 1 2 3 4 5 6 7 8 9 10(人初始位置在5)

为了使问题简化,假设在接下来的一段时间里,馅饼都掉落在0-10这11个位置。开始时gameboy站在5这个位置,因此在第一秒,他只能接到4,5,6这三个位置中其中一个位置上的馅饼。问gameboy最多可能接到多少个馅饼?(假设他的背包可以容纳无穷多个馅饼)

Input

输入数据有多组。每组数据的第一行为以正整数n(0

Output
每一组输入数据对应一行输出。输出一个整数m,表示gameboy最多可能接到m个馅饼。
提示:本题的输入数据量比较大,建议用scanf读入,用cin可能会超时。

Sample Input

6
5 1
4 1
6 1
7 2
7 2
8 3
0

Sample Output

4


思路:一道动规题。dp题一般都先用递归的思路想,因为dp的题本质上都是可以用递归解决的,只不过递归有很多缺陷,所以我们为了优化,采用dp的办法来做。既然往递归方向来想,很明显,每一步我只能往前走一步,或往后走一步,或停在原地不动只有这三种决策吧,所以大概的递归思路已经出来了,就是设一dfs函数,dfs(x,t )表示当前的位置x和当前的时刻t,所以递归方程出来了:dfs(x, t) = max(dfs(x + 1, t + 1), dfs(x - 1, t + 1), dfs(x, t + 1)) + 到达此位置能拿到的馅饼数!

所以这个时候,我们大概可以把这个题的递归版本做法写出来了:

/*
递归版本 
*/
#include
using namespace std;

struct pan{
     
	int loc;
	int time;
};
const int maxn = 100;
int n;
pan a[maxn];
int max_time;

int dfs(int x, int t)
{
     
	//递归出口 
	if(t > max_time)
		return 0;
	
	//此位置能拿到的馅饼数,res 
	int res = 0;
	for(int i = 1;i <= n;i++)
	{
     
		if(a[i].loc == x && a[i].time == t)
			res++;
	}
	//递归搜其他的决策 
	if(x == 0)
		return (dfs(x + 1, t + 1), dfs(x, t + 1)) + res;
	else if(x == 10)
		return (dfs(x - 1, t + 1), dfs(x, t + 1)) + res;
	else if(x > 0 && x < 10)
	{
     
		int dp1 = dfs(x + 1, t + 1);
		int dp2 = dfs(x - 1, t + 1);
		int r = max(dp1, dp2);
		return r + res;
	}
}

int main()
{
     
	while(scanf("%d", &n) && n != 0)
	{
     
		max_time = 0;
	//	Clear();
		for(int i = 1;i <= n;i++)
		{
     
			scanf("%d%d", &a[i].loc, &a[i].time);
		}
		for(int i = 1;i <= n;i++)
		{
     
			if(a[i].time > max_time)
				max_time = a[i].time;
		}
		cout << dfs(5, 0) << endl;
	}
	return 0;
}

递归版本虽然很好理解,也很方便写 ,但正如前面所说,递归效率太低了,此题如果直接提交递归版本,那么一定是超时!所以我们必须采用更为高效的动态规划算法来改写这道题。

那么我们开始第2次尝试,如何用dp算法改写这题呢?在我们用递归算法解决这道题的时候,我们其实已经将这题的状态设好了,就是dfs(x, t),即位置和时刻两个状态的组合。一旦设好状态,剩下的事其实就好做了,就是推状态转移方程

推状态转移方程我喜欢自己先手动的把dp数表填起来,就用题中的这个样本输入吧,这个样本的dp表应该如下图所示
HDU 1176 免费馅饼(DP详细的分析)_第1张图片在填表的过程中,也证实了我们前面的递归方程是正确的,就是此题的状态转移方程!即dp[i][j] = max(dp[i - 1][j - 1], dp[i + 1][j - 1], dp[i][j - 1]) + (i, j)这个点的馅饼数

由于每一项dp[i][j]都要确保dp[i - 1][j - 1], dp[i + 1][j - 1], dp[i][j - 1]这三个量都求出才能得到,所以这一题的遍历顺序应该是先从上往下,再从左至右

那么我们就可以开始写代码了:

/*
动态规划版本 
要想办法把求res的那个for循环去掉,经过思考后,无非是拿空间换时间
用二维数组应该可以 
*/ 
#include
using namespace std;

struct pan{
     
	int loc;
	int time;
};
const int maxn = 1000005;
int n;
pan a[maxn];
int dp[11][maxn];
int max_time;

int Max(int x, int y, int z)
{
     
	int temp = max(x, y);
	return max(temp, z);
}

int Dp()
{
     
	//先处理边界条件,即第一个时刻
	for(int l = 0;l <= 10;l++)			//这里的l表示loc 
	{
     
		for(int i = 1;i <= n;i++)
		{
     
			if(a[i].loc == l && a[i].time == 1)
				dp[l][1] = 1;
		}
	} 
	
	//再来常规情况,首先要明确状态转移方程,然后明确遍历顺序才能写这部分的代码
	for(int j = 2;j <= max_time;j++)
	{
     
		for(int i = 0;i <= 10;i++)
		{
     
			int res = 0;
			for(int k = 1;k <= n;k++)
			{
     
				if(a[k].loc == i && a[k].time == j)
					res++;
			}
			if(i == 0)
				dp[i][j] = max(dp[i + 1][j - 1], dp[i][j - 1]) + res;
			else if(i == 10)
				dp[i][j] = max(dp[i][j - 1], dp[i - 1][j - 1]) + res;
			else if(i > 0 && i < 10)
				dp[i][j] = Max(dp[i][j - 1], dp[i + 1][j - 1], dp[i - 1][j - 1]) + res;
		}
	}
	
	//找出dp数组中的最大值,最后将其返回 
	int max_res = 0;
	for(int i = 0;i <= 10;i++)
	{
     
		for(int j = 1;j <= max_time;j++)
		{
     
			if(dp[i][j] > max_res)
				max_res = dp[i][j];
		}
	}
	/*
	打印dp数组测试 
	for(int i = 0;i <= 10;i++)
	{
		for(int j = 1;j <= n;j++)
		{
			cout << dp[i][j] << " ";
		}
		cout << endl;
	}
	*/
	return max_res;
}

//清理之前的dp数组残余的值 
void Clear()
{
     
	for(int i = 0;i <= 10;i++)
	{
     
		for(int j = 1;j <= max_time;j++)
		{
     
			dp[i][j] = 0;
		}
	}
}

int main()
{
     
	while(scanf("%d", &n) && n != 0)
	{
     
		max_time = 0;
		for(int i = 1;i <= n;i++)
		{
     
			scanf("%d%d", &a[i].loc, &a[i].time);
		}
		for(int i = 1;i <= n;i++)
		{
     
			if(a[i].time > max_time)
				max_time = a[i].time;
		}
		Clear();
		cout << Dp() << endl;
	}
	return 0;
}

但遗憾的是,这次提交还是超时,为什么呢?因为32–49行 ,我用了三重循环求解,时间复杂度成了O(n^2 * 10), 而题中的n最大值可以到10的6次方,所以必然会超时。经过分析,三重循环中,外面两层毫无疑问需要,最后一层求res的循环可以省掉。那我们只能采取用空间换时间的方法,可以设一个二维数组来解决这个问题,就可以将res用O(1)复杂度求出,整个算法就可以降为O(10n)!

同时,这个版本还忽略了一些问题,比如输入1 2,即1位置在2时刻掉下一个馅饼,这个程序输出1个!这就错了。这里得加一个判断!


于是我的第三次代码出来了:

/*
动态规划版本 
*/ 
#include
#include
using namespace std;

struct pan{
     
	int loc;
	int time;
};
const int maxn = 1000005;
int n;
pan a[maxn];
int x[12][maxn];
int dp[12][maxn];
int max_time;

int Max(int x, int y, int z)
{
     
	int temp = max(x, y);
	return max(temp, z);
}

int Dp()
{
     
	//先处理边界条件,即第一个时刻
	//此处尤为注意!因为(7, 1)和(3,1)这是不可能捡到的,所以1时刻只可能在4--6之间捡到 
	for(int l = 4;l <= 6;l++)
	{
     
		dp[l][1] = x[l][1];
	}
	
	//再来常规情况,首先要明确状态转移方程,然后明确遍历顺序才能写这部分的代码
	for(int j = 2;j <= max_time;j++)
	{
     
		for(int i = 0;i <= 10;i++)
		{
     
			if(abs(5 - i) > j)			//此处注意!如果5到它的距离无法在j时刻内走到,那就是0 
			{
     
				dp[i][j] = 0;
			}
			else
			{
     
				int res = 0;
				res = x[i][j];			//利用x数组,将这一步的时间复杂度降为O(1)!
				if(i == 0)
					dp[i][j] = max(dp[i + 1][j - 1], dp[i][j - 1]) + res;
				else if(i == 10)
					dp[i][j] = max(dp[i - 1][j - 1], dp[i][j - 1]) + res;
				else if(i > 0 && i < 10)
					dp[i][j] = Max(dp[i + 1][j - 1], dp[i - 1][j - 1], dp[i][j - 1]) + res;	
			}
		}
	}
	
	//找出dp数组中的最大值,最后将其返回 
	int max_res = 0;
	for(int i = 0;i <= 10;i++)
	{
     
		for(int j = 1;j <= max_time;j++)
		{
     
			if(dp[i][j] > max_res)
				max_res = dp[i][j];
		}
	}
	
	//打印dp数组测试 
	/*for(int i = 0;i <= 10;i++)
	{
		for(int j = 1;j <= max_time;j++)
		{
			cout << dp[i][j] << " ";
		}
		cout << endl;
	}
	cout << max_time << endl;*/
	return max_res;
}

//清理之前的dp数组与x数组残余的值 
void Clear()
{
     
	for(int i = 0;i <= 10;i++)
	{
     
		for(int j = 1;j <= max_time;j++)
		{
     
			dp[i][j] = 0;
			x[i][j] = 0;
		}
	}
}

int main()
{
     
	while(scanf("%d", &n) && n != 0)
	{
     
		max_time = 0;
		for(int i = 1;i <= n;i++)
		{
     
			scanf("%d%d", &a[i].loc, &a[i].time);
		}
		for(int i = 1;i <= n;i++)
		{
     
			if(a[i].time > max_time)
				max_time = a[i].time;
		}
		Clear();
		for(int i = 1;i <= n;i++)
		{
     
			int lc = a[i].loc;
			int te = a[i].time;
			if(x[lc][te] == 0)
				x[lc][te] = 1;
			else
				x[lc][te]++;
		}
		cout << Dp() << endl;
	}
	return 0;
}

这次终于AC了,历经磨难啊。。。
这题是很好的一个dp题,要稍微想一想,但又不至于太难,很适合刚刚进阶的选手

你可能感兴趣的:(DP,数据结构与算法,算法)