线性DP

线性 DP 尽管看起来很简单,实际上也确实很简单还是有必要了解一下套路的

线性 DP 的套路就是将状态设为 \(f_{i,s}\),其中 \(i\) 表示序列的前 \(i\) 项,\(s\) 为当前状态,有时还需进行一些小优化

先看一个简单例题

P1541 [NOIP2010 提高组] 乌龟棋 很裸的dp
考虑设状态 \(f_{i,j,k,l}\)\(i\)\(1\)\(j\)\(2\)\(k\)\(3\)\(l\)\(4\) 时的最大分数,暴力四次方转移即可

然后看两个经典问题:LCS 与 LIS

首先看一下 LIS,也就是最长上升子序列

考虑 \(f_i\) 表示前 \(i\) 项以 \(i\) 结尾的 LIS长度
这样就有 \(f_i=\max\limits_{j=1}^{i-1}\{[a_j\le a_i]f_j\}+1\)
然后答案就是 \(\max\{f_i\}\) 了,总复杂度为 \(O(n^2)\)
考虑进行一个化的优:把 \(f_i\) 塞进树状数组/线段树,这样就可以 \(\log n\)转移了,总复杂度 \(O(n\log n)\)

然后看一下 LCS,也就是最长公共子序列

容易想到 \(f_{i,j}\) 为两个序列分别在前 \(i\) 位和前 \(j\) 位的LCS
首先如果 \(a_i\neq b_j\),那么就从 \(f_{i,j-1}\)\(f_{i-1,j}\) 继承答案
如果 \(a_i=b_j\),那么同时也要用 \(f_{i-1,j-1}+1\) 更新答案
总复杂度 \(O(n^2)\)

上面的三个问题还算简单,接下来看看下面这几道其实还是很简单

P4059 [Code+#1] 找爸爸
那么根据套路,设状态为 \(f_{i,j,0/1/2}\)
\(i\)\(j\) 仍然表示在序列中的位置,0/1/2 表示 没空格/一个是空格/另一个是空格
直接 \(O(nm)\) 暴力DP即可

#include<bits/stdc++.h>
using namespace std;
const int Max=3005;
const int inf=1e9+7;
int l_hash(char ch)
{
    if(ch=='A')
    {
        return 1;
    }
    if(ch=='T')
    {
        return 2;
    }
    if(ch=='G')
    {
        return 3;
    }
    if(ch=='C')
    {
        return 4;
    }
}
int A,B;
int d[5][5];
int f[Max][Max][3];
int n,m;
char s[Max],t[Max];
int a[Max],b[Max];
int main()
{
    cin>>s+1>>t+1;
    for(int i=1;i<=4;i++)
    {
        for(int j=1;j<=4;j++)
        {
            cin>>d[i][j];
        }
    }
    cin>>A>>B;
    n=strlen(s+1);
    m=strlen(t+1);
    for(int i=1;i<=n;i++)
    {
        a[i]=l_hash(s[i]);
    }
    for(int i=1;i<=m;i++)
    {
        b[i]=l_hash(t[i]);
    }
    for(int i=0;i<=n;i++)
    {
        for(int j=0;j<=m;j++)
        {
            f[i][j][0]=f[i][j][1]=f[i][j][2]=-inf;
            if(i==0&&j==0)
            {
                f[i][j][0]=0;
                continue;
            }
            if(i==0)
            {
                f[i][j][1]=-A-B*(j-1);
                continue;
            }
            if(j==0)
            {
                f[i][j][2]=-A-B*(i-1);
                continue;
            }
            f[i][j][0]=max({f[i-1][j-1][0],f[i-1][j-1][1],f[i-1][j-1][2]})+d[a[i]][b[j]];
            f[i][j][1]=max({f[i][j-1][0]-A,f[i][j-1][1]-B,f[i][j-1][2]-A});
            f[i][j][2]=max({f[i-1][j][0]-A,f[i-1][j][1]-A,f[i-1][j][2]-B});
        }
    }
    cout<<max({f[n][m][0],f[n][m][1],f[n][m][2]});
    return 0;
}

P9753 [CSP-S 2023] 消消乐
\(f_i\) 为以第 \(i\) 位结尾的方案数,\(pre_i\)\(i\) 前面最进的一个位置使 \([pre_i,i]\) 合法,那么显然有 \(f_i=f_{pre_i}+1\)
至于 \(pre_i\) 暴力跳就可以了,然后利用等价类可证暴力跳复杂度为 \(O(|\Sigma|n)\)因为我太懒了所以代码不放了

然后看一个 LIS 的升级版:
给出 \(a\)\(b\),求 \(a\) 的一个子序列 \(c\) 的最大长度,使 \(c_{i+1}\gt c_i\times b_i\)
然后 \(n\le 1000\) 有50分
\(b_i=1\) 有15分
\(b_i>1\) 有15分
对于全部数据,\(n \le 10^6,a_i\le 10^{12},b_i\le 10^6\)

\(b_i=1\) 时,退化为 LIS,然后就有了 15 分
\(b_i>1\) 时,答案一定不超过 \(\log n\),考虑 \(f_{i,j}\) 表示前 \(i\) 位,答案为 \(j\)\(c_j\) 的最小值
然后就可以xjb转移,过程中更新答案,复杂度 \(O(n\log n)\)
然后再来考虑 \(n^2\) 的部分分。
发现其实就和 \(b_i>1\) 一样,就是因为答案大小没了那么好的性质,导致复杂度变成了垃圾的 \(O(n^2)\)
然后就成功得到了80分的好成绩
然后考虑进行一个化的优
考虑枚举第一维\(i\),然后动态进行一个 \(f_j\) 的维护,然后这里 \(f_j\) 表示的是 \(\min\{a_k\times b_j\}\),然后发现因为 \(f\) 内部维护的东西一定递增,所以其实 \(f\) 是具有单调性的!!!
然后就可以二分出来第一个比 \(a_i\) 小的 \(f_j\),然后再更新 \(f\) 数组
答案同样在过程中统计。

#include<bits/stdc++.h>
using namespace std;
const int Max=1e6+5;
int n;
long long a[Max],b[Max],f[Max];
int main()
{
    freopen("C.in","r",stdin);
    freopen("C.out","w",stdout);
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    for(int i=1;i<=n;i++)
    {
        cin>>b[i];
    }
    memset(f,0x3f,sizeof(f));
    long long ans=0;
    for(int i=1;i<=n;i++)
    {
        int j=lower_bound(f+1,f+n+1,a[i])-f-1;
        f[j+1]=min(f[j+1],a[i]*b[j+1]);
        ans=max(ans,1ll*j+1);
    }
    cout<<ans<<'\n';
    return 0;
}

然后我们就又有了一种优化LIS的方法
然后发现这类DP的优化就是枚举 \(i\),再动态维护 \(f_s\),具体方法就看 \(f\) 的性质了

最后看一个有一点迷惑性的题目
给出 \(a_i\),然后求 \(b_i\) 的个数,让 \(b_i\)\(a_i\) 的一个排列使每个数在 \(a_i\) 与 在 \(b_i\) 中的位置距离不超过 \(1\)\(n\le10^5\)
看着很难以的一个计数,实际上比较显然,就是考虑分类讨论一下
设状态 \(f_i\) 表示 \([1,i]\) 的方案数
如果 \(a_i\neq a_{i-1}\),那么考虑 \(a_i\) 换或不换两种,于是就有 \(f_i=f_{i-1}+f_{i-2}\)
如果 \(a_i\neq a_{i-1}\),我们发现这时 \(a_i\) 换了就跟换了一样,于是 \(f_i=f_{i-1}\)
代码就不贴了

总结:还是比较简单的吧,如果不会就多做点题,做多了就好了

posted @ 2023-11-12 15:35  Coffins  阅读(12)  评论(0)    收藏  举报