(笔记)一类区间推平问题 珂朵莉树 ODT

根据zh_dou大佬的讲解,我们知道过期的迭代器是不能使用的,否则直接原地起飞。
——Leianha题解 P5350 【序列】

我们知道要是没有区间推平操作/随机数据 ODT 就是个假的算法。

如果操作仅依赖于区间推平操作,而不需要查询时遍历 set 上若干个区间,这也是对的,时间复杂度大概在 \(O(n\log n)\) 左右。

颜色段均摊有两层含义,都是建立在存在区间赋值(推平)操作的基础上。

  1. 随机数据:任意时刻的颜色段个数期望 \(O(\log n)\)
  2. 非随机数据:每次推平时访问的颜色段个数为均摊 \(O(n)\)

——EnofTaiPeople颜色段均摊和 CDQ 分治

简介与实现方法

该结构是通过 STL set 来维护存储相同颜色(相同值)连续段的暴力数据结构,需要在 set 中存储若干个三元组 \((l,r,k)\) 表示 \([l,r]\) 的所有颜色(值)都是 \(k\)

它的使用条件是数据随机。我们把区间覆盖或群体赋值称为区间推平操作。根据神秘理论这个东西在完全随机数据下是全局线性的,如果该操作较多单一操作是 \(O(\log n)\) 的,特殊构造下可以退化到单一操作 \(O(n\log^2 n)\),所以泛用性并不强。不建议在正式比赛中在没有复杂度保证的情况下使用。

\(\text{Split}\)

该操作 \(Split(pos)\) 通过找到一个区间使得 \(pos\in[l,r]\),然后根据 \(pos\) 将其分成 \([l,pos-1],[pos,r]\) 两个区间,并且返回指向 \([pos,r]\) 的迭代器。

typedef long long LL;
struct Node{
	int l,r;
	mutable LL v;
	bool operator<(const Node &a)const{return l<a.l;}
};
typedef set<Node>::iterator IT;
set<Node>s;
IT split(int pos){
	IT it=s.lower_bound(Node{pos,0,0});
	if(it!=s.end()&&it->l==pos)return it;
	it--;
	if((it->r)<pos)return s.end();
	int l=it->l,r=it->r;
	LL v=it->v;
	s.erase(it);
	s.insert(Node{l,pos-1,v});
	return s.insert(Node{pos,r,v}).first;
}

其中 mutable 的作用是使直接用迭代器访问元素时能够修改该值。需要注意一些特判,如能找到 \([pos,r]\) 就直接返回该区间。如果 \(pos>\max r\),那么该区间不存在,返回 s.end() 即可。

\(\text{Assign}\)

区间推平操作。具体来说,首先需要分出区间 \([l,r]\),然而 \(l,r\) 可能都不是 ODT 上面挂的三元组区间端点,所以需要先进行 \(\text{Split}\) 操作,让 \(r\) 变成某个区间的右端点,\(l\) 变成某个区间的左端点。那么先找到右侧迭代器 itr=split(r+1),然后找到左侧迭代器 itl=split(l),顺序不能反。原因也很简单,因为 split(r+1) 可能会影响 \(l\) 之后的区间形态,先找左再找右可能导致原来找的那个东西已经被删去了。但 split(l) 因为已经有了 split(r+1),所以并不会对 \(r\) 后面区间形态造成影响(\(l\) 所在区间右端点最大都只能是 \(r\))。暴力删掉两个迭代器间所有值然后加一个新的区间进去即可。

void assign(int l,int r,LL x){
	IT itr=split(r+1),itl=split(l);
	s.erase(itl,itr);
	s.insert(Node{l,r,x});
}

例题

好啦然后我们就写完了。由于其结构的特殊性,它能做很多一般数据结构不能做的事情,因为一个连续区间内值相等,所以计算什么 \([l,r]\) 内元素排序后第 \(x\) 个或者 \(\sum_{i=l}^r {a_i}^k\) 都是不在话下,直接上 set 迭代器暴力扫即可。

这是一个美味的缺省源
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
struct Node{
	int l,r;
	mutable LL v;
	bool operator<(const Node &a)const{return l<a.l;}
};
typedef set<Node>::iterator IT;
set<Node>s;
IT split(int pos){
	IT it=s.lower_bound(Node{pos,0,0});
	if(it!=s.end()&&it->l==pos)return it;
	it--;
	if((it->r)<pos)return s.end();
	int l=it->l,r=it->r;
	LL v=it->v;
	s.erase(it);
	s.insert(Node{l,pos-1,v});
	return s.insert(Node{pos,r,v}).first;
}
void assign(int l,int r,LL x){
	IT itr=split(r+1),itl=split(l);
	s.erase(itl,itr);
	s.insert(Node{l,r,x});
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	
	return 0;
}

CF915E Physical Education Lessons

区间推平,值域 \([0,1]\),数 \(1\) 的个数,直接上 ODT,维护一个全局变量记录和,在每次区间推平时访问要删掉的区间然后相应修改即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<LL,int> PII;
const LL MOD=1e9+7;
struct Node{
	int l,r;
	mutable int v;
	Node(int ll, int rr=0, int vv=0) : l(ll), r(rr), v(vv) {}
	bool operator<(const Node &a)const{return l<a.l;}
};
typedef set<Node>::iterator IT;
set<Node>s;
int n,q;
IT split(int pos){
	IT it=s.lower_bound(Node(pos));
	if(it!=s.end()&&it->l==pos)return it;
	it--;
	if((it->r)<pos)return s.end();
	int l=it->l,r=it->r,v=it->v;
	s.erase(it);
	s.insert(Node(l,pos-1,v));
	return s.insert(Node(pos,r,v)).first;
}
int ans;
void assign(int l,int r,int x){
	IT itr=split(r+1),itl=split(l);
	for(IT it=itl;it!=itr;it++)
		ans-=(it->v)*((it->r)-(it->l)+1);
	s.erase(itl,itr);
	s.insert(Node(l,r,x));
	ans+=(r-l+1)*x;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>q;
	s.insert(Node(1,n,1));
	ans=n;
	while(q--){
		int l,r,k;
		cin>>l>>r>>k;
		assign(l,r,k-1);
		cout<<ans<<'\n';
	}
	return 0;
}

CF896C Willem, Chtholly and Seniorious

区间加,区间推平,类似在线可持久化线段树的功能还有求区间 \(y\) 次方和。对于所有操作都可以转化为 \(Split\) 后遍历所有 \([l,r]\) 拆成的区间并统计答案。对于区间第 \(k\) 大的功能,把所有区间存下来按照颜色排个序即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<LL,int> PII;
const LL MOD=1e9+7;
const int N=1e5+5;
struct Node{
	int l,r;
	mutable LL v;
	Node(int ll, int rr=0, LL vv=0) : l(ll), r(rr), v(vv) {}
	bool operator<(const Node &a)const{return l<a.l;}
};
typedef set<Node>::iterator IT;
set<Node>s;
int n,m;
LL seed,ret,vmax,a[N];
int rnd(){
	ret=seed;
  seed=(seed*7+13)%MOD;
	return ret;
}
IT split(int pos){
	IT it=s.lower_bound(Node(pos));
	if(it!=s.end()&&it->l==pos)return it;
	it--;
	if((it->r)<pos)return s.end();
	int l=it->l,r=it->r;
	LL v=it->v;
	s.erase(it);
	s.insert(Node(l,pos-1,v));
	return s.insert(Node(pos,r,v)).first;
}
void assign(int l,int r,LL x){
	IT itr=split(r+1),itl=split(l);
	s.erase(itl,itr);
	s.insert(Node(l,r,x));
}
void add(int l,int r,LL x){
	IT itr=split(r+1),itl=split(l);
	for(IT it=itl;it!=itr;it++)
		it->v+=x;
}
LL qkpow(LL x,LL y,LL mod){
	LL res=1;
	x%=mod;
	while(y){
		if(y&1)res=res*x%mod;
		x=x*x%mod;
		y>>=1;
	}
	return res;
}
LL query1(int l,int r,int x){
	IT itr=split(r+1),itl=split(l);
	vector<PII>rk;
	for(IT it=itl;it!=itr;it++)
		rk.emplace_back(make_pair(it->v,(it->r)-(it->l)+1));
	sort(rk.begin(),rk.end());
	int now=0;
	for(PII v:rk){
		now+=v.second;
		if(now>=x)return v.first;
	}
	return 114514;
}
LL query2(int l,int r,LL x,LL y){
	LL res=0;
	IT itr=split(r+1),itl=split(l);
	for(IT it=itl;it!=itr;it++)
		(res+=qkpow(it->v,x,y)*((it->r)-(it->l)+1)%y)%=y;
	return res;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m>>seed>>vmax;
	for(int i=1;i<=n;i++)
    a[i]=rnd()%vmax+1,
    s.insert(Node(i,i,a[i]));
  for(int i=1;i<=m;i++){
  	int op=rnd()%4+1;
		LL x;
  	int l=rnd()%n+1,r=rnd()%n+1;
  	if(l>r)swap(l,r);
  	if(op==1){
  		x=rnd()%vmax+1;
  		add(l,r,x);
		}
		else if(op==2){
			x=rnd()%vmax+1;
			assign(l,r,x);
		}
  	else if(op==3){
  		x=rnd()%(r-l+1)+1;
  		cout<<query1(l,r,x)<<'\n';
		}
		else {
			x=rnd()%vmax+1;
			LL y=rnd()%vmax+1;
			cout<<query2(l,r,x,y)<<'\n';
		}
	}
	return 0;
}

P11945 [KTSC 2025] 军事基地 / safezone

某测试的 T4,哇 ODT 真的非常好写。我们用 ODT 来记录区间颜色编号即矩形编号,扫描线过程中我们在每个矩形左边界加入时就 \(Split\) 然后查询每个与其相交的区间,然后对应连边。我们难以做删除操作,因此每次连边时我们再查询这个区间 \([l,r]\) 目前有没有被矩形覆盖(和 \(>0\)),如果有就直接连。线段树区间和到矩形左边界加,右边界减即可。由于神秘 ODT 理论线性,瓶颈居然在线段树维护区间加减上,时间复杂度 \(O(n\log n)\)。跑得飞快,仅比正解慢了 \(0.1s\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
const int N=1e6+5;
int xa[N],ya[N],xb[N],yb[N];
int lshx[N],cntx,lshy[N],cnty;
struct Sc{int pos,l,r,op,id;}Q[N];
bool cmp(Sc x,Sc y){
	if(x.pos==y.pos)return x.op>y.op;
	return x.pos<y.pos;
}
int fa[N];
inline int fr(int x){return fa[x]==x?x:fa[x]=fr(fa[x]);}
void ins(int x,int y){
	int frx=fr(x),fry=fr(y);
	if(frx==fry)return ;
	fa[frx]=fry;
}
#define ls p<<1
#define rs p<<1|1
#define mid ((l+r)>>1)
LL sum[N<<2],tg[N<<2];
void pushup(int p){sum[p]=sum[ls]+sum[rs];}
void mktag(int p,int l,int r,LL v){
	sum[p]+=v*(r-l+1);
	tg[p]+=v;
}
void pushdown(int p,int l,int r){
	if(tg[p]){
		mktag(ls,l,mid,tg[p]);
		mktag(rs,mid+1,r,tg[p]);
		tg[p]=0;
	}
}
void update(int p,int l,int r,int L,int R,int v){
	if(L<=l&&r<=R){
		mktag(p,l,r,v);
		return ;
	}
	pushdown(p,l,r);
	if(L<=mid)update(ls,l,mid,L,R,v);
	if(R>mid)update(rs,mid+1,r,L,R,v);
	pushup(p);
}
LL query(int p,int l,int r,int L,int R){
	if(L<=l&&r<=R)return sum[p];
	LL res=0;
	pushdown(p,l,r);
	if(L<=mid)res+=query(ls,l,mid,L,R);
	if(R>mid)res+=query(rs,mid+1,r,L,R);
	return res;
}
#undef ls
#undef rs
#undef mid
struct Node{
	int l,r;
	mutable int v;
	Node(int ll, int rr=0, int vv=0) : l(ll), r(rr), v(vv) {}
	bool operator<(const Node &a)const{return l<a.l;}
};
typedef set<Node>::iterator IT;
set<Node>s;
IT split(int pos){
	IT it=s.lower_bound(Node(pos));
	if(it!=s.end()&&it->l==pos)return it;
	it--;
	if((it->r)<pos)return s.end();
	int l=it->l,r=it->r,v=it->v;
	s.erase(it);
	s.insert(Node(l,pos-1,v));
	return s.insert(Node(pos,r,v)).first;
}
void assign(int l,int r,int x){
	IT itr=split(r+1),itl=split(l);
	for(IT it=itl;it!=itr;it++){
		int l=it->l,r=it->r,v=it->v;
		if(v&&query(1,1,cnty,l,r))ins(v,x);
	}
	s.erase(itl,itr);
	s.insert(Node(l,r,x));
}
int shot[N],cnt;
vector<int> find_union(int n, vector<int> A, vector<int> B, vector<int> C,vector<int> D){
	vector<int>res;
	for(int i=1;i<=n;i++){
		fa[i]=i;
		xa[i]=A[i-1],ya[i]=B[i-1];
		xb[i]=C[i-1],yb[i]=D[i-1];
		lshx[++cntx]=xa[i];
		lshx[++cntx]=xb[i];
		lshy[++cnty]=ya[i];
		lshy[++cnty]=yb[i];
	}
	sort(lshx+1,lshx+1+cntx);
	cntx=unique(lshx+1,lshx+1+cntx)-(lshx+1);
	sort(lshy+1,lshy+1+cnty);
	cnty=unique(lshy+1,lshy+1+cnty)-(lshy+1);
	for(int i=1;i<=n;i++){
		xa[i]=lower_bound(lshx+1,lshx+1+cntx,xa[i])-lshx;
		xb[i]=lower_bound(lshx+1,lshx+1+cntx,xb[i])-lshx;
		ya[i]=lower_bound(lshy+1,lshy+1+cnty,ya[i])-lshy;
		yb[i]=lower_bound(lshy+1,lshy+1+cnty,yb[i])-lshy;
	}
	for(int i=1;i<=n;i++){
		Q[2*i-1]=(Sc){xa[i],ya[i],yb[i],1,i};
		Q[2*i]=(Sc){xb[i],ya[i],yb[i],-1,i};
	}
	s.insert(Node(1,cnty,0));
	int pos=1;
	sort(Q+1,Q+1+2*n,cmp);
	for(int i=1;i<=cntx;i++){
		while(pos<=2*n&&Q[pos].pos<=i){
			if(Q[pos].op==1)assign(Q[pos].l,Q[pos].r,Q[pos].id);
			update(1,1,cnty,Q[pos].l,Q[pos].r,Q[pos].op);
			pos++;
		}
	}
	for(int i=1;i<=n;i++){
		int fri=fr(i);
		if(!shot[fri])shot[fri]=++cnt;
		res.push_back(shot[fri]-1);
	}
	return res;
}

P5350 序列

\(1,2,3\) 操作前面都讲过。序列复制思想是把 \([l,r]\) 分裂后若干区间先存下来(复制),然后 \(\text{assign}\) 给需要赋值的区间,每次 \(\text{assign}\) 前清空 \([l,r]\) 即可。复制下来的方法是记录所有 \([l',r']\) 区间的 \([l'-l_a,r'-l_a]\),然后赋值的时候再加回 \(l_b\)。翻转同理,只不过每个区间 \([l',r']\) 需要一个 \(siz=r-l\),然后变成 \([siz-r',siz-l']\) 然后就能做啦。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<LL,int> PII;
const LL MOD=1e9+7;
const int N=3e5+5;
struct Node{
	int l,r;
	mutable LL v;
	bool operator<(const Node &a)const{return l<a.l;}
}a[N],b[N];
typedef set<Node>::iterator IT;
set<Node>s;
IT split(int pos){
	IT it=s.lower_bound(Node{pos,0,0});
	if(it!=s.end()&&it->l==pos)return it;
	it--;
	if((it->r)<pos)return s.end();
	int l=it->l,r=it->r;
	LL v=it->v;
	s.erase(it);
	s.insert(Node{l,pos-1,v});
	return s.insert(Node{pos,r,v}).first;
}
void assign(int l,int r,LL x){
	IT itr=split(r+1),itl=split(l);
	s.erase(itl,itr);
	s.insert(Node{l,r,x});
}
void add(int l,int r,LL x){
	IT itr=split(r+1),itl=split(l);
	for(IT it=itl;it!=itr;it++)
		(it->v+=x)%=MOD;
}
LL sum(int l,int r){
	IT itr=split(r+1),itl=split(l);
	LL res=0;
	for(IT it=itl;it!=itr;it++)
		(res+=(it->v)*((it->r)-(it->l)+1)%MOD)%=MOD;
	return res;
}
int n,m,cnta,cntb;
void get(int l,int r,bool tf){
	IT itr=split(r+1),itl=split(l);
	for(IT it=itl;it!=itr;it++){
		if(!tf)a[++cnta]=Node{(it->l)-l,(it->r)-l,it->v};
		else b[++cntb]=Node{(it->l)-l,(it->r)-l,it->v};
	}
}
void assign_on(int l,int r,bool tf){
	IT itr=split(r+1),itl=split(l);
	s.erase(itl,itr);
	int lim=(tf?cntb:cnta);
	for(int i=1;i<=lim;i++){
		if(!tf)s.insert(Node{a[i].l+l,a[i].r+l,a[i].v});
		else s.insert(Node{b[i].l+l,b[i].r+l,b[i].v});
	}
}
void assign_rev(int l,int r,bool tf){
	IT itr=split(r+1),itl=split(l);
	s.erase(itl,itr);
	int lim=(tf?cntb:cnta);
	int siz=r-l;
	for(int i=1;i<=lim;i++){
		int nl=a[i].l,nr=a[i].r;
		a[i].l=siz-nr,a[i].r=siz-nl;
		if(!tf)s.insert(Node{a[i].l+l,a[i].r+l,a[i].v});
		else s.insert(Node{b[i].l+l,b[i].r+l,b[i].v});
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		LL v;cin>>v;
		s.insert(Node{i,i,v});
	}
	while(m--){
		int op;cnta=cntb=0;
		cin>>op;
		if(op==1){
			int l,r;cin>>l>>r;
			cout<<sum(l,r)<<'\n';
		}
		else if(op==2){
			int l,r;
			LL v;cin>>l>>r>>v;
			assign(l,r,v);
		}
		else if(op==3){
			int l,r;
			LL v;cin>>l>>r>>v;
			add(l,r,v);
		}
		else if(op==4){
			int la,ra,lb,rb;
			cin>>la>>ra>>lb>>rb;
			get(la,ra,0);
			assign_on(lb,rb,0);
		}
		else if(op==5){
			int la,ra,lb,rb;
			cin>>la>>ra>>lb>>rb;
			get(la,ra,0);get(lb,rb,1);
			assign_on(la,ra,1);
			assign_on(lb,rb,0);
		}
		else {
			int l,r;cin>>l>>r;
			get(l,r,0);
			assign_rev(l,r,0);
		}
	}
	for(auto it:s){
		int siz=(it.r)-(it.l)+1;
		LL v=it.v;
		for(int i=1;i<=siz;i++)
			cout<<v<<' ';
	}
	return 0;
}

CF1423G Growing flowers

思想是正难则反,考虑离散化所有颜色共 \(cnt\) 个,假设每次询问首先每个区间内都有 \(cnt\) 个,答案即为 \(cnt\times(n-k+1)\)。然后考虑对于每个颜色 \(i\) 维护其在序列中的极长连续空序列,用 \(cnt\)set 实现,然后对于每个这样长度为 \(x\) 的连续段,其对全局答案的贡献即为 \(-\max(0,x-k+1)\)(考虑 \(x\) 内包含若干种 \(k\))。这个东西可以用两个下标为 \(x\) 的树状数组快速统计。

然后由于只有区间推平,用 ODT 实现,拆区间的时候顺便维护一下那 \(cnt\)set 还有树状数组信息即可。具体细节请前往题解区查看,实在是不想写了。时间复杂度 \(O((n+q)\log n)\)

点击查看代码
#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=2e5+5;
int n,a[N],qn;
int lsh[N],cnt;
struct Node{
	int l,r;
	mutable int v;
	bool operator <(const Node &a)const{return l<a.l;}
};
typedef set<Node>::iterator IT;
struct option{int op,l,r,x;}q[N];
struct BIT{
	LL av[N];
	int lowbit(int x){return x&-x;}
	void ins(int p,LL x){if(!p){return ;}for(int i=p;i<=n;i+=lowbit(i))av[i]+=x;}
	LL que(int p){LL res=0;for(int i=p;i;i-=lowbit(i)){res+=av[i];}return res;}
}T1,T2;
inline void change(int siz,int v){
	if(!siz)return ;
	T1.ins(siz,v*siz);T2.ins(siz,v);
}
set<Node>s,pos[N];
IT split(int Pos){
	IT it=s.lower_bound(Node{Pos,0,0});
	if(it!=s.end()&&(it->l)==Pos)return it;
	it--;
	if((it->r)<Pos)return s.end();
	int l=it->l,r=it->r,v=it->v;
	s.erase(it);
	s.insert(Node{l,Pos-1,v});
	pos[v].erase(Node{l,r,v});
	pos[v].insert(Node{l,Pos-1,v});
	pos[v].insert(Node{Pos,r,v});
	return s.insert(Node{Pos,r,v}).first;
}
inline void ins(const Node &u){
	int x=u.v,l=u.l,r=u.r;
	IT itl=pos[x].insert(u).first,itr=itl;
	int pre=(--itl)->r,nxt=(++itr)->l;
	change(nxt-pre-1,-1);
	change(l-pre-1,1);
	change(nxt-r-1,1);
}
inline void del(const Node &x){
	int v=x.v,L=x.l,R=x.r;
	IT itl=pos[v].find(x),itr=itl;
	int pre=(--itl)->r,nxt=(++itr)->l;
	change(L-pre-1,-1);
	change(nxt-R-1,-1);
	change(nxt-pre-1,1);
	pos[v].erase(++itl);
}
void assign(int l,int r,int x){
	IT itr=split(r+1),itl=split(l);
	for(IT it=itl;it!=itr;it++)del(*it);
	Node MY=Node{l,r,x};
	s.erase(itl,itr);
	s.insert(MY);
	ins(MY);
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>qn;
	for(int i=1;i<=n;i++)
		cin>>a[i],lsh[++cnt]=a[i];
	for(int i=1;i<=qn;i++){
		cin>>q[i].op;
		if(q[i].op==1){
			cin>>q[i].l>>q[i].r>>q[i].x;
			lsh[++cnt]=q[i].x;
		}
		else cin>>q[i].x;
	}
	sort(lsh+1,lsh+1+cnt);
	cnt=unique(lsh+1,lsh+1+cnt)-(lsh+1);
	for(int i=1;i<=n;i++)
		a[i]=lower_bound(lsh+1,lsh+1+cnt,a[i])-lsh;
	for(int i=1;i<=qn;i++)
		if(q[i].op==1)q[i].x=lower_bound(lsh+1,lsh+1+cnt,q[i].x)-lsh;
	for(int i=1;i<=cnt;i++)
		pos[i].insert(Node{0,0,0}),pos[i].insert(Node{n+1,n+1,0});
	T1.ins(n,1ll*n*cnt);
	T2.ins(n,cnt);
	for(int i=1;i<=n;i++){
		Node MY=Node{i,i,a[i]};
		s.insert(MY);
		ins(MY);
	}
	for(int i=1;i<=qn;i++){
		int op=q[i].op;
		if(q[i].op==1){
			int l=q[i].l,r=q[i].r,x=q[i].x;
			assign(l,r,x);
		}
		else {
			int k=q[i].x;
			cout<<1ll*cnt*(n-k+1)-((T1.que(n)-T1.que(k-1))-(k-1)*(T2.que(n)-T2.que(k-1)))<<'\n';
		}
	}
	return 0;
}

P5251 [LnOI2019] 第二代图灵机

ODT + 双指针的简单应用,\(3,4\) 操作搞两个桶然后讨论几种情况即可。考虑到有单点修改权值和区间颜色推平,我们用线段树维护区间和、区间最小值、区间最大值,其他都是一样的。时间复杂度 \(O(n\log n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+5,INF=1e9+1;
int n,m,c,buk[N],cnt,a[N];
struct Sgt{
	#define ls p<<1
	#define rs p<<1|1
	#define mid ((l+r)>>1)
	int sum[N<<2],mn[N<<2],mx[N<<2];
	void pushup(int p){
		sum[p]=sum[ls]+sum[rs];
		mn[p]=min(mn[ls],mn[rs]);
		mx[p]=max(mx[ls],mx[rs]);
	}
	void build(int p,int l,int r){
		if(l==r){sum[p]=mn[p]=mx[p]=a[l];return ;}
		build(ls,l,mid);build(rs,mid+1,r);
		pushup(p);
	}
	void modify(int p,int l,int r,int pos,int v){
		if(l==r){sum[p]=mn[p]=mx[p]=v;return ;}
		if(pos<=mid)modify(ls,l,mid,pos,v);
		else modify(rs,mid+1,r,pos,v);
		pushup(p);
	}
	int quesum(int p,int l,int r,int L,int R){
		if(L>R)return 0;
		if(L<=l&&r<=R)return sum[p];
		int res=0;
		if(L<=mid)res+=quesum(ls,l,mid,L,R);
		if(R>mid)res+=quesum(rs,mid+1,r,L,R);
		return res;
	}
	int quemx(int p,int l,int r,int L,int R){
		if(L>R)return 0;
		if(L<=l&&r<=R)return mx[p];
		int res=0;
		if(L<=mid)res=max(res,quemx(ls,l,mid,L,R));
		if(R>mid)res=max(res,quemx(rs,mid+1,r,L,R));
		return res;
	}
	int quemn(int p,int l,int r,int L,int R){
		if(L>R)return INF;
		if(L<=l&&r<=R)return mn[p];
		int res=INF;
		if(L<=mid)res=min(res,quemn(ls,l,mid,L,R));
		if(R>mid)res=min(res,quemn(rs,mid+1,r,L,R));
		return res;
	}
	#undef ls
	#undef rs
	#undef mid
}T;
struct Node{
	int l,r;
	mutable int v;
	bool operator <(const Node &a)const{return l<a.l;}
};
typedef set<Node>::iterator IT;
set<Node>s;
IT split(int pos){
	IT it=s.lower_bound(Node{pos,0,0});
	if(it!=s.end()&&(it->l)==pos)return it;
	it--;
	if((it->r)<pos)return s.end();
	int l=it->l,r=it->r,v=it->v;
	s.erase(it);
	s.insert(Node{l,pos-1,v});
	return s.insert(Node{pos,r,v}).first;
}
void assign(int l,int r,int v){
	IT itr=split(r+1),itl=split(l);
	s.erase(itl,itr);
	s.insert(Node{l,r,v});
}
int que_all(int l,int r){
	IT itr=split(r+1),itl=split(l);
	if(c==1)return T.quemn(1,1,n,l,r);
	IT L=itl;
	int res=INF;
	for(IT R=itl;R!=itr;R++){
		if(!buk[R->v])cnt++;
		buk[R->v]++;
		while(L!=R&&buk[L->v]>1)buk[L->v]--,L++;
		if(cnt==c)res=min(res,T.quesum(1,1,n,L->r,R->l));
	}
	cnt=0;
	for(IT R=itl;R!=itr;R++)
		buk[R->v]=0;
	return (res==INF?-1:res);
}
int que_unrep(int l,int r){
	IT itr=split(r+1),itl=split(l);
	IT L=itl;
	int res=T.quemx(1,1,n,l,r);
	for(IT R=itl;R!=itr;R++){
		int vnow=R->v;buk[vnow]++;
		while(L!=R&&buk[vnow]>1){buk[L->v]--,L++;}
		if(L==R){
			if(L!=itl&&prev(L)->v!=L->v)res=max(res,T.quesum(1,1,n,(L->l)-1,L->l));
		}
		else {
			if(L!=itl&&!buk[prev(L)->v])res=max(res,T.quesum(1,1,n,(L->l)-1,(R->l)));
			else res=max(res,T.quesum(1,1,n,L->l,R->l));
		}
		if((R->r)-(R->l)>0){
			while(L!=R)buk[L->v]--,L++;
			buk[L->v]--,L++;
		}
	}
	for(IT R=itl;R!=itr;R++)
		buk[R->v]=0;
	return res;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m>>c;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	T.build(1,1,n);
	for(int i=1;i<=n;i++){
		int b;cin>>b;
		s.insert(Node{i,i,b});
	}
	while(m--){
		int op;cin>>op;
		if(op==1){
			int x,y;cin>>x>>y;
			a[x]=y;
			T.modify(1,1,n,x,y);
		}
		else if(op==2){
			int l,r,y;
			cin>>l>>r>>y;
			assign(l,r,y);
		}
		else if(op==3){
			int l,r;cin>>l>>r;
			cout<<que_all(l,r)<<'\n';
		}
		else {
			int l,r;cin>>l>>r;
			cout<<que_unrep(l,r)<<'\n';
		}
	}
	return 0;
}

扩展 ODT

实际上对于部分需要维护连续信息的题来说,这已经不能叫 ODT 了,只是一种 set 维护区间的技巧。具体来说,这时候 ODT 不依赖于推平操作,因为我们没有类似区间查询等需要遍历多个迭代器的需求,而是找到一个即可。当然可能有 split 操作借鉴了 ODT,且有区间合并操作。

P11833 [省选联考 2025] 推箱子

考虑按照限制时间从小到大处理,这玩意为什么要用扩展 ODT 啊好难写啊啊啊。然后这个其实是一个维护区间内数严格连续的区间的 ODT,不妨叫它扩展 ODT(乱起的名字),那么我们不妨维护过程中 it->v 表示 \(a_l\) 的值,其它分别是 \(a_l+1,a_l+2,\dots\) 都可以递推出来。假设当前处理到 \(a_i\) 移动到 \(to\)\((i,to)\)),考虑到我们当找到一个冲突点时(以存在 \(a_i<b_i\)为例,且有 \(a_{i+1}\le b_i\),我们显然希望 \(a_{i+1}\) 移到 \(b_i+1\)),那么就可以递归到 \((i+1,b_i+1)\)。在这个过程中我们会 \(O(n)\) 次地处理一个 \(i\) 相同的点,时间复杂度 \(O(n^2)\)

考虑优化该做法,发现我们经过第一次操作后会把一些冲突的点排到若干个连续的数上面(\(b_i,b_i+1,b_i+2,\dots\)),而显然 \(b_i\) 是不用动的,那我们的时间耗散就在于我们还要接着移动后面的所有点到它们对应的 \(b_{pos}\) 上。发现最劣情况中如果从左到右依次这样平移是很不好的,考虑优化这一连续段平移操作,让每次集体平移变成可以接受的复杂度,使用扩展 ODT,记 \((l,r,v)\) 表示 \([l,r]\) 连续且 \(a_l=v\)\(a_{l+1}=v+1,a_{l+2}=v+2,\dots,a_r=v+(r-l)\)),然后每次平移到一起的时候合并区间,每次要平移的时候拆开区间即可。由于我们没有用到区间推平操作,且这样能够保证被推过的任意点 \(i\) 在自己想要到 \(b_i\) 之前的所有操作都可以被包含在推包含它的连续段操作中,瓶颈在于每次的 setlower_bound,时间复杂度 \(O(n\log n)\)

代码史中史,还要写分类讨论的 split 操作,感觉不如线段树 \(\frac{1}{10^{100}}\),且跑得飞慢,成功跑到疑似最劣解。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=2e5+5;
int a[N],b[N],pos[N],rk[N],n;
LL t[N],mytime;
bool legal;
struct Node{
	int l,r;
	mutable int v;
	bool operator<(const Node&a)const{return l<a.l;}
};
bool cmp(int x,int y){return t[x]<t[y];}
typedef set<Node>::iterator IT;
set<Node>s;
IT split(int pos,bool tf){
	IT it=s.lower_bound(Node{pos,0,0});
	if(it==s.end()||(it!=s.end()&&it->l>pos))it--;
	if(it->r<pos)return s.end();
	if(it->l==pos&&tf)return it;
	if(it->r==pos&&!tf)return it;
	int l=it->l,r=it->r,v=it->v;
	s.erase(it);
	if(tf){
		s.insert(Node{l,pos-1,v});
		return s.insert(Node{pos,r,v+(pos-l)}).first;
	}
	else {
		s.insert(Node{pos+1,r,v+(pos+1-l)});
		return s.insert(Node{l,pos,v}).first;
	}
}
int query(int pos){
	IT it=s.lower_bound(Node{pos,0,0});
	if(it!=s.end()&&it->l==pos)return it->v;
	else {
		it--;
		return it->v+pos-(it->l);
	}
}
IT option(int pos,int to){
	int nowp=query(pos);
	if(nowp<to){
		IT it=split(pos,1),itp=next(it);
		int l=pos,r=it->r,rpos=it->v+(r-l);
		int v=it->v;
		if(rpos+to-nowp>=itp->v){
			itp=option(itp->l,rpos+to-nowp+1);
			int pr=itp->r;
			s.erase(it);s.erase(itp);
			it=s.insert(Node{l,pr,v+(to-nowp)}).first;
		}
		else it->v+=to-nowp;
		mytime+=1ll*(r-l+1)*(to-nowp);
		return it;
	}
	else if(nowp>to){
		IT it=split(pos,0),itp=prev(it);
		int l=it->l,r=pos,lpos=it->v;
		int v;
		if(lpos-(nowp-to)<=itp->v+(itp->r-itp->l)){
			itp=option(itp->r,lpos-(nowp-to)-1);
			int pl=itp->l;v=itp->v;
			s.erase(it);s.erase(itp);
			it=s.insert(Node{pl,r,v}).first;
		}
		else it->v-=nowp-to;
		mytime+=1ll*(r-l+1)*(nowp-to);
		return it;
	}
	else return s.end();
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int cc,TT;
	cin>>cc>>TT;
	while(TT--){
		s.clear();mytime=0;
		cin>>n;legal=1;
		a[0]=b[0]=0;a[n+1]=b[n+1]=1e9+1;
		s.insert(Node{0,0,0});
		s.insert(Node{n+1,n+1,(int)(1e9+1)});
		for(int i=1;i<=n;i++){
			cin>>a[i]>>b[i]>>t[i];
			s.insert(Node{i,i,a[i]});
			rk[i]=i;
		}
		sort(rk+1,rk+1+n,cmp);
		for(int i=1;i<=n;i++){
			int u=rk[i];
			option(u,b[u]);
			if(mytime>t[u]){legal=0;break;}
		}
		cout<<(legal?"Yes\n":"No\n");
	}
	return 0;
}

P5102 [JOI 2016 Final] 领地 / Territory

我们容易得到一步观察:令第一步起点到第二步起点向量\((\Delta x,\Delta y)\),则第一步的每个 \((x,y)\) 都会扩展到 \(i\) 步时的 \((x+(i-1)\Delta x,y+(i-1)\Delta y)\),那么我们第 \(i\) 步所在点坐标就可以写成 \((x,y,i-1)(i\in[1,k])\)\((x,y,i)(i\in[0,k-1])\)。我们把第一轮点称为原始点,第一轮及其他轮点称为复制点(其中第一轮看作对自己的复制即可)。

我们发现这样还是不是很可做,因为判定合法性需要找到 \((x,y+1)(x+1,y)(x+1,y+1)\),然而当前状态下我们甚至要枚举 \(k\) 才能得到。考虑怎么快速找到这些点,这启发我们把每个坐标按照 \(\bmod {\Delta x}\) 剩余系分类,这样每个 \((x+i\Delta x,y+i\Delta y)\) 就可以写成 \((x,y,i)\),其中 \(x\in[0,\Delta x)\)。对应地,我们需要在数轴上平移 \(i\) 的定义域。把 \(i\) 定义域放上来就变成了 \((x,y,[l,r])(i\in [l,r])\)

这样我们就只需要做第 \(1\) 轮就可以表示出所有坐标,如果 \([l,r]\) 加入 \((x,y,S)\) 就让 \(S\)\([l,r]\) 取并。统计答案时,枚举左下角点,我们只需要对这四个点定义域取交就可以得到左下角点的多少个复制点是领地点。

其实这玩意严格来说不算 ODT,只是觉得挺类似的。扩展 ODT 维护区间交并。具体来说,每个集合都有若干个区间 \([l,r]\),需要实时计算集合之间两两的交集与并集。考虑对一个集合加入/删除若干区间即可,用双指针可以维护(并不容易,有很多细节)。

然后还有一些分讨,我们在代码中对 \(\Delta x\) 进行了取模,这意味着 \(\Delta x>0\)。但是实际情况是可能出现 \(\Delta x\le 0\),我们的处理方法分别是:

  1. $\Delta x<0 $,直接交换原字符串中 E,W 所代表方向即可。

  2. $\Delta x=0 $,直接交换 \(x,y\) 轴即可。

  3. \(\Delta x=0\land\Delta y=0\),只做第一轮即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<LL,LL> PII;
typedef set<PII>::iterator IT;
const int N=1e5+5;
int n,k;char S[N];
int Dx[4]={1,0,-1,0},Dy[4]={0,1,0,-1};
int rmp[30],op[N];
LL X[N],Y[N];
map<PII,set<PII> >mp;
set<PII>u;
void ins(PII id,PII my){
	if(!mp[id].size()){mp[id].insert(my);return ;}
	LL l=my.first,r=my.second;
	IT ITp=mp[id].lower_bound(my);
	if(ITp!=mp[id].begin()&&(*prev(ITp)).second>=l)
		ITp=prev(ITp),l=min(l,ITp->first);
	if((*ITp).first>r||ITp==mp[id].end())
		{mp[id].insert(make_pair(l,r));return ;}
	IT ITs=mp[id].upper_bound(make_pair(r,0));
	r=max(r,(*prev(ITs)).second);
	mp[id].erase(ITp,ITs);
	mp[id].insert(make_pair(l,r));
}
inline void cap(int x,int y,bool tf){
	PII id=make_pair(x,y);
	if(u.empty())return ;
	if(mp[id].empty()){u.clear();return ;}
	IT itp=mp[id].begin();
	set<PII>ex;
	for(IT it=u.begin();it!=u.end();it++){
		LL l=it->first,r=it->second;
		while(itp!=mp[id].end()&&(itp->second)-tf<l)itp++;
		if(itp==mp[id].end()){swap(ex,u);return ;}
		while(itp!=mp[id].end()&&(itp->first)-tf<=r){
			ex.insert(make_pair(max((itp->first)-tf,l),
			min((itp->second)-tf,r)));
			++itp;
		}
		if(itp!=mp[id].begin())itp--;
	}
	swap(ex,u);
}
LL ans;
map<int,map<int,bool> >tg,rcd;
inline void check(int x,int y){
	if(rcd[x][y])return ;
	if(tg[x][y]&&tg[x+1][y]&&tg[x][y+1]&&tg[x+1][y+1]){
		rcd[x][y]=1;
		ans++;
	}
}
void naive(){
	tg[0][0]=1;
	LL x=0,y=0;
	for(int i=1;i<=n;i++){
		x+=Dx[op[i]],y+=Dy[op[i]];
		if(!tg[x][y]){
			tg[x][y]=1;
			check(x,y);check(x-1,y);
			check(x,y-1);check(x-1,y-1);
		}
		tg[x][y]=1;
	}
}
int main(){
	rmp['E'-'A']=0,rmp['N'-'A']=1,
	rmp['W'-'A']=2,rmp['S'-'A']=3;
	scanf("%d%d",&n,&k);k--;
	scanf("%s",S+1);
	for(int i=1;i<=n;i++)
		op[i]=rmp[S[i]-'A'];
	LL x=0,y=0;
	for(int i=1;i<=n;i++)
		x+=Dx[op[i]],y+=Dy[op[i]];
	LL dx=x,dy=y;
	if(!dx&&!dy){
		naive();
		printf("%lld",ans);
		return 0;
	}
	if(!dx)swap(rmp['E'-'A'],rmp['N'-'A']),
		swap(rmp['W'-'A'],rmp['S'-'A']),swap(dx,dy);
	if(dx<0)swap(rmp['E'-'A'],rmp['W'-'A']),dx=-dx;
	for(int i=1;i<=n;i++)
		op[i]=rmp[S[i]-'A'];
	x=0,y=0;
	ins(make_pair(x,y),make_pair(0,k));
	for(int i=1;i<=n;i++){
		x+=Dx[op[i]],y+=Dy[op[i]];
		LL t=(x-(x%dx+dx)%dx)/dx;
		X[i]=x-dx*t,Y[i]=y-dy*t;
		ins(make_pair(X[i],Y[i]),make_pair(t,t+k));
	}
	for(int i=0;i<=n;i++){
		LL x=X[i],y=Y[i];
		if(!rcd[x][y]){
			u=mp[make_pair(x,y)];
			cap(x,y+1,0);
			if(x==dx-1){
				cap(0,y-dy,1);
				cap(0,y+1-dy,1);
			}
			else cap(x+1,y,0),cap(x+1,y+1,0);
			for(IT it=u.begin();it!=u.end();it++)
				ans+=(it->second)-(it->first)+1;
			rcd[x][y]=1;
		}
	}
	printf("%lld",ans);
	return 0;
}
posted @ 2025-09-18 20:24  TBSF_0207  阅读(29)  评论(0)    收藏  举报