CodeForces 1334F - Strange Function

eJOI做不动了,来水个以前留下的坑(

洛谷题目页面传送门 & CodeForces题目页面传送门

题意见洛谷里的翻译。(翻译得够清楚了吧,translated by Alex_Wei/se/qq)

先探索\(f(x)=y\)\(\forall i\in[1,|y|],y_i=x_{z_i}\)的充要条件。显然\(z_1=1\)。不妨设\(z_{|y|+1}=|x|+1\),那么\(\forall i\in[1,|y|]\):为了让\((z_i,z_{i+1})\)内所有元素都留不下来,前面必定要有元素大于等于它们,又因为\(x_{z_i}\)留下来了,所以\(x_{z_i}\)大于前面所有元素,于是\(\forall j\in(z_i,z_{i+1}),x_j\leq x_{z_i}\)是必要的;为了让\(x_{z_{i+1}}\)(如果存在的话)留下来,它肯定要大于前面所有元素,即\(\forall j\in(z_i,z_{i+1}),x_j<x_{z_{i+1}}\)是必要的。由于\(y_i<y_{i+1}\)(题目保证),综合一下就变成了\(\forall i\in[1,|y|],\forall j\in(z_i,z_{i+1}),x_j\leq x_{z_i}\)作为必要条件。此时不难发现充分性也成立。over。

不妨令\(n=n+1,m=m+1\)并令\(a_n=n,b_m=m\)。设\(id=b^{-1}\)。就有了一个比较显然的DP:若\(a_i\in\{b_j\}\)\(dp_i\)有意义且表示考虑到位置\(i\),若\(a_i\)\(b\)的最后一位,需要花的最小代价。边界\(dp_0=0\),目标\(dp_n\)\(=+\infty\)\(\texttt{No}\))(体现了之前在\(a,b\)结尾分别追加\(n+1,m+1\)的意义,为了方便让\(a\)的最后一位也是\(b\)的最后一位)。

转移的话,考虑枚举决策\(j\),将\((j,i)\)之间该删的删掉,\(p\)值为负的也顺带删一下,则

\[dp_i=\min_{j\in[0,i),a_j=b_{id_{a_i}-1}}\left\{dp_j+\sum_{k=j+1}^{i-1}\left[a_k>b_{id_{a_i}-1}\lor p_k<0\right]p_k\right\} \]

这个\(\sum\)里的东西可以分成两类:\(a_k>b_{id_{a_i}-1},p_k\geq0\)\(p_k<0\),两种都用前缀和变一下形,然后two-pointers优化一下DP即可。其中第\(1\)类的前缀和需要值域BIT\(\log\)求。

\(\mathrm O(n\log 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\n",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\n"<<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\n",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\n"<<dp[n];
	else puts("NO");//目标 
	return 0;
}

最终还是要尛Alex_Wei啊,没有他我根本get不到这题的精髓/cy

posted @ 2020-06-15 23:08  ycx060617  阅读(346)  评论(2编辑  收藏  举报