【高效复习】算法合集 (四)——动态规划(LIS、LCS、01背包等)

动态规划(dynamic programming ,dp)

一、DP简介

一种用来解决一类最优化问题的算法思想
特点:
1)动态规划会将每个求解过的子问题的解记录下来。避免重复计算,使用dp[]
以斐波那切数列为例:

int F(int n)
{
    if(n==0 || n==1) return 1;
    if(dp[n]!=-1) return  dp[n]; //计算过时,直接返回
    else{
        dp[n]=F(n-1)+F(n-2);
        return dp[n];
    }
}

2)递归写法是自顶而下(top-down),又叫,记忆化搜索。引申概念——重叠子问题(Overlapping Subproblems)一个问题可以被分解为若干子问题,且这些子问题会重复出现。

3)递推写法——自底向上(bottom-up)
以数塔为例:
dp[i][j]=max(dp[i+1][j],dp[i+1][j+1]+f[i][j])
dp[i][j]叫状态,上面的式子叫状态转移方程。其中dp[n][j]==f[n][j]这也是边界。
即,递推写法就是把这些边界通过状态转移方程扩散到整个dp[][]中。

for(int j=1;j<n;j++)
{
    dp[n][j]=f[n][j];
}
for(int i=n-1;i>=1;i--)
    for(int j=1;j<=i;j++){
        dp[i][j]=max(dp[i+1][j],dp[i+1][j+1]+f[i][j]);
    }

4)最优子结构:一个问题的最优解可以由子问题的最优解有效地构造出来。

小结:必须具备重叠性和最优子结构性才可以用动态规划。而分治问题可以不具备以上两个性质,并且递归本身就有分治的思想。贪心问题不具备重叠性,即现在只考虑下一步,不需要关心之前的情况。

常见题型分类:
(1)最大连续子序列和
dp[i]表示以A[i]作为末尾的连续序列的最大和
(2)最长不下降子序列 LIS
dp[i]表示以A[i]结尾的最长不下降子序列长度
(3)最长公共子序列 LCS
dp[i][j]表示字符串A的i号位和字符串B的j号位之前的LCS长度
(4)最长回文子串
dp[i][j]表示S[i]至S[j]所表的子串是否是回文子串
(5)数塔DP
dp[i][j]表示从第i行第j个数字出发的到达最底层的所有路径上所能得到的最大和
(6)DAG最长路
dp[i]表示从i号顶点出发能获得的最大路径长度
(7)01背包
dp[i][v]表示前i件物品恰好装入容量为v的背包中能获得的最大价值
(8)完全背包
dp[i][v]表示前i件物品恰好装入容量为v的背包中能获得的最大价值
用语说明:
(1)-(4):
a、dp[i]表示以A[i]结尾(或开头)的xxx;b、dp[i][j]表示A[i]至A[j]区间的xxx;xxx为原问题描述
(5)-(8)
分析问题是几维表示:a、恰好为i;b、前i
大多数情况都可以把动态规划看作是一个DAG有向无环图,图中结点就是状态,边就是状态转移方向,求解顺序就是按照拓扑序进行求解。

二、最大连续子序列和

dp[i]是以A[i]结尾的连续序列,
有两种情况:
1)A[i]只有这一个,即dp[i]=A[i];
2)从A[p]到A[i],即A[p]+A[p+1]+…A[i-1]+A[i]=dp[i-1]+A[i],取max(dp[i-1]+A[i],A[i])最大的情况
dp[i]=max(dp[i-1]+A[i],A[i]);
其中,A[0]=dp[0];是边界。

for(int i=0;i<n;i++)
{
    dp[i]=max(dp[i-1]+A[i],A[i]);
}
k=0;
for(int i=0;i<n;i++)
{
    if(dp[i]>dp[k]) k=i; //找最大的子序和
}

小结:
1)状态无后效性:当前状态记录了历史信息,一旦当前状态确定,就不会再改变,且未来的决策只能在已有的一个或若干个状态的基础上进行,历史信息只能通过已有的状态去影响未来的决策。
2)如何设计zhuangtaidp[i]和状态状态转移方程才是dp的核心,其中先决条件是做到状态无后效性。

三、最长不下降子序列

两种情况
1)如果存在A[i]之前的元素A[j](j<i)使得A[j]<=A[i]且dp[j]+1>dp[i],那么就把A[i]跟在以A[j]结尾的LIS后面,,即dp[i]=dp[j]+1;
2)A[i]之前的元素逗比A[i]大,那么A[i]只好形成一条LIS,但是长度为1,即只有一个A[i].
综上可以发现,dp[i]只与前面A[i]>A[j]的dp[j]有关
得出状态转移方程:dp[i]=max(1,dp[j]+1);条件是(j=1,2…i-1 && A[j]<A[i])
边界是dp[i]=1;

int ans=-1;
for(int i=0;i<n;i++)
{
    dp[i]=1;
    for(int j=0;j<i;j++)
    {
        if(A[i]>A[j] && dp[j]+1>dp[i]) dp[i]=dp[j]+1;
    }
    ans=max(ans,dp[i]);
}

四、最长公共子序列

两种决策:
1)A[i]==B[j],如“admins”与“sads”中A[6]与B[4]都是‘s’,则dp[i][j]=dp[i-1][j-1]+1,解释:A[5]与B[3]继承了前面"ad"出现的情况,现在出现‘s’只需加1即可
2)A[i]!=B[j]时,根据继承性,只需找出dp[i-1][j]与dp[i][j-1]中大的一个。
在这里插入图片描述

状态转移方程:
dp[i][j]=dp[i-1][j-1]+1 ,A[i]==B[j] 或者 max(dp[i-1][j],dp[i][j-1]),A[i]!=B[j]
边界dp[i][0]=0与dp[0][j]=0,即尚未比较时。

for(int i=0;i<n;i++)
    for(j=0;j<m;j++)
        if(i==0 || j==0) dp[i][j]=0;

for(int i=0;i<n;i++)
{
    for(int j=0;j<i;j++)
    {
        if(A[i]==B[j]) dp[i][j]=dp[i-1][j-1]+1;
        else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
    }
    ans=max(ans,dp[i]);
}

五、最长回文子串

介绍一种容易理解的方法:
转移情况分类两类:
1)s[i]==s[j] 只要s[i+1]至s[j-1]是回文子串,那么s[i]至s[j]就是回文子串;
2)s[i]!=s[j] 肯定就是dp[i][j]=0
状态转移方程:
dp[i][j]=dp[i+1][j-1], s[i]==s[j] 或 0,s[i]!=s[j]
边界:dp[i][i]=1;dp[i][i+1]=(s[i]==s[i+1])?1:0;
在这里插入图片描述

优化:有一个问题,图11-4
dp[0][2]->dp[1][1]
dp[0][3]->dp[1][2]
若求dp[0][4]时,转移至dp[1][3]无法转移,也就是需要新的枚举方式.
方案:以长度L为,见下图11-5。
其他方案:二分+hash,O(nlogn),ManacherO(n);

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int maxn=1010;
char s[maxn];
int dp[maxn][maxn];

int main()
{
    gets(s);
    int len=strlen(s),ans=1;
    memset(dp,0,sizeof(dp));
    //边界
    for(int i=0;i<len;i++)
    {
        dp[i][i]=1;
        if(i<len-1)
        {
            if(s[i]==s[i+1]){
                dp[i][i+1]=1;
                ans=2;  //初始化时,当前最长回文子串长度
            }
        }
    }
    
    //状态转移方程
    for(int L=3;L<=len;L++){  //枚举子串的长度
        for(int i=0;i+L-1<len;i++) //枚举子串的起始点
        {
            int j=i+L-1;//子串的右端点
            if(s[i]==s[j] && dp[i+1][j-1]==1){
                dp[i][j]=1;
                ans=L; //更新最长回文子串长度
            }
        }
    }
    printf("%d",ans);  
    return 0;
}

六、DAG最长路
DAG的最长路和最短路求解思想一致。
两个问题:
1)求整个DAG中的最长路径(即不固定起点跟终点)
2)固定终点,求DAG的最长路径。
dp[i]表示从i号顶点出发能获得的最大路径长度,这样所有dp[i]的最大值就是整个DAG的最长路径长度。
详情,后续更新。

七、背包问题

实质多阶段动态规划问题分析
如图
在这里插入图片描述
A、01背包问题
问题描述:有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品总价值最大,。其中每件物品都只有1件。
对比上图分析该题,再结合DFS中求解因子分解(递归思想)可以很好解决此问题
dp[i][v]表示前i件物品恰好装入容量为v的背包中能获得的最大价值。
情况分析
1)情况一,不放入第i件物品,问题转化为前i-1种物品的最大价值,dp[i-1][v]
2)情况二,放入第i件物品,问题转化为i-1种物品装好v-w[i]的最大价值
状态转移方程:dp[i][v]=max( dp[i-1][v],dp[i-1][v-w[i]]+c[i]);
这样dp[i][v]只与dp[i-1][]有关,边界dp[0][v]=0;

for(int m=v;m>=0;m--) dp[0][m]=0;

for(int i=1;i<=n;i++)
{
    for(int v=w[i];v<=V;v++)
    {
        dp[i][v]=max(dp[i-1][v],dp[i-1][v-w[i]]+c[i]);
    }
}
优化:

1)i从1到n,v从V到0,因为dp[i+1][]求解只与dp[i][]有关,如下图。当逆序枚举时,从右上角开始往左下角前进(这就是逆向枚举)

转移方程:dp[v]=max(dp[v],dp[v-w[i]]+c[i]);
这个技巧就是滚动数组

逆序枚举:

for(int i=1;i<=n;i++)
    for(int v=V;v>=w[i];v--)
        dp[v]=max(dp[v],dp[v-w[i]]+c[i]);

小结:1)对于划分阶段的问题来说,都可以尝试把阶段作为状态的一维。
2)如果当前设计的状态不满足无后效性,那么不妨把状态进行升维,即增加一维或若干维来表示相应的信息。

B、 完全背包问题
问题分析:有n种物品,每种物品的单件重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都有无穷件。
与01背包最大区别:选择放入第i件回溯之前的dp[i-1][v-w[i]],变为dp[i][v-w[i]];
状态转移方程:
dp[i][v]=max(dp[i-1][v],dp[i][v-w[i]]+c[i]);边界dp[0][v]=0;

一维形式:dp[v]=max(dp[v],dp[v-w[i]]+c[i]);边界dp[v]=0;
一维形式与01的区别:要正向枚举。

八、dp常用处理套路

1)滚动数组
2)递归+记忆数组
3)高级套路:状态压缩、升维、单调性、四边形不等式
4)进阶:能分析出dp[i]/dp[i][j]与原状态什么有关。
leetcod:85/91/97/120/131/132/139/140/152 ,后续更新,敬情期待

猜你喜欢

转载自blog.csdn.net/lyly1995/article/details/87876866
今日推荐