石中2024寒假集训 Day4

今天学了线段树。要注意:

  1. 节点 pp 的懒惰标记是代表要下传的,当前节点的懒惰标记应该用于更新子节点并下传给子节点。
  2. 在查询与更新时都要下传,防止子节点没有及时更新。

例题 1:线段树 1

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5+10;
int a[N],n,m;
struct sgtree{
	#define l(i) ((i)<<1)
	#define r(i) ((i)<<1|1)
	#define mid (l+r>>1)
	int tag,v;
}c[N<<2];
void pushu(int p) {
	c[p].v = c[l(p)].v+c[r(p)].v;
}
void pushd(int p,int l,int r) {
	c[l(p)].tag+=c[p].tag, c[r(p)].tag+=c[p].tag;
	c[l(p)].v+=(mid-l+1)*c[p].tag, c[r(p)].v+=(r-mid)*c[p].tag;
	c[p].tag = 0;
}
void build(int p,int l,int r) {
	c[p].tag = 0;
	if(l==r) {c[p].v=a[l];return ;}
	build(l(p),l,mid);
	build(r(p),mid+1,r);
	pushu(p);
}
void upd(int nl,int nr,int p,int l,int r,int k) {
	if(nl<=l&&r<=nr) {
		c[p].tag+=k;
		c[p].v+=(r-l+1)*k;
		return ;
 	}
 	pushd(p,l,r);
 	if(nl<=mid) upd(nl,nr,l(p),l,mid,k);
 	if(nr>mid) upd(nl,nr,r(p),mid+1,r,k);
 	pushu(p);
}
int ans(int nl,int nr,int p,int l,int r) {
	int s = 0;
	if(nl<=l&&r<=nr) return c[p].v;
	pushd(p,l,r);
	if(nl<=mid) s+=ans(nl,nr,l(p),l,mid);
	if(nr>mid) s+=ans(nl,nr,r(p),mid+1,r);
	return s;
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	build(1,1,n);
	while(m--) {
		int op,x,y,k;
		cin>>op>>x>>y;
		if(op==1) {
			cin>>k;
			upd(x,y,1,1,n,k);
		}
		else {
			cout<<ans(x,y,1,1,n)<<"\n";
		}
	}
	return 0;
}

动态开点

假如要存的值域是 10910^9,但是只有 10510^5 个数呢?可以离散化,但是来回地映射增加实现难度。

在上文的模板中,节点 pp 的儿子是 p×2,p×2+1p\times2,p\times2+1

这就导致空间的浪费。如果我们像链式前向星那样,就可以减少空间使用。以下是一个动态开点权值线段树的简单题。

中位数

查询 kk 小值可以用二分实现。

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
int a[N],n,m,ix,rt;
struct sgtree{
	#define l(i) (c[i].ls)
	#define r(i) (c[i].rs)
	#define mid (l+r>>1)
	int tag,v,ls,rs;
}c[N*80];
void pushu(int p) {
	c[p].v = c[l(p)].v+c[r(p)].v;
}
void pushd(int p,int l,int r) {
	if(!l(p)) l(p)=++ix,c[l(p)].tag=0;//开点
	if(!r(p)) r(p)=++ix,c[r(p)].tag=0;
	c[l(p)].tag+=c[p].tag, c[r(p)].tag+=c[p].tag;
	c[l(p)].v+=(mid-l+1)*c[p].tag, c[r(p)].v+=(r-mid)*c[p].tag;
	c[p].tag = 0;
}
//void build(int p,int l,int r) {
//	c[p].tag = 0;
//	if(l==r) {c[p].v=a[l];return ;}
//	build(l(p),l,mid);
//	build(r(p),mid+1,r);
//}
void upd(int nl,int nr,int &p,int l,int r,int k) {
	if(!p) p=++ix,c[p].tag=0;//开点
	if(nl<=l&&r<=nr) {
		c[p].tag+=k;
		c[p].v+=(r-l+1)*k;
		return ;
 	}
 	pushd(p,l,r);
 	if(nl<=mid) upd(nl,nr,l(p),l,mid,k);
 	if(nr>mid) upd(nl,nr,r(p),mid+1,r,k);
 	pushu(p);
}
int ans(int nl,int nr,int p,int l,int r) {
	if(!p) return 0;//如果当前点没开,也就没有访问过,无需记录。
	int s = 0;
	if(nl<=l&&r<=nr) return c[p].v;
	pushd(p,l,r);
	if(nl<=mid) s+=ans(nl,nr,l(p),l,mid);
	if(nr>mid) s+=ans(nl,nr,r(p),mid+1,r);
	return s;
}
signed main() {
	cin>>n;
	for(int i=1;i<=n;i++) {
		cin>>a[i];
		upd(a[i],a[i],rt,0,1e9,1);
		if(i&1) {
			int l=-1,r=1e9+1;
			while(l+1<r) {
//				cout<<mid<<"\n";
				if(ans(0,mid,rt,0,1e9)<i/2+1) l=mid;
				else r=mid;
			}
			cout<<l+1<<"\n";
		}
	}
	return 0;
}

应用

线段树可以做一些树状数组做不到的东西,比如区间最值,区间加区间平方和之类的。以下是练习题。

T1:方差

区间加,区间方差。其实是推式子题。方差的公式为:i=lr(xiavg)2/(rl+1),avg=(i=lrxi)/(rl+1)\sum\limits_{i=l}^{r}(x_i-avg)^2/(r-l+1),avg=(\sum\limits_{i=l}^{r}x_i)/(r-l+1)。化简一下就可以得到方差为

i=lrxi2+(rl+1)×avg22avg×i=lrxirl+1\large\frac{\sum\limits_{i=l}^{r}x_i^2+(r-l+1)\times avg^2-2avg\times\sum\limits_{i=l}^{r}x_i}{r-l+1} =i=lrxi2rl+1avg2\large=\frac{\sum\limits_{i=l}^{r}x_i^2}{r-l+1}-avg^2

平方和的求法:

i=lrxi2i=lr(xi+k)2\sum\limits_{i=l}^{r}x_i^2 \to \sum\limits_{i=l}^{r}(x_i+k)^2 =i=lrxi2+2k×i=lrxi+(rl+1)k2=\sum\limits_{i=l}^{r}x_i^2+2k\times\sum\limits_{i=l}^{r}x_i+(r-l+1)k^2

使用线段树可以方便地维护,注意平方和要先更新。

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5+10;
double a[N];int n,m;
struct sgtree {
	#define l(i) ((i)<<1)
	#define r(i) ((i)<<1|1)
	#define mid (l+r>>1)
	double tag, v;
}c[N<<2],d[N<<2];//和,平方和 
void pushu(int p) {c[p].v = c[l(p)].v+c[r(p)].v; d[p].v=d[l(p)].v+d[r(p)].v;}
void pushd(int p,int l,int r) {
	d[l(p)].tag+=d[p].tag, d[r(p)].tag+=d[p].tag;
	d[l(p)].v+=2*d[p].tag*c[l(p)].v+(mid-l+1)*d[p].tag*d[p].tag,
	d[r(p)].v+=2*d[p].tag*c[r(p)].v+(r-mid)*d[p].tag*d[p].tag;
	d[p].tag = 0;
	c[l(p)].tag+=c[p].tag, c[r(p)].tag+=c[p].tag;
	c[l(p)].v+=(mid-l+1)*c[p].tag, c[r(p)].v+=(r-mid)*c[p].tag;
	c[p].tag = 0;
}
void build(int p,int l,int r) {
	c[p].tag = 0, d[p].tag = 0;
	if(l==r) {c[p].v=a[l],d[p].v=a[l]*a[l];return ;}
	build(l(p),l,mid);
	build(r(p),mid+1,r);
	pushu(p);
}
void upd(int nl,int nr,int p,int l,int r,double k) {
	if(nl<=l&&r<=nr) {
		d[p].tag+=k;
		d[p].v+=2*k*c[p].v+(r-l+1)*k*k; 
		c[p].tag+=k;
		c[p].v+=(r-l+1)*k;
		return ;
 	}
 	pushd(p,l,r);
 	if(nl<=mid) upd(nl,nr,l(p),l,mid,k);
 	if(nr>mid) upd(nl,nr,r(p),mid+1,r,k);
 	pushu(p);
}
double ansc(int nl,int nr,int p,int l,int r) {
	double s = 0;
	if(nl<=l&&r<=nr) return c[p].v;
	pushd(p,l,r);
	if(nl<=mid) s+=ansc(nl,nr,l(p),l,mid);
	if(nr>mid) s+=ansc(nl,nr,r(p),mid+1,r);
	return s;
}

double ansd(int nl,int nr,int p,int l,int r) {
	double s = 0;
	if(nl<=l&&r<=nr) return d[p].v;
	pushd(p,l,r);
	if(nl<=mid) s+=ansd(nl,nr,l(p),l,mid);
	if(nr>mid) s+=ansd(nl,nr,r(p),mid+1,r);
	return s;
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	build(1,1,n);
	while(m--) {
		int op,x,y;double k;
		cin>>op>>x>>y;
		if(x>y) swap(x,y);
		if(op==1) {
			cin>>k;
			upd(x,y,1,1,n,k);
		}
		if(op==2) {
			printf("%.4lf\n",ansc(x,y,1,1,n)/(y-x+1));
		}
		if(op==3) {
			double xb=ansc(x,y,1,1,n)/(y-x+1);
			printf("%.4lf\n",ansd(x,y,1,1,n)/(y-x+1)-xb*xb);
		}
	}
	return 0;
}

T2

给一个长度是 nn 的数列 aa 接下来在数列 aa 上进行如下 mm 次操作,操作类型共两种:

  1. 1 x yi=xyxi\sum\limits_{i=x}^yx_i

  2. 2 x y zaiaja_i\sim a_j 分别 z\oplus z

我们对于区间异或束手无策。但看到位运算就该想到按二进制位拆分来算。

如果我们一位位地统计,那么只需知道所有数中该位上 11 的个数。根据异或的性质,如果 zz 在该位为 00,没有影响,如果为 11,那么 aa 序列会被取反,也就意味着我们可以维护每个区间 1 的个数了。我们只要开大概 20 个线段树维护,统计答案时按位加权求和即可。

#pragma GCC optimize(3,"Ofast","inline")
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
int a[N],n,m;
struct sgtree{
	#define l(i) ((i)<<1)
	#define r(i) ((i)<<1|1)
	#define mid (l+r>>1)
	struct sg{
		int tag,v;
	}c[N<<2];
	//tag:当前区间^1 的个数 区间 1 的个数
	void pushu(int p) {
	c[p].v = c[l(p)].v+c[r(p)].v;
	}
	void pushd(int p,int l,int r) {
		c[l(p)].tag+=c[p].tag, c[r(p)].tag+=c[p].tag;
//		cout<<l<<" "<<r<<"\n"<<c[l(p)].tag<<" "<<c[r(p)].tag<<endl;
		if(c[p].tag&1) c[l(p)].v=(mid-l+1)-c[l(p)].v;
		if(c[p].tag&1) c[r(p)].v=(r-mid)-c[r(p)].v;
		c[p].tag = 0;
	}
	void build(int p,int l,int r) {
		c[p].tag = 0;
		if(l==r) {c[p].v=a[l];return ;}
		build(l(p),l,mid);
		build(r(p),mid+1,r);
		pushu(p);
	}
	void upd(int nl,int nr,int p,int l,int r) {
		if(nl<=l&&r<=nr) {
			c[p].tag++;
			c[p].v=(r-l+1)-c[p].v; 
			return ;
	 	}
	 	pushd(p,l,r);
	 	if(nl<=mid) upd(nl,nr,l(p),l,mid);
	 	if(nr>mid) upd(nl,nr,r(p),mid+1,r);
	 	pushu(p);
	}
	int ans(int nl,int nr,int p,int l,int r) {
		int s = 0;
		if(nl<=l&&r<=nr) return c[p].v;
		pushd(p,l,r);
		if(nl<=mid) s+=ans(nl,nr,l(p),l,mid);
		if(nr>mid) s+=ans(nl,nr,r(p),mid+1,r);
		return s;
	}
}f[22];
int t[N];
int op,x,y,k;
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
	cin>>n;
	int mt=0;
	for(int i=1;i<=n;i++) {
		cin>>t[i]; mt=max(mt,t[i]);
	}
	for(int i=0;i<=__lg(mt);i++) {
		int k = 1<<i;
		for(int j=1;j<=n;j++) {
			a[j]=(t[j]>>i)&1;
		}
//		for(int i=1;i<=n;i++) cout<<a[i]<<" ";
//		cout<<endl;
		f[i].build(1,1,n);
	}
	int ad=0;
//	for(int i=0;i<18;i++) cout<<f[i].ans(1,n,1,1,n)*(1<<i)<<"\n";
//	cout<<"FU:"<<ad<<"\n";
	cin>>m;
	while(m--) {
		cin>>op>>x>>y;
		if(op==1) {
			long long ans = 0;
			for(int i=0;i<=__lg(mt);i++) {
//			cout<<"I="<<i<<endl;
				ans+=f[i].ans(x,y,1,1,n)*(1LL<<i);
			}
			cout<<ans<<"\n";
		}
		else {
			cin>>k;
			for(int i=0;i<=__lg(k);i++) {
				if((k>>i)&1) f[i].upd(x,y,1,1,n);
			}
		}
	}
	return 0;
}
/*
5
1 1 1 1 1
4
2 1 3 1
1 1 1
1 1 1
1 1 1
*/
posted @ 2024-01-28 17:59  cjrqwq  阅读(12)  评论(0)    收藏  举报  来源