线性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}\)
代码就不贴了
总结:还是比较简单的吧,如果不会就多做点题,做多了就好了

浙公网安备 33010602011771号