• 线段树的鬼畜操作


    一 线段树维护区间取模

    题面

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

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

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

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

    \(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

  • 相关阅读:
    eclipse打包
    java reflect 小例子
    linux查看java jdk安装路径和设置环境变量
    mapreduce (一) 物理图解+逻辑图解
    java url 解码 编码 奇怪的解码两次
    cygwin+hadoop+eclipse (三) 运行wordcount实例
    nutch 与 solr 的结合
    一个项目可以有多个源代码路径
    SHAppBarMessage
    记录系统开机启动登录注销等信息
  • 原文地址:https://www.cnblogs.com/genshy/p/13358490.html
Copyright © 2020-2023  润新知