数据结构专练(北京集训)

A. [CF522D] Closest Equals

显然只有 \(O(n)\) 个数对(相邻的相同点)有可能对答案产生贡献,拉出来二维数点即可 \(O(n\log n)\)

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int n,m,tot,c[N],ans[N];
unordered_map<int,int>mp;
struct msq{
	int x,y,op;
}cc[N*2];
inline bool cmp(msq x,msq y){
	return x.x!=y.x?x.x<y.x:x.y!=y.y?x.y<y.y:x.op<y.op;
}
inline void init(){
	for(int i=1;i<=n;i++) c[i]=1e9;
}
inline void add(int x,int v){
	for(;x<=n;x+=x&-x) c[x]=min(c[x],v);
}
inline int minn(int x){
	int re=1e9;
	for(;x;x-=x&-x) re=min(re,c[x]);
	return re;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m,init();
	for(int i=1,a;i<=n;i++){
		cin>>a;
		if(mp.count(a))
			cc[++tot]={n-mp[a]+1,i,mp[a]-i};
		mp[a]=i;
	}
	for(int i=1,l,r;i<=m;i++)
		cin>>l>>r,cc[++tot]={n-l+1,r,i};
	sort(cc+1,cc+tot+1,cmp);
	for(int i=1;i<=tot;i++){
		if(cc[i].op>0) ans[cc[i].op]=minn(cc[i].y);
		else add(cc[i].y,-cc[i].op);
	}
	for(int i=1;i<=m;i++)
		cout<<(ans[i]==1e9?-1:ans[i])<<"\n";
	return 0;
}

B. [Ynoi2004] rsxc

考虑该问题等价于 \(x\) 个数能异或出 \(x\) 个数的区间数量。根据线性基相关知识,我们可以发现区间中数的种类数为 \(2^k\),线性基大小为 \(k\),且必须有 \(0\)

对于每个 \(k\),我们双指针出右端点为 \(i\) 时,可行的左端点区间 \([l,r]\),满足对于 \(j\in[l,r]\)\(|\{a_j,a_{j+1},\cdots,a_i\}|=2^k\)。我们边跑边建立前缀线性基用于查询,这样就可以找出所有合法区间,表示为 \((l,r,i)\)。时间复杂度 \(O(n\log^2n)\)。当然,我们也可以预处理出所有的前缀线性基,虽然减小了时间常数,但是空间常数增大。

查询时,考虑对于询问的 \(L,R\),贡献分为两类:第一类,\(l<L\le r\le i\le R\);第二类,\(L\le l\le r\le i\le R\)。我们可以通过找到第一个 \(\ge L\)\(r\)\(l\),以及最后一个 \(\le R\)\(i\),就可以通过前缀和等方式求出总贡献。(这种方法成立的原因是因为对于每种 \(k\) 的任意两个数对,\(l,r,i\) 的大小关系都一样。)

考虑提前维护每个位置后面对于每个 \(k\) 的第一个 \(r\)、第一个 \(l\),和每个位置前面对于每个 \(k\) 的第一个 \(i\);再对于每个 \(k\) 维护前缀 \(l,r\) 之和即可。时间复杂度 \(O(q\log n)\)

发现对于加强版来说,时空都太大,考虑卡常。对于空间而言,我们可以将每个数对 \((l,r,i)\) 压成一个数,就可以将 \(3\)\(int\) 转化成一个 \(long\ long\),减少空间消耗。

时间考虑将预处理的两种方法结合一下。对于一个阙值 \(B\),前 \(B\) 个前缀线性基预处理,之后则暴力推即可。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=6e5+5,M=30;
int n,q,a[N];
struct lbas{
	int ps[M],pos[M];
	inline void add(int x,int id){
		for(int i=M-1;~i;i--) if((x>>i)&1){
			if(!ps[i]){
				ps[i]=x,pos[i]=id;
				return;
			}
			else if(pos[i]<id){
				swap(pos[i],id);
				swap(ps[i],x);
			}
			x^=ps[i];
		}
	}
	inline int num(int l){
		int re=0;
		for(int i=M-1;~i;i--) re+=(pos[i]>=l);
		return re;
	}
}lbd[90005],lb;
int cnt,tgl[N],tgr[N],numl,numr,e[N],ck=(1<<20)-1;
unordered_map<int,int>mp;
ll cc[N][20],suma[20][N],sumb[20][N];
inline void init(){
	int zr=0;
	for(int i=1;i<=n;i++){
		if(i<=90000){
			lbd[i]=lbd[i-1];
			lbd[i].add(a[i],i);
		}
		if(!mp.count(a[i])){
			e[mp[a[i]]=++cnt]=a[i];
			if(!a[i]) zr=cnt;
		}
		a[i]=mp[a[i]];
	}
	for(int i=0,tp;(1<<i)<=cnt;i++){
		lb=lbd[90000],tp=numl=numr=0;
		for(int j=1;j<=cnt;j++) tgl[j]=tgr[j]=0;
		int lsl=0,lsr=0,lsj=0;
		for(int j=1,l=1,r=1;j<=n;j++){
			tgl[a[j]]++,tgr[a[j]]++;
			if(tgl[a[j]]<2) numl++;
			if(tgr[a[j]]<2) numr++;
			while(numl>(1<<i)){
				if(tgl[a[l]]<2) numl--;
				tgl[a[l++]]--;
			}
			while(numr>=(1<<i)){
				if(tgr[a[r]]<2) numr--;
				tgr[a[r++]]--;
			}
			if(j>90000) lb.add(e[a[j]],j);
			if(numl==(1<<i)&&tgl[zr]&&l<r) if((j<=90000?lbd[j]:lb).num(l)==i){
				tp++;
				suma[i][tp]=suma[i][tp-1]+l-1;
				sumb[i][tp]=sumb[i][tp-1]+r-1;
				for(int k=lsj;k<j;k++) cc[k][i]^=(tp-1ll)<<40;
				for(int k=lsr+1;k<r;k++) cc[k][i]^=(tp-1ll)<<20;
				for(int k=lsl+1;k<=l;k++) cc[k][i]^=tp-1;
				lsl=l,lsr=r-1,lsj=j;
			}
		}
		for(int j=lsj;j<=n;j++) cc[j][i]^=(ll)tp<<40;
		for(int j=lsr+1;j<=n;j++) cc[j][i]^=(ll)tp<<20;
		for(int j=lsl+1;j<=n;j++) cc[j][i]^=tp;
	}
}
inline ll solve(int l,int r){
	ll re=0;
	for(int i=0,lk,mk,rk;(1<<i)<=cnt;i++){
		lk=(cc[l][i]>>20)&ck,mk=cc[l][i]&ck,rk=cc[r][i]>>40;
		if(lk<rk){
			re+=sumb[i][rk]-sumb[i][lk];
			if(rk>mk)
				re+=suma[i][mk]-(l-1ll)*(mk-lk)-suma[i][rk];
			else re-=(l-1ll)*(rk-lk);
		}
	}
	return re;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	uint64_t s;
	cin>>n>>q>>s;
	string r;
	cin>>r;
	for(int i=1;i<=n;i++)
		for(int s=5*i-5;s<5*i;s++)
			a[i]=(a[i]*64+(int)(r[s])-(int)('0'));
	init();
	uint64_t state=0;
	auto splitmix64=[&](uint64_t v){
		uint64_t z=(v+0x9e3779b97f4a7c15);
		z=(z^(z>>30))*0xbf58476d1ce4e5b9;
		z=(z^(z>>27))*0x94d049bb133111eb;
		return z^(z>>31);
	};
	mt19937_64 rng(s);
	for(int i=1;i<=q;i++){
		int l=(rng()^state)%n,r=(rng()^state)%n;
		ll ans=solve(min(l,r)+1,max(l,r)+1);
		state=splitmix64(state+ans);
		if(i%(1<<15)==0) cout<<state<<"\n";
    }
	cout<<state<<"\n";
	return 0;
}

C. [Ynoi Easy Round 2023] TEST_107

和 A 一样的地方在于都有一个贡献是由相邻的相同点构成的,同样用二维数点处理,不同的地方在于还需要维护每种颜色在一段区间内的第一个位置和最后一个位置,扫描线即可 \(O(n\log n)\)

#include<bits/stdc++.h>
using namespace std;
char buf[1<<20],*p1,*p2;
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<20,stdin),p1==p2)?0:*p1++)
template<typename T>
inline void read(T &x){
	x=0;
	char c=getchar();
	bool fl=0;
	while(c>57||c<48) fl=(c=='-'),c=getchar();
	while(c>=48&&c<=57)
		x=(x<<1)+(x<<3)+c-48,c=getchar();
	x=(fl?-x:x);
}
template<typename T>
inline void write(T x){
	if(x<0) x=-x,putchar('-');
	if(x>9) write(x/10);
	putchar(x%10+48);
}
const int N=2e6+5;
int n,m,a[N],ls[N],lc[N],rc[N],ans[N];
vector<int>qua[N],qub[N];
int mn[N<<2];
inline void build(int x,int l,int r){
	mn[x]=1e9;
	if(l==r) return;
	int mid=(l+r)>>1;
	build(x<<1,l,mid),build(x<<1|1,mid+1,r);
}
inline void chgn(int x,int l,int r,int k,int v){
	if(l==r){
		mn[x]=v;
		return;
	}
	int mid=(l+r)>>1;
	if(k<=mid) chgn(x<<1,l,mid,k,v);
	else chgn(x<<1|1,mid+1,r,k,v);
	mn[x]=min(mn[x<<1],mn[x<<1|1]);
}
inline int minn(int x,int l,int r,int L){
	if(L<=l) return mn[x];
	int mid=(l+r)>>1;
	int re=minn(x<<1|1,mid+1,r,L);
	if(L<=mid) re=min(re,minn(x<<1,l,mid,L));
	return re;
}
int mx[N<<2];
inline void chgx(int x,int l,int r,int k,int v){
	if(l==r){
		mx[x]=v;
		return;
	}
	int mid=(l+r)>>1;
	if(k<=mid) chgx(x<<1,l,mid,k,v);
	else chgx(x<<1|1,mid+1,r,k,v);
	mx[x]=max(mx[x<<1],mx[x<<1|1]);
}
inline int maxn(int x,int l,int r,int R){
	if(R>=r) return mx[x];
	int mid=(l+r)>>1;
	int re=maxn(x<<1,l,mid,R);
	if(mid<R) re=max(re,maxn(x<<1|1,mid+1,r,R));
	return re;
}
int tot,c[N];
struct lxl{
	int x,y,id;
}xlx[N<<1];
inline bool cmp(lxl x,lxl y){
	return x.x!=y.x?x.x<y.x:x.y!=y.y?x.y<y.y:x.id<y.id;
}
inline void add(int x,int v){
	for(;x<=n;x+=x&-x) c[x]=max(c[x],v);
}
inline int maxx(int x){
	int re=0;
	for(;x;x-=x&-x) re=max(re,c[x]);
	return re;
}
int main(){
	read(n),read(m);
	for(int i=1;i<=n;i++) read(a[i]);
	for(int i=1;i<=m;i++){
		read(lc[i]),read(rc[i]);
		qua[rc[i]].push_back(i);
		qub[lc[i]].push_back(i);
		xlx[++tot]={n-lc[i]+1,rc[i],i};
	}
	build(1,1,2e6);
	for(int i=1;i<=n;i++){
		if(ls[a[i]]){
			chgn(1,1,2e6,ls[a[i]],1e9);
			xlx[++tot]={n-ls[a[i]]+1,i,ls[a[i]]-i+1};
		}
		chgn(1,1,2e6,i,i),ls[a[i]]=i;
		for(int j:qua[i])
			ans[j]=max(i-minn(1,1,2e6,lc[j]),0);
	}
	for(int i=1;i<=2e6;i++) ls[i]=0;
	for(int i=n;i;i--){
		if(ls[a[i]])
			chgx(1,1,2e6,ls[a[i]],0);
		chgx(1,1,2e6,i,i),ls[a[i]]=i;
		for(int j:qub[i])
			ans[j]=max(maxn(1,1,2e6,rc[j])-i,ans[j]);
	}
	sort(xlx+1,xlx+tot+1,cmp);
	for(int i=1;i<=tot;i++){
		if(xlx[i].id<=0) add(xlx[i].y,-xlx[i].id);
		else ans[xlx[i].id]=max(ans[xlx[i].id],maxx(xlx[i].y));
	}
	for(int i=1;i<=m;i++)
		write(ans[i]),putchar('\n');
	return 0;
}

D. [CF407E] k-d-sequence

显然,他让求得就是一个最长的子串,满足每一项 \(\bmod d\) 都相等,且 \(\frac{mx-mn}d-sz<k\),同时内部不能有相同项。

第一个可以拿双指针跑出来,第三个可以存起来,他们共同确定了一个下界 \(L\)。第二个看起来没有单调性,但是我们又不能线性或者根号,考虑线段树二分。发现每一次修改实际上都代表了一个单调栈中的元素被 \(pop\) 或者 \(push\),所以线段树区间加的次数是 \(O(n)\) 的,可以通过,时间复杂度 \(O(n\log n)\)

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5; 
int n,k,d,a[N],l=1,r;
int m,b[N],mn[N<<2],ad[N<<2];
int stx[N],tpx,stn[N],tpn;
unordered_map<int,int>pr;
inline void build(int x,int l,int r){
	mn[x]=ad[x]=0;
	if(l==r) return;
	int mid=(l+r)>>1;
	build(x<<1,l,mid);
	build(x<<1|1,mid+1,r);
}
inline void down(int x,int v){
	ad[x]+=v,mn[x]+=v;
}
inline void push_down(int x){
	down(x<<1,ad[x]),down(x<<1|1,ad[x]),ad[x]=0;
}
inline void add(int x,int l,int r,int L,int R,int v){
	if(L<=l&&r<=R) return down(x,v);
	int mid=(l+r)>>1;
	push_down(x);
	if(L<=mid) add(x<<1,l,mid,L,R,v);
	if(R>mid) add(x<<1|1,mid+1,r,L,R,v);
	mn[x]=min(mn[x<<1],mn[x<<1|1]);
}
inline int find(int x,int l,int r,int L,int v){
	if(mn[x]>v) return -1;
	if(l==r) return l;
	int mid=(l+r)>>1,re=-1;
	push_down(x);
	if(L<=mid) re=find(x<<1,l,mid,L,v);
	if(re>=0) return re;
	return find(x<<1|1,mid+1,r,L,v);
}
inline void solve(int c){
	build(1,1,m),pr.clear(),tpn=tpx=0;
	for(int i=1,mx=0;i<=m;i++){
		while(tpn&&b[i]<b[stn[tpn]])
			add(1,1,m,stn[tpn-1]+1,stn[tpn],(b[stn[tpn]]-b[i])/d),tpn--;
		while(tpx&&b[i]>b[stx[tpx]])
			add(1,1,m,stx[tpx-1]+1,stx[tpx],(b[i]-b[stx[tpx]])/d),tpx--;
		mx=max(mx,pr[b[i]]);
		int j=find(1,1,m,mx+1,k);
		stn[++tpn]=stx[++tpx]=pr[b[i]]=i;
		if(i-j>r-l) r=i+c,l=j+c;
		add(1,1,m,1,i,-1);
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>k>>d;
	for(int i=1;i<=n;i++) cin>>a[i];
	if(!d){
		for(int i=1,j=0;i<=n;i=j+1){
			while(j<n&&a[i]==a[j+1]) j++;
			if(j-i>r-l) l=i,r=j;
		}
		cout<<l<<" "<<r,exit(0);
	}
	for(int i=1,j=0;i<=n;i=j+1){
		while(j<n&&(a[i]%d+d)%d==(a[j+1]%d+d)%d) j++;
		for(int c=i;c<=j;c++) b[c-i+1]=a[c];
		m=j-i+1,solve(i-1);
	}
	cout<<l<<" "<<r;
	return 0;
}

E. [JOISC 2021] フードコート (Day1)

当时其实已经考虑写区修单查的二分套主席树了,不过发现了更高更妙の扫描线做法。

考虑我们只需要对离队人数进行计算,就可以将问题转化成一个只剩加入的东西,方法是将 \(B\) 加上离队人数。

离队人数是难以直接计算的,考虑容斥成入队总人数-剩余人数。前者树状数组秒了,后者可以吉司机(然鹅傻白虎好久没写过吉司机了导致调了超久)。

显然是要做线段树二分的,考虑如何更新。考虑将时间和店铺编号作为两个维度,我们依次枚举店铺编号,就可以直接进行线段树二分,问题转化为区修,假如使用差分即可转化为单修。时间复杂度 \(O(n\log n)\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=250005;
int n,m,q,ans[N],cc[N];
struct lxl{
	int x,id;
};
vector<lxl>ve[N],ac[N];
int c[N];
inline void addc(int x,int v){
	for(;x<=n;x+=x&-x) c[x]+=v;
}
inline int answ(int x){
	int re=0;
	for(;x;x-=x&-x) re+=c[x];
	return re;
}
int mn[N<<2],nn[N<<2],adm[N<<2],ad[N<<2];
inline void build(int x,int l,int r){
	nn[x]=1e18;
	if(l==r) return;
	int mid=(l+r)>>1;
	build(x<<1,l,mid);
	build(x<<1|1,mid+1,r);
}
inline void push_up(int x){
	mn[x]=min(mn[x<<1],mn[x<<1|1]);
	nn[x]=min(nn[x<<1],nn[x<<1|1]);
	if(mn[x<<1]>mn[x]) nn[x]=min(nn[x],mn[x<<1]);
	if(mn[x<<1|1]>mn[x]) nn[x]=min(nn[x],mn[x<<1|1]);
}
inline void down(int x,int vm,int v){
	mn[x]+=vm,adm[x]+=vm,nn[x]+=v,ad[x]+=v;
}
inline void push_down(int x){
	int lc=mn[x<<1],rc=mn[x<<1|1];
	down(x<<1,(rc>=lc?adm[x]:ad[x]),ad[x]);
	down(x<<1|1,(lc>=rc?adm[x]:ad[x]),ad[x]);
	adm[x]=ad[x]=0;
}
inline void adda(int x,int l,int r,int L,int R,int v){
	if(L<=l&&r<=R) return down(x,v,v);
	int mid=(l+r)>>1;
	push_down(x);
	if(L<=mid) adda(x<<1,l,mid,L,R,v);
	if(R>mid) adda(x<<1|1,mid+1,r,L,R,v);
	push_up(x);
}
inline void chgn(int x,int l,int r){
	if(mn[x]>=0) return;
	if(nn[x]>0) return down(x,-mn[x],0);
	int mid=(l+r)>>1;
	push_down(x);
	chgn(x<<1,l,mid);
	chgn(x<<1|1,mid+1,r);
	push_up(x);
}
inline int num(int x,int l,int r,int ps){
	if(l==r) return mn[x];
	int mid=(l+r)>>1;
	push_down(x);
	if(ps<=mid) return num(x<<1,l,mid,ps);
	return num(x<<1|1,mid+1,r,ps);
}
int sum[N<<2];
inline void chg(int x,int l,int r,int ps,int v){
	if(l==r){
		sum[x]+=v;
		return;
	}
	int mid=(l+r)>>1;
	if(ps<=mid) chg(x<<1,l,mid,ps,v);
	else chg(x<<1|1,mid+1,r,ps,v);
	sum[x]=sum[x<<1]+sum[x<<1|1];
}
inline int find(int x,int l,int r,int v){
	if(l==r) return l;
	int mid=(l+r)>>1;
	if(sum[x<<1]>=v) return find(x<<1,l,mid,v);
	return find(x<<1|1,mid+1,r,v-sum[x<<1]);
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m>>q;
	build(1,1,n);
	for(int i=1,opt,l,r,k,sm;i<=q;i++){
		cin>>opt>>l>>r;
		ans[i]=-1;
		if(opt==1){
			cin>>cc[i]>>k;
			addc(l,k),addc(r+1,-k);
			adda(1,1,n,l,r,k);
			ac[l].push_back({k,i});
			ac[r+1].push_back({-k,i});
		}
		if(opt==2) cin>>k,adda(1,1,n,l,r,-k),chgn(1,1,n);
		if(opt==3){
			int smy=answ(l),now=num(1,1,n,l);
			if(now<r){
				ans[i]=0;
				continue;
			}
			ve[l].push_back({r+smy-now,i});
		}
	}
	for(int i=1;i<=n;i++){
		for(lxl j:ac[i]) chg(1,1,q,j.id,j.x);
		for(lxl j:ve[i]) ans[j.id]=find(1,1,q,j.x);
	}
	for(int i=1;i<=q;i++) if(ans[i]>=0) cout<<cc[ans[i]]<<"\n";
	return 0;
}

F. [luoguP11398] 众数

这才是更高更妙的数据结构啊!

那显然我们要将 \(\sum k\) 尽可能单独的拎出来,那么就要考虑 \(O(1)\) 的查询。当然,这肯定要基于一些之前得到的信息。

考虑加入比删除显然更容易(后者可以用线段树做到 \(O(\log n)\),但前者是实打实的 \(O(1)\)),再凭借着 \(k\le 300\) 这档部分分给我们的启发,我们想到设置一些关键点,记录其前缀每个数的数量,然后从最大的关键点开始依次枚举,直到找到最小值。

一种方式是分块 \(O((n+m)\sqrt n+\sum k)\) 解决,但不够优。考虑能不能进行块长不均的分块。因此考虑倍增分块,时间复杂度 \(O((n+m)\log n+\sum k)\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+5;
int n,m,k,cnt,a[N],b[N],c[N],sm[N],ps[20],mx[20],mp[20][N],e[N];
unordered_map<int,int>mpc;
inline int cmax(int i,int x,int y){
	return mp[i][x]!=mp[i][y]?(mp[i][x]>mp[i][y]?x:y):(x>y?x:y);
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i]>>b[i],e[++k]=b[i];
	sort(e+1,e+k+1),k=unique(e+1,e+k+1)-e-1,ps[0]=n;
	for(int i=1;i<=k;i++) mpc[e[i]]=i;
	for(int i=1;i<=n;i++) b[i]=mpc[b[i]];
	while(ps[cnt]) cnt++,ps[cnt]=max(0ll,n-(1<<cnt));
	for(int i=1;i<=cnt;i++) for(int j=1;j<=ps[i];j++)
		mp[i][b[j]]+=a[j],mx[i]=cmax(i,b[j],mx[i]);
	while(m--){
		int opt,x,y;
		cin>>opt>>x;
		if(opt<2){
			cin>>y,a[x]+=y;
			for(int i=1;ps[i]>=x;i++)
				mp[i][b[x]]+=y,mx[i]=cmax(i,b[x],mx[i]);
		}
		else for(int i=1,xm;i<=cnt;i++){
			xm=mx[i];
			for(int j=ps[i]+1;j<=ps[i-1];j++)
				mp[i][b[j]]+=a[j],xm=cmax(i,b[j],xm),c[j]=e[xm]*a[j];
			for(int j=ps[i]+1;j<=ps[i-1];j++) mp[i][b[j]]-=a[j];
			int flag=0;
			for(int j=ps[i-1];j>ps[i];j--){
				c[j]^=c[j+1];
				if(c[j]==x){
					cout<<n-j+1<<"\n",flag=1;
					break;
				}
			}
			if(flag) break;
		}
	}
	return 0;
} 

G. [CF1814F] Communication Towers

显然每条边都有一个出现时间,那连通性问题,边有出现时间,直接想到线段树分治。每次到线段树叶子的时候直接给 1 所在的并查集根打个标记,分裂时下传,合并时隐退即可,时间复杂度 \(O(n\log^2n)\)

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m,lc[N],rc[N];
int fa[N],sz[N],id[N],tp;
struct edge{
	int u,v;
};
vector<edge>ve[N<<1];
struct lxl{
	int x,y,idx;
}st[N<<1];
inline void ret(lxl x){
	sz[x.x]-=sz[fa[x.y]=x.y];
	if(id[x.x]) id[x.y]=1;
	else id[x.x]=x.idx;
}
inline int find(int x){
	return x==fa[x]?x:find(fa[x]);
}
inline void unite(int x,int y){
	x=find(x),y=find(y);
	if(x==y) return;
	if(sz[x]<sz[y]) swap(x,y);
	st[++tp]={x,y,id[x]};
	id[x]=0,fa[y]=x,sz[x]+=sz[y];
}
inline void add(int x,int l,int r,int L,int R,edge v){
	if(L<=l&&r<=R){
		ve[x].push_back(v);
		return;
	}
	int mid=(l+r)>>1;
	if(L<=mid) add(mid<<1,l,mid,L,R,v);
	if(R>mid) add(mid<<1|1,mid+1,r,L,R,v);
}
inline void solve(int x,int l,int r){
	int now=tp;
	for(edge ed:ve[x]) unite(ed.u,ed.v);
	int mid=(l+r)>>1;
	if(l==r) id[find(1)]=1;
	else solve(mid<<1,l,mid),solve(mid<<1|1,mid+1,r);
	while(tp>now) ret(st[tp--]);
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>lc[i]>>rc[i];
	for(int i=1,u,v;i<=m;i++){
		cin>>u>>v;
		add(1,1,2e5,max(lc[u],lc[v]),min(rc[u],rc[v]),{u,v});
	}
	for(int i=1;i<=n;i++) sz[fa[i]=i]=1;
	solve(1,1,2e5);
	for(int i=1;i<=n;i++) if(id[i]) cout<<i<<" ";
	return 0;
}

H. [ROI 2023] 生产计划 (Day 2)

做完题不知道数据结构在哪里。

显然一种效率能存在当且仅当 \(x\in[L,R]\),其中 \(L\)\(a_i=l_i\) 时的答案,\(R\) 代表 \(a_i=r_i\) 时的答案,显然是容易 \(dp\) 的。

然后就好办了,我们可以钦定一个改变顺序,顺序按照 \(dfs\) 序走,每次将一个 \(a_i\) 一步步变成 \(r_i\)。那么我们可以求出 \(dfs\) 序上前 \(i\) 个位置变成 \(r_i\) 后,最大独立集的大小 \(f_i\),直接换根 \(dp\) 即可。

然后考虑一个 \(a_i\) 影响答案一定是在他操作一段后缀中,所以二分找出第一个大于 \(v_i\)\(f_j\),那么 \(dfs\) 序上前 \(j-1\)\(r\),后 \(n-j\) 个为 \(l\),中间一个为 \(r-f_j+v_i\)

总体时间复杂度 \(O(\sum n\log n)\)

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=2e5+5;
int T,n,q,xc,yc,mc,a[N],lc[N],rc[N],md[N],mdn[N];
int idx,ps[N],pr[N],sf[N],dp[N][2],f[N],dfn[N],L,R;
vector<int>g[N];
inline void dfs1(int x,int fa){
	dp[x][0]=0,dp[x][1]=lc[x];
	for(int y:g[x]) if(y!=fa)
		dfs1(y,x),dp[x][0]+=max(dp[y][0],dp[y][1]),dp[x][1]+=dp[y][0];
}
inline void swp(int fa,int x){
	dp[fa][0]-=max(dp[x][0],dp[x][1]);
	dp[fa][1]-=dp[x][0];
	dp[x][0]+=max(dp[fa][0],dp[fa][1]);
	dp[x][1]+=dp[fa][0];
}
inline void dfs2(int x,int fa){
	swp(fa,ps[dfn[x]=++idx]=x);
	dp[x][1]+=rc[x]-lc[x];
	f[idx]=max(dp[x][0],dp[x][1]);
	for(int y:g[x]) if(y!=fa) dfs2(y,x);
	swp(x,fa);
}
inline void solve(){
	for(int i=1;i<=n;i++) g[i].clear();
	cin>>n>>q>>xc>>yc>>mc;
	sf[n+1]=idx=0;
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		g[u].push_back(v);
		g[v].push_back(u);
	}
	for(int i=1;i<=n;i++) cin>>lc[i]>>rc[i];
	dfs1(1,0),L=dp[0][0]=max(dp[0][1]=dp[1][0],dp[1][1]);
	dfs2(1,0),R=max(dp[1][0],dp[1][1]);
	for(int i=1;i<=idx;i++) pr[i]=pr[i-1]^((xc*ps[i]+yc*rc[ps[i]]%mc*rc[ps[i]])%mc);
	for(int i=idx;i;i--) sf[i]=sf[i+1]^((xc*ps[i]+yc*lc[ps[i]]%mc*lc[ps[i]])%mc);
	for(int i=1;i<=q;i++) cin>>a[i];
	for(int i=1;i<=q;i++){
		if(a[i]<L||a[i]>R){
			md[i]=-1,cout<<"-1 ";
			continue;
		}
		md[i]=lower_bound(f+1,f+idx+1,a[i])-f;
		mdn[i]=rc[ps[md[i]]]-f[md[i]]+a[i];
		cout<<(pr[md[i]-1]^sf[md[i]+1]^((ps[md[i]]*xc+mdn[i]*mdn[i]%mc*yc)%mc))<<" ";
	}
	cout<<endl;
	while(1){
		int x;
		cin>>x;
		if(!x) return;
		if(md[x]<0) cout<<"-1"<<endl;
		else{
			for(int i=1;i<=n;i++)
				cout<<(dfn[i]<md[x]?rc[i]:dfn[i]>md[x]?lc[i]:mdn[x])<<" ";
			cout<<endl;
		}
	}
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>T;
	while(T--) solve();
	return 0;
}

I. [北大集训2021] 小明的树

相信各位同学看到本题的第一反应一定不是 \(LCT\)

那不是 \(LCT\),就不能把树直接建出来,因此考虑能否把问题转化。

首先,白色连通块都是子树和黑色连通块只有一个等价,根据点边容斥,若这是在第 \(i\) 次点亮操作后,则黑点数为 \(n-i\),问题转化为求解两端点都为黑色的边数。设第 \(i\) 个点的点亮时间为 \(b_i\),则每一条边都会给 \(\min(b_u,b_v)\) 及其后面的所有时刻 \(+1\),线段树即可。

考虑白色连通块数。由于白色连通块全部都是子树,所以两端点异色的边数就是白色连通块个数,每条边给 \([\min(b_u,b_v),\max(b_u,b_v)\) 做贡献。

这俩东西扔一块线段树即可。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+5;
int n,m,b[N],u[N],v[N];
struct seg{
	int c,v,num;
}sg[N<<2];
int adc[N<<2],adv[N<<2];
inline seg operator+(seg x,seg y){
	seg re={min(x.c,y.c),0,0};
	if(x.c==re.c) re.v+=x.v,re.num+=x.num;
	if(y.c==re.c) re.v+=y.v,re.num+=y.num;
	return re;
}
inline void build(int x,int l,int r){
	if(l==r){
		sg[x]={2-l,0,1};
		return;
	}
	int mid=(l+r)>>1;
	build(x<<1,l,mid);
	build(x<<1|1,mid+1,r);
	sg[x]=sg[x<<1]+sg[x<<1|1];
}
inline void down(int x,int vc,int vv){
	sg[x].c+=vc,sg[x].v+=vv*sg[x].num,adc[x]+=vc,adv[x]+=vv;
}
inline void push_down(int x){
	down(x<<1,adc[x],adv[x]);
	down(x<<1|1,adc[x],adv[x]);
	adc[x]=adv[x]=0;
}
inline void add(int x,int l,int r,int L,int R,int vc,int vv){
	if(L<=l&&r<=R) return down(x,vc,vv);
	int mid=(l+r)>>1;
	push_down(x);
	if(L<=mid) add(x<<1,l,mid,L,R,vc,vv);
	if(R>mid) add(x<<1|1,mid+1,r,L,R,vc,vv);
	sg[x]=sg[x<<1]+sg[x<<1|1];
}
inline void chg(int x,int y,int v){
	add(1,1,n,min(b[x],b[y]),n,v,0);
	add(1,1,n,min(b[x],b[y]),max(b[x],b[y])-1,0,v);
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	build(1,1,n),b[1]=n+1;
	for(int i=1;i<n;i++) cin>>u[i]>>v[i];
	for(int i=2,x;i<=n;i++) cin>>x,b[x]=i;
	for(int i=1;i<n;i++) chg(u[i],v[i],1);
	cout<<(sg[1].c==1)*sg[1].v<<"\n";
	while(m--){
		int ua,va,ub,vb;
		cin>>ua>>va>>ub>>vb;
		chg(ua,va,-1),chg(ub,vb,1);
		cout<<(sg[1].c==1)*sg[1].v<<"\n";
	}
	return 0;
}

J. [CF1344E] Train Tracks

考虑一辆列车走到一个站点时,假如上一次这个站点的岔口和这次的岔口不一样,在上一辆车经过这个站点的时刻和这辆车的时刻之间就必须有一次针对这个站点的修改操作。抽象一下,我们可以得到多个区间 \([l_i,r_i]\),要求对于每个区间匹配一个区间内的整点,且没有任意两个区间匹配点相同。这是经典问题,直接对左端点排序,枚举 \(i\),每次将所有左端点 \(\le i\) 的区间的右端点塞进优先队列里,每次取出最小的那个,和 \(i\) 比大小即可,时间复杂度 \(O(k\log k)\),其中 \(k\) 表示这样的区间个数。

直觉告诉我们 \(k\) 应该不是很大,考虑用树剖证明。我们发现每当一个位置的岔口从重儿子变成轻儿子时,势能增加一,而每次势能只会增加 \(\log n\) 次,所以 \(k\)\(O(n\log n)\) 级别的。我们对于每个重链,记录所有岔口指向轻儿子的节点,和每次将重儿子变成轻儿子的操作所影响的前缀。显然后者只需要记录一个类似单调栈的东西即可。但对于所有的岔口指向轻儿子的节点,我们需要记录他指向的轻儿子,否则可能算多。

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

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,sz[N],sn[N],dep[N],dis[N],ft[N],tp[N];
int m,cc[N],now[N],cnt,ans;
struct edge{
	int to,cs;
};
vector<edge>g[N];
list<int>lt[N];
list<edge>st[N];
struct chg{
	int l,r;
}cg[N*17];
inline bool cmp(chg x,chg y){
	return x.l!=y.l?x.l<y.l:x.r<y.r;
}
inline void dfs1(int x,int fa){
	sz[x]=1,dep[x]=dep[ft[x]=fa]+1;
	if(!g[x].size()) now[x]=0;
	else now[x]=g[x][g[x].size()-1].to;
	if(fa==now[x]){
		if(g[x].size()<2) now[x]=0;
		else now[x]=g[x][g[x].size()-2].to;
	}
	for(edge y:g[x]) if(y.to!=fa){
		dis[y.to]=dis[x]+y.cs;
		dfs1(y.to,x),sz[x]+=sz[y.to];
		if(sz[sn[x]]<sz[y.to]) sn[x]=y.to;
	}
}
inline void dfs2(int x,int top){
	tp[x]=top,cc[x]=-1e18;
	if(now[x]!=sn[x]) lt[top].push_back(x);
	if(sn[x]) dfs2(sn[x],top);
	for(edge y:g[x]) if(y.to!=ft[x]&&y.to!=sn[x]) dfs2(y.to,y.to);
}
inline void access(int x,int t){
	int ls=x,y;
	while(x=tp[ft[x]]){
		while(lt[x].size()&&dep[lt[x].front()]<dep[ls]-1)
			cg[++cnt]={cc[y=lt[x].front()]+dis[y]+1,t+dis[y]},lt[x].pop_front();
		while(st[x].size()&&dep[st[x].front().to]<dep[ls]) st[x].pop_front();
		if(ls!=tp[ls]){
			if(lt[x].size()&&lt[x].front()==ft[ls]){
				cg[++cnt]={cc[ft[ls]]+dis[ft[ls]]+1,t+dis[ft[ls]]};
				lt[x].pop_front();
			}
			st[x].push_front({sn[ft[ls]],t}),ls=x;
			continue;
		}
		if(!lt[x].size()||lt[x].front()!=ft[ls]){
			int ad=st[x].size()?st[x].front().cs:-1e18;
			cg[++cnt]={dis[ft[ls]]+ad+1,dis[ft[ls]]+t};
			lt[x].push_front(ft[ls]);
		}
		else if(now[ft[ls]]!=ls)
			cg[++cnt]={cc[ft[ls]]+dis[ft[ls]]+1,t+dis[ft[ls]]};
		now[ft[ls]]=ls,cc[ft[ls]]=t;
		st[x].push_front({ft[ls],t}),ls=x;
	}
}
priority_queue<int>q;
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1,u,v,d;i<n;i++){
		cin>>u>>v>>d;
		g[u].push_back({v,d});
		g[v].push_back({u,d});
	}
	dfs1(1,0),dfs2(1,1);
	while(m--){
		int x,t;
		cin>>x>>t;
		access(x,t);
	}
	for(int i=1;i<=cnt;i++) cg[i].l=max(cg[i].l,1ll);
	sort(cg+1,cg+cnt+1,cmp);
	for(int i=1,j=1;j<=cnt||q.size();i++){
		if(!q.size()) i=cg[j].l;
		while(j<=cnt&&cg[j].l<=i) q.push(-cg[j++].r);
		int t=-q.top();
		q.pop();
		if(t<i){
			for(int id=1;id<=cnt;id++)
				if(cg[id].r<t) ans++;
			cout<<t<<" "<<ans;
			return 0;
		}
	}
	cout<<"-1 "<<cnt;
	return 0;
}

K. [NOI2024] 登山

基本上自己做出来了,但是常数巨大。

考虑每个点都有一个一步能到达的深度区间 \([dep_i-l_i,dep_i-r_i]\),但是由于 \(h\) 的限制,这个范围变成了 \([dep_i-l_i,\min(dep_i-r_i,dep_k-h_k-1)]\),其中 \(k\) 表示一条路径 \((i,j)\) 上的一点,其中 \(j\)\(i\) 的祖先。

假如没有 \(\min\) 限制,显然是可以线段树合并的,加上限制后,我们可以考虑对每个节点的线段树合并结果做一个后缀删除。这个删除显然是可以做到 \(O(n\log n)\) 的。这里由于要对线段树进行区间操作,所以我们采用标记永久化的思想。

显然,我们的总结点数是 \(O(n\log n)\),但是由于线段树合并重复使用了某些节点,导致我们不能直接便利整棵线段树。我们考虑在计算过一个线段树区间后,记录此时计算的答案,以及这个点到根路径上的标记之和。这样我们就可以 \(O(1)\) 的计算出一个已经计算过的线段树节点的贡献了,时间复杂度是炒大肠 \(O(n\log n)\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,sz[N],sn[N],dep[N],dis[N],ft[N],tp[N];
int m,cc[N],now[N],cnt,ans;
struct edge{
	int to,cs;
};
vector<edge>g[N];
list<int>lt[N];
list<edge>st[N];
struct chg{
	int l,r;
}cg[N*17];
inline bool cmp(chg x,chg y){
	return x.l!=y.l?x.l<y.l:x.r<y.r;
}
inline void dfs1(int x,int fa){
	sz[x]=1,dep[x]=dep[ft[x]=fa]+1;
	if(!g[x].size()) now[x]=0;
	else now[x]=g[x][g[x].size()-1].to;
	if(fa==now[x]){
		if(g[x].size()<2) now[x]=0;
		else now[x]=g[x][g[x].size()-2].to;
	}
	for(edge y:g[x]) if(y.to!=fa){
		dis[y.to]=dis[x]+y.cs;
		dfs1(y.to,x),sz[x]+=sz[y.to];
		if(sz[sn[x]]<sz[y.to]) sn[x]=y.to;
	}
}
inline void dfs2(int x,int top){
	tp[x]=top,cc[x]=-1e18;
	if(now[x]!=sn[x]) lt[top].push_back(x);
	if(sn[x]) dfs2(sn[x],top);
	for(edge y:g[x]) if(y.to!=ft[x]&&y.to!=sn[x]) dfs2(y.to,y.to);
}
inline void access(int x,int t){
	int ls=x,y;
	while(x=tp[ft[x]]){
		while(lt[x].size()&&dep[lt[x].front()]<dep[ls]-1)
			cg[++cnt]={cc[y=lt[x].front()]+dis[y]+1,t+dis[y]},lt[x].pop_front();
		while(st[x].size()&&dep[st[x].front().to]<dep[ls]) st[x].pop_front();
		if(ls!=tp[ls]){
			if(lt[x].size()&&lt[x].front()==ft[ls]){
				cg[++cnt]={cc[ft[ls]]+dis[ft[ls]]+1,t+dis[ft[ls]]};
				lt[x].pop_front();
			}
			st[x].push_front({sn[ft[ls]],t}),ls=x;
			continue;
		}
		if(!lt[x].size()||lt[x].front()!=ft[ls]){
			int ad=st[x].size()?st[x].front().cs:-1e18;
			cg[++cnt]={dis[ft[ls]]+ad+1,dis[ft[ls]]+t};
			lt[x].push_front(ft[ls]);
		}
		else if(now[ft[ls]]!=ls)
			cg[++cnt]={cc[ft[ls]]+dis[ft[ls]]+1,t+dis[ft[ls]]};
		now[ft[ls]]=ls,cc[ft[ls]]=t;
		st[x].push_front({ft[ls],t}),ls=x;
	}
}
priority_queue<int>q;
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1,u,v,d;i<n;i++){
		cin>>u>>v>>d;
		g[u].push_back({v,d});
		g[v].push_back({u,d});
	}
	dfs1(1,0),dfs2(1,1);
	while(m--){
		int x,t;
		cin>>x>>t;
		access(x,t);
	}
	for(int i=1;i<=cnt;i++) cg[i].l=max(cg[i].l,1ll);
	sort(cg+1,cg+cnt+1,cmp);
	for(int i=1,j=1;j<=cnt||q.size();i++){
		if(!q.size()) i=cg[j].l;
		while(j<=cnt&&cg[j].l<=i) q.push(-cg[j++].r);
		int t=-q.top();
		q.pop();
		if(t<i){
			for(int id=1;id<=cnt;id++)
				if(cg[id].r<t) ans++;
			cout<<t<<" "<<ans;
			return 0;
		}
	}
	cout<<"-1 "<<cnt;
	return 0;
}
posted @ 2025-12-30 16:30  white_tiger  阅读(7)  评论(0)    收藏  举报