动态规划学习笔记

在讲述DP算法的时候,一个经典的例子就是数塔问题,它是这样描述的:有如下所示的数塔,要求从顶层走到底层,若每一步只能走到相邻的结点,则经过的结点的数字之和最大是多少?
在这里插入图片描述
递归

int maxsum(int x,int y)
{
    if(x==n) return mp[x][y];
    int sum1=maxsum(x+1,y);
    int sum2=maxsum(x+1,y+1);
    if(sum1>sum2) return sum1+mp[x][y];
    return sum2+mp[x][y];
}
cout<<maxsum(1,1)<<endl;

结果:Time Limit Exceed!

分析:是由于进行了过多的重复计算
每次计算maxsum(x,y)的时候,都要去计算一次maxsum(x+1,y+1),而每次计算maxsum(x,y+1)的时候,也要计算一次maxsum(x+1,y+1),这就产生了重复计算。
在这里插入图片描述
从上图可以看出,最后一行的计算次数为16,倒数第二行为8.可以总结出,对于n行的三角形,计算次数总和为20+21+…+2(n-1)=2n-1 O(2n)
显然,这是指数级的复杂度
当n为100或者更大时,计算次数是非常巨大的

既然问题出在重复计算,那么解决的办法就是:记忆化搜索。一个值一旦算出来就要记住(备忘录),以后不必重新计算。即第一次算出maxsum(x,y)的值时,就将该值存放起来,下次再需要计算时,直接取该值就可以了,不必再次调用maxsum函数进行递归运算了。这样,maxsum(x,y)都只需要计算一次即可,总的计算次数就是三角形中数字的总数,为:1+2+3+…+n=n*(n+1)/2 O(n2)

如何存放计算出来的maxsum(x,y)的值呢?显然,用一个二维数组ans[n][n]就可以解决。ans[x][y]就存放maxsum(x,y)的计算结果。下次再需要maxsum(x,y)的值时,不必再调用maxsum函数,只需要直接取ans[x][y]的值即可。

参考程序:

int maxsum(int x,int y)
{
    if(x==n) return mp[x][y];
    if(ans[x+1][y]==-1)//如果maxsum(x+1,y)没有计算过
        ans[x+1][y]=maxsum(x+1,y);
    if(ans[x+1][y+1]==-1)//如果maxsum(x+1,y+1)没有计算过
        ans[x+1][y+1]=maxsum(x+1,y+1);
    return max(ans[x+1][y],ans[x+1][y+1])+mp[x][y];
}
cout<<maxsum(1,1)<<endl;

分析
这种将一个问题分解为子问题递归求解,并且将中间结果保存以避免重复计算的办法,就叫做“动态规划”。动态规划通常用来求最优解,能用动态规划解决的求最优解问题,必须满足,最优解的每个局部解也都是最优的。以上题为例,最佳路径上面的每个数字到底部的那一段路径,都是从该数字出发到达到底部的最佳路径。

实际上,递归的思想在编程时未必要实现为递归函数。在上面的例子里,有递推公式:

   if(x==n)
          ans[x][y]=mp[x][y];
    else
          ans[x][y]=max(ans[x+1][y],ans[x+1][y+1])+mp[x][y];

因此,不需要写递归函数,从ans[N]这一行元素开始向上逐行递推,就能求得ans[1][1]的值了。

     cin>>n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
            cin>>mp[i][j];
    for(int i=1;i<=n;i++)
        ans[n][i]=mp[n][i];
    for(int i=n;i>1;i--)
        for(int j=1;j<i;j++)
           ans[i-1][j]=max(ans[i][j],ans[i][j+1])+mp[i-1][j];
    cout<<ans[1][1]<<endl;

自顶向下的递推

    cin>>n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
            cin>>mp[i][j];
    ans[1][1]=mp[1][1];
    for(int i=2;i<=n;i++)
        for(int j=1;j<=i;j++)
           ans[i][j]=max(ans[i-1][j-1],ans[i-1][j])+mp[i][j];
    int maxm=ans[n][1];
    for(int i=2;i<=n;i++)
        maxm=max(maxm,ans[n][i]);
    cout<<maxm<<endl;
    

一维空间

 cin>>n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
            cin>>mp[i][j];
    ans[1]=mp[1][1];
    for(int i=2;i<=n;i++)
        for(int j=i;j>=1;j--)
           ans[j]=max(ans[j-1],ans[j])+mp[i][j];
    int maxm=ans[1];
    for(int i=2;i<=n;i++)
        maxm=max(maxm,ans[i]);
    cout<<maxm<<endl;

动态规划解题的一般思路

能采用动态规划求解的问题的一般要具有3个性质:
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性 :即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

用动态规划解题,如何寻找“子问题”,定义“状态”,“状态转移方程”是什么样的,并没有一定之规,需要具体问题具体分析,题目做多了就会有感觉。
甚至,对于同一个问题,分解成子问题的办法可能不止一种,因而“状态”也可以有不同的定义方法。不同的“状态”定义方法可能会导致时间、空间效率上的区别。


最长上升子序列

一个数的序列bi,当b1 < b2 < … < bS 的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, …, aN),我们可以得到一些上升的子序列(ai1, ai2, …, aiK),这里1 <= i1 < i2 < … <iK <= N。比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7,9), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8).
你的任务,就是对于给定的序列,求出最长上升子序列的长度。

问题描述

输入数据

输入的第一行是序列的长度N (1 <= N <= 1000)。第二行给出序列中的N 个整数,这些整数的取值范围都在0 到10000。

输出要求

最长上升子序列的长度。

输入样例
7
1 7 3 5 9 4 8

输出样例

4

解题思路
如何把这个问题分解成子问题呢?
经过分析,发现 “求以ak(k=1, 2, 3…N)为终点的最长上升子序列的长度”是个好的子问题
――这里把一个上升子序列中最右边的那个数,称为该子序列的“终点”。虽然这个子问题和原问题形式上并不完全一样,但是只要这N 个子问题都解决了,那么这N 个子问题的解中,最大的那个就是整个问题的解。

假定dp[k]表示以ak 做为“终点”的最长上升子序列的长度,那么:
dp[1] = 1
dp[i] = max { dp [j](i > j 且 ai > aj )} + 1
这个状态转移方程的意思就是:dp[i]的值,就是在ai左边,数值小于ai,且长度最大的那个上升子序列的长度再加1。因为ai左边任何“终点”小于ai 的子序列,加上ai 后就能形成一个更长的上升子序列。
实际实现的时候,可以不必编写递归函数,因为从 dp[1]就能推算出dp[2],有了dp[1]和dp[2]就能推算出dp[3]……

参考程序:

int ans=1;
dp[1]=1;
for(int i=2;i<=n;i++)
{
	 for(int j=1;j<i;j++)
   	 if(a[i]>a[j])
        dp[i]=max(dp[i],dp[j]+1);
	 ans=max(ans,dp[i]);
}
cout<<ans<<endl;
最长公共子串

给出两个字符串序列,求出这样的一个最长的公共子串:字串中的每个字符都能在两个原串中找到,而且每个字符的顺序和原串中的顺序一致。要求会求出最长公共子串的长度。比如:abcd和cdba的最长公共子串长度为2。一般题目中字符串的长度为1000.

暴力

暴力法是解决最长公共子序列问题最容易想到的方法,即对S的每一个子序列,检查是否为T的子序列,从而确定它是否为S和T的公共子序列,并且选出最长的公共子序列。
S和T的所有子序列都检查过后即可求出S和T的最长公共子序列。S的一个子序列相应于下标序列1,2,…,n的一个子序列。因此,S共有2n个子序列。当然,T也有2m个子序列。
因此,暴力法的时间复杂度为O(2^n * 2^m),这是指数级别的。

动态规划

在这里插入图片描述
这需要遍历一遍S串,每次遍历时都要遍历一遍T串,时间复杂度为O(n*m)

参考程序

 for(int i=0;i<n;i++)
    for(int j=0;j<m;j++)
    {
        if(s1[i]==s2[j]) dp[i+1][j+1]=dp[i][j]+1;
        else dp[i+1][j+1]=max(dp[i+1][j],dp[i][j+1]);
    }
printf("%d\n",dp[n][m]);
01背包问题

给定n种物品和一个背包,物品i的重量是wi,其价值为vi,背包的容量为C。
背包问题是如何选择装入背包的物品,使得装入背包中物品的总价值最大?
如果在选择装入背包的物品时,对每种物品i只有两种选择:装入背包或不装入背包,则称为0/1背包问题。

在0/1背包问题中,物品i或者被装入背包,或者不被装入背包,设xi表示物品i装入背包的情况,则当xi=0时,表示物品i没有被装入背包,xi=1时,表示物品i被装入背包。根据问题的要求,有如下约束条件和目标函数:
在这里插入图片描述
于是,问题归结为寻找一个满足约束条件式6.9,并使目标函数式6.10达到最大的解向量X=(x1, x2, …, xn)。

例如,有5个物品,其重量分别是{2, 2, 6, 5, 4},价值分别为{6, 3, 5, 4, 6},背包的容量为10。
用一个(n+1)×(C+1)的二维表f,f[i][j]表示把前i个物品装入容量为j的背包中获得的最大价值。
在这里插入图片描述
填表的过程,是按只放第1个物品、只放前2个、只放前3个…一直到放完,这样的顺序考虑。(从小问题扩展到大问题)
1、只装第1个物品。(横向是递增的背包容量)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

状态转移方程

dp[i][j]=max{dp[i-1][j],dp[i-1][j-c[i]]+w[i]}
for(i = 1; i<=n; i++)  
    for(j = m; j>=c[i]; j--)
        dp[i][j]=max(dp[i-1][j],dp[i-1][j-c[i]]+w[i]);  
空间优化

观察发现dp[i][j]只与dp[i-1][k]有关,与dp[i-2][k]已经没有关系了,那我们是不是能把前面用不到的都优化掉呢?

滚动数组优化
dp[i&1][j]=max(dp[~i&1][j],dp[~i&1][j-c[i]]+w[i]);

第一维度的长度只开为2就可以了,以前的都不需要了,只需要重复滚动的使用01这两维来进行更新即可。

一维数组优化

思考能否把数组变成一维的?

  #include<bits/stdc++.h>
using namespace std;
int dp[1000010],n,m,k,t;
int main()
{
    int i,j;
    scanf(“%d%d”,&n,&m);
    while(n--)
    {
        scanf("%d%d",&k,&t);
        for(i=m;i>=k;i--)//倒序输出确保不会出现重复
            dp[i]=max(dp[i],t+dp[i-k]);//在经过遍历后,数据不再需要,用新数据覆盖
    }
    printf("%d\n",dp[m]);
    return 0;
}
完全背包问题

给定n种物品和一个背包,物品i的重量是wi,其价值为vi,每种物品的数量是无限的,背包的容量为C。
背包问题是如何选择装入背包的物品,使得装入背包中物品的总价值最大?
与0/1背包相似,不过是把每种物品只有一个变成了每种物品有无限多个。

此时我们可以借鉴优化成一维后的0/1背包的做法,只需要将第二维的倒序改为正序。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int n,m,dp[maxn];
int main()
{
    int i,j,k,t;
    scanf("%d%d",&n,&m);
    while(n--)
    {
        scanf("%d%d",&j,&k);
        for(i=j;i<=m;i++)dp[i]=max(dp[i],dp[i-j]+k);
    }
    printf("%d\n",dp[m]);
    return 0;
}
多重背包问题

给定n种物品和一个背包,物品i的重量是wi,其价值为vi,物品i的个数是确定的ai,背包的容量为C。
背包问题是如何选择装入背包的物品,使得装入背包中物品的总价值最大?

一下子就能想到我们可以再在0/1背包的基础上加一维枚举每个物品要取多少个?

for(int i=1;i<=n;i++){
        for(int j=c;j>=0;j--){
            for(int k=0;k<=a[i];k++){
                if(j-k*w[i]<0)break;
                dp[j] = max(dp[j],dp[j-k*w[i]]+k*v[i]);
            }
        }
    }
二进制优化

二进制是一个神奇的东西,二进制可以表示任何数值,而且每位只有0/1两种情况。
我们可以考虑二进制的思想,将第 i 种物品分为若干件物品,这若干件物品可以将1~ai种物品全都表示出来。
每件物品有一个系数,分别为1,2,4,…,2(k-1),n-2k+1,k是满足n-2k+1>0的最大整数。eg: n为13,拆分为1,2,4,6四件物品,13以内的数都可以由这四个数组合得到。
所以,n件物品就拆分成log(n)件物品。
再转化为0/1背包即可。

二进制优化代码

 for(int i=1;i<=n;i++)
{
        int k=1;     //对于每一种,k准备取1 2 4 8...
        int temp=a[i];  //a[i]为第i种的数量
        for(k; k<=temp ; k*=2)
        {
            value[++num]=k*v[i];  //v[i]为第i种的每个的价值
            temp-=k;
        }
        if(temp>0)
            value[++num]=temp*v[i];  
}

例题
在这里插入图片描述
题意分析:
有一个序列,序列中的每个数都是一个权值,你从0位置开始,按一定顺序使用完两种卡片(第一种卡片每次跳va步,第二种卡片每次跳vb步)并最大化权值总和。

状态设计1:
dp[i][j][k]代表到第i个数使用了j张a卡片,k张b卡片的最优解,答案为dp[n][na][nb].
不难写出状态转移方程:
dp[i][j][k]=max(dp[i-va][j-1][k],dp[i-vb][j][k-1])+A[i]

状态设计2:
dp[i][j]代表使用了i张a卡片,j张b卡片后的最优解,答案为dp[na][nb]
转移方程:
dp[i][j]=max(dp[i-1][j],dp[i][j-1])+a[iva+jvb]

状态设计3:
dp[i][j]代表总共使用了i张卡片其中a卡片用了j张的最优解。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值