动态规划通常应用于最优化问题,即要做出一组选择以达到一个最优解。在做选择的同时,经常出现同样形式的子问题。当某一特定的子问题可能出自于多于一种选择的集合时,动态规划是很有效的;关键技术是存储每一个子问题的解,以备它重复出现。利用这种简单思想,可将时间复杂度从指数级别降低到多项式级别。
贪心算法通常也是应用于最优化问题。在这种算法中,要做出一组选择以达到一个最优解。该算法的思想是以局部最优的方式来做每一个选择。对许多问题来说,采用贪心法可以比动态规划更快地给出一个最优解。但是,不容易判断贪心法的正确性。可用回顾拟阵理论进行判断。
贪心算法与动态规划有一个显著的区别,在于使用最优子结构的顺序,前者是自顶向下。贪心算法会先做选择,在当时看起来是最优的选择,然后再求解一个结果子问题,而不是先寻找子问题的最优解,然后再做选择。
动态规划是通过组合子问题的解而解决整个问题的。动态规划适用于子问题不是独立的情况,即各子问题包含公共的子子问题。动态规划算法对每个子子问题只求解一次,将其结果保存在一张表中,从而避免重复计算子问题。
动态规划通常应用于最优化问题。此类问题可能有很多种可行解。每个解有一个值,而我们希望找出一个具有最优(最大或最小)值的解。称这样的解为该问题的“一个”最优解(而不是“确定的”最优解),因为可能存在多个取最优值的解。
算法设计步骤:
1)描述最优解的结构。
以正序扩展可见区域,考虑从起点到区域边界的最优解。
定义问题,假设其包含了子问题并加以证明。
定义子问题,利用子问题的最优解来构造原问题的一个最优解。
2)递归定义最优解的值。
列出包含边界情况的递归公式(形式类似于分段函数)
3)按自底向上的方式计算最优解的值。
方向由递归公式确定。
4)由计算出的结果构造一个最优解。
第1~3步构成问题的动态规划解的基础。第4步在只要求计算最优解的值时可以略去。如果的确做了第4步,则有时要在第3步的计算中记录一些附加信息,使构造一个最优解变得容易。
适合采用动态规划方法的最优化问题的两个要素:最优子结构和重叠子问题。注意子问题要互相独立,即不共享资源。
如果一个问题的一个最优解中包含了子问题的最优解,则该问题具有最优子结构。在这种情况下,贪心也可能适用。
寻找时遵循的模式:
1)问题的一个解可以是做一个选择,做这种选择会得到一个或多个有待解决的子问题。
2)假设对一个给定的问题,已知一个可以得出最优解的选择,不必关心如何确定这个选择。
3)在已知这个选择之后,确定哪些子问题会随之产生,以及如何最好地描述所得到的子问题空间。规则是,尽量保持这个空间简单,然后在需要时再扩充它,在合适的情况下没有必要再去尝试一个更具一般性的子问题空间。
4)利用“剪贴”技巧来证明在问题的一个最优解中,使用的子问题的解本身也必须是最优的。假设子问题的解不是最优,通过“剪除”非最优解再“贴上”最优解,就证明了可以得到原问题的一个更好的解,因此,这与得到的是最优解的假设矛盾。
最优子结构在问题域中以两种方式变化:
1)要在多少种选择中选出最优。
2)每种选择使用的子问题数量。
动态规划以自底向上的方式来利用最优子结构。也就是说,首先找到子问题的最优解,解决子问题,然后找到问题的一个最优解。
当一个递归算法不断地调用同一问题时,称该最优问题包含重叠子问题。子问题的空间要“很小”,也就是用来解原问题的递归算法可反复地解同样的子问题,而不是总在产生新的子问题。动态规划使用表格把时间复杂度从指数级别降低到多项式级别。
有一个形如金字塔的数塔,从顶层走到底层,若每一步只能走到相邻的结点,求经过的结点的数字之和的最大值。
最优子结构: 该问题(找出从顶点出发的最大路线)的最优解包含了子问题(找出从与顶点相邻的点出发的最大路线)的一个最优解。
利用子问题的最优解来构造原问题的一个最优解: 从顶点出发的路线必定经过从与顶点相邻的点。因此,经过顶点的最大路线只能是二者之一:从顶点出发走到点(2,1),走从点(2,1)出发的最大路线;从顶点出发走到点(2,2),走从点(2,2)出发的最大路线。
递归定义最优解的值: 选择从(i,j)出发的最大路线的问题作为子问题,层编号i=1,2,…,n。第i层的列编号j=1,2,…,i。令t[i][j]
表示从点(i,j)出发可能得到的最大值。
我们的最终目标是确定从(1,1)出发得到的最大值。当i==n时,路线仅包含自身的点。递归公式:
t [ i ] [ j ] = { t [ i ] [ j ] + m a x ( t [ i + 1 ] [ j ] , t [ i + 1 ] [ j + 1 ] ) , 1 ≤ i < n t [ i ] [ j ] , i = n t[i][j]=\left\{\begin{aligned} t[i][j]+max(t[i+1][j],t[i+1][j+1]),\quad 1\le i
对于1<=it[i+1][j]和t[i+1][j+1]
的值。通过以递减i的顺序来计算t[i][j]
的值。
从上往下,如果每次选择的子结点对应的子塔数值最大,则父塔数值一定最大。
【法一】递归 将父结点对应的最优解转化为子结点对应的最优解。另设数组存储子塔最大值,减少重复计算。
【法二】递推 从下往上,算出各级子塔的最优解。
【法一】递归
#include
#include
using namespace std;
int max(int x,int y)
{if(x>y) return x;
return y;}
int n,t[105][105],m[105][105];//m存储子塔最大值
int dfs(int i,int j){
if(m[i][j]>=0) return m[i][j];
if(i==n-1) return m[i][j]=t[i][j];
return m[i][j]=t[i][j]+max(dfs(i+1,j),dfs(i+1,j+1));
}
int main(){
int c;
cin>>c;
while(c--){
int i,j;
cin>>n;
for(i=0;i<n;i++)
for(j=0;j<=i;j++)
cin>>t[i][j];
memset(m,-1,sizeof(m));//-1表示未扩展
cout<<dfs(0,0)<<endl;
}
return 0;
}
【法二】递推
#include
using namespace std;
int max(int x,int y)
{if(x>y) return x;
return y;}
int main(){
int c,t[105][105]={0};
cin>>c;
while(c--){
int n,i,j;
cin>>n;
for(i=0;i<n;i++)
for(j=0;j<=i;j++)//注意等号
cin>>t[i][j];
for(i=n-1;i>=0;i--)
for(j=0;j<=i;j++)
t[i][j]+=max(t[i+1][j],t[i+1][j+1]);
cout<<t[0][0]<<endl;
}
return 0;
}
有一种跳棋游戏,棋子在棋盘上呈线性排列。除起点与终点外,每个棋子上都标着一个正整数。玩家必须从起点跳到终点,在途中可以访问棋子,但每次只能向前访问更大的棋子,也可以不访问棋子。玩家在途中访问的棋子的数值之和就是最终得分。求最大得分。
描述最优子结构: 该问题(找出从1出发能得到的最大和)的最优解包含了子问题(找出从1右边的点出发能得到的最大和)的一个最优解。 【注】由于起点与终点具有对称性,从终点跳到起点得分是相同的,可以从前往后递推。 有一天gameboy正走在回家的小径上,忽然天上掉下大把的馅饼。将小径视为数轴上一条线段,最小值为0,最大值为10,馅饼都掉在线段上的整数点,gameboy只能在线段上移动,0秒时他处于整数5的位置,每秒钟最多移动1米。求他最多可以接到的馅饼个数。 本问题可画出与2084 数塔类似的塔状结构,表示玩家移动的路径。 有一个骨头收藏家带着一个容积为V的背包来到墓地收集骨头,每个骨头体积不同,价值也不同,求他能带走的骨头的价值总和的最大值。 01背包原题 贪心解法错误的原因:对象量子性。由于对象不可分割,所以背包不一定装满。 从n件物品中选2*k件搬到另一个地方,每次搬一对。现定义一次搬运产生的疲劳度为这对物品的重量之差的平方。给定k及其他数据,求完成所有搬运任务后累计的疲劳度的最低值。 可以证明,当每次提的物品重量相邻时,疲劳度最小。因此,首先对输入数据进行排序。 描述最优子结构: 该问题(在前i个物品中选择j对的最低疲劳度)的最优解包含了子问题(在前i-1个物品中选择j对的最低疲劳度 与 在前i-2个物品中选择j-1对的最低疲劳度)的一个最优解。证明:令 递归公式:设m[i]为排序后第i个物品的重量, 递归定义最优解的值: 给出一些砖块,堆叠砖块建塔。砖块有n种类型,每种类型数量不限,形状为长方体,且可以改变放置方向。如果上层砖块的长、宽必须小于下层砖块的长、宽,求塔的最大高度。 而本问题则要求满足塔中面积相邻的两块砖之前满足一定关系,即上层砖比下层砖小。因此,最好先进行排序,按顺序进行建塔。然而,排序的操作对象只能是有限个数量。在这里要看出陷阱:同一类型、同一摆放位置的砖块只能使用一次。1种砖块有3个维度,对应6种摆放方式,等效看成6种砖。注意,本题中与问题有关的参数是子问题空间,与01背包中允许选择的物品数量类似。 最优子结构: 将1→6扩充后的各砖块先按长度,后按宽度进行排序。问题(前i个砖能构成的塔的最大高度)的最优解包含了子问题(前j(j 利用子问题的最优解来构造原问题的一个最优解: 把问题涉及到一个子问题,该子问题需要在所有j 递归定义最优解的值: 选择在状态(i)下的最优解的问题作为子问题,子问题空间i=1,2,…,len。(len为砖块总数)。注意dp[i]不一定是递增的,所以我们的最终目标是确定所有状态下的最优的解。 递归公式: n头奶牛站成一列(1≤n≤80000),一只奶牛a可以看到前面比它矮的奶牛,但不能看到比它高或等的奶牛b及b之后的奶牛。输入奶牛的高度,输出每只奶牛可以看到的奶牛数量之和。 奶牛的高度形成一个数列。 输入包含多组数据,每组为一个老鼠序列,包含每只老鼠的体重与速度信息,输出长度最大的序列的长度及老鼠编号(其在原序列中出现的顺序),满足体重递增、速度递减。 将输入数据首先按体重递增排序,其次按速度递减排序,这样,按顺序遍历各老鼠时,体重递增的要求自动满足。题目可抽象为寻找一个最长递减子序列。 给定一个序列X= LCS的最优子结构 不要妄想一步登天。“子序列”本身也是一种数据集合,因此其可能存在最优子结构。不要一开始就想着解的最优子结构。 如果一个数的因数只有2,3,5,7(含它们的倍数),则称这个数为丑数。输入正整数n(1 <= n <= 5842),输出第n个丑数的值。 第一个丑数为“1”,后面的每一个丑数都是由已经生成的丑数乘2、3、5或7而来,那么后一个丑数就是已经生成的丑数乘2、3、5或7得到的最小值
利用子问题的最优解来构造原问题的一个最优解:
递归公式:设data[i]为第i个棋子的数值,则
dp[i]= data[i]+maxi程序
#include
HDOJ-1176 免费馅饼
题目
思路
易发现其与原始数塔的不同之处:多了辐射方向,第7层(第6秒)开始,层宽度不再增加。
描述最优子结构: 该问题(找出在第0秒从5出发的最大路线)的最优解包含了子问题(找出在第0秒从与5相邻的点出发的最大路线)的一个最优解。因此,在第0秒从顶点5出发的最大路线只能是三者之一:从顶点出发走到4,走在第1秒从4出发的最大路线;在5停留1秒,走在第1秒从5出发的最大路线;从顶点出发走到6,走在第1秒从6出发的最大路线。
利用子问题的最优解来构造原问题的一个最优解: 在求解状态(i,j)之前,dp[i][j]
存储第i秒在j位置落下的馅饼总数,求解之后表示第i秒从j出发得到的最大值。
递归公式:设tm为馅饼落下的最晚时刻,
i=tm时,dp[i][j]=dp[i][j]+0
;
0dp[i][j]=dp[i][j]+
{ m a x ( d p [ i + 1 ] [ 0 ] , d p [ i + 1 ] [ 1 ] ) , j = 0 m a x ( d p [ i + 1 ] [ j − 1 ] , d p [ i + 1 ] [ j ] , d p [ i + 1 ] [ j + 1 ] ) , 1 ≤ j ≤ 9 m a x 2 ( d p [ i + 1 ] [ 9 ] , d p [ i + 1 ] [ 10 ] ) , j = 10 \left\{\begin{aligned} max(dp[i+1][0],dp[i+1][1]),\quad j=0\\ max(dp[i+1][j-1],dp[i+1][j],dp[i+1][j+1]),\quad 1 \le j \le 9\\ max2(dp[i+1][9],dp[i+1][10]), \quad j=10\\ \end{aligned}\right. ⎩ ⎨ ⎧max(dp[i+1][0],dp[i+1][1]),j=0max(dp[i+1][j−1],dp[i+1][j],dp[i+1][j+1]),1≤j≤9max2(dp[i+1][9],dp[i+1][10]),j=10
以资源换代码复杂度: 从数塔可以看出,在6层以上,某些状态是没有意义的,如(4 的顺序来计算。代码
#include
HDOJ-2602 Bone Collector
题目
思路
**最优子结构:**该问题(确定i个物品装到容积为j的背包产生的最大价值)的最优解包含了子问题(确定i-1个物品装到容积为j的背包产生的最大价值)的一个最优解。证明:令dp[i][j]
表示i个物品装到容积为j的背包产生的最大价值。从i-1到i,多了一种骨头可供选择,可选择替换或不替换背包中原有的一个。若不替换,则dp[i][j]=dp[i-1][j],dp[i-1][j]
必须是最优的;若替换,须保证替换后取得更优解,则dp[i-1][j]
必须是最优的。从j-1到j,背包容积增加1,可选择替换或不替换背包中原有的一个。若不替换,则dp[i][j]=dp[i-1][j],dp[i-1][j]
必须是最优的;若替换,须保证替换后取得更优解,则dp[i-1][j]
必须是最优的。
利用子问题的最优解来构造原问题的一个最优解: 把问题转化为一个子问题和在子问题基础上做出的决策。在必须进行替换的情况下,最小代价是背包腾出新物品所需空间。至于为什么是dp[i-1][j-vo[i]]
而不是dp[i][j-vo[i]]
,是因为dp[i][j-vo[i]]
考虑了物品i。
**递归定义最优解的值:**选择在状态(i,j)下的最优解的问题作为子问题,物品数量i=1,2,…,n。背包容积j=0,1,2,…,v。
我们的最终目标是确定在状态(n,v)下的最优解。当i=0时,dp[i][j]=0
。递归公式:
{ 0 , i = 0 m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v o [ i ] ] + v a [ i ] ) , i ≥ 1 \left\{\begin{aligned} 0,\quad i=0\\ max(dp[i-1][j],dp[i-1][j-vo[i]]+va[i]), \quad i \ge 1\\ \end{aligned}\right. {0,i=0max(dp[i−1][j],dp[i−1][j−vo[i]]+va[i]),i≥1
对于i>=1,dp[i][j]
的每一个值仅依赖于dp[i-1][j]和dp[i-1][j-vo[i]]
的值。因此,先有dp[i-1][x]后有dp[i][x]
,通过以递增i的顺序来计算dp[i][j]
的值,j的值变化顺序任意。
空间复杂度优化:新计算出的dp[i][x]可以直接覆盖dp[i-1][x]
,则dp[j]=
{ 0 , i = 0 m a x ( d p [ j ] , d p [ j − v o [ i ] ] + v a [ i ] ) , i ≥ 1 \left\{\begin{aligned} 0,\quad i=0\\ max(dp[j],dp[j-vo[i]]+va[i]), \quad i \ge 1\\ \end{aligned}\right. {0,i=0max(dp[j],dp[j−vo[i]]+va[i]),i≥1
在计算dp[j]的时候需要引用dp[j-vo[i]]的值,因此以递减j的顺序来计算dp[j]的值,以避免dp[j-vo[i]]在被引用之前变化。代码
#include
HDOJ-1421 搬寝室
题目
思路
dp[i][j]
表示在前i个物品中选择j对的最低疲劳度。从i-1到i,多了一种物品可供选择,可选择替换或不替换原有的一对。若不替换,则dp[i][j]=dp[i-1][j],dp[i-1][j]
必须是最优的;若替换,须保证替换后取得更优解,则dp[i-2][j-1]
必须是最优的。
利用子问题的最优解来构造原问题的一个最优解: 一般来说,i每增加2,j的最大取值便可增加1。由于j的最大取值会变化,将dp[i][j]
(j*2>i)记为无穷大,表示不可用。
d p [ i ] [ j ] = { 0 , i = 0 或 j = 0 m i n ( d p [ i − 1 ] [ j ] , d p [ i − 2 ] [ j − 1 ] + ( m [ i − 1 ] − m [ i ] ) 2 ) , i ≥ 2 dp[i][j]=\left\{\begin{aligned} 0,\quad i=0或j=0\\ min(dp[i-1][j],dp[i-2][j-1]+(m[i-1]-m[i])^2 ), \quad i \ge 2\\ \end{aligned}\right. dp[i][j]={0,i=0或j=0min(dp[i−1][j],dp[i−2][j−1]+(m[i−1]−m[i])2),i≥2dp[i][j]的每一个值仅依赖于dp[i+1][j-1],dp[i+1][j],dp[i+1][j+1]
的值。通过以递增i的顺序来计算。代码
#include
HDOJ-1069 Monkey and Banana
题目
思路
dp[i]=h[i]+max(0,dp[j]) (1<=j dp[i]的每一个值仅依赖于dp[j]的值。因此,通过以递增i的顺序来计算dp[i]的值。代码
#include
USACO 2006 November Silver - Bad Hair Day
题目
思路
“俯视序列”:对于给定的i
i的俯视序列的最大值c[i]具有最优子结构:如果后面的牛i比前面j高,那么前面能看到的后面也能看到。最大俯视序列一直到比i高的牛为止。如果前面的牛能看到底,c[i]=c[j]+1;如果只能看到一部分,那么求出j的最大俯视序列,从该序列末尾后一个继续判断,c[i]为所有最大俯视序列之和。代码
#include
HDOJ-1160 FatMouse’s Speed
题目
思路
考虑到子序列中的元素是离散分布在原系列当中的,题目又要求输出序列元素,可采取链表式的结构存储信息。一个结点中有2个成员,len表示子串最大长度,next表示下一项编号,将一项添加到链表末尾称为“连接”,其操作为:记该项的len为末项len+1,该项的next为末项编号。
dp[i].len=1+0,m[i].s>m[i+1].s1+maxi+1代码
#include
HDOJ-1159 Common Subsequence
题目
思路
设X=
1)如果x[m]=y[n],那么z[k]=x[m]=y[n]而且Z(k-1)是X(m-1)和Y(n-1)的一个LCS。
2)如果x[m]≠y[n]且z[k]≠x[m],那么Z是X(m-1)和Y的一个LCS。
3)如果x[m]≠y[n]且z[k]≠y[n],那么Z是X和Y(n-1)的一个LCS。
两个维度:序列X的长度和序列Y的长度。
一个递归解
在找X=
定义c[i,j]为序列X(i)和Y(j)的一个LCS的长度。
c[i][j]
=0,i=0或j=0ci-1,j-1+1,i>0且j>0且xi=y[j]max(ci,j-1,ci-1,j),i>0且j>0且xi≠y[j]参考代码
#include
总结
善用代数工具,学会抽象定义:两个序列的最长子序列也是序列HDOJ-1058 Humble Numbers
题目
思路
第一个:1
第二个:1*2,1*3,1*5或1*7
中最小为2,2已乘过第1个丑数,更新为2与第2个丑数相乘
第三个:2*2,1*3,1*5或1*7
中最小为3,3已乘过第1个丑数,更新为3与第2个丑数相乘
第四个:2*2,2*3,1*5或1*7
中最小为4,2已乘过第2个丑数,更新为2与第3个丑数相乘
……参考代码
# include