动态规划(DP)——入门篇(11.24更新)

零、先修课程

首先,在开始理解DP的思想前,你需要

1. 完成HDU里面的递推求解专题练习(For Beginner)那7道题(这些题很简单,题解请在博客中搜索),这对你理解DP有很大的帮助。

2. 对递归搜索(比如深度优先搜索,DFS)有一定了解。


一、递归与记忆化搜索

我们从POJ 3176入手来学习这一思想。(题目很短,请快速读完)

从上往下看,最大和值无非是往左走和往右走这两条路的较大者。这样,我们可以写出下面这个重要的递归函数:(这里i表示行,j表示列)

int f(int i, int j)
{
	if (i == n - 1) return a[i][j];
	return a[i][j] + max(f(i + 1, j), f(i + 1, j + 1));
}

但是,当我们编完整个程序并通过了样例后,提交后却返回了一串鲜红的文字:


What happened?

我们仔细分析程序,发现当我们从(0,0)往下走时,路越走越多,和细胞分裂一样,最终的路大约有2^n条!

这实在是太吓人了,当我们回过神来,程序早已崩溃(爆栈)。

冷静点,孩子。让我们再仔细地看看所给的数据吧,嗯,一个三角形大小的数据,准确地说,是n(n+1)/2个数。

按常理来说,实际的路也只有约n^2条,怎么可能会产生指数级别的路径数呢?

没错,重复。

重复的计算造成了程序的最终崩溃,而减少重复的计算是程序优化的一个永恒的主题。

那么,重复在哪里,如何减少重复的计算?

再来看看题目所给的样例,我们把目光放在1这个点上:我们可以从7-3-1到达1,继续往下面算,也可以从7-8-1到达1,继续往下面算。因此,在上面的代码中,在1这个点我们算了两遍!同样的遭遇也发生在1下面的那些点中。大量的重复计算最终导致了指数条路径的产生。

所以,我们不妨在计算一个点之前,先看看它有没有已经被计算出来。

怎么看?嗯……也许我们需要一个辅助的数组来保存计算的结果,比如这样:

int f(int i, int j)
{
	if (dp[i][j] >= 0) return dp[i][j];
	if (i == n - 1) return dp[i][j] = a[i][j];
	return dp[i][j] = a[i][j] + max(f(i + 1, j), f(i + 1, j + 1));
}
由于a[i][j]可能为0,所以需要在main()中把dp初始化为-1:

memset(dp, -1, sizeof(dp));
最终,程序返回了我们想看到的结果:



此题的完整代码见这篇文章

PS:关于打印路径的方法,见下面的第三部分。


二、递推与状态转移方程

在数据更大情况下,即使我们用了记忆化搜索,递归仍可能会爆栈。

为对付这种情况,需要逆向思考

注意到上面的return语句实际上可以写成这种形式:

dp[i][j] = a[i][j] + max(dp[i + 1][j], dp[i + 1][j + 1])

我们称其为状态转移方程。

如果在计算dp[i][j]时,dp[i + 1][j],和dp[i + 1][j + 1]都已经计算出来就好了,如何做到这一点呢?——从下往上计算。

for (i = n - 1; i >= 0; --i)
    for (j = 0; j <= i; ++j)
        if (i == n - 1) dp[i][j] = a[i][j];
        else dp[i][j] = a[i][j] + max(a[i + 1][j], a[i + 1][j + 1]);
最后dp[0][0]就是我们想要的结果。

但是注意到数组a本身就具有状态转移的性质,所以上面的代码可以继续化简为

for (i = n - 2; i >= 0; --i)
    for (j = 0; j <= i; ++j)
        a[i][j] += max(a[i + 1][j], a[i + 1][j + 1]);
最后a[0][0]就是我们想要的结果。


此题的完整代码见这篇文章


三、打印路径

在进行状态转移时(上面代码中的比较大小部分),如果附带记录下转移的来源,最后通过递归即可完成路径的打印(见代码)。


递归的写法:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 355;

int a[maxn][maxn], dp[maxn][maxn], n;
pair<int, int> path[maxn][maxn];

int f(int i, int j)
{
	if (dp[i][j] >= 0) return dp[i][j];
	if (i == n - 1) return dp[i][j] = a[i][j];
	if (f(i + 1, j) > f(i + 1, j + 1))
	{
		dp[i][j] = a[i][j] + f(i + 1, j);
		path[i][j] = make_pair(i + 1, j);
	}
	else
	{
		dp[i][j] = a[i][j] + f(i + 1, j + 1);
		path[i][j] = make_pair(i + 1, j + 1);
	}
	return dp[i][j];
}

void output(int i, int j)
{
	if (i == n - 1)
	{
		printf("%d %d\n", i, j);
		return;
	}
	printf("%d %d\n", i, j);
	output(path[i][j].first, path[i][j].second);
}

int main()
{
	int i, j;
	scanf("%d", &n);
	for (i = 0; i < n; ++i)
		for (j = 0; j <= i; ++j)
			scanf("%d", &a[i][j]);
	memset(dp, -1, sizeof(dp));
	printf("%d\n", f(0, 0));
	output(0, 0);
	return 0;
}

递推的写法:

#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 355;

int a[maxn][maxn], n;
pair<int, int> path[maxn][maxn];

void output(int i, int j)
{
	if (i == n - 1)
	{
		printf("%d %d\n", i, j);
		return;
	}
	printf("%d %d\n", i, j);
	output(path[i][j].first, path[i][j].second);
}

int main()
{
	int i, j;
	scanf("%d", &n);
	for (i = 0; i < n; ++i)
		for (j = 0; j <= i; ++j)
			scanf("%d", &a[i][j]);
	for (i = n - 2; i >= 0; --i)
		for (j = 0; j <= i; ++j)
		{
			if (a[i + 1][j] > a[i + 1][j + 1])
			{
				a[i][j] += a[i + 1][j];
				path[i][j] = make_pair(i + 1, j);
			}
			else
			{
				a[i][j] += a[i + 1][j + 1];
				path[i][j] = make_pair(i + 1, j + 1);
			}
		}
	printf("%d\n", a[0][0]);
	output(0, 0);
	return 0;
}

四、更多类型的DP(请在博客中搜索)

LCS
LIS
DAG上的最长路
整数拆分
区间DP


五、后续内容

背包、树形DP、概率DP、状态压缩

由于0-1背包和完全背包经常与其他DP相组合,具有较强的综合性,我把相关内容放在后续的中级篇进行介绍,敬请期待……


****转载请注明:http://blog.csdn.net/synapse7/article/details/16922779****

  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值