专题2 - 线性dp

未经许可,禁止转载。

 

动态规划不同于贪心,贪心注重于局部最优解,期望通过局部最优解推出全局最优解,而这一步需要严谨的证明,否则就是一个假贪心。而动态规划本质上是通过解决子问题或记忆化搜索,进行状态转移,从而解决最终的问题。

动态规划大致有两种方法可以实现:

1. 通过记忆化搜索

2. 通过循环,正向求解

两种方法都可以实现,在写出状态转移方程之后可以根据二者实现状态转移的难易程度决定使用哪种写法。

 

一、背包问题

背包问题是最为基础的dp问题,下面就用一道题目展开讲讲。

HDU - 6968

这道题我是写过题解的,我觉得这是一道较为综合的背包题目。(或者说这道题搞懂了背包思想就等于懂了?)

有若干个复习资料,对于一套资料而言,只需花费\(x\)天就能使该门课提升\(y\)分。问能否在\(t\)天之内完成逆袭使得挂科数小于\(p\),如果能够创造奇迹,输出能获得的最大分数。

首先需要处理\(m\)份的复习资料,这一部分的状态转移方程为\(f[i][j]=f[i][j-w]+t\),其中\(f[i][j]\)表示第\(i\)门课得到\(j\)分数所需要的最短天数,\(w\)表示这份复习资料能够获得的分数,\(t\)表示这门课所需要的天数,然后找到\(f[i][j]\)的最小值即可,并且在实现dp过程中要确认循环的方向,必须从分数高往分数低方向循环,不然会导致一本复习资料所获得的分数无限叠加。

随后就可以对最终的问题进行求解。如果当前的\(f[i][l]\)小于60,状态转移方程为\(dp[i+1][j+f[i][l]][k+1]=max(dp[i+1][j+f[i][l]][k+1],dp[i][j][k]+l)\),反之状态转移方程为\(dp[i+1][j+f[i][l]][k]=max(dp[i+1][j+f[i][l]][k],dp[i][j][k]+l)\)其中用\(dp[i][j][k]\)表示\(i-1\)门课时花费\(j\)天且挂科数为\(k\)时能达到的最高分数。这样有了状态转移方程,就当作是A了(?)

#include<bits/stdc++.h>
#define fast ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
#define ll long long
#define pb push_back
using namespace std;
const int maxn = 60;
const int maxm = 15010;
string c[maxn];
int f[maxn][110];
map<string,int> mp;
int dp[maxn][510][4];
int main()
{
    fast;
    int T;
    cin>>T;
    while(T--)
    {
        mp.clear();
        int n,m;
        string s;
        int x,y;
        cin>>n;
        for(int i=1;i<=n;i++)
        {
            cin>>c[i];
            mp[c[i]]=i;
            for(int j=0;j<=100;j++)
            {
                if(j==0) f[i][j]=0;
                else f[i][j]=-1;
            }
        }
        cin>>m;
        for(int i=1;i<=m;i++)
        {
            cin>>s>>x>>y;
            int tmp=mp[s];
            for(int j=100;j>=0;j--)
            {
                if(f[tmp][j]!=-1)
                {
                    if(f[tmp][min(j+x,100)]==-1) f[tmp][min(j+x,100)] = f[tmp][j]+y;
                    else f[tmp][min(j+x,100)] = min(f[tmp][min(j+x,100)],f[tmp][j]+y);
                }
            }
        }
        int day,limit;
        cin>>day>>limit;
        int minn=1e9;
        int maxx=-1;
        int sum=0;
        int flag=0;
        for(int i=0;i<=n+1;i++)
        {
            for(int j=0;j<=day;j++)
            {
                for(int k=0;k<=limit;k++)
                {
                    dp[i][j][k]=-1;
                }
            }
        }
        dp[1][0][0]=0;
        for(int i=1;i<=n;i++)
        {
            for(int j=0;j<=day;j++)
            {
                for(int k=0;k<=3;k++)
                {
                    // if(k!=3) dp[i+1][j][k+1]=max(dp[i+1][j][k+1],dp[i][j][k]);
                    for(int l=0;l<=100;l++)
                    {
                        if(j+f[i][l]<=day && f[i][l]!=-1 && dp[i][j][k]!=-1)
                        {
                            if(l>=60) dp[i+1][j+f[i][l]][k]=max(dp[i+1][j+f[i][l]][k],dp[i][j][k]+l);
                            else
                            {
                                if(k!=3) dp[i+1][j+f[i][l]][k+1]=max(dp[i+1][j+f[i][l]][k+1],dp[i][j][k]+l);
                            }
                        }
                    }
                }
            }
        }
        /*for(int i=2;i<=n+1;i++)
        {
            for(int j=0;j<=day;j++)
            {
                for(int k=0;k<=limit;k++)
                {
                    cout<<i<<' '<<j<<' '<<k<<' '<<dp[i][j][k]<<'\n';
                }
            }
        }*/
        for(int i=0;i<=day;i++)
        {
            for(int j=0;j<=limit;j++)
            {
                maxx=max(dp[n+1][i][j],maxx);
            }
        }
        if(maxx!=-1) cout<<maxx<<'\n';
        else cout<<"-1\n";
    }
}

 

二、最长上升子序列(LIS)

给定一个序列,求出这个序列的最长上升子序列的长度。

例如给出序列为\(\{2,1,5,3,4\}\),最长上升子序列的长度就为3(\(\{2,3,4\},\{1,3,4\}\))。

思路一:暴力型dp求解

用\(f[i]\)表示到第\(i\)个位置时前面的最长上升子序列长度,状态转移方程为\(f[i]=(a[i]>a[j])?f[j]+1:1\),\(j\)为小于\(i\)的所有值,最后\(f[i]\)取最大值即可。

这样做下来的时间复杂度为\(O(n^2)\),接下来就是优化思路。

思路二:二分

建一个单调数组,遍历\(a[i]\),如果当前大于数组中的最大值,就插入该值,长度\(+1\),否则,二分查找大于该值的最小值,然后将其替换。

拿上面的序列做例子:

\(i = 1\),\(a[1]=2\),数组为空,将2插入数组。此时数组为\(\{2\}\)。

\(i = 2\),\(a[2]=1\),将数组中的2替换。此时数组为\(\{1\}\)。

\(i = 3\),\(a[3]=5\),大于该数组的最大值,将其插入数组。此时数组为\(\{1,5\}\)。

\(i = 4\),\(a[4]=3\),二分查找后将5替换。此时数组为\(\{1,3\}\)。

\(i = 5\),\(a[5]=4\),大于该数组的最大值,将其插入数组。此时数组为\(\{1,3,4\}\)。

最后数组的长度就是答案3。

整一个过程可以理解为:

(1) 如果数组中没有比当前数大的数,那么它一定能使得答案\(+1\)。

(2) 否则,替换掉大于该值的最小值,替换之后如果出现了(1)的情况,例如i = 2和i = 3的时候,虽然1替代了2,但不影响\(\{2,5\}\)也是当前情况下最长的上升子序列,而后续会有更大的可能出现(1)这种情况(因为整个数组的值在变小),属于是一点点的小贪心。

而这种做法成功地将时间复杂度降到了\(O(nlogn)\)。

这一部分的代码将会在例题中体现

洛谷P1439

表面上是一道最长公共子序列的模板题(注:最长公共子序列的状态转移方程为\(f[i][j]=(a[i]==b[j])?f[i-1][j-1]+1:max(f[i][j-1],f[i-1][j]\),其中\(f[i][j]\)表示在\(a\)数组第\(i\)个位置,\(b\)数组第\(j\)个位置的最长公共子序列长度。

然而很明显的是,这道题不适用这种做法,\(n\)的最大值为\(10^5\),开个二维数组直接见祖宗了,所以应该另寻角度。

注意到,两个序列都是\(1..n\)的序列,每个数只会在一个序列中出现一次。

我们可以将问题转化,将序列1中的每个数字的位置记录下来,而后映射到序列2。以样例为例,序列1中各个数字的位置映射到序列2可以得到序列\(\{3,2,1,4,5\}\),其实就是求这个新序列的最长上升子序列。

这边就是用到了二分求LIS的方法,不然直接T飞了。

#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define fast ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
using namespace std;
const int maxn = 1e5+10;
int a[maxn],b[maxn],f[maxn];
map<int,int> mp;
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        mp[a[i]]=i;
    }
    for(int i=1;i<=n;i++)
    {
        cin>>b[i];
    }
    f[0]=0;
    int len=0;
    for(int i=1;i<=n;i++)
    {
        if(len==0) f[++len]=mp[b[i]];
        else
        {
            if(mp[b[i]]>f[len])
            {
                f[++len]=mp[b[i]];
            }
            else
            {
                int l=0,r=len;
                while(l<r)
                {
                    int mid=(l+r)/2;
                    if(f[mid]>mp[b[i]]) r=mid;
                    else l=mid+1;
                }
                f[l]=min(mp[b[i]],f[l]);
            }
        }
    }
    cout<<len<<'\n';
}

 

posted @ 2021-10-02 00:12  cyanine_告别  阅读(34)  评论(0编辑  收藏  举报