线性DP

线性DP

  具有线性阶段划分的动态规划算法统称为线性DP;
  线性DP与数学中的线性空间概念类似,如果一个动态规划的状态包含多个维度,但是在每一个维度上都具有线性变化的阶段,那么该动态规划算法称为线性DP。
  常见的如LIS,LCS等都是线性DP的经典问题。

例题

杨老师的照相排列

#include<bits/stdc++.h>
using namespace std;

int main(){
    //好题目啊,线性DP的经典题目
    while(true){
        int k;
        scanf("%d",&k);
        if(k==0)break;
        int num[5];
        memset(num,0,sizeof(num));
        for(int i=0;i<k;i++)scanf("%d",&num[i]);
        long long dp[num[0]+1][num[1]+1][num[2]+1][num[3]+1][num[4]+1];
        memset(dp,0,sizeof(dp));
        //for(int i=0;i<num[0];i++)cout<<dp[i][0][0][0][0]<<" ";cout<<endl;
        int n=0;
        dp[0][0][0][0][0]=1;
        for(int i=0;i<k;i++)n+=num[i];
        //cout<<n<<endl;
        //开始按列或者行填充,由于我们是按照从高到低的顺序填充的,所以结果一定正确。
        for(int d1=0;d1<=num[0];d1++){
            for(int d2=0;d2<=num[1];d2++){
                if(d2>d1)break;
                for(int d3=0;d3<=num[2];d3++){
                    if(d3>d2)break;
                    for(int d4=0;d4<=num[3];d4++){
                        if(d4>d3)break;
                        for(int d5=0;d5<=num[4];d5++){
                            if(d5>d4)break;
                            if(d1>d2)dp[d1][d2][d3][d4][d5]+=dp[d1-1][d2][d3][d4][d5];
                            if(d2>d3)dp[d1][d2][d3][d4][d5]+=dp[d1][d2-1][d3][d4][d5];
                            if(d3>d4)dp[d1][d2][d3][d4][d5]+=dp[d1][d2][d3-1][d4][d5];
                            if(d4>d5)dp[d1][d2][d3][d4][d5]+=dp[d1][d2][d3][d4-1][d5];
                            if(d5>=1)dp[d1][d2][d3][d4][d5]+=dp[d1][d2][d3][d4][d5-1];
                        }
                    }
                }
            }
        }
        //for(int i=0;i<num[0];i++)cout<<dp[i][0][0][0][0]<<" ";cout<<endl;
        printf("%lld \n",dp[num[0]][num[1]][num[2]][num[3]][num[4]]);
    }
    return 0;
}
简要分析

  本题是非常经典的线性DP问题,但是本题的技巧性是比较高的,如果比较了解动态规划的三要素,那么我们可以发现本题中的状态表述和地推顺序是相辅相成的,本题中我们使用从左到右,从前到后的顺序进行填充的,这样降低了中间态表示的复杂的,因为我们是按照身高从高到低的顺序填充的,所以我们只需要保证每个中间态是合法的即可。那么什么样的中间态是合法的呢?显然当我们填充第i高的人时,我们要保证当前可填充的位置不会使得身高低的排在身高高的前面即可,那么我们只需要保证中间态中,上一行的人数一定大于等于下一行即可,此时我们就可以选择第一行,或者某一行人数少于上一行的进行填充,此时由于已经填充的人都比i高,并且填充的位置一定保证了身高递减的条件,所以填充一定时可行的,那么我们中间态只需要记录每一行的人数即可,在于地推的拓扑顺序相结合就可以得到一个比较简单的动态规划地推公式。
  反之,如果我们不使用从高到低的顺序进行填充,那么中间态就需要保存已经填充的人的身高信息。

例题

最长公共子序列

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int n=text1.size();
        int m=text2.size();
        int dp[n+1][m+1];
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                //不论text1[i]是否等于text2[j],我们都先要考虑不匹配时的最大公共子串的长度。
                dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
                //当匹配时,我们再额外考虑匹配时的长度
                if(text1[i-1]==text2[j-1]){
                    dp[i][j]=max(dp[i-1][j-1]+1,dp[i][j]);
                }
            }
        }
        return dp[n][m];
    }
};

例题

最长上升子序列

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n=nums.size();
        vector<int> arr;
        int ans=0;
        for(int i=0;i<n;i++){
            int l=0;int r=arr.size()-1;
            for(;l<=r;){
                int m=(l+r)/2;
                if(nums[arr[m]]>=nums[i])r=m-1;
                if(nums[arr[m]]<nums[i])l=m+1;
            }
            //arr中保存的值一定时已经
            //找到最优的更新即可
            //新的元素插入
            if(l==arr.size())arr.push_back(i);
            else arr[l]=i;
        }
        return arr.size();

    }
};

例题

最长公共递增子序列

//首先我们要了解最长上升子序列的几种优化方法,可以将平方级别的时间复杂度降低到O(nlogn)级别
#include<bits/stdc++.h>
using namespace std;

int main(){
    int n;
    scanf("%d",&n);
    int arr1[n+1];
    int arr2[n+1];
    for(int i=1;i<=n;i++)scanf("%d",&arr1[i]);
    for(int i=1;i<=n;i++)scanf("%d",&arr2[i]);
    //典型的线性dp中的区间dp方法
    int dp[n+1][n+1];//表示以Bj结尾的最长递增子序列的长度
    memset(dp,0,sizeof(dp));//用来记录最长上升子序列的长度
    //最长公共子序列是否包含最长递增子序列?不一定的
    for(int i=1;i<=n;i++){
        int now=dp[i][0];
        //dp[i][j]到低代表着什么意思呢?
        
        for(int j=1;j<=n;j++){
            //这里怎么优化呢?
            //这里的优化是很妙的,我们需要新的中间态表示方式;
            //值得注意的是,dp数组中保存的是以Bj结尾的最长递增公共子序列的长度。
            //那么我们一定可以肯定的是结尾一定时Bj,所以当Ai!=Bj时我们只需要将i往前推一位即可。
            //当A[i]=B[j]时,我们只需要记录dp[i-1][k],k<j中的最大值即可
            if(arr1[i]==arr2[j])dp[i][j]=now+1;//每一位只会遍历到一次,所以这里一定取最大值。
            else dp[i][j]=dp[i-1][j];
            if(arr1[i]>arr2[j])now=max(now,dp[i-1][j]);//时刻维持当前的结尾值小于arr1[i-1]的最优值,作为后续更新的依据。
        }
    }
    //值得注意的是我们定义的时候是以dp[i][j]保存的是以Bj结尾的最长公共递增子序列的长度,
    //所以我们要统计所有可能的Bj中的最大值作为最终结果。
    int ans=0;
    for(int i=0;i<=n;i++)ans=max(ans,dp[n][i]);
    printf("%d",ans);
    return 0;
}

  分析
  本题是最长递增子序列,和最长公共子序列的综合,所以我们要综合两者的dp特性设计一个合理的dp策略,考虑到最长递增子序列的问题需要考虑元素之间的顺序,而最长公共子序列,则没有顺序的要求,所以我们要在求解最长公共子序列的基础上添加一个序的特性,使得我们可以平滑地进行状态转移。
  考虑到求解最长公共子序列的问题中,dp[i][j]表示的是数组1前i个元素,以及数组2前j个元素这个子问题的最长公共子序列的长度,这里arr1[i]不一定与arr2[j],但是元素有序的情况下,我们需要重新描述dp数组的含义,因为求解最长公共子序列的状态表示中,要想推导出dp[i][j]我们需要找到最大的dp[k][p],其中arr1[k]==arr2[p],同时arr1[k]<arr1[i]成立,朴素的方法需要O(n^2)来进行扫描寻找,整个dp的时间复杂度就达到了O(n^4),显然是不可行的。
  ①一种优化方式是,我们在最开始就讲dp数组定义为有序的,也即dp[i][j]可以定义成以Bj结尾的最长公共递增子序列的长度,这样我们再转移上只需要就近转移即可。上面的代码就是按照第二种方式实现的;

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值