线段树的鬼畜操作

一 线段树维护区间取模

题面

题目大意是让你维护区间求和,区间取模,单点修改。

如果我们去掉区间取模的这个操作话,就会发现这就是一个线段树单点修改区间求和的板子。

那么我们只需要考虑的就只有区间取模这个操作。

我们可以联想到模对加法是封闭的。

\(a \% p + b \% p + c \% p + d \% p\) = \((a+b+c+d) \% p\);

所以我们可以正常维护区间和,取模后,区间的和 = \(区间修改之前的和 \% p\)

模法的另一个性质

当模数大于一个数时,他取不取模都一样。这就可以看做一个剪枝。

我们维护一个区间最大值,当这个数比模数要小时,直接\(return\) 就行了。

这就是本题比较重要的一个优化。

对于大于模数的数,我们可以直接暴力修改。(暴力出奇迹

然后本题没了。。。。

代码

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define LL long long
const int N = 1e5+10;
int n,m,opt,l,r,x;
LL a[N];
struct tree{
	#define l(o) tr[o].lc
	#define r(o) tr[o].rc
	#define sum(o) tr[o].sum
	#define maxn(o) tr[o].maxn
	struct node{
		int lc,rc;
		LL sum,maxn;
	}tr[N<<2];
	void up(int o)
	{
		sum(o) = sum(o<<1) + sum(o<<1|1);//区间和
		maxn(o) = max(maxn(o<<1),maxn(o<<1|1));//维护区间最大值
	}
	void build(int o,int L,int R)//正常建树
	{
		l(o) = L, r(o) = R;
		if(L == R)
		{
			sum(o) = maxn(o) = a[L];
			return ;
		}
		int mid = (L + R)>>1;
		build(o<<1,L,mid);
		build(o<<1|1,mid+1,R);
		up(o);
	}
	void change(int o,int x,int val)//单点修改
	{
		if(l(o) == r(o))
		{
			sum(o) = maxn(o) = val;
			return ;
		}
		int mid = (l(o) + r(o))>>1;
		if(x <= mid) change(o<<1,x,val);
		if(x > mid) change(o<<1|1,x,val);
		up(o);
	}
	void chenge(int o,int L,int R,int mod)//区间取模
	{
		if(maxn(o) < mod) return;//当区间最大值都小于模数时,直接return
		if(l(o) == r(o))//单点暴力修改
		{
			sum(o) = sum(o) % mod;
			maxn(o) = maxn(o) % mod;
			return ;
		}
		int mid = (l(o) + r(o))>>1;
		if(L <= mid) chenge(o<<1,L,R,mod);
		if(R > mid) chenge(o<<1|1,L,R,mod);
		up(o);
	}
	LL ask(int o,int L,int R)//正常查询
	{
		LL ans = 0;
		if(L <= l(o) && R >= r(o))
		{
			return sum(o);
		}
		int mid = (l(o) + r(o))>>1;
		if(L <= mid) ans += ask(o<<1,L,R);
		if(R > mid) ans += ask(o<<1|1,L,R);
		return ans;
	}
}tree;
int main()
{
	scanf("%d%d",&n,&m);
	for(int i = 1; i <= n; i++) scanf("%lld",&a[i]);
	tree.build(1,1,n);
	for(int i = 1; i <= m; i++)
	{
		scanf("%d",&opt);
		if(opt == 1)
		{
			scanf("%d%d",&l,&r);
			printf("%lld\n",tree.ask(1,l,r));//区间求和
		}
		if(opt == 2)
		{
			scanf("%d%d%d",&l,&r,&x);
			tree.chenge(1,l,r,x);//区间取模
		//	for(int i = 1; i <= n; i++) cout<<tree.ask(1,i,i)<<endl;
		}
		if(opt == 3)
		{
			scanf("%d%d",&l,&x);
			tree.change(1,l,x);//单点修改
		}
	}
	return 0;
}

二:线段树维护等差数列

题面

题目大意就是让线段树维护区间加等差数列,和单点查询。

前置芝士

差分数组

定义:对于已知有n个元素的数列d,建立记录它每项与前一项差值的差分数组d 即 \(d[i]\) = \(a[i] - a[i-1]\)

性质:计算数列各项的值 a[x] = 原数列a[x]的值 + \(\displaystyle\sum_{i=1}^{x} d[i]\)

我们考虑到等差数列每一项的差值是一定的,所以我们可以考虑线段树维护一个差分数组


查询某个节点的值时,就是用原数列 \(a[x]\) 的值加上\(1-x\)的区间和。

这样查询操作就可以解决了。那么修改操作呢???

1.\(L\)\(L-1\)的差值为\(k\),所以我们可以在\(L\)的差分数组上加\(K\),用线段树来单点修改

2.从\(L+1\)\(R\) 其中每一项与前一项的差值都为\(d\),所以我们在\(L+1-R\)的差分数组都加上\(d\),用线段树来维护区间修改

3.\(R+1\)\(R\)这一项的差值为\(-(k + (R-L) * d)\),其实就是等差数列的和,直接线段树单点修改就可以了。

线段树和普通的线段树一样,直接维护区间修改,区间求和就可以了。(才不会告诉你我懒得写单点修改呢
代码

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 1e5+10;
int k,d,x;
int a[N],n,m,opt,l,r;
struct tree{//普通的线段树
	#define l(o) tr[o].lc
	#define r(o) tr[o].rc
	#define add(o) tr[o].add
	#define sum(o) tr[o].sum 
	struct node{
		int lc,rc;
		int add,sum;
	}tr[N<<2];
	void up(int o){
		sum(o) = sum(o*2) + sum(o*2+1);
	}
	void down(int o){
		if(add(o)){
	    	add(o*2) += add(o);
	    	add(o*2+1) += add(o);
	    	sum(o*2) += add(o) *(r(o*2) - l(o*2) +1);
	    	sum(o*2+1) += add(o) * (r(o*2+1) - l(o*2+1) +1);
	    	add(o) = 0;
	    }
	}
	void build(int o,int L,int R){
		l(o) = L; r(o) = R;
		if(L == R){
			sum(o) = 0;//一开始差分数组都定义为0
			return ;
		}
		int mid =  (L + R) / 2;
		build(o*2,L,mid);
		build(o*2+1,mid+1,R);
		up(o);
	} 
	void chenge(int o,int L,int R,int val){
		if(L <= l(o) && R >= r(o)){
			add(o) += val;
			sum(o) += val * (r(o) - l(o) + 1);
			return ;
		}
		down(o);
		int mid = (l(o) + r(o)) / 2;
		if(L <= mid) chenge(o*2,L,R,val);
		if(R > mid) chenge(o*2+1,L,R,val);
		up(o);
	}
	int ask(int o,int L,int R){
		int ans = 0;
		if(L <= l(o) && R >= r(o)){
			return sum(o);
		}
		down(o);
		int mid = (l(o) + r(o)) / 2;
		if(L <= mid) ans += ask(o*2,L,R);
		if(R > mid) ans += ask(o*2+1,L,R);
		return ans;
	}
}tree;
int main(){
	scanf("%d%d",&n,&m);
	for(int i = 1; i <= n; i++) scanf("%d",&a[i]);
	tree.build(1,1,n);
	for(int i = 1; i <= m; i++){
		scanf("%d",&opt);
		if(opt == 1){
			scanf("%d%d%d%d",&l,&r,&k,&d);
			tree.chenge(1,l,l,k);
			if(r > l) tree.chenge(1,l+1,r,d);
			if(r < n)tree.chenge(1,r+1,r+1, -(k + (r-l) * d));
		}
		else{
			scanf("%d",&x);
			printf("%d\n",a[x] + tree.ask(1,1,x));//差分数组的性质
		}
	}
	return 0;
}

突然觉得自己以前的代码写的好丑,没写位运算QAQ。(down函数也贼丑)

三 线段树维护区间排序

题面

题目大意是让我们维护每次区间排序后的结果,并询问排完序后第\(pos\)位置的值。

看到这道题,我们第一眼可能不会想到线段树,而是暴力快排。

然而,这道题可以转换为线段树(当然你也可以用珂朵莉树

我们可以将这个序列转换为01序列在进行排序。

当我们转换为01序列后,我们可以用线段树来维护,记录每个区间1的个数为\(cnt\)

1.对于升序排序,我们可以将 \(L\)\(R-cnt\)这一段区间全部变为0,并用线段树维护。

再将 \(R-cnt+1\)\(R\)这一段区间全部变为1,、同理用线段树维护。因为在升序排序中

0肯定要排在1前面

2.同理,对于降序排序,我们将 \(L\)\(L+cnt-1\)这一段区间变为1,将\(L+cnt\)\(R\)

这一段区间全部变为0,并用线段树来维护。

那么怎么转换为01序列呢????

题目中给出的序列为\(1-n\)的全排列,那么我们可以二分一个答案 mid.

我们将大于等于 mid 的数变为1,小于的则变为0。

并用线段树来模拟排序。当我们要查询的pos这个位置的数字为1代表我们的答案

是大于等于mid 的,所以我们上扩大二分下限。反之减小上限。

坑点

1.tag 标记要为 0,1,-1, 三种状态。-1表示没有标记。

0为区间变为0,1的话则为区间变为1.

2.要注意线段树模拟排序的区间范围。

代码

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define l(o) tr[o].lc
#define r(o) tr[o].rc
#define sum(o) tr[o].sum
#define add(o) tr[o].add
const int N = 2e5+10;
int n,m,ans,pos;
int a[N],b[N],add[N];
struct question{
	int opt,l,r;
}q[N];
struct tree
{
	struct node{
		int lc,rc;
		int add,sum;
	}tr[N<<2];
	void Add(int o,int val)
	{
		add(o) = val;
		sum(o) = val * (r(o) - l(o) + 1);
	}
	void up(int o)
	{
		sum(o) = sum(o<<1) + sum(o<<1|1);
	}
	void down(int o)
	{
		if(add(o) == -1) return;//没有标记直接返回 
	    Add(o<<1 , add(o)); Add(o<<1|1 , add(o));//下传 
    	add(o) = -1;
	}
	void build(int o,int L,int R)
	{
		l(o) = L, r(o) = R;
		add(o) = -1;//清空标记 
		if(L == R)
		{
			sum(o) = b[L];
			return;
		}
		int mid = (L + R)>>1;
		build(o<<1 , L , mid);
		build(o<<1|1 , mid+1 ,R);
		up(o);
	}
	void chenge(int o,int L,int R,int val)//区间赋值 
	{
		if(L <= l(o) && R >= r(o))
		{
			Add(o , val); return;
		}
		down(o);
		int mid = (l(o) + r(o))>>1;
		if(L <= mid) chenge(o<<1 , L , R , val);
		if(R > mid) chenge(o<<1|1 , L , R , val);
		up(o);
	}
	int ask(int o,int L,int R)
	{
		int ans = 0;
		if(L <= l(o) && R >= r(o))
		{
			return sum(o);
		}
		down(o);
		int mid = (l(o) + r(o))>>1;
		if(L <= mid) ans += ask(o<<1 , L , R);
		if(R > mid) ans += ask(o<<1|1 , L , R);
		return ans;
	}
}tree;
bool judge(int k)
{
	for(int i = 1; i <= n; i++)//把大于等于mid的变为1,反之变为0 
	{
		if(a[i] >= k) b[i] = 1;
		else b[i] = 0;
	}
	tree.build(1,1,n);
	for(int i = 1; i <= m; i++)//线段树模拟排序 
	{
		int tot = tree.ask(1,q[i].l,q[i].r);//区间中1的个数 
		if(q[i].opt == 0)//升序排列 
		{
			tree.chenge(1, q[i].l , q[i].r - tot , 0);//区间变为0 
			tree.chenge(1, q[i].r - tot + 1 , q[i].r , 1);//区间赋1 
		}
		if(q[i].opt == 1)//降序排序 
		{
			tree.chenge(1 , q[i].l , q[i].l + tot - 1 , 1);//区间赋1 
			tree.chenge(1 , q[i].l + tot ,q[i].r ,0);//区间赋0 
		}
	}
	return tree.ask(1,pos,pos);//查询pos这个位置为0或1 
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i = 1; i <= n; i++) scanf("%d",&a[i]);
	for(int i = 1; i <= m; i++) scanf("%d%d%d",&q[i].opt,&q[i].l,&q[i].r);//开个结构体储存每个询问的信息 
	scanf("%d",&pos);
	int L = 1,R = n;
	while(L <= R)//二分答案 
	{
		int mid = (L + R)>>1;
		if(judge(mid))
		{
			ans = mid;
			L = mid + 1;
		}
		else R = mid - 1;
	}
	printf("%d\n",ans);
	fclose(stdin); fclose(stdout); 
	return 0;
} 

四 线段树维护约数个数

Link

一句话题意:让你维护一个数据结构支持区间求和以及单点修改。

这都是线段树的常规操作,但每次修改操作都单点修改一次复杂度是不能接受的。

但当一个数小于等于 \(2\) 的时候,无论再怎么修改,他的值也不会再改变。

因此我们可以在维护一个区间最大值,当最大值大于 \(2\) 的时候直接单点修改,反之 return

复杂度 O(能过)

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define LL long long
const int N = 3e5+10;
const int M = 1e6+10;
LL n,m,opt,l,r,tot;
int f[M],g[M],a[N],prime[M];
bool check[M];
inline LL read()
{
	LL s =  0,w = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9'){s = s * 10 + ch - '0'; ch = getchar();}
	return s * w;
}
struct node
{
	int lc,rc;
	LL sum,maxn;
}tr[N<<2];
#define l(o) tr[o].lc
#define r(o) tr[o].rc
#define sum(o) tr[o].sum
#define maxn(o) tr[o].maxn
void up(int o)
{
	sum(o) = sum(o<<1) + sum(o<<1|1);
	maxn(o) = max(maxn(o<<1),maxn(o<<1|1)); 
}
void build(int o,int L,int R)
{
	l(o) = L, r(o) = R;
	if(L == R)
	{
		sum(o) = maxn(o) = a[L];
		return;
	}
	int mid = (L + R)>>1;
	build(o<<1,L,mid);
	build(o<<1|1,mid+1,R);
	up(o);
}
void chenge(int o,int L,int R)
{
	if(maxn(o) <= 2) return;
	if(l(o) == r(o))
	{
		sum(o) = maxn(o) = f[sum(o)];
		return;
	}
	int mid = (l(o) + r(o))>>1;
	if(L <= mid) chenge(o<<1,L,R);
	if(R > mid) chenge(o<<1|1,L,R);
	up(o);
}
LL query(int o,int L,int R)
{
	LL res = 0;
	if(L <= l(o) && R >= r(o)) return sum(o);
	int mid = (l(o) + r(o))>>1;
	if(L <= mid) res += query(o<<1,L,R);
	if(R > mid) res += query(o<<1|1,L,R);
	return res;
}
void YYCH()
{
	g[1] = f[1] = 1;
	for(int i = 2; i <= M-5; i++)
	{
		if(!check[i])
		{
			prime[++tot] = i;
			g[i] = 1;
			f[i] = 2;
		}
		for(int j = 1; j <= tot && i * prime[j] <= M-5; j++)
		{
			check[i * prime[j]] = 1;
			if(i % prime[j] == 0)
			{
				g[i * prime[j]] = g[i] + 1;
				f[i * prime[j]] = f[i] * (g[i * prime[j]] + 1) / (g[i] + 1);
			}
			else
			{
				g[i * prime[j]] = 1;
				f[i * prime[j]] = f[i] * f[prime[j]];
			}
		}
	}
}
int main()
{
	n = read(); m = read(); YYCH();
	for(int i = 1; i <= n; i++) a[i] = read();
	build(1,1,n);
	for(int i = 1; i <= m; i++)
	{
		opt = read(); l = read(); r = read();
		if(opt == 1) chenge(1,l,r);
		else printf("%lld\n",query(1,l,r));
	}
	return 0;
}

五 线段树维护区间GCD

不会,先把坑占上,后期再补

To Be Continue

posted @ 2020-07-22 08:05  genshy  阅读(284)  评论(0编辑  收藏  举报