eJOI做不动了,来水个以前留下的坑(
洛谷题目页面传送门 & CodeForces题目页面传送门
题意见洛谷里的翻译。(翻译得够清楚了吧,translated by Alex_Wei/se/qq)
先探索(f(x)=y)且(forall iin[1,|y|],y_i=x_{z_i})的充要条件。显然(z_1=1)。不妨设(z_{|y|+1}=|x|+1),那么(forall iin[1,|y|]):为了让((z_i,z_{i+1}))内所有元素都留不下来,前面必定要有元素大于等于它们,又因为(x_{z_i})留下来了,所以(x_{z_i})大于前面所有元素,于是(forall jin(z_i,z_{i+1}),x_jleq x_{z_i})是必要的;为了让(x_{z_{i+1}})(如果存在的话)留下来,它肯定要大于前面所有元素,即(forall jin(z_i,z_{i+1}),x_j<x_{z_{i+1}})是必要的。由于(y_i<y_{i+1})(题目保证),综合一下就变成了(forall iin[1,|y|],forall jin(z_i,z_{i+1}),x_jleq x_{z_i})作为必要条件。此时不难发现充分性也成立。over。
不妨令(n=n+1,m=m+1)并令(a_n=n,b_m=m)。设(id=b^{-1})。就有了一个比较显然的DP:若(a_iin{b_j}),(dp_i)有意义且表示考虑到位置(i),若(a_i)是(b)的最后一位,需要花的最小代价。边界(dp_0=0),目标(dp_n)((=+infty)则( exttt{No}))(体现了之前在(a,b)结尾分别追加(n+1,m+1)的意义,为了方便让(a)的最后一位也是(b)的最后一位)。
转移的话,考虑枚举决策(j),将((j,i))之间该删的删掉,(p)值为负的也顺带删一下,则
这个(sum)里的东西可以分成两类:(a_k>b_{id_{a_i}-1},p_kgeq0)和(p_k<0),两种都用前缀和变一下形,然后two-pointers优化一下DP即可。其中第(1)类的前缀和需要值域BIT(log)求。
(mathrm O(nlog n))。(还是挺简单的吧)
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int inf=0x3f3f3f3f3f3f3f3f;
int lowbit(int x){return x&-x;}
const int N=500001,M=N;
int n,m;
int a[N+1],p[N+1],b[M+1];
int id[N+1];
struct bitree{//BIT
int sum[N+1];
void init(){memset(sum,0,sizeof(sum));}
void add(int x,int v){
while(x<=n)sum[x]+=v,x+=lowbit(x);
}
int Sum(int x){
int res=0;
while(x)res+=sum[x],x-=lowbit(x);
return res;
}
int _sum(int l,int r){return Sum(r)-Sum(l-1);}
}bit;
int now[M+1];//two-pointers优化DP
int dp[N+1];
signed main(){
cin>>n;
for(int i=1;i<=n;i++)scanf("%lld",a+i);
n++;a[n]=n;
for(int i=1;i<n;i++)scanf("%lld",p+i);
cin>>m;
for(int i=1;i<=m;i++)scanf("%lld",b+i),id[b[i]]=i;
b[++m]=n;id[n]=m;//追加
bit.init();
memset(now,0x3f,sizeof(now));
now[0]=0;
int ne=0;//目前第2类的前缀和
for(int i=1;i<=n;i++){//DP
if(id[a[i]]){
if(now[id[a[i]]-1]<inf)dp[i]=now[id[a[i]]-1]+bit._sum(b[id[a[i]]-1]+1,n)+ne;
else dp[i]=inf;//计算DP值
if(p[i]>=0)bit.add(a[i],p[i]);
else ne+=p[i];//加入a[i],更新某类的前缀和
if(dp[i]<inf)now[id[a[i]]]=min(now[id[a[i]]],dp[i]-bit._sum(a[i]+1,n)-ne);//two-pointers
// printf("dp[%lld]=%lld
",i,dp[i]);
}
else{//加入a[i],更新某类的前缀和
if(p[i]>=0)bit.add(a[i],p[i]);
else ne+=p[i];
}
}
if(dp[n]<inf)cout<<"YES
"<<dp[n];
else puts("NO");//目标
return 0;
}
然鹅听wjz鸽鸽说有(mathrm O(n))的方法但是不会(orz),为了吊打他,我当然要把(mathrm O(n))方法做出来啊(
于是苦思冥想,想怎么不带(log)地求第(1)类的前缀和,结果发现这就是个二维数点啊怎么可能不带(log)。。就zbl。于是去看(mathrm O(n))题解,恍然大悟,太妙了吧/qiang
不妨重构状态转移方程。从整体来考虑,计算(dp_i)的时候从一开始一共删去了哪些第(1)类元素呢(第(2)类照原来方法计算)?以下标为横坐标,值为纵坐标,不难发现删去的是一个直角顶点在左上角的直角三角形框住的内部,因为随坐标增大,删除的值的下限(b_{id_{a_i}-1})越高。原来的状态转移方程是延(y)轴的平行线切片的,现在我们延(x)轴的平行线切片,每到计算(dp_i)时就加上切出来的那一片的纵坐标段内所有的元素(因为横坐标是一个前缀),然后two-pointers稍微维护一下即可。至于怎么知道每个元素属于哪个切片?直观上应该是lower_bound
的,但那样是带(log)的,于是离线装桶+two-pointers即可线性。
我知道你不懂我在讲什么,没关系的,我自己看懂就可以了((
(mathrm O(n))。
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pb push_back
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=500001,M=N;
int n,m;
int a[N+1],p[N+1],b[M+1];
int id[N+1];
vector<int> pos[N+1];
int lwb[N+1];//two-pointers算出来的lower_bound值
int add[M+1];//此切片目前应该加的
int now[M+1];//two-pointers优化DP
int dp[N+1];
signed main(){
cin>>n;
for(int i=1;i<=n;i++)scanf("%lld",a+i);
n++;a[n]=n;
for(int i=1;i<n;i++)scanf("%lld",p+i);
cin>>m;
for(int i=1;i<=m;i++)scanf("%lld",b+i),id[b[i]]=i;
b[++m]=n;id[n]=m;//追加
for(int i=1;i<=n;i++)pos[a[i]].pb(i);//装桶
for(int i=1,j=1;i<=n;i++){//two-pointers计算lwb
if(b[j]<i)j++;
lwb[i]=j;
}
memset(now,0x3f,sizeof(now));
now[0]=0;
int ne=0;//目前第2类的前缀和
for(int i=1;i<=n;i++){//DP
if(id[a[i]]){
if(now[id[a[i]]-1]<inf)dp[i]=now[id[a[i]]-1]+add[id[a[i]]]+ne;
else dp[i]=inf;//计算DP值
if(p[i]>=0)add[lwb[a[i]]]+=p[i];
else ne+=p[i];//加入a[i],更新某类的前缀和
if(dp[i]<inf)now[id[a[i]]]=min(now[id[a[i]]],dp[i]-ne);//two-pointers
// printf("dp[%lld]=%lld
",i,dp[i]);
}
else{//加入a[i],更新某类的前缀和
if(p[i]>=0)add[lwb[a[i]]]+=p[i];
else ne+=p[i];
}
}
if(dp[n]<inf)cout<<"YES
"<<dp[n];
else puts("NO");//目标
return 0;
}
最终还是要尛Alex_Wei啊,没有他我根本get不到这题的精髓/cy