(一)前言
线性结构是最常见也是最重要的一种数据结构,N个数据元素以有序的方式排列。访问线性结构一般采用由前至后的遍历方法。线性动态规划就是在线性数据的基础上,通过某种递推方式(状态转移方程)得到最终结构的一种规划算法。这是最简单也是最基础的动态规划算法,一般可分为一维线性规划或二维线性规划两大类。
(二)动态规划的概念
动态规划英文原词为dynamic programming,规划一般就是指“求解最优”。规划问题并不是转化为“解方程组”的求解问题,而是把规划问题视为一个多阶段的决策问题,每个阶段的最佳状态作为下一个阶段的基础。
每次决策依赖于当前状态,决策后又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的。这种多阶段最优化决策解决问题的过程就称为动态规划。
(三)动态规划算法的设计
要培养一种递归性思考问题的方式。对于问题规模N的问题,先思考下如果N=1是否有解,然后借助递归思想,去尝试能否通过比N规模小的问题集合中推导出N的结果。由于是线性结构,如果我们界定N的问题是区间[1,N]的话,比N规模小的问题区间[1,X]是区间[1,N]的一部分,也就是上面提及的子问题。动态规划简言之就是通过计算子问题的解,最终得到问题N的解。
设计一个动态规划算法,有以下四个步骤:
1、刻画一个最优解的结构特征。(最优解是什么?多数情况是线性序列的一个子序列)
2、递归地定义最优解的值。(子序列元素间限定或特殊关系,递归思想发现求解规律)
3、计算最优解的值,通常采用自底向上的方法。(定义状态转移方程,就是如何推导出dp[i])
4、利用计算出的信息构造一个最优解。(问题的最优解不一定是最后一个元素的解,思考下)
(四)线性动态规划
本部分通过一些例题展示动态规划算法的四个步骤。
例题一:8596 最长上升子序列(优先做)
序列 (1, 7, 3, 5, 9, 4, 8) 就有许多个上上子序列,比如(1, 7), (3, 4, 8) 等。
所有这些上升子序列中最长的长度为4,比如 (1, 3, 5, 8).
(1)最优解结构特征。要得到子序列,序列元素不但升序,且元素数量最多。
(2)递归最优解的值。假定a[i]是上升序列最后一个元素,那么a[i]一定值最大,原始序列中在其左侧且值比其小的元素都可以作为其前驱。设定dp[i]为以a[i]为最后一个元素的最长上升子序列长度,那么可以由其左侧比a[i]小的元素的最长上升子序列,再加上a[i]得到新的上升子序列,找到一个最长的,即为dp[i]。
(3)计算最优解的方法。怎么计算dp[i]呢,根据上述分析,状态转移方程
dp[i]=max(dp[k])+1 (1<=k<=i-1&&a[i]>a[k]),注意dp[i]至少为1。
(4)怎么得到最优解。从样例就能发现最长上升子序列最后一个元素不一定是整个序列最后一个元素,因此在计算dp[i](最优值)时顺便统计下最优解。算法复杂度O(n^2)
#include <iostream>
using namespace std;
int main()
{int i,j,n,dp[100005],a[100005];
while(cin>>n&&n)
{
int ans=0;
for(i=1; i<=n; i++)
cin>>a[i],dp[i]=1;
for(i=1; i<=n; i++)
{
for(j=i+1; j<=n; j++)
{/**< a[i]<a[j],状态转移方程的实现 */
if(a[i]<a[j]&&dp[j]<dp[i]+1)
dp[j]=dp[i]+1;
}
ans=max(ans,dp[i]);
}
cout<<ans<<endl;
}return 0}
--------------------------------------------------------------------------------------------------------------------------
例题二:17098 广告牌最佳安放问题
有一条路从西向东M公里,是一段旅行的公路。在这段公路上放置n块广告牌,广告牌的地点:x1,x2,...,xn。如果你放一块广告牌在地点xi,就能获得ri的收益(ri>0)。
该地公路局规定:两块广告牌不能小于或等于5公里。现在请你挑选并安排这些广告牌放置地点,使得你的总收益在公路局规定的限制下达到最大。
(1)最优解结构特征。还是得到一个子序列,子序列任意两个广告牌距离大于5,且总收益最大。
(2)递归最优解的值。假定x[i]是序列最后一个元素,其左侧和其距离超过5的元素都可以作为其前驱。设定dp[i]为以x[i]为最后一个元素的子序列的最大收益,那么dp[i]可以由其左侧距离超过5的所有元素的最大收益推导出来。
(3)计算最优解的方法。根据上述分析,状态转移方程
dp[i]=max(dp[k])+r[i] (1<=k<=i-1&&x[i]-x[k]>5),注意dp[i]至少为r[i]。
(4)怎么得到最优解。思考可得最大收益时最后一个广告牌不一定放广告,因此和上题一样可以在计算dp[i](最优值)时顺便更新下最优解。ans=max(ans,dp[i]);
#include <iostream>#include <cstring>#include <algorithm>
using namespace std;
int n,m,a[100005],b[100005],dp[100005];/**< dp[i],i位置放置最后一个广告牌 最大收益 */
int main()
{ /**< 本代码使用了一些小技巧 */
ios::sync_with_stdio(0),cin.tie(0);
int i,j,k;
cin>>m>>n;
for(i=1;i<=n;i++)
cin>>a[i];
a[n+1]=a[n]+6;/**< 额外添加一个a[n+1],这样设计,就不用计算dp[i]最大值了 */
for(i=1;i<=n;i++)
cin>>b[i],dp[i]=b[i];
/**< 这里有一个提升性能的小技巧,因为距离是5,而每个广告牌都是整数,所以如果两个广告牌
间隔数量超过11个广告牌,那么再往前计算就没有意义了,由此使得算法复杂度降为O(n) */
for(i=2;i<=n+1;i++)
for(j=i-1;j>=i-11&&j>=0;j--)
if(a[i]-a[j]>5)
dp[i]=max(dp[i],dp[j]+b[i]);
cout<<dp[n+1];
return 0;}
例题三:17099 周工作计划安排
你正在某信息公司管理一个项目工作团队,每周你必须选择一项工作让团队来承担。所有的工作分为高压的低压的两类:
(1)低压的,诸如,为小学生建立Web管理网站。低压的项目可以获得li万元。
(2)高压的,诸如,涉及国家秘密或大型商业机遇的项目。高压的项目可以获得hi万元。
且高压项目,必须在前一周用一周的时间来准备(前一周不做任何其他工作)。你作为项目管理者,每周,要选择一项高压的或低压的工作来做。
问题模型:给定一组值l1,l2,...,ln和h1,h2,...,hn,找出一个最大价值的最优计划。
(1)最优解结构特征。仍然是线性结构的一个子序列,子序列每个元素有两种状态,L和H(低压或高压),如果序列中某个元素是高压H,那么其前驱元素下标和其相差为2,也就是选择了a[i],那么就不能选择a[i-1]。
(2)递归最优解的值。假定i是最后一周,计算器最大值dp[i],如果第i周选择低压,前一周的最大值+本周Li,如果第i周选高压,前一周必须休息,前2周最大值+本周Hi
(3)计算最优解的方法。根据上述分析,状态转移方程
dp[i]=max(dp[i-1]+li,dp[i-2]+hi) ,注意边界,只有第二周才能开始做高压工作。
(4)怎么得到最优解。和前两个例题不同,显然最后一周的结果必为最优解。
#include <iostream>
using namespace std;
int n,low[10005],high[10005],dp[10005];
int main()
{ ios::sync_with_stdio(0),cin.tie(0);
int i,j;
cin>>n;
for(i=1; i<=n; i++)
cin>>low[i];
for(i=1; i<=n; i++)
cin>>high[i];
dp[1]=low[1];/**< 第一周只能低压工作 */
for(i=2; i<=n; i++)
dp[i]=max(dp[i-1]+low[i],dp[i-2]+high[i]);
cout<<dp[n];
return 0;}
(五)拓展
线性动态规划是最简单的动态规划问题,也是是理解动态规划算法基础。
读者可以自行参看以下题目,尝试按上述四个步骤分析和解决问题。