论线段树:二

《论线段树》

——线段树精细讲解第二篇   ——Yeasion_Nein出品

>《论线段树:一》

>《论线段树:二》

在上一节中,我们大概了解了线段树的运算机制以及基本的代码运行 ,在这一节,我们主要针对一个例题进行更深层次的讲解。

首先来看一道题。(题目原链接:线段树2

【模板】线段树 2

题目描述

如题,已知一个数列,你需要进行下面三种操作:

1.将某区间每一个数乘上x

2.将某区间每一个数加上x

3.求出某区间每一个数的和

输入输出格式

输入格式:

第一行包含三个整数N、M、P,分别表示该数列数字的个数、操作的总个数和模数。

第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。

接下来M行每行包含3或4个整数,表示一个操作,具体如下:

操作1: 格式:1 x y k 含义:将区间[x,y]内每个数乘上k

操作2: 格式:2 x y k 含义:将区间[x,y]内每个数加上k

操作3: 格式:3 x y 含义:输出区间[x,y]内每个数的和对P取模所得的结果

输出格式:

输出包含若干行整数,即为所有操作3的结果。

好了,首先我们来分析一下这个题目和《论线段树:一》中的例题有什么不一样的地方。

乍一看好像就是多了一个乘法机制嘛? 没错,就是这么一个机制,把它从 普及+/提高 的难度拔到了 提高+/省选 的难度。。。

至于为什么呢.... 我们首先来考虑怎么做这道题,你就明白为什么了。

大家基本一看就知道:该题的难点就是Lazy Tag的下放。有了lazy tag,我们就可以直接更新节点的值(也就是询问3中的当前答案),然后其他的保存在lazy tag中,那么我们就可以达到近似于O(nlogn)的速度。

这一次,我们不在用struct来定义{left,right,sum},因为这样会很繁琐。我们再次定义一个stuct里面包含三个元素。

1.此时的答案,也就是真实值。2.表示乘法意义上的lay tag。3.表示加法意义上的lazy tag。

struct point
{
    ll value;                  //表示此时的答案
    ll mul;              //表示lazy tag的乘法意义
    ll add;              //表示lazy tag的加法意义
}edge[MAXN*4];

然后便是建树了,build和以前有一个不同点在开头有一个初始化mul和add的操作。由于我们要进行乘法和加法操作,于是我们将mul初始化为1,将add初始化为0.。(你想想要是mul被初始化为0了怎么乘.....)

void update(int now)//更新操作
{
    edge[now].value=(edge[leftson].value+edge[rightson].value)%p;
    //其实就是该节点的值等于他的左儿子的值和右儿子的值的和嘛
}
//当然在这个build函数里面并没有使用update,看了就知道为啥了qwq...
void build(int now,int left,int right)
{
    edge[now].mul=1;      //初始化乘法意义为1
    edge[now].add=0;      //初始化加法意义为0
    if(left==right)
    edge[now].value=data[left];
    else
    {
        int mid=(left+right)/2;       //取中间节点
        build(rightson,mid+1,right);    //向右边建树
        build(leftson,left,mid);      //向左边建树
        edge[now].value=edge[leftson].value+edge[rightson].value;    //更新value的值
    }
      edge[now].value%=p;
      return ;
}

然后就是针对乘法的一个change函数,就且叫做One_change好了(怎么顺溜怎么起)。

在这里有一个问题:就是乘法和加法的优先顺序。换句话说就是:我们是先考虑乘法呢,还是先考虑加法呢?

我们先假设考虑加法优先会怎么样:其实也就是我们规定:

edge[leftson].value=((edge[leftson].value+edge[now].add)*edge[now].mul)%p;

edge[rightson].value=((edge[rightson].value+edge[now].add)*edge[mul])%p;

那么你会发现,更新操作就会变的很坑爹,只要稍微变一下add的值,mul就会跟着大大小小地变,精度损失很大。

然后我们考虑乘法优先:其实就是规定

edge[leftson].value=((edge[leftson].value*edge[now].muledge[now].add*(当前区间的长度))%p;

edge[rightson].value=((edge[rightson].value*edge[now].muledge[now].add*(当前区间的长度))%p;

然后我们就发现要改变add的数值的话只需改变add就可以了,改变mul的时候吧add也对应地乘一下就可以了,并不会损失精度。

所以我们就知道了:为了不损失精度,我们优先考虑乘法

这里有一个和《论线段树:一》不一样的地方就是他的修改,这里我们要同时修改value,mul,add三个值,当然,最后不要忘记%p。

//One_change 表示乘法
void One_change(int now,int now_left,int now_right,int left,int right,int num)
//now表示节点编号,now_left和now_right是当前遍历到的线段的左右端点。
//left和right是要操作的左右端点。num是要乘的数。
//注意:这里的端点是和《论线段树:一》相反的,不要看叉!(Yeasion手贱打错了qnq....
{
    if(now_left>right||now_right<left)//如果取不到交集
    return ;
    if(left<=now_left&&right>=now_right)//如果当前遍历到的范围被要操作的范围完全包含
    {
        edge[now].value=(edge[now].value*num)%p;//更新当前的答案值
        edge[now].mul=(edge[now].mul*num)%p;//更新lazy tag的乘法值
        edge[now].add=(edge[now].add*num)%p;//更新lazy tag的加法值
        return ;
    }
    put(now,now_left,now_right);//下放标记
    int mid=(now_left+now_right)/2;//取中
    One_change(rightson,mid+1,now_right,left,right,num);//向右继续遍历
    One_change(leftson,now_left,mid,left,right,num);//向左继续遍历
    update(now); return ;//更新然后返回
}

然后就是加法,这里加法比乘法要少一个mul的更新,因为我们知道改变mul的时候add也要跟着乘,但是改变add的时候只改变其add自身就可以了。

//Two_change 表示加法
void Two_change(int now,int now_left,int now_right,int left,int right,int num)
//now表示节点编号,now_left和now_right是当前遍历到的线段的左右端点。
//left和right是要操作的左右端点。num是要乘的数。
//注意:这里的端点是和《论线段树:一》相反的,不要看叉!(Yeasion手贱打错了qnq....
{
    if(now_left>right||now_right<left)//如果取不到交集
    return ;
    if(left<=now_left)//如果完全包含
    if(right>=now_right)
    {
        edge[now].value=(edge[now].value+num*(now_right-now_left+1))%p;//更新当前答案
        edge[now].add=(edge[now].add+num)%p;//更新加法tag值
        return ;
    }
    put(now,now_left,now_right);//下放一波标记
    int mid=(now_left+now_right)/2;//取中间
    Two_change(rightson,mid+1,now_right,left,right,num);//向右继续遍历
    Two_change(leftson,now_left,mid,left,right,num);//向左继续遍历
    update(now); return ;//更新now节点值
}

。。。好了下面就是比较繁琐的put:下放lazy tag函数了(蛤蛤)

由于我们是在加法和乘法之后都有put函数,因此我们的put函数要同时包含加法和乘法。

并且我们知道,按照我们规定的优先度,一个节点的值=其值*父节点的mul+父节点的add*(本区间的长度),最后再%p

当然在最后我们还要想build函数开始时那样,初始化节点的mul和add值。

inline void put(int now,int left,int right)
{
    int mid=(left+right)/2;
    edge[rightson].value=(edge[rightson].value*edge[now].mul+edge[now].add*(right-mid))%p;
    //我们知道一个节点的值=当前值*其父亲的乘法tag加上其父亲的加法tag然后一个%(注意:先乘再加)
    edge[leftson].value=(edge[leftson].value*edge[now].mul+edge[now].add*(mid-left+1))%p;
    //同理,rightson和leftson都是这个样子
    edge[rightson].mul=(edge[rightson].mul*edge[now].mul)%p;
    edge[leftson].mul=(edge[leftson].mul*edge[now].mul)%p;
    edge[rightson].add=(edge[rightson].add*edge[now].mul+edge[now].add)%p;
    edge[leftson].add=(edge[leftson].add*edge[now].mul+edge[now].add)%p;
    edge[now].mul=1; edge[now].add=0; return ;
}

最后的询问就很简单了,一个简单的递归完事。(注意递归的是其左儿子的值和其右儿子的值的和)。

int query(int now,int now_left,int now_right,int left,int right)
{
    if(right<now_left||left>now_right)//如果娶不到交集
    return 0;
    if(right>=now_right)//如果完全包含
    if(left<=now_left)
    return edge[now].value;//返回当前节点的值
    put(now,now_left,now_right);//下方标记
    int mid=(now_right+now_left)/2;//取中
    return (query(rightson,mid+1,now_right,left,right)+query(leftson,now_left,mid,left,right))%p;
    //递归
}

好的,那么整个的线段树二的代码就是这样,那么现在你应该明白它为什么是一个提高+/省选的题了吧。因为他需要考虑的地方和细节非常多,一不小心就会写错。大家写线段树二的时候一定要非常小心,有可能打错了一个代码整个代码就全盘崩溃了。

下面是全部代码:

//Yeasion_Nein出品
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define MAXN 100010
#define leftson now*2
#define rightson now*2+1
#define ll long long
using namespace std;
ll n,m,p,data[MAXN];
struct point
{
    ll value;                  //表示此时的答案
    ll mul;              //表示lazy tag的乘法意义
    ll add;              //表示lazy tag的加法意义
}edge[MAXN*4];
void update(int now)//更新操作
{
    edge[now].value=(edge[leftson].value+edge[rightson].value)%p;
    //其实就是该节点的值等于他的左儿子的值和右儿子的值的和嘛
}
inline void put(int now,int left,int right)
{
    int mid=(left+right)/2;
    edge[rightson].value=(edge[rightson].value*edge[now].mul+edge[now].add*(right-mid))%p;
    //我们知道一个节点的值=当前值*其父亲的乘法tag加上其父亲的加法tag然后一个%(注意:先乘再加)
    edge[leftson].value=(edge[leftson].value*edge[now].mul+edge[now].add*(mid-left+1))%p;
    //同理,rightson和leftson都是这个样子
    edge[rightson].mul=(edge[rightson].mul*edge[now].mul)%p;
    edge[leftson].mul=(edge[leftson].mul*edge[now].mul)%p;
    edge[rightson].add=(edge[rightson].add*edge[now].mul+edge[now].add)%p;
    edge[leftson].add=(edge[leftson].add*edge[now].mul+edge[now].add)%p;
    edge[now].mul=1; edge[now].add=0; return ;
}
void build(int now,int left,int right)
{
    edge[now].mul=1;      //初始化乘法意义为1
    edge[now].add=0;      //初始化加法意义为0
    if(left==right)
    edge[now].value=data[left];
    else
    {
        int mid=(left+right)/2;       //取中间节点
        build(rightson,mid+1,right);    //向右边建树
        build(leftson,left,mid);      //向左边建树
        edge[now].value=edge[leftson].value+edge[rightson].value;    //更新value的值
    }
      edge[now].value%=p;
      return ;
}
//One_change 表示乘法
void One_change(int now,int now_left,int now_right,int left,int right,int num)
//now表示节点编号,now_left和now_right是当前遍历到的线段的左右端点。
//left和right是要操作的左右端点。num是要乘的数。
//注意:这里的端点是和《论线段树:一》相反的,不要看叉!(Yeasion手贱打错了qnq....
{
    if(now_left>right||now_right<left)//如果取不到交集
    return ;
    if(left<=now_left&&right>=now_right)//如果当前遍历到的范围被要操作的范围完全包含
    {
        edge[now].value=(edge[now].value*num)%p;//更新当前的答案值
        edge[now].mul=(edge[now].mul*num)%p;//更新lazy tag的乘法值
        edge[now].add=(edge[now].add*num)%p;//更新lazy tag的加法值
        return ;
    }
    put(now,now_left,now_right);//下放标记
    int mid=(now_left+now_right)/2;//取中
    One_change(rightson,mid+1,now_right,left,right,num);//向右继续遍历
    One_change(leftson,now_left,mid,left,right,num);//向左继续遍历
    update(now); return ;//更新然后返回
}
//Two_change 表示加法
void Two_change(int now,int now_left,int now_right,int left,int right,int num)
//now表示节点编号,now_left和now_right是当前遍历到的线段的左右端点。
//left和right是要操作的左右端点。num是要乘的数。
//注意:这里的端点是和《论线段树:一》相反的,不要看叉!(Yeasion手贱打错了qnq....
{
    if(now_left>right||now_right<left)//如果取不到交集
    return ;
    if(left<=now_left)//如果完全包含
    if(right>=now_right)
    {
        edge[now].value=(edge[now].value+num*(now_right-now_left+1))%p;//更新当前答案
        edge[now].add=(edge[now].add+num)%p;//更新加法tag值
        return ;
    }
    put(now,now_left,now_right);//下放一波标记
    int mid=(now_left+now_right)/2;//取中间
    Two_change(rightson,mid+1,now_right,left,right,num);//向右继续遍历
    Two_change(leftson,now_left,mid,left,right,num);//向左继续遍历
    update(now); return ;//更新now节点值
}
int query(int now,int now_left,int now_right,int left,int right)
{
    if(right<now_left||left>now_right)//如果娶不到交集
    return 0;
    if(right>=now_right)//如果完全包含
    if(left<=now_left)
    return edge[now].value;//返回当前节点的值
    put(now,now_left,now_right);//下方标记
    int mid=(now_right+now_left)/2;//取中
    return (query(rightson,mid+1,now_right,left,right)+query(leftson,now_left,mid,left,right))%p;
    //递归
}
int main()
{
    scanf("%lld%lld%lld",&n,&m,&p);
    for(int i=1;i<=n;i++)
        scanf("%lld",&data[i]);
    build(1,1,n);
    for(int i=1;i<=m;i++)
    {
        int opt;
        scanf("%d",&opt);
        if(opt==1)
        {
            ll x,y,k;
            scanf("%lld%lld%lld",&x,&y,&k);
            One_change(1,1,n,x,y,k);
        }
        else if(opt==2)
        {
            ll x,y,k;
            scanf("%lld%lld%lld",&x,&y,&k);
            Two_change(1,1,n,x,y,k);
        }
        else
        {
            ll x,y;
            scanf("%lld%lld",&x,&y);
            printf("%lld\n",query(1,1,n,x,y));
        }
    }
    return 0;
}

原文地址:https://www.cnblogs.com/Yeasio-Nein/p/Segtree2.html

时间: 2024-10-04 00:02:29

论线段树:二的相关文章

HDU6638 Snowy Smile (线段树+二维最大子段和)

2019杭电多校第六场的一道签到题 这次我们显然要求的二维矩阵的最大值,分析题目我们可以得到几个细节. 1.首先数据很大,肯定要离散化. 2.离散化后,我们想象有很多点在一个平面内,要统计矩阵最大值 3.我们之前接触过如何求一条线上的最大子段和,只要用线段树维护四个值就能够解决 4.根据已知,我们发现求矩阵和也是可以这么做的,因为他是一个矩形,所以我们假如我们有两行,其实可以把第二行的对应数据加到第一行上去,进行压维操作,再求一维的最大子段和. 5.我们要考虑所有的情况,因此我们以x轴作为线段树

CF1321-World of Darkraft: Battle for Azathoth (线段树+二维偏序)

题意: 题目大致意思是给你n把武器,m件防具,p个怪兽,接下来n行每行告诉你该武器的攻击力和花费, 接下来m行告诉你该防具的防御力和花费,然后p行每行告诉你这个怪兽的攻击力,防御力以及打败这个 怪兽可以获得的金钱数,当你的攻击力大于怪兽的防御力,并且你的防御力大于怪兽的攻击力时,你可 以打败这个怪兽.你必须至少购买1件武器和1件防具,问你最多可以获得多少钱. 链接:https://codeforces.com/contest/1321/problem/E 思路: 看了大神的题解,第一次知道二维偏

线段树&#183;二

1.UVA 11525 Permutation 题意:求1~k这k个数中第N个排列.(N从0开始记).N=sum(Si*(k-i)!)(1≤i≤k) 思路:根据N的值的性质,联系康拓展开,不妨发现第i位的值为剩下没用的数中从小到大第Si+1个.可以用线段树来记录区间内没有用的数的个数. 1 #include<iostream> 2 using namespace std; 3 int k; 4 const int maxk = 50010; 5 int numk[maxk]; 6 int tr

线段树(二)

第一篇以一道简单的题目为背景介绍了线段树的基本结构和基本性质,这一篇我们使用线段树来解决几个常见的问题 1. 查询区间最大(小)值 支持两种操作:a. 修改某个点的值 b. 查询某个区间的最大(小)值 1 #include <stdio.h> 2 #define N 1024 3 4 typedef struct { 5 int min_value; 6 int left; 7 int right; 8 } ST; 9 10 int input[N + 1]; 11 int father[N

线段树の二 区间乘+区间加

具体就不解释了,看上一篇文章 放代码 注意点:!!!! 注意运算符优先级 比如: a*=b%p 是b先mod p再与a相乘 参见:https://baike.baidu.com/item/%E8%BF%90%E7%AE%97%E7%AC%A6%E4%BC%98%E5%85%88%E7%BA%A7/4752611?fr=aladdin /******************************* 线段树V2.0 支持区间加.区间乘.区间和查询 *************************

线段树二(区间修改)

概述 区间修改即将一个区间内所有值改为一个值(或加上一个值),为了执行快速,我们通常用"懒"标记维护整个区间值的情况,在需要是再将这个"懒"标记传到该节点的两个子节点上. 模版(此为在整个区间上加上一个值)(洛谷p3372) #include<iostream>#include<cstdio>#include<cstring>#include<string>#include<algorithm>#inclu

Luck and Love(二维线段树)

Luck and Love Time Limit: 10000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 54 Accepted Submission(s): 21   Problem Description 世界上上最远的距离不是相隔天涯海角而是我在你面前可你却不知道我爱你                ―― 张小娴 前段日子,枫冰叶子给Wiskey做了个征婚启事,聘

HDU1823-Luck and Love-二维线段树(模板)

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1823 好吧,给这题跪了...orz.... 一道很基础的二维线段树的模板题: 但是细节很多:尤其注意了: swap函数会丢失精度,用double就等着WA到死吧...orz... 还有就是给你的区间不一定是按顺序的,得加一个判断:真的是坑...orz.... #include<iostream> #include<string> #include<cstdio> #incl

[IOI2018]werewolf狼人——kruskal重构树+可持久化线段树

题目链接: IOI2018werewolf 题目中编号都是从0开始,太不舒服了,我们按编号从1开始讲QAQ. 题目大意就是询问每次从一个点开始走只能走编号在[l,n]中的点,在任意点变成狼,之后只能走[0,r]中的点,是否能到达另一个点. 后一部分其实就是找有哪些点只走[0,r]中的点能到达终点,那么反过来看,就是终点只走[0,r]中的点能到达哪些点. 那么只要起点能到达的点和终点能到达的点中有交集就有解. 因为起点只能走一些编号较大的点,那么我们求出原图的最大生成树,建出kruskal重构树,