CF10D LCIS(DP 状态设计,LIS,LCS,PST,ATE,VAI,调整转移顺序)
看到题只能想到离散化。
纯粹暴力
设 \(f(i,j,k)\) 表示用去 \(A_i,B_j\) 且以 \(k\) 结尾答案。
这真的非常暴力了,空间三次方,时间四次方。
update 2026/2/22
其实并非纯粹,其实这里隐含着 ATE 思想,
这里不是通过观察转移性质,而是通过优化状态设计的方式,
下面之所以不能勒令两个,就是要通过一维 ATE 保证不需要循环去做,
总而节省了时间复杂度。
优化状态设计 I
首先可以优化一下状态设计,以 \(k\) 结尾答案改为以不超过 \(k\) 为结尾,效果是一样的,性质和速度就来了。
时空皆是三次方,记录不难,完全等同于 AT-DP LCS 那一道题。
update 2026/2/22
同样 ATE 思想。
优化状态设计 II
但有个问题,空间不够。
我们当然可以压缩掉 \(i\) 这一维,但路径输出离不开他。
小性质(真的吗?):输出路径不支持常规内存压缩方式。
发现我们可以勒令 \(B_j=k\) 去设计,于是内存变为平方,状态同时减少。
转移优化:Planning for the Same Task
对于同一个 \(i\),\(A_i\) 相同,所以求 \(\mathrm{[if\ A_i=B_j]}\max\limits_{j'<j,B_{j'}<B_j}\set{f(i-1,j')+1}\) 除了 \(j'<j\) 的时间戳限制外本质相同。
定义其为 \(\text{the Same Task}\),所谓 \(\text{Plan}\),就是把循环分隔开,用前缀累计的方式去求解,本质上是广义前缀和。
类似的还有求和,求 \(\max/\min\) 等等。
放到这道题上,就是在过程中统计,由于 \(A_i\) 不变,循环中一个变量顺便记录一下即可。
时空复杂度 \(O(n^2)\)。
#include<bits/stdc++.h>
using namespace std;
const int N=606;
int n,m,A[N],B[N];
vector<int> busket;
struct notebook{int a,b,x,dp;} k[N][N],tmp;
stack<int> ans;
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&A[i]),busket.push_back(A[i]);
scanf("%d",&m);
for(int i=1;i<=m;i++) scanf("%d",&B[i]),busket.push_back(B[i]);
sort(busket.begin(),busket.end());
auto End=unique(busket.begin(),busket.end());
for(int i=1;i<=n;i++) A[i]=lower_bound(busket.begin(),End,A[i])-busket.begin()+1;
for(int i=1;i<=m;i++) B[i]=lower_bound(busket.begin(),End,B[i])-busket.begin()+1;
for(int i=1,pos,Plan;i<=n;i++){
Plan=pos=0;
for(int j=1;j<=m;j++){
k[i][j]={i-1,j,0,k[i-1][j].dp};
if(A[i]==B[j]&&k[i][j].dp<Plan+1)
k[i][j]={i-1,pos,A[i],Plan+1};
if(A[i]>B[j]&&Plan<k[i-1][j].dp)
pos=j,Plan=k[i-1][j].dp;
}
}
int best=1;
for(int i=2;i<=m;i++) if(k[n][best].dp<k[n][i].dp) best=i;
printf("%d\n",k[n][best].dp);
for(int i=n,j=best;i>0&&j>0;){
if(k[i][j].x) ans.push(busket[k[i][j].x-1]);
tmp=k[i][j],i=tmp.a,j=tmp.b;
}
while(!ans.empty()) printf("%d ",ans.top()),ans.pop();
return 0;
}
二倍经验 P10954。
#include<bits/stdc++.h>
using namespace std;
const int N=3106;
int n,A[N],B[N],dp[N][N];
vector<int> busket;
stack<int> ans;
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&A[i]),busket.push_back(A[i]);
for(int i=1;i<=n;i++) scanf("%d",&B[i]),busket.push_back(B[i]);
sort(busket.begin(),busket.end());
auto End=unique(busket.begin(),busket.end());
for(int i=1;i<=n;i++) A[i]=lower_bound(busket.begin(),End,A[i])-busket.begin()+1;
for(int i=1;i<=n;i++) B[i]=lower_bound(busket.begin(),End,B[i])-busket.begin()+1;
for(int i=1,Plan;i<=n;i++){
Plan=0;
for(int j=1;j<=n;j++){
dp[i][j]=dp[i-1][j];
if(A[i]==B[j])
dp[i][j]=max(Plan+1,dp[i][j]);
else if(A[i]>B[j])
Plan=max(Plan,dp[i-1][j]);
}
}
int ans=-1;
for(int i=1;i<=n;i++) ans=max(ans,dp[n][i]);
printf("%d\n",ans);
return 0;
}
总结:
首先考虑到状态只能设二维,先不考虑 VAI 情况,
至多有一维实现 ATE,另一维必须卡紧,否则需要加入一维。
于是设状态 \(f(i,j)\) 表示卡住 \(A_i\) 对于 \(B_j\) 及其以前 ATE。
那么写出转移:
\(f(i,j)=\max\set{f(i,j-1),\mathrm{[if\ A_i=B_j]}\max\limits_{i'<i,A_{i'}<A_i} f(i',j-1)+1}\)
注意到 j 始终在变,直接转移无法实现 PST,
于是更改转移顺序,先枚举 \(j\) 后枚举 \(i\),\(A_i\) 不变,可以 PST。
VAI:他死了吗?
设 \(f(i,j)\) 表示卡 \(A_i\) 答案为 \(j\) 对应 \(B\) 最小位置。
\(f(i,j)=nxt_{A_i\ in\ B}\set{\min\limits_{i'<i,A_{i'}<A_i}(f(i',j-1))}\)
树状数组貌似 \(O(n^2\log n)\) 能过。
进一步优化状态设计,进行 ATE,(预先离散化)
设 \(f(i,j)\) 表示枚举至当前位置,结尾不大于 \(i\) 且答案为 \(j\) 最小 \(B\) 下标。
转移 \(f(A_i,j)=\min \set{(A_i-1,j),f(A_i-1,j-1).nxt}\),
但是这里受 ATE 状态限制需要递推保证单调性,综合来看三次方。
考虑调整转移顺序,先枚举 \(j\),每次转移结束后再统一递推保单调。
综合时间复杂度 \(O(n^2)\)。

浙公网安备 33010602011771号