学习动态规划DP(一)——DAG模型

之前初学了一点关于动态规划的知识,但没有系统的学习,最近在空闲时间根据紫书(算法竞赛入门经典)开始了比较有计划的学习,先写下这篇博客,作为笔记。

一、我对动态规划的看法。
动态规划,即是把原问题划分为各个规模更小的问题去解决,原问题的最优解包括了各个子问题的最优解(感觉本质上是分治法)。所以动态规划用于求解最优值,解题过程中最重要的是确定好状态转换方程。对于动态规划的学习,我觉得需要知道的基础概念不多,就是了解了动态规划的想法,然后就是做题训练了,从题目中学习DP才比较有效率。

二、一般的解题思路、步骤。
1、定义状态:一个状态遵循什么规则需要我们自己定义,我们可以通过定义不同的规则来定义不同的状态,但要写出每一个状态的转移方程的难度不同,所以需要我们有目的的筛选,这就需要从题目训练中学习。通过量的累积达到质的变化。
2、判断一个状态有几种可能的决策:在遵循自己对状态所定义的规则的条件下,确保对条件的列举不存在缺漏的情况下,列举出某一状态下可能出现的不同决策,即各种可能的状态转变方向。
3、根据不同的决策列出对应的状态转移方程:状态转移方程需要确保可以得到最优的结果并且对各种可能出现的情形没有漏举的情况。

三、如何定义状态
做了一些题,我发现对状态的定义,我们都是用数组来表示,可能是一维数组,也可能是多维数组。数组的维度根据不同的题目要求而定,有时可以有多解,就是可以用不同维度的数组来解题,但这种情况下一般多维的解题数组可以降维到最简。下面还是通过一些基础、经典的例题来说明。

四、DP例题(一下两题都是紫书的,暂时找不到具体来源)
DAG模型(DAG——有向无环图):即是把动态规划的题目的解题转为有向无环图去解题。
1、矩形嵌套
题目描述:有n个矩形,每一个矩形可以用两个整数a、b描述表示长和宽,如矩形X(a,b)。矩形可以90°旋转。当一个矩形的长和宽都分别严格小于另一个矩形的长和宽时,该矩形可以嵌套在另一个矩形之中。如(1,5)可以嵌套在(2,6)中,但不可以嵌套在(3,4)中。要求选出尽量多的矩形排成一行,使得除了最后一个之外,每一个矩形都可以嵌套在下一个矩形之内。如果有多解,矩形编号的字典序应该尽量的小。

分析:一个矩形要嵌套在另一个矩形中需要有一点的条件(前一个矩形较长的一条边小于后一个矩形的较长的一条边,以及前一个矩形较短的一条边小于后一个矩形的较短的一条边),这样可以把前后两个矩形看成两个不同的节点,前一个矩形嵌套在后一个矩形里,看成有一条从后一个矩形指向前一个矩形的有向边。这样就可以把问题转换为求解有向图的最长路且这个有向图是无环的,即DAG。
要求最长路,我们可以定义一个状态,如 d[i] 表示从结点 i 出发的最长路长度,这样的话可以求出,状态转移方程为 d[i] = max(d[i],d[j]+1),其中 j 是结点 i 的下一个结点,即存在一条有向边从 i 指向 j。
为什么状态转换方程是这样呢?首先在每一个当前状态下,我们只有一种决策可行,就是前往下一个,即使下一个点有多种选择,但本质上都一样,就是要前往下一个点。然后将当前状态和下一个状态比较,d[i]表示从结点 i 出发的最长路径,因为存在从 i 到 j 的有向边,所以如果从结点 j 出发的最长路径加上从 i 到 j 之间的路径的和得到的新的路径比原来从结点 i 出发的最长路径还长,那么我们就更新从结点 i 出发的最长路径长度,也就得出了上面的状态转移方程。因为每一个结点我们都和与它相连的结点相比较,所以最终得出的是最优结果。
(关于状态的选取,我觉得还是需要多做题从题目中学习,以后自己做的时候才会有比较清晰的思路)

代码:

#include 
#include 
#include 
using namespace std;

const int MAXN=1e3+5;
// g[i][j]为邻接矩阵,表示一张图,这张图上有从 i 到 j 的有向边
// d[i]为状态数组
int g[MAXN][MAXN],d[MAXN],n;

//记录矩形的信息
struct Rec
{
	int x,y;
} rec[MAXN];

int DP(int i)
{
	//记忆化搜索,如果该点已经计算过就不用再算,防止重复运算,在一些数据大的题中可能会导致超时
	if(d[i]>0) return d[i];
	for(int j=1; j<=n; j++) //遍历i的所有可达出边
		if(g[i][j]) //判断是否有从 i 到 j 的有向边
			//注意max(,)中后者不是 d[j]+1,而是DP(j)+1,因为这时候 d[j]可能还未计算,所以我们用是的DP(j),
			//因为DP函数的作用就是要来求出 d[j] 的值的。
			d[i]=max(d[i],DP(j)+1);
	return d[i];
}
//按最小字典序输出最优决策下的矩形编号
//选取最大的 d[i] 对应的 i ,如果有多个 i,则选择最小的i 才能保证字典序最小 
void print(int i)
{
	cout<>m;//例子数
	while(m--)
	{
		memset(d,0,sizeof(d));
		memset(g,0,sizeof(g));
		cin>>n;
		for(i=1; i<=n; i++)
			cin>>rec[i].x>>rec[i].y;
		// 确定哪两个点之间有边连接
		for(i=1; i<=n; i++)
			for(j=1; j<=n; j++)
				if(i!=j)
					g[i][j]=check(rec[i],rec[j]);
		DP(1);
		int ans=-1,dex;
		for(i=1; i<=n; i++)
			if(ans

2、硬币问题
题目描述:有 n 种硬币,面值分别为V1,V2,…,Vn,每种都有无限多,给定非负整数S,可以选用多少个硬币使得面值之和恰好为S?输出硬币数目的最小值和最大值。1<=n<=100,0<=S<=10000,1<=Vi<=S。

分析:这类型的题其实也可以用DAG模型去解题。如我们把每种面值看作一个点,表示“还需要凑足的面值”。比如 d[i] 中 i 表示还需要凑足 i 元,而整个 d[i]数组,我们可以表示凑足 i 需要多少硬币。故初始状态为 d[S],目标状态为d[0]。(初学时觉得很巧妙,用模型解题,思路会比较清晰)。
接下来确定状态转移方程。同样的,我们要明确在每一个状态下,下一种状态有多少中决策。因为我们有各种面值的硬币,所以把这些面硬币根据输入的顺序编号,选择面值小于我们当前所需面值的硬币来凑足我们需要的面值。比如我们使用了第 j 个硬币,那么d[i] 的下一个状态就是
d[ i - V[j] ] + 1。( +1 ,加上使用的面值为 V[j] 硬币),由于题目要求最大和最小数目,所以我们把当前状态与下一个状态比较,分别取最大值和最小值即可。因此,求最大值状态转移方程为:d[i] = max(d[i],d[i-V[j]]+1),最小值为:d[i] = min(d[i],d[i-V[j]]+1)。

下面给出求最大值,以及同时求最大和最小值的核心代码

//最长路
int dp_max(int s)// 第一个 s 是题目中的 S。 
{
	//记忆化搜索已经计算过的点不用重复计算
	if(vis[s])
		return d[s];
	//标记该结点已经访问过 
	vis[s] = 1;
	//因为题目中 S 可以为 0,所以初始将 d[i]设置为正常情况下取不到的负数。 
	d[s]=-1000;
	for(int i=1; i<=n; ++i)
		// 如果硬币面值小于目前还需要凑足的面值才进行下一步状态转换 
		if(s>=v[i])
			d[s] = max(d[s],dp_max(s-v[i])+1);
	return d[s];
}

//同时求最大最小值 
void max_min()
{
	// 先把数组初始化为不能取到的值,min 和 max 函数才有效
	memset(minv,INF,sizeof(minv));
	memset(maxv,-INF,sizeof(maxv));
	//如果需要凑足的面值为 0,那么当然不需要任何硬币 
	maxv[0] = minv[0] = 0; 
	//依次计算每一种面值需要的硬币数 
	for(int i=1; i<=s; ++i)
		for(int j=1; j<=n; ++j)
			if(i>=v[j])
			{
				// 当前状态 与 如果选择了当前的硬币 v[j] 后的状态比较
				// 因为对任意钱数,都是由已有的硬币凑成
				// 对任意钱数都选择最少或最多的组成,再往上凑即也是最少或最多
				maxv[i]=max(maxv[i],maxv[i-v[j]]+1);
				minv[i]=min(minv[i],minv[i-v[j]]+1);
			}
	cout<minv[i-v[j]]+1)
	{
		minv[i]=minv[i-v[j]]+1;
		min_coin[i]=j;
	}*/
}

小结:
个人感觉学习动态规划,涉及的知识挺多的,最好是一次学习一种题型,然后自己做好归纳。比如这类题型的状态一般怎么定义,可以用什么模型去做等。比如上面的两道例题都是跟DAG最长最短路有关,对于这种题型,一般可以定义两种状态:
(1)设d[i]为从 i 出发的最长路,则d[i]=max{d[j]+1,d[i]}((i,j)∈E)。
(2)设d[i]为以 i 结束的最短路,则d[i]=min{d[j]+1,d[i]}((j,i)∈E)。
然后就是刷题,题做多了,我感觉对动态规划的理解会自然而然的深。

你可能感兴趣的:(学习动态规划DP(一)——DAG模型)