做了几道关于动态规划的例题,小结一下,以便以后回顾(ps:code和例题基本上来自于《算法入门经典》,只不过加入了自己的一些理解,有错误的地方,请见谅)
暴力求解(回溯)— 剪枝 — 动态规划,逐步减少对于问题的搜索力度,减少不必要的搜索,达到优化的目的。
动态规划和分治法的区别
可以用动态规划进行求解的问题的一些特点:
Ø 最优子结构
Ø 子问题重叠
Ø 边界
Ø 子问题独立
状态转移方程是动态规划问题的关键。
在实现动态规划的时候,通常采用用数组记录已经求得的子问题的结果,当再次访问到该节点时可以通过访问数组元素得到;在求解问题时通常会采用倒着求解的方法,当然也可以正着求解了。
通过例题加深题解:
数字三角形问题:有一个非负整数组成的三角形,第一行只有一个数,除了最下行之外每个数的左下方和右下方各有一个数。从第一行的数开始,每次可以往左下和右下各走一格,直到走到最下行,把沿途经过的数全部加起来,如何走才能使得这个和尽量大?
《算法竞赛入门经典》中是这样解释的:把当前的位置(I,j)看成是一个状态,然后定义状态(I, j)的指标函数d(I, j)为从格子(I,j)出发时能得到的最大和,包括格子(I, j)本身。
解为:d(0,0)
状态转移方程: d(I,j) = buf[i][j] + max(d(i+1,j),d(i+1, j+1))
可以采用递归的方式实现:
buf[][] //存储数字三角形中的数据
int dpfunc(int p, int q){
return buf[p][q] + (p == n ? 0 : ((tmp1 = dpfunc(p+1, q)) > (tmp2 > dpfunc(p+1, q+1)) ? tmp1 :tmp2));
}
采用记忆化搜索的方式:
buf[][] //存储数字三角形中的数据
d[][] //存储从i, j 出发到达最后一行所经过节点的最大值
int dpfunc(int p, int q){
ans = &d[p][q];
if(ans ) return ans;
return ans = buf[p][q]+ (p == n ? 0 : ((tmp1 = dpfunc(p+1, q)) > (tmp2 >dpfunc(p+1, q+1)) ? tmp1 : tmp2));
}
可以采用递推的方式实现, 结果为buf[0][0]:
void dpfunc(){
for(I = n - 1; I >= 0; --i)
for(j = 0; j < I; ++j){
if(buf[i+1][j] >= buf[i+1][j])
buf[i][j] += buf[i+1][j];
else
buf[i][j] += buf[i+1][j+1];
}
}
DAG(有向无环图)模型上的动态规划
有向无环图上的动态规划是学习动态规划的基础,很多问题都可以转化为DAG上的动态最长路、最短路或路径计数问题。
通过例题加深题解
例题1:嵌套矩形问题
问题描述如下:有n个矩形,每个矩形可以用两个整数a,b描述,表示它的长和宽,矩形X(a, b)可以嵌套在矩形Y(c, d)中,当且仅当a<c,b<d或者a<d,b<c,选择尽量多的矩形排成一行,使得除了最后一个之外,每一个矩形都可以嵌套在下一个矩形内。
将每个矩形都抽象成一个点,两个点之间邻接,则矩形之间存在嵌套关系,显然,是有向无环图。
状态转移方程为:d(i) = max(d(j)+1)
int dpfunc(int i){
int ans = &d[i];
if(ans) return ans;
ans = 1;
for(j = 0; j < n;++j) if(G[i][j] && ans <dpfunc(j)+1) ans = dpfunc(j)+1;
return ans;
}
例题2:硬币问题
问题描述如下:有n种硬币,面值分别为v1,v2,。。。vn,每种都有无限多,给定非负整数S,可以选用多少个硬币,使得面值之和恰好为S?输出硬币数目的最小值和最大值。1=<n<=100,0=<S<=10000。
状态转移方程为: d(i) = max(d(I – v(j))+1)
解法1:可以选用和上个例题一样的思路,但是可以不用邻接表存储状态之间的关系,将当前面值和x抽象为点,则点集合为0,1,。。。,s,当存在任意i(i<n)时,v[i]<x,则x与x-v[i]则有一条边。因此代码可以如下:
//仅给出求最小值的情况
int dpfunc(int x){
ans = &d[x];
if(ans != -1) return ans;
ans = 1<<30; //用于区别无解的情况
for(I = 0; I < n;++i) if(v[i] <= x && ans > dpfunc(x-v[i])+1) ans=dpfunc(x-v[i])+1;
return ans;
}
d[0] = 0
解法2:可以采用递推的方法,解为max[s],min[s]
void dpfunc(){
for(I = 1; I <= s;++i){max[i] = -INF; min[i]= INF;}
min[0] = 0; max[0] = 0;
for(I = 1; I <= s;++i)
for(j = 0; j < n; ++j){
if(I >= v[j]){
min[i] = min[i]> min[i-v[j]]+1 ? min[i] : min[i-v[j]]+1;
max[i] = max[i]< max[i-v[j]]+1 ? max[i] : max[i-v[j]]+1;
}
}
}
例题3:0-1背包问题
有n种物品,每种只有一个,第i个物品的体积为vi,重量为wi,选一些物品装到一个容量为C的背包,使得背包内物品在总体积不超过C的前提下重量尽量大。1=<n<=100, 1=<C<=10000,1=<wi<=1000000.
状态转移方程:d[i][j] = max(d[i-1][j-1], d[i-1][j-v[i]] + w[i]);
解法1:这个似乎不能理解为DAG模型问题,解为d[n][c]代码如下:
v[];
w[];
d[][]; //存储背包总体积不超过j的前提下i个物品所能盛放的最大重量
void dpfunc(){
memset(&d[0][0],0, maxn * sizeof(int));
for(I = 1; I <=n; ++i)
for(j = 0; j <=c; ++j){
d[i][j] = d[i-1][j];
if(j >= v[i] && d[i][j] <d[i-1][j-v[i]]+w[i]) d[i-1][j-v[i]]+w[i];
}
}
和例题2比较,可能会对动态规划有更近一步的认识,虽然两个问题都可以用递推的方式解决问题,但是可以看到外层循环和内层循环是不一样的;0-1背包问题在计算的时候讲究计算的层次或者说是计算的次序、顺序。
解法2:运用滚动数组的形式,将二维数组化为一维数组,解为d[c], 代码如下:
v;
w;
d[];
void dpfunc(){
memset(d, 0, sizeof(d));
for(I = 1; I <= n;++i){
scanf(“%d%d”,&v, &w);
for(j = c; j >=v;--j)
if(d[j] < d[j-v]+ w) d[j] = d[j-v] + w;
}
}