Loading

【小结】JOISC 2021 Final

T1: とてもたのしい家庭菜園 4

区间加,比较套路的做法是差分,转化为单点加。

单峰序列等价于差分序列存在一个断点,断点之前都是正整数,断点之后都是负整数。

我们枚举一下断点即可。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 200005
using namespace std;
int n,a[N];long long p[N],q[N];
int main(){
	scanf("%d",&n);
	rep(i,1,n)scanf("%d",&a[i]);
	rep(i,1,n-1)a[i]=a[i+1]-a[i];
	rep(i,1,n-1)p[i]=p[i-1]+max(0,1-a[i]);
	pre(i,n-1,1)q[i]=q[i+1]+max(0,a[i]+1);
	long long mn=0x7fffffffffffffffLL;
	rep(i,1,n)mn=min(mn,max(p[i-1],q[i]));
	printf("%lld\n",mn);
	return 0;
}

T2:雪玉

提供一个好想的方法。

观察到对于第 \(i\) 个雪球,跨过第 \(i-1\) 或第 \(i+1\) 个雪球的贡献为零。

所以我们只用考虑相邻两个雪球之间的一段,多少属于左端点,多少属于右端点。

断点具有单调性,我们直接二分答案即可。

时间复杂度 \(\mathcal{O}(N\log Q)\)

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 200005
#define int long long
using namespace std;
int n,m,ll[N],rr[N],a[N],ans[N];
void calc(int x,int len){
	if(ll[m]+rr[m]<=len){ans[x]+=rr[m],ans[x+1]+=ll[m];return ;}
	int l=1,r=m,ed=m;
	while(l<=r){
		int mid=(l+r)>>1;
		if(ll[mid]+rr[mid]>len)ed=mid,r=mid-1;
		else l=mid+1;
	}
	if(ll[ed]==ll[ed-1])ans[x+1]+=ll[ed],ans[x]+=len-ll[ed];
	else ans[x]+=rr[ed],ans[x+1]+=len-rr[ed];
}
signed main(){
	scanf("%lld%lld",&n,&m);
	rep(i,1,n)scanf("%lld",&a[i]);
	int cur=0;
	rep(i,1,m){
		int x;scanf("%lld",&x);cur+=x;
		ll[i]=max(ll[i-1],-cur);
		rr[i]=max(rr[i-1],cur);
	}
	ans[1]+=ll[m];ans[n]+=rr[m];
	rep(i,1,n-1)calc(i,a[i+1]-a[i]);
	rep(i,1,n)printf("%lld \n",ans[i]);
	return 0;
} 

T3:集合写真

\(a_i<a_{i+1}+2\),等价于 \(a_i-1\le a_{i+1}\)

如果我们将 \(a_i\) 看成台阶,那么每次最多只能下降一个。

所以满足条件的排列一定可以分成若干段连续下降的台阶。

我们定义状态 \(f[i]\) 表示前 \(i\) 个数合法的最小代价。

转移方程为 \(f[i]=\min\limits_{0\le j<i}\{f[j]+calc(j+1,i)\}\)

其中代价函数 calc(l,r) 表示将区间 \([l,r]\) 还原成一个下降的台阶的最小代价。

由于是邻项交换,所以考虑逆序对。

分开讨论。

对于 \(<l\) 的数,在 \(f[l-1]\) 中已经考虑过了,直接忽略。

对于 \(\ge l\)\(\le r\) 的数,计算顺序对 \(i<j\)\(a_i <a_j\) 的个数。

对于 \(>r\) 的数,只会与在它后面的 \(\le r\) 的数产生贡献。

直接 \(\mathcal{O}(N^2)\) 计算 calc 函数,总时间复杂度 \(\mathcal{O}(N^4)\) ,可以得到 \(44\) 分。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 5005
using namespace std;
int n,a[N],f[N];
int calc(int l,int r){
	int sum=0;
	rep(i,1,n)rep(j,i+1,n){
		if(a[i]<l||a[j]<l)continue;
		if(a[i]>r&&a[j]>r)continue;
		if(a[i]<=r&&a[j]<=r)sum+=a[i]<a[j];
		else if(a[i]>r)sum++;
	}
	return sum;
}
int main(){
	scanf("%d",&n);
	rep(i,1,n)scanf("%d",&a[i]);
	memset(f,0x3f,sizeof(f));f[0]=0;
	rep(i,1,n)rep(j,0,i-1)f[i]=min(f[i],f[j]+calc(j+1,i));
	printf("%d\n",f[n]);
	return 0;
}

观察一下发现 calc 函数本质上还是计算逆序对,用两个树状数组维护一下即可。

我们可以通过 calc(l,r) 基础上加一个数快速计算 calc(l,r+1) ,总时间复杂度 \(\mathcal{O}(N^2\log N)\)

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 5005
using namespace std;
int c[2][N],n,a[N],f[N],mat[N];
inline void add(int op,int x,int y){for(;x<=n;x+=x&-x)c[op][x]+=y;}
inline int ask(int op,int x){int sum=0;for(;x;x-=x&-x)sum+=c[op][x];return sum;}
int main(){
	scanf("%d",&n);
	rep(i,1,n)scanf("%d",&a[i]),mat[a[i]]=i;
	memset(f,0x3f,sizeof(f));f[0]=0;
	rep(i,0,n-1){
		memset(c,0,sizeof(c));
		int sum=0;
		rep(j,i+1,n)add(1,mat[j],1);
		rep(j,i+1,n){
			sum+=2*ask(0,mat[j])+i+ask(1,mat[j])-j;
			add(1,mat[j],-1);add(0,mat[j],1);
			f[j]=min(f[j],f[i]+sum);
		}
	}
	printf("%d\n",f[n]);
	return 0;
}

T4:ロボット

如果当前边已经是唯一的边,那么我们可以直接连一条边权为 \(0\) 的有向边。

否则我们可以花费 \(P_i\) 的代价将它变成唯一的边,所以我们连一条边权为 \(P_i\) 的有向边。

然后跑最短路,得到 $0 $ 分的好成绩(

手算以下发现还有两类情况没有考虑到。

首先 \(P_i\) 可以不相等,这意味着我们可以保留当前边,然后花费其它多条边的代价使之成为唯一边可能会更优。

还有一种情况就是例如 \(1-2\)\(2-3\) ,两条边颜色相同,只用任意改动一条边就能使两条边同时成为唯一边。

扩展以下,我们将直接花费 \(P_i\) 代价改变一条边的操作为 \(\mathbf{A}\) 操作,保留当前边而修改其他所有边的操作为 \(\mathbf{B}\) 操作。

那么先进行 \(\mathbf{A}\) 操作,再进行 \(\mathbf{B}\) 操作,第一次 \(\mathcal{A}\) 操作的代价可以省去。

所以对于每条边建立一个虚点,进入这个虚点表示进行一次 \(\mathbf{A}\) 操作,离开这个虚点表示进行一次 \(\mathbf{B}\) 操作 ,然后进行连边操作。

一共 \(N+M\) 个点,\(8M\) 条有向边,时间复杂度 \(\mathcal{O}((N+M)\log (N+M))\)

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 500005
using namespace std;
int n,m,h[N],tot,v[N],idx;long long d[N],c[N<<1];map<int,int>u[N];
struct edge{int to,nxt;long long val;}e[N<<2];
void add(int x,int y,long long z){e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;e[tot].val=z;}
struct node{int y,c,p;};vector<node>a[N];
priority_queue<pair<long long,int> >q;
void dij(){
	memset(d,0x3f,sizeof(d));d[1]=0;q.push(make_pair(0,1));
	while(!q.empty()){
		int x=q.top().second;q.pop();v[x]=1;
		for(int i=h[x];i;i=e[i].nxt)if(d[x]+e[i].val<d[e[i].to])
			d[e[i].to]=d[x]+e[i].val,q.push(make_pair(-d[e[i].to],e[i].to));
		while(!q.empty()&&v[q.top().second])q.pop();
	}
}
int main(){
	scanf("%d%d",&n,&m);idx=n;
	rep(i,1,m){
		int x,y,z,w;scanf("%d%d%d%d",&x,&y,&z,&w);
		node cur;cur.y=y;cur.c=z;cur.p=w;
		a[x].push_back(cur);cur.y=x;a[y].push_back(cur);
	}
	rep(x,1,n){
		for(int i=0;i<(int)a[x].size();i++)if(!c[a[x][i].c])u[x][a[x][i].c]=++idx,c[a[x][i].c]=1;
		for(int i=0;i<(int)a[x].size();i++)c[a[x][i].c]=0;
	}
	rep(x,1,n){
		for(int i=0;i<(int)a[x].size();i++)c[a[x][i].c]+=a[x][i].p;
		for(int i=0;i<(int)a[x].size();i++){
			add(x,a[x][i].y,a[x][i].p);
			add(x,a[x][i].y,c[a[x][i].c]-a[x][i].p);
			add(u[x][a[x][i].c],a[x][i].y,c[a[x][i].c]-a[x][i].p);
			add(x,u[a[x][i].y][a[x][i].c],0);
		}
		for(int i=0;i<(int)a[x].size();i++)c[a[x][i].c]=0;
	}
	dij();printf("%lld\n",d[n]==0x3f3f3f3f3f3f3f3fLL?-1LL:d[n]);
	return 0;
} 

T5:ダンジョン 3

贪心神题。

手算一下不难得到 \(\mathcal{O}(N^2)\) 的贪心。

我们从 \(S\) 点出发,每次选择与当前点距离 \(\le U\)\(B\) 最小的点,如果这个点的 \(B\) 比当前点小,则将能量填充至恰好到达该点,否则就将燃料填满。

直接二分可以做到 \(\mathcal{O}(N^2)\) ,离线一下可以 \(\mathcal{O}(N^2)\)

接下来是最关键的模型转换,我们将这个过程转化为线段覆盖的模型。

对于每一层,我们看成数轴上的一个点,相邻两点的距离就是 \(A\) 。我们将第 \(i\) 个点的位置记作 \(S_i\)

对于在第 \(i\) 层恢复 \(j\) 的能量,相当于以 \(B_i\) 的单位代价覆盖线段 \([S_i,S_i+j]\)

\(U_i\) 的限制条件,相当于以 \(B_i\) 的单位代价,只能选择覆盖线段 \([S_i,S_i+U_i]\)

注意上面的覆盖不一定从 \(S_i\) 开始,因为开始一段可以被前面的点覆盖。

那么如果 \(U\) 是定值,我们只用开一个栈维护覆盖每个区间的最小的代价。时间复杂度 \(\mathcal{O}(N\log N)\)

接着考虑子任务 \(3\)\(T_{i}=N+1\),非常明显地提示了考虑对询问按 \(S\) 从大到小排序的离线。

\(U\) 会变化,考虑观察一下当 \(S,T\) 固定事,随着 \(U\) 的变化答案会怎么变。

随着 \(U\) 的增大,答案越来越小,对于每一层,都是先慢慢覆盖后面的一段,然后停滞不前,接着被前面更优的层覆盖(如果存在更优的),最后被完全覆盖。

这四部分都是关于 \(U\) 的一次函数。

第一部分是形如 \(y=kx(k>0)\) 的函数,对应的区间是从 \(0\) 开始一直到,第 \(i\) 层在碰到后面第一个 \(B_j<B_i\)\(j\) 层之后停止。

第二部分是形如 \(y=a\) 的函数,\(a\) 是常数,对应的区间是停止向后扩展,但还没有被前面更优的区间覆盖。

第三部分是形如 \(y=kx+b(k<0)\) ,的函数,对应的区间是当前层正在被前面第一个 \(\le B_i\) 的区间覆盖。

被完全覆盖后的贡献为 \(0\) ,这一部分可以省略。

这样我们运用子任务 \(2\) 的单调栈找出每一层前面和后面的更优区间。然后倒叙枚举当前区间,用树状数组维护这若干个分段函数的和。

具体操作是用两个树状数组维护每一个位置此时的斜率和截距,\(U\) 的范围很大,需要离散化。

对于 \(T_i\) 任意的情况,考虑转化为子任务 \(3\)

我们发现在 \([S_i,T_i]\) 中与 \(T_i\) 的距离 \(\le U_i\)\(B\) 最小的位置 \(x\) ,覆盖 \([x,N+1]\) 时在 \(T_i\) 后面的覆盖方案与 \([S_i,N+1]\) 相同,在\(T_i\) 前面时是连续的一段,前者是子任务 \(3\) ,后者可以 \(\mathcal{O(1)}\) 求得。所以我们只用先二分出对应区间,然后用 ST表 找出位置 \(x\)

这里给一章官方题解的图。

时间复杂度 \(\mathcal{O}(N\log N)\)

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 200005
#define int long long
using namespace std;
int n,m,a[N],b[N],t,lg[N],c[2][N],o[N],u[N],T,s[N],sta[N],top,pr[N],nx[N],ans[N];
struct node{int s,t,u;}q[N];
namespace st1{
	int f[N][20];
	inline int ck(int x,int y){if(b[x]<b[y])return x;return y;}
	int ask(int l,int r){int cur=lg[r-l+1];return ck(f[l][cur],f[r-(1<<cur)+1][cur]);}
	void init(){
		rep(i,1,n+1)f[i][0]=i;
		rep(j,1,t)rep(i,1,n-(1<<j)+2)f[i][j]=ck(f[i][j-1],f[i+(1<<(j-1))][j-1]);
	}
}
namespace st2{
	int f[N][20];
	inline int ck(int x,int y){if(a[x]>a[y])return x;return y;}
	int ask(int l,int r){int cur=lg[r-l+1];return ck(f[l][cur],f[r-(1<<cur)+1][cur]);}
	void init(){
		rep(i,1,n+1)f[i][0]=i;
		rep(j,1,t)rep(i,1,n-(1<<j)+2)f[i][j]=ck(f[i][j-1],f[i+(1<<(j-1))][j-1]);
	}
}
inline void add(int op,int x,int val){for(;x<=T;x+=x&-x)c[op][x]+=val;}
inline int ask(int op,int x){int sum=0;for(;x;x-=x&-x)sum+=c[op][x];return sum;}
inline void change(int op,int l,int r,int k){
	if(l>r)return;
	l=lower_bound(u+1,u+T+1,l)-u;
	r=upper_bound(u+1,u+T+1,r)-u-1;
	add(op,l,k);add(op,r+1,-k);
}
vector<node>w[N];vector<int>dl[N];
signed main(){
	scanf("%lld%lld",&n,&m);t=log2(n+1);
	lg[0]=-1;rep(i,1,n)lg[i]=lg[i>>1]+1;
	rep(i,1,n)scanf("%lld",&a[i]),s[i+1]=s[i]+a[i];
	rep(i,1,n)scanf("%lld",&b[i]);
	st1::init();st2::init();
	rep(i,1,m)scanf("%lld%lld%lld",&q[i].s,&q[i].t,&q[i].u),o[i]=q[i].u;
	sort(o+1,o+m+1);rep(i,1,m)if(o[i]!=o[i-1])u[++T]=o[i];
	rep(i,1,m){
		int l=q[i].s,r=q[i].t-1,ed=q[i].t-1;
		while(l<=r){
			int mid=(l+r)>>1;
			if(s[q[i].t]-s[mid]<=q[i].u)ed=mid,r=mid-1;
			else l=mid+1;
		}
		int cur=st1::ask(ed,q[i].t-1);
		node now;now.u=q[i].u,now.s=i,now.t=1;
		w[q[i].s].push_back(now);
		now.s=i,now.t=-1;w[cur].push_back(now);
		ans[i]+=(s[q[i].t]-s[cur])*b[cur];
	}
	pre(i,n,1){
		while(top&&b[sta[top]]>=b[i]){
			pr[sta[top]]=i;top--;
		}
		if(top)nx[i]=sta[top];
		sta[++top]=i;
	}
	rep(i,1,n)if(!nx[i])nx[i]=n+1;
	pre(i,n,1){
		change(0,0,s[nx[i]]-s[i],b[i]);
		change(1,s[nx[i]]-s[i]+1,0x7fffffff,b[i]*(s[nx[i]]-s[i]));
		dl[pr[i]].push_back(i);
		for(int j=0;j<(int)dl[i].size();j++){
			int x=dl[i][j];
			change(0,s[x]-s[i],s[nx[x]]-s[i],-b[x]);
			change(1,s[x]-s[i],s[nx[x]]-s[i],b[x]*(s[x]-s[i]));
			change(1,s[nx[x]]-s[i]+1,0x7fffffff,-b[x]*(s[nx[x]]-s[x]));
		}
		for(int j=0;j<(int)w[i].size();j++){
			int cur=lower_bound(u+1,u+T+1,w[i][j].u)-u;
			ans[w[i][j].s]+=w[i][j].t*(w[i][j].u*ask(0,cur)+ask(1,cur));
		}
	}
	rep(i,1,m){
		if(q[i].u<a[st2::ask(q[i].s,q[i].t-1)])puts("-1");
		else printf("%lld\n",ans[i]);
	}
	return 0;
} 
posted @ 2021-06-06 21:11  7KByte  阅读(115)  评论(0编辑  收藏  举报