钱易数据结构杂题选讲

【IOI2021】registers

【IOI2021】dungeons

考虑一个关键性质:打死一个怪以后,英雄的体力值会增长怪物的血量,也就是说,如果英雄打死了一个和自己势均力敌的怪物,那么其血量会成倍增长。

发现了这个性质之后,我们考虑对权值分层,即设第 \(i\) 层表示血量区间 \([2^i,2^{i+1})\),假设现在英雄血量位于第 \(i\) 层表示的区间,那显然血量 \(<2^i\) 的怪都可以被英雄秒掉,而对于血量 \(≥2^i\) 的怪,我们不知道其是否能被英雄打败,因此我们考虑这样的策略:先假设它们都不能被英雄打败,这样每次英雄经过血量 \(<2^i\) 的房间就会获得 \(s_j\) 的血量并到达房间 \(w_i\),经过血量 \(≥2^i\) 的房间就会获得 \(p_j\) 的血量并到达房间 \(l_i\)。我们考虑对此做倍增,即设:

  • \(to[i][j][k]\) 表示在第 \(i\) 层,从 \(j\) 开始,每经过一个血量 \(<2^i\) 的怪都到达 \(w_i\),经过一个 \(≥2^i\) 的怪都到达 \(l_i\),这样走 \(2^k\) 步后会到达哪个房间。

  • \(sum[i][j][k]\) 表示在第 \(i\) 层,从 \(j\) 开始,每经过一个血量 \(<2^i\) 的怪都积累 \(s_i\) 的血量,经过一个 \(≥2^i\) 的怪都积累 \(p_i\) 的血量,这样走 \(2^k\) 步后血量会增加多少。

  • \(upp[i][j][k]\) 表示在第 \(i\) 层,从 \(j\) 开始,初始血量至多为多少,才能保证在接下来 \(2^k\) 步中打不败任何一个 \(≥2^i\) 的怪。

上面三个数组都可以在预处理时候求出。这样每次查询时,每进入一层,就倍增找到后面第一个可以打败的血量 \(≥2^i\) 的怪,根据上面的推论,打掉这个怪之后肯定会进入下一层,因此总复杂度 \(O((n+q)\log^2n)\),由于 \(n\) 较大,直接这样做会 \(\text{MLE}\)

发现对于血量不在 \([2^i,2^{i+1})\) 之间的怪,操作是比较简单的,考虑将 \([2^i,2^{i+1})\) 之间的怪单独拿出来。

  • \(to[i][j]\) 表示在第 \(i\) 层,从 \(j\) 开始,遇到的第一个血量在 \([2^i,2^{i+1})\) 之间的怪。

  • \(sum[i][j]\) 表示在第 \(i\) 层,从 \(j\) 开始,遇到的第一个血量在 \([2^i,2^{i+1})\) 之间的怪时血量增加了多少。

  • \(pos[j][k]\) 表示从 \(j\) 开始,在当前层内跳 \(k\) 步会跳到哪里。

  • \(val[j][k]\) 表示从 \(j\) 开始,每经过一个血量小于当前层的怪都积累 \(s_i\) 的血量,否则积累 \(p_i\) 的血量,这样走 \(2^k\) 步后血量会增加多少。

  • \(upp[j][k]\) 表示从 \(j\) 开始,初始血量至多为多少,才能保证在接下来 \(2^k\) 步中打不败任何一个血量高于当前层的怪。

我们从小到大枚举层数,如果遇到第一个血量在 \([2^i,2^{i+1})\) 之间的怪时,英雄的血量已经超过了 \(2^{i+1}\) ,那么这一层的怪物就不会形成阻碍,可以按照下一层来处理。

否则,跳到这个点,然后在不超过当前层的地方倍增地向前跳,跳到第一个可以升到上一层的点即可,总时间复杂度仍然是 \(O((n+q)\log^2n)\) ,空间 \(O(n\log n)\)

点击查看代码
#include"dungeons.h"
#include<bits/stdc++.h>
using namespace std;
int n,rnd,lb,ub;
int s[400005],p[400005],w[400005],l[400005];
int nxt[25][400005],pos[25][400005];
long long sum[25][400005],val[25][400005],upp[25][400005];
bool vis[400005];
void dfs(int x){
	if(vis[x])return ;
	vis[x]=1;
	int z=s[x]<lb?w[x]:l[x];
	dfs(z);
	nxt[rnd][x]=nxt[rnd][z];
	sum[rnd][x]=sum[rnd][z]+(s[x]<lb?s[x]:p[x]);
}
void init(int n,vector<int> S,vector<int> P,vector<int> W,vector<int> L){
	::n=n;
	for(int i=1;i<=n;i++)s[i]=S[i-1];
	for(int i=1;i<=n;i++)p[i]=P[i-1];
	for(int i=1;i<=n;i++)w[i]=W[i-1]+1;
	for(int i=1;i<=n;i++)l[i]=L[i-1]+1;
	for(rnd=0;rnd<=24;rnd++){
		lb=(1<<rnd);ub=2*lb-1;
		for(int i=1;i<=n;i++)vis[i]=0;
		vis[n+1]=1;nxt[rnd][n+1]=n+1;sum[rnd][n+1]=0;
		for(int i=1;i<=n;i++){
			if(lb<=s[i]&&s[i]<=ub)vis[i]=1,nxt[rnd][i]=i,sum[rnd][i]=0;
		}
		for(int i=1;i<=n;i++)dfs(i);
		for(int i=1;i<=n;i++){
			if(lb<=s[i]&&s[i]<=ub){
				pos[0][i]=nxt[rnd][l[i]];val[0][i]=sum[rnd][l[i]]+p[i];
				upp[0][i]=max(0ll,ub-val[0][i]);
				if(upp[0][i]>=s[i])upp[0][i]=s[i]-1;
			}
		}
	}
	for(int i=1;i<=24;i++){
		for(int j=1;j<=n;j++){
			pos[i][j]=pos[i-1][pos[i-1][j]];
			val[i][j]=val[i-1][j]+val[i-1][pos[i-1][j]];
			upp[i][j]=min(upp[i-1][j],max(0ll,upp[i-1][pos[i-1][j]]-val[i-1][j]));
		}
	}
}
long long simulate(int x,int Z){
	long long z=Z;++x;
	for(int rnd=0;rnd<=24;++rnd){
		int lb=(1<<rnd),ub=2*lb-1;
		if(nxt[rnd][x]&&z+sum[rnd][x]<=ub){
			z+=sum[rnd][x];x=nxt[rnd][x];
			if(x==n+1)return z;
			for(int i=24;i>=0;i--){
				if(z<=upp[i][x]&&pos[i][x])z+=val[i][x],x=pos[i][x];
			}
			if(x==n+1)return z;
			if(z<s[x])z+=p[x],x=l[x];
			else z+=s[x],x=w[x];
			if(x==n+1)return z;
		}
	}
	z+=sum[24][x];
	return z;
}

#671. 【UNR #5】诡异操作

首先可以想到一个 \(O(128×n+128×q\log n)\) 的做法,空间开不下,时间也过不去。

考虑这一做法在计算机中的储存方式,每一个节点维护区间内 \(128\) 个数位每位 \(1\) 的数量,每个数量用 \(1\)\(32\) 位的 \(int\) 来储存,实际上有用的不超过 \(\log(r−l+1)\) 位。

容易发现这是一个 \(128×\log(r−l+1)\)\(01\) 表,我们考虑沿对角线翻转它,复杂度就变成了 \(O(n\log n+q\log^2 n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,q;
inline int read(){
	int res=0;char c=getchar();
	while(c<'0'||c>'9')c=getchar();
	while(c>='0'&&c<='9')res=10*res+c-'0',c=getchar();
	return res;
}
typedef __uint128_t u128;
inline u128 read_u(){
    static char buf[200];
    scanf("%s",buf);u128 res=0;
    for(int i=0;buf[i];i++)res=res<<4|(buf[i]<='9'?buf[i]-'0':buf[i]-'a'+10);
    return res;
}
inline void output(u128 res){
    if(res>=16)output(res/16);
    putchar(res%16>=10?'a'+res%16-10:'0'+res%16);
}
u128 *vec[1200005],pool[2000005],*lim=pool;
int empty[1200005],cnt[1200005];
u128 a[300005],lazy[1200005];
inline void pushup(int x){
	u128 flag=0;
	for(int i=0;i<cnt[x];i++){
		u128 a=(cnt[x<<1]>i?vec[x<<1][i]:0),b=(cnt[x<<1|1]>i?vec[x<<1|1][i]:0);
		vec[x][i]=a^b^flag;
		flag=((a^b)&flag)|(a&b);
	}
	empty[x]=(empty[x<<1]&empty[x<<1|1]);
}
inline void And(int x,u128 v){
	if(empty[x])return ;
	for(int i=0;i<cnt[x];i++){
		vec[x][i]&=v;
	}
	lazy[x]&=v;
}
inline void pushdown(int i){
	if(~lazy[i])And(i<<1,lazy[i]),And(i<<1|1,lazy[i]);
	lazy[i]=-1;
}
void build(int l=1,int r=n,int i=1){
	while((1<<cnt[i])<=r-l+1)cnt[i]++;
	vec[i]=lim;lim+=cnt[i];lazy[i]=-1;
	if(l==r){
		vec[i][0]=a[l];empty[i]=!a[l];
		return ;
	}
	int mid=(l+r)>>1;
	build(l,mid,i<<1);build(mid+1,r,i<<1|1);
	pushup(i);
}
void Div(int fr,int to,u128 v,int l=1,int r=n,int i=1){
	if(fr>r||to<l||empty[i])return ;
	if(l==r){
		vec[i][0]/=v;empty[i]=(!vec[i][0]);
		return ;
	}
	int mid=(l+r)>>1;pushdown(i);
	Div(fr,to,v,l,mid,i<<1);Div(fr,to,v,mid+1,r,i<<1|1);
	pushup(i);
}
void update(int fr,int to,u128 v,int l=1,int r=n,int i=1){
	if(fr>r||to<l||empty[i])return ;
	if(fr<=l&&to>=r){
		And(i,v);return ;
	}
	int mid=(l+r)>>1;pushdown(i);
	update(fr,to,v,l,mid,i<<1);update(fr,to,v,mid+1,r,i<<1|1);
	pushup(i);
}
u128 query(int fr,int to,int l=1,int r=n,int i=1){
	if(fr>r||to<l||empty[i])return 0;
	if(fr<=l&&to>=r){
		u128 res=0;
		for(int j=0;j<cnt[i];j++)res+=(vec[i][j]<<j);
		return res;
	}
	int mid=(l+r)>>1;pushdown(i);
	return query(fr,to,l,mid,i<<1)+query(fr,to,mid+1,r,i<<1|1);
}
int main(){
	n=read();q=read();
	for(int i=1;i<=n;i++)a[i]=read_u();
	build();
	while(q--){
		int op,l,r;u128 v;
		op=read();l=read();r=read();
		if(op==1){
			v=read_u();
			if(v!=1)Div(l,r,v);
		}
		if(op==2){
			v=read_u();
			update(l,r,v);
		}
		if(op==3)output(query(l,r)),puts("");
	}

	return 0;
}

#592. 新年的聚会

对于一张 \(m\) 条边的图,我们有办法将它的点集分为 \(O(\sqrt m)\) 个独立集。

我们可以通过至多 \(n\sqrt m\) 次询问暴力把这些独立集找出来,考虑两个独立集之间的连边,将较大独立集划分为两个部分,对于与较小独立集有连边的部分递归下去,这样我们就可以通过 \(\log n\) 次询问找出一条边,我们可以通过 \(m\log n\) 询问找出所有的边。

点击查看代码
#include<bits/stdc++.h>
#include "meeting.h"
using namespace std;
vector<pair<int,int> >edge;
vector<int> s[1005];
inline void conquer(int id1,int id2,int l1,int r1,int l2,int r2){
	if(r1-l1>r2-l2){
		swap(id1,id2); 
		swap(l1,l2);
		swap(r1,r2);
	}
	if(l1==r1&&l2==r2){
		edge.emplace_back(s[id1][l1],s[id2][l2]);
		return ;
	}
	int mid=(l2+r2)>>1;
	vector<int> A,B;
	for(int i=l1;i<=r1;++i){
		A.emplace_back(s[id1][i]);
		B.emplace_back(s[id1][i]);
	}
	for(int i=l2;i<=mid;++i) A.emplace_back(s[id2][i]);
	for(int i=mid+1;i<=r2;++i) B.emplace_back(s[id2][i]);
	if(!meeting(A)) conquer(id1,id2,l1,r1,l2,mid);
	if(!meeting(B)) conquer(id1,id2,l1,r1,mid+1,r2);
	return ;
}
int cnt=0;
vector<pair<int,int> >solve(int n){
	for(int i=0;i<n;++i){
		bool mark=0;
		for(int j=1;j<=cnt;++j){
			s[j].emplace_back(i);
			if(meeting(s[j])){
				mark=1;
				break;
			}
			s[j].pop_back();
		}
		if(!mark) s[++cnt].emplace_back(i);
	}
	for(int i=1;i<=cnt;++i){
		for(int j=i+1;j<=cnt;++j){
			int siz1=s[i].size(),siz2=s[j].size();
			conquer(i,j,0,siz1-1,0,siz2-1);
		}
	}
	return edge;
}

#604. 【UER #9】赶路

考虑分治,令 \(\text{solve(s,vec,t)}\) 表示构造一条从 \(s\) 点出发,经过 \(\text{vec}\) 中的点,到达 \(t\) 的合法路径。

我们只需从 \(\text{vec}\) 中随便选出一点 \(\text{mid}\) ,做向量 \(s\to \text{mid}\) ,将 \(\text{vec}\) 中与 \(t\)\(s\to \text{mid}\) 同侧的放入 \(R\) 集合,否则放入 \(L\) 集合,有:

\[\text{solve(s,vec,t)}=\text{solve(s,L,mid)}+\text{mid}+ \text{solve(mid,R,t)} \]

期望复杂度 \(O(n\log n)\),最劣复杂度 \(O(n^2)\) ,数据范围很小,没有明显区别。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int T;
int n;
struct node{
	long long x,y;
	node(long long _x=0,long long _y=0){
		x=_x;y=_y;
	}
	inline node operator -(const node &b)const{
		return node(x-b.x,y-b.y);
	}
	inline long long operator *(const node &b)const{
		return x*b.y-y*b.x;
	}
}a[505];
vector<int> Get(int s,vector<int> vec,int t){
	if(vec.empty())return vec;
	auto mid=vec.back();vec.pop_back();
	long long tmp=(a[mid]-a[s])*(a[t]-a[s]);tmp/=abs(tmp);
	vector<int> L,R;
	for(auto it:vec){
		if((a[mid]-a[s])*(a[it]-a[s])*tmp<0)L.push_back(it);
		else R.push_back(it);
	}
	auto l=Get(s,L,mid),r=Get(mid,R,t);
	vector<int> res;
	for(auto it:l)res.push_back(it);
	res.push_back(mid);
	for(auto it:r)res.push_back(it);
	return res;
}
inline void solve(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		int x,y;scanf("%d%d",&x,&y);
		a[i]=node(x,y);
	}
	vector<int> tmp;
	for(int i=2;i<n;i++)tmp.push_back(i);
	auto res=Get(1,tmp,n);
	printf("%d ",1);
	for(auto it:res)printf("%d ",it);
	printf("%d\n",n);
}
int main(){
	scanf("%d",&T);
	while(T--)solve();

	return 0;
}

CF1208H Red Blue Tree

CF566C Logistical Questions

假设现在重心为 \(u\),考虑向 \(v\) 移动会不会更优。

设现在重心在 \((u,v)\) 这条边上,离 \(u\) 的距离为 \(x\),那么权值为:

\[\begin{aligned} \sum_{i\notin tree_v} a_i(d_i+x)^{3\over 2}+\sum_{i\in tree_v} a_i(d_i-x)^{3\over 2} \end{aligned} \]

求导之后可得:

\[\begin{aligned} \sum_{i\notin tree_v} {3\over 2}a_i(d_i+x)^{1\over 2}-\sum_{i\in tree_v} {3\over 2}a_i(d_i-x)^{1\over 2} \end{aligned} \]

只需判断一下此时是否有 \(v\) 满足导数大于 \(0\) 即可,若有则往 \(v\) 移动,我们每次可以以点分治的重心作为 \(u\) 来进行上面的过程,这样最多只会移动 \(O(\log)\) 次,总时间复杂度为\(O(n\log n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
int ver[400005],ne[400005],head[400005],cnt,val[400005];
inline void link(int x,int y,int v){
	ver[++cnt]=y;
	ne[cnt]=head[x];
	head[x]=cnt;val[cnt]=v;
}
long long dis[200005];
int siz[200005],rt,mxp[200005];
bool vis[200005];
void findrt(int x,int fi,int tot){
	siz[x]=1;mxp[x]=0;
	for(int i=head[x];i;i=ne[i]){
		int u=ver[i];
		if(vis[u]||u==fi)continue;
		findrt(u,x,tot);siz[x]+=siz[u];
		mxp[x]=max(mxp[x],siz[u]);
	}
	mxp[x]=max(mxp[x],tot-siz[x]);
	if(mxp[rt]>mxp[x])rt=x;
}
double tmp,f[200005];
void calc(int x,int fi,int top,double dep){
	tmp+=dis[x]*dep*sqrt(dep);
	f[top]+=dis[x]*sqrt(dep);
	for(int i=head[x];i;i=ne[i]){
		int u=ver[i];
		if(u==fi)continue;
		calc(u,x,top,dep+val[i]);
	}
}
double ANS;
int ans;
void solve(int x){
	vis[x]=1;tmp=0;
	double sum=0;
	for(int i=head[x];i;i=ne[i]){
		int u=ver[i];
		f[u]=0;calc(u,x,u,val[i]);
		sum+=f[u];
	}
	if(!ans||tmp<ANS)ANS=tmp,ans=x;
	for(int i=head[x];i;i=ne[i]){
		int u=ver[i];
		if(vis[u])continue;
		if(2*f[u]>sum){
			mxp[rt=0]=n;findrt(u,u,siz[u]);
			solve(rt);break;
		}
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%lld",&dis[i]);
	for(int i=1;i<n;i++){
		int x,y,v;scanf("%d%d%d",&x,&y,&v);
		link(x,y,v);link(y,x,v);
	}
	mxp[rt=0]=n;findrt(1,1,n);
	solve(rt);
	printf("%d %.10lf\n",ans,ANS);

	return 0;
}



CF1083C Max Mex

我们要找的是一个最小的 \(x\) 使得包含编号为 \([1,x]\) 的点在树上的最小连通块不是一条链。

对于一个区间 \([l,r]\) ,区间内的点在树上的形态是可以维护并快速合并的,线段树维护每个区间内的点是否在同一条链上,以及链的两个端点,线段树上二分即可,时间复杂度 \(O(n\log^2 n)\)\(O(n\log n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,q;
int ver[400005],ne[400005],head[200005],tot;
inline void link(int x,int y){
	ver[++tot]=y;
	ne[tot]=head[x];
	head[x]=tot;
}
int siz[200005],son[200005],fa[200005],dep[200005];
void dfs1(int x,int fi){
	siz[x]=1;fa[x]=fi;dep[x]=dep[fi]+1;
	for(int i=head[x];i;i=ne[i]){
		int u=ver[i];
		if(u==fi)continue;
		dfs1(u,x);siz[x]+=siz[u];
		if(siz[u]>siz[son[x]])son[x]=u;
	}
}
int top[200005],dfn[200005],cnt;
void dfs2(int x,int fi){
	top[x]=fi;dfn[x]=++cnt;
	if(son[x])dfs2(son[x],fi);
	for(int i=head[x];i;i=ne[i]){
		int u=ver[i];
		if(u==fa[x]||u==son[x])continue;
		dfs2(u,u);
	}
}
inline int lca(int x,int y){
	while(top[x]!=top[y]){
		if(dep[top[x]]>dep[top[y]])x=fa[top[x]];
		else y=fa[top[y]];
	}return dep[x]>dep[y]?y:x;
}
inline int dist(int x,int y){
	int lc=lca(x,y);
	return dep[x]+dep[y]-2*dep[lc];
}
inline bool in(int x,int y,int z){
	return dist(x,z)+dist(z,y)==dist(x,y);
}
struct node{
	int l,r;bool flag;
	node(int _l=0,int _r=0,bool _flag=1){
		l=_l;r=_r;flag=_flag;
	}
	inline node operator +(const node &b)const{
		if(flag||b.flag)return node(0,0,1);
		{
			node res(l,r,0);
			if(in(res.l,res.r,b.l)&&in(res.l,res.r,b.r))return res;
		}
		{
			node res(l,b.r,0);
			if(in(res.l,res.r,b.l)&&in(res.l,res.r,r))return res;
		}
		{
			node res(b.l,r,0);
			if(in(res.l,res.r,l)&&in(res.l,res.r,b.r))return res;
		}
		{
			node res(b.l,b.r,0);
			if(in(res.l,res.r,l)&&in(res.l,res.r,r))return res;
		}
		{
			node res(l,b.l,0);
			if(in(res.l,res.r,r)&&in(res.l,res.r,b.r))return res;
		}
		{
			node res(r,b.r,0);
			if(in(res.l,res.r,l)&&in(res.l,res.r,b.l))return res;
		}
		return node(0,0,1);
	}
}tree[800005];
void update(int loc,int v,int l=0,int r=n,int i=1){
	if(loc<l||loc>r)return ;
	if(l==r){
		tree[i]=node(v,v,0);
		return ;
	}
	int mid=(l+r)>>1;
	update(loc,v,l,mid,i<<1);update(loc,v,mid+1,r,i<<1|1);
	tree[i]=tree[i<<1]+tree[i<<1|1];
}
void query(node v=node(0,0,1),int l=0,int r=n,int i=1){
	if(l==r){
		printf("%d\n",l);
		return ;
	}
	int mid=(l+r)>>1;node tmp=tree[i<<1];
	if(l)tmp=v+tmp;
	if(tmp.flag)query(v,l,mid,i<<1);
	else query(tmp,mid+1,r,i<<1|1);
}
int a[200005];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=2;i<=n;i++){
		int x;scanf("%d",&x);
		link(x,i);link(i,x);
	}
	dfs1(1,1);
	dfs2(1,1);
	for(int i=1;i<=n;i++)update(a[i],i);
	scanf("%d",&q);
	while(q--){
		int op;scanf("%d",&op);
		if(op==1){
			int x,y;scanf("%d%d",&x,&y);
			swap(a[x],a[y]);update(a[x],x);update(a[y],y);
		}
		else query();
	}

	return 0;
}





数据结构题一

考虑 \(k=2\) 时的情况,答案就是直径,我们将直径的一个端点作为根,答案相当于选 \(k-1\) 条自上而下的链,第一个选的一定是直径,将直径上的权值设为 \(0\) ,然后再选择一条最长链,以此类推。

容易发现这样一来求出的就是长链剖分后前 \(k-1\) 长的长链,考虑怎样维护这些链,当我们给一条点加上一个权值时,要向上更新,看看将链顶父亲的重儿子改为链顶是否会更优,这个操作可以用 \(\text{LCT}\) 维护,当更新到直径时,如果当前的根不在直径上,还需要将根换为直径的另一个端点。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
namespace Treap{
	int rt;
	int le[100005],ri[100005],rk[100005],siz[100005],cnt;
	long long val[100005],tot[100005];
	queue<int> q;
	inline int New(long long v){
		if(!q.empty()){
			int x=q.front();q.pop();le[x]=ri[x]=0;
			val[x]=tot[x]=v;siz[x]=1;rk[x]=rand();
			return x;
		}++cnt;
		val[cnt]=tot[cnt]=v;siz[cnt]=1;rk[cnt]=rand();
		return cnt;
	}
	inline void pushup(int x){
		siz[x]=siz[le[x]]+1+siz[ri[x]];
		tot[x]=tot[le[x]]+val[x]+tot[ri[x]];
	}
	int merge(int x,int y){
		if(!x||!y)return x|y;
		if(rk[x]>rk[y]){
			ri[x]=merge(ri[x],y);
			pushup(x);return x;
		}
		le[y]=merge(x,le[y]);pushup(y);
		return y;
	}
	void split(int x,long long y,int &a,int &b){
		if(!x){a=b=0;return ;}
		if(val[x]>=y){
			split(ri[x],y,ri[x],b);
			pushup(x);a=x;return ;
		}
		split(le[x],y,a,le[x]);pushup(x);b=x;
	}
	inline void insert(long long v){
		int a=0,b=0,c=0;split(rt,v+1,a,b);split(b,v,b,c);b=merge(b,New(v));
		rt=merge(a,merge(b,c));
	}
	inline void del(long long v){
		int a=0,b=0,c=0;split(rt,v+1,a,b);split(b,v,b,c);
		if(b)q.push(b);b=merge(le[b],ri[b]);rt=merge(a,merge(b,c));
	}
	long long Getk(int x,int k){
		if(siz[x]<=k)return tot[x];
		if(siz[le[x]]>=k)return Getk(le[x],k);
		if(siz[le[x]]+1==k)return tot[le[x]]+val[x];
		return tot[le[x]]+val[x]+Getk(ri[x],k-siz[le[x]]-1);
	}
	void print(int x=rt){
		if(!x)return ;
		print(le[x]);
		printf("%lld ",val[x]);
		print(ri[x]);
	}
}
namespace LCT{
	int son[2][100005],fa[100005];
	inline bool isroot(int x){
		return son[0][fa[x]]!=x&&son[1][fa[x]]!=x;
	}
	long long val[100005],siz[100005];
	inline void pushup(int x){
		siz[x]=siz[son[0][x]]+val[x]+siz[son[1][x]];
	}
	inline void rotate(int x){
		int y=fa[x],z=fa[y];
		if(!isroot(y))son[son[1][z]==y][z]=x;
		bool is=(son[1][y]==x);
		son[is][y]=son[!is][x];fa[son[is][y]]=y;
		son[!is][x]=y;fa[y]=x;fa[x]=z;pushup(y);pushup(x);
	}
	bool rev[100005];
	inline void push(int x){
		if(rev[x]){
			swap(son[0][x],son[1][x]);
			rev[son[0][x]]^=1;rev[son[1][x]]^=1;
		}rev[x]=0;
	}
	int stk[100005],top;
	inline void splay(int x){
		stk[++top]=x;
		for(int i=x;!isroot(i);i=fa[i])stk[++top]=fa[i];
		while(top)push(stk[top--]);
		while(!isroot(x)){
			int y=fa[x],z=fa[y];
			if(!isroot(y)){
				if((son[1][y]==x)^(son[1][z]==y))rotate(x);
				rotate(y);
			}rotate(x);
		}
	}
	inline int Top(int x){
		while(!isroot(x))x=fa[x];
		return x;
	}
	inline void access(int x,long long v){
		splay(x);Treap::del(siz[x]);val[x]+=v;siz[x]+=v;
		int i=x;x=fa[x];
		while(x){
			splay(x);int z=son[1][x];
			if(!x)break;
			if(!fa[Top(x)]){
				int T=Top(x);
				if(2*siz[z]>siz[T]-val[x]){
					while(son[1][z])z=son[1][z];//cout<<"makeroot "<<z<<endl;
					splay(z);rev[z]^=1;splay(x);z=son[1][x];
				}
			}
			if(siz[z]>=siz[i])break;
			Treap::del(siz[x]);Treap::insert(siz[z]);son[1][x]=i;pushup(x);
			i=x;x=fa[x];
		}
		Treap::insert(siz[i]);
	}
}
int n,q;
int ver[200005],ne[200005],head[100005],cnt;
inline void link(int x,int y){
	ver[++cnt]=y;
	ne[cnt]=head[x];
	head[x]=cnt;
}
void dfs(int x,int fi){
	LCT::fa[x]=fi;
	for(int i=head[x];i;i=ne[i]){
		int u=ver[i];
		if(u==fi)continue;
		dfs(u,x);
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<n;i++){
		int x,y;scanf("%d%d",&x,&y);
		link(x,y);link(y,x);
	}
	dfs(1,0);
	for(int i=1;i<=n;i++){
		int w;scanf("%d",&w);
		LCT::access(i,w);
	}
//	Treap::print();
//	puts("\n---");
	scanf("%d",&q);
	while(q--){
		int op;scanf("%d",&op);
		if(op==0){
			int x,y;scanf("%d%d",&x,&y);
			LCT::access(x,y);
		}
		else {
			int k;scanf("%d",&k);
			printf("%lld\n",Treap::Getk(Treap::rt,k-1));
		}
//	Treap::print();
//	puts("\n---");
	}


	return 0;
}

数据结构题二

「JOISC 2019 Day3」穿越时空 Bitaro

首先我们可以消除路程的影响,对于每个 \(i\) ,令 \(L[i]\leftarrow L[i]-i\)\(R[i]\leftarrow R[i]-i-1\)

问题变为连续穿过几个区间,求整个路程中向下的最小距离,显然有贪心解法,且一个区间信息的具有结合律,考虑线段树维护。

一共有两种区间,当区间内的区间交集不为空时,整个区间等价于这个区间内所有区间的交集,否则最优方案一定是从某个高度走到另一个高度,可以通过分类讨论的方式维护。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,q;
int L[300005],R[300005];
struct node{
	bool op;
	int l,r;long long v;
	node(bool _op=0,int _l=-1e9,int _r=1e9,long long _v=0){
		op=_op;l=_l;r=_r;v=_v;
	}
	inline node operator +(node b){
		if(op){
			if(b.op)return node(1,l,b.r,v+max(r-b.l,0)+b.v);
			if(r>b.r)return node(1,l,b.r,v+r-b.r+b.v);
			return node(1,l,max(r,b.l),v+b.v);
		}
		if(b.op){
			if(b.l>=l&&b.l<=r)return b;
			if(b.l<l)return node(1,l,b.r,v+l-b.l+b.v);
			return node(1,r,b.r,v+b.v);
		}
		if(max(l,b.l)<=min(r,b.r))return node(0,max(l,b.l),min(r,b.r),v+b.v);
		if(r<b.l)return node(1,r,b.l,v+b.v);
		return node(1,l,b.r,v+l-b.r+b.v);
	}
}tree[1200005],rtree[1200005];
void build(int l=1,int r=n-1,int i=1){
	if(l==r){
		tree[i]=node(0,L[l]-l,R[l]-l-1,0);
		rtree[i]=node(0,L[l]-n+l-1,R[l]-n+l-2,0);
		return ;
	}
	int mid=(l+r)>>1;
	build(l,mid,i<<1);build(mid+1,r,i<<1|1);
	tree[i]=tree[i<<1]+tree[i<<1|1];
	rtree[i]=rtree[i<<1|1]+rtree[i<<1];
}
void update(int loc,int l=1,int r=n-1,int i=1){
	if(loc<l||loc>r)return ;
	if(l==r){
		tree[i]=node(0,L[l]-l,R[l]-l-1,0);
		rtree[i]=node(0,L[l]-n+l-1,R[l]-n+l-2,0);
		return ;
	}
	int mid=(l+r)>>1;
	update(loc,l,mid,i<<1);update(loc,mid+1,r,i<<1|1);
	tree[i]=tree[i<<1]+tree[i<<1|1];
	rtree[i]=rtree[i<<1|1]+rtree[i<<1];
}
node query(int fr,int to,int l=1,int r=n-1,int i=1){
	if(fr>r||to<l)return node();
	if(fr<=l&&to>=r)return tree[i];
	int mid=(l+r)>>1;
	return query(fr,to,l,mid,i<<1)+query(fr,to,mid+1,r,i<<1|1);
}
node rquery(int fr,int to,int l=1,int r=n-1,int i=1){
	if(fr>r||to<l)return node();
	if(fr<=l&&to>=r)return rtree[i];
	int mid=(l+r)>>1;
	return rquery(fr,to,mid+1,r,i<<1|1)+rquery(fr,to,l,mid,i<<1);
}
int main(){
	scanf("%d%d",&n,&q);
	for(int i=1;i<n;i++)scanf("%d%d",&L[i],&R[i]);
	if(n>1)build();
	while(q--){
		int op;scanf("%d",&op);
		if(op==1){
			int p;scanf("%d",&p);
			scanf("%d%d",&L[p],&R[p]);update(p);
		}
		else {
			int a,b,c,d;
			scanf("%d%d%d%d",&a,&b,&c,&d);
			if(a<c){
				node res=node(1,b-a,b-a,0)+query(a,c-1)+node(1,d-c,d-c,0);
				printf("%lld\n",res.v);
			}
			else {
				node res=node(1,b-n+a-2,b-n+a-2,0)+rquery(c,a-1)+node(1,d-n+c-2,d-n+c-2,0);
				printf("%lld\n",res.v);
			}
		}

	}


	return 0;
}

CF GYM 102979 K. Knowledge Is...

我们将所有区间按左端点从小到大排序,那么不难发现当我们尝试匹配一个区间 \([l_i,r_i]\) 时,如果存在一个候选集合中的区间能与之匹配,我们肯定会贪心地选择能匹配的区间中右端点 \(<l_i\) 且最小的,这个显然可以 \(\text{set}\) 维护。

考虑如下 \(\text{hack}\)

4
1 2
3 5
4 7
6 8

原因在于你选择将 \([1,2]\)\([3,5]\) 匹配后就将这个区间固定下来了,实际上 \([4,7]\) 也能匹配 \([1,2]\),但其不能与 \([6,8]\) 匹配,而 \([3,5]\) 匹配,也就是说实际上 \([4,7]\)\([1,2]\) 匹配更加能够尽可能用完右端点大的区间,因此我们考虑加个反悔的元素:我们将所有已经匹配的区间对中较右的再扔进一个 \(\text{set}\),如果一个区间 \([l_i,r_i]\) 不能匹配候选集合中的区间,我们就考虑反悔集合中右端点最小的区间 \([L,R]\),如果 \(R<r_i\) 我们就用 \([l_i,r_i]\) 匹配 \([L,R]\) 匹配的区间然后将 \([L,R]\) 加入候选集合。

时间复杂度 \(O(n\log n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m,k;
int X[300005],Y[300005],Ans[300005],Cnt;
struct Q1{
	int x,y,id;
	inline bool operator <(const Q1 &B)const{
		return y^B.y?y<B.y:id<B.id;
	};
}S[300005];
struct Q2{
	int x,I1,I2;
	inline bool operator <(const Q2 &B)const{
		return x^B.x?x<B.x:I2<B.I2;
	};
}Tp,Ns;
set<Q2> T1,T2,T3;
set<Q2>::iterator P1,P2;
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)scanf("%d%d",&X[i],&Y[i]),S[i].x=X[i],S[i].y=Y[i],S[i].id=i;
	sort(S+1,S+n+1);
	for(int i=n;i;i--){
		P1=T1.lower_bound((Q2){S[i].y+1,0,0});if(P1==T1.end()) {
			P1=P2=T2.lower_bound((Q2){S[i].y+1,0,0});for(;P2!=T2.end();P2++) T3.insert((Q2){X[(*P2).I2],(*P2).I1,(*P2).I2});T2.erase(P1,T2.end());
			if(T3.empty())T1.insert((Q2){S[i].x,S[i].y,S[i].id});
			else{
				P2=T3.end(),P2--;Tp=*P2;T3.erase(P2);Ns=(Q2){X[Tp.I1],Tp.I2,Tp.I1};T3.count(Ns)&&(T3.erase(Ns),0);Ns.x=X[Tp.I2];T2.count(Ns)&&(T2.erase(Ns),0);
				Ans[S[i].id]=Ans[Tp.I1];
				Ans[Tp.I2]=0;T1.insert((Q2){X[Tp.I2],Y[Tp.I2],Tp.I2});T2.insert((Q2){S[i].x,S[i].id,Tp.I1});T2.insert((Q2){X[Tp.I1],Tp.I1,S[i].id});
			} continue;
		}
		Ans[S[i].id]=Ans[(*P1).I2]=++Cnt;T2.insert((Q2){S[i].x,S[i].id,(*P1).I2});T2.insert((Q2){X[(*P1).I2],(*P1).I2,S[i].id});T1.erase(P1);
	}
	for(int i=1;i<=n;i++){
		if(!Ans[i])Ans[i]=++Cnt;
		printf("%d ",Ans[i]>m?0:Ans[i]);
	}

	return 0;
}

CF GYM 102586 L. Yosupo’s Algorithm

首先考虑最暴力的做法:枚举可以匹配的点对,假设第 \(i\) 个点对的两个点的横坐标分别为 \(a_i,b_i\),那么我们将 \((a_i,b_i)\) 看作二维平面上的一个点,问题就转化为二维数点,离线 \(+\) 树状数组解决。

考虑优化,注意到我们将所有 \(O(n^2)\) 个点对都存下来有点浪费,因此考虑删除一些无用点对。我们考虑将所有点按 \(y\) 坐标排序后分治,分治到区间 \([l,r]\) 时,设 \(mid=\frac{l+r}{2}\),那么有这样一个性质:对于 \([l,mid]\) 中的红点 \(i\)\([mid+1,r]\) 中的蓝点 \(j\),点对 \((i,j)\) 是有用的当且仅当 \(i\)\([l,mid]\) 中红点权值最大的,或者 \(j\)\([mid+1,r]\) 中蓝点权值最大的。

证明异常容易的,考虑反证法,假设对于 \([l,mid]\) 中的红点 \(i\)\([mid+1,r]\) 中的蓝点 \(j\),点对 \((i,j)\)\([L,R]\) 询问的最优答案,但 \(i\) 不是 \([l,mid]\) 中红点权值最大的,且 \(j\) 不是 \([mid+1,r]\) 中蓝点权值最大的,那么我们考虑 \(i,j\) 在不在 \([L,R]\) 中的状态,显然它们要么同时在 \([L,R]\) 中要么同时不在,考虑 \([l,mid]\) 中权值最大的红点 \(X\) 以及 \([mid+1,r]\) 中权值最大的蓝点 \(Y\),如果 \(X,Y\) 在不在 \([L,R]\) 中的状态至少一者与 \(i(j)\) 相同,那么我们把其中一个点换成这个点答案肯定最大,否则 \(X,Y\) 关于 \([L,R]\) 肯定相同,它们肯定会对答案产生更大的贡献。

这样点对数就降到了 \(n\log n\),时间复杂度 \(O(n\log^2n+q\log n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
using pi=std::pair<int,int>;
char ibuf[1<<25|1],*iS=ibuf;
inline int read(){
	int x=0,f=1;
	while(isspace(*iS))++iS;if(*iS=='-')f=-1,++iS;
	while(isdigit(*iS))(x*=10)+=*iS++&15;
	return f*x;
}
int n,q,cx,cp,tx[1200005],ans[1200005],bit[1200005];
pair<int,int> qry[1200005],pr[4000005];
vector<int>vec[1200005];
struct node{
	int x,y,w,col;
	inline bool operator <(const node &b){
		return y<b.y;
	}
}a[200005];
inline void add(int p,int v){
	for(;p<=cx;p+=p&-p)bit[p]=max(bit[p],v);
}
inline int ask(int p){
	int r=-1e9;
	for(;p;p^=p&-p)r=max(r,bit[p]);
	return r;
}
void solve(int l,int r){
    if(l==r) return;
    int mid=(l+r)/2,pos;solve(l,mid),solve(mid+1,r);
    pos=0;
	for(int i=l;i<=mid;i++){
		if(!a[i].col&&a[i].w>a[pos].w)pos=i;
	}
    if(pos)for(int i=mid+1;i<=r;i++){
		if(a[i].col)pr[++cp]={pos,i};
	}
    pos=0;
	for(int i=mid+1;i<=r;i++){
		if(a[i].col&&a[i].w>a[pos].w)pos=i;
	}
    if(pos)for(int i=l;i<=mid;i++){
		if(!a[i].col)pr[++cp]={i,pos};
	}
}
int main(){
    fread(ibuf,1,1<<25,stdin),n=read();
    for(int i=1;i<=n;i++){
		a[i]={tx[++cx]=read(),read(),read(),0};
	}
    for(int i=1;i<=n;++i){
		a[i+n]={tx[++cx]=read(),read(),read(),1};
	}
    sort(a+1,a+n+n+1,[](const node&a,const node&b){return a.y<b.y;});
	solve(1,n+n);
	q=read();
	memset(ans+1,-1,4*q);
    for(int i=1;i<=q;i++){
		qry[i]={read(),read()},tx[++cx]=qry[i].first;tx[++cx]=qry[i].second;
	}
    sort(tx+1,tx+cx+1),cx=std::unique(tx+1,tx+cx+1)-tx-1;
    for(int i=1;i<=n+n;i++){
		a[i].x=lower_bound(tx+1,tx+cx+1,a[i].x)-tx;
	}
    for(int i=1;i<=q;i++){
		qry[i]={lower_bound(tx+1,tx+cx+1,qry[i].first)-tx,lower_bound(tx+1,tx+cx+1,qry[i].second)-tx},vec[qry[i].first].push_back(i);
	}
    sort(pr+1,pr+cp+1,[](const pi&i,const pi&j){return a[i.first].x<a[j.first].x;}),memset(bit+1,-0x3f,4*cx);
    for(int i=1,j=1;i<=cx;i++){
		for(;j<=cp&&a[pr[j].first].x<=i;++j) add(cx-a[pr[j].second].x+1,a[pr[j].first].w+a[pr[j].second].w);
		for(int id:vec[i]) ans[id]=std::max(ans[id],ask(cx-qry[id].second+1));
    }
    reverse(pr+1,pr+cp+1);memset(bit+1,-0x3f,4*cx);
    for(int i=cx,j=1;i;i--){
		for(;j<=cp&&a[pr[j].first].x>=i;++j) add(a[pr[j].second].x,a[pr[j].first].w+a[pr[j].second].w);
		for(int id:vec[i]) ans[id]=std::max(ans[id],ask(qry[id].second));
    }
    for(int i=1;i<=q;i++)printf("%d\n",ans[i]);


	return 0;
}

[Ynoi2019] 美好的每一天~ 不连续的存在

posted @ 2022-08-05 11:30  一粒夸克  阅读(158)  评论(0编辑  收藏  举报