线段树是非常经典的树形数据结构,其在ACM中也是经常出现的,下面对线段树进行说明并就相关例题展开。
如果我们有 n(n>1e5)
个数组成的数组,我们有 q(q>1e5)
查询,每次查询 [li,ri] 范围内的数之和,可以使用前缀和来解决,这样每次查询的时间复杂度都是O(1)。
还是有 n(n>1e5)
个数组成的数组,我们有 u(u>1e5)
次修改,每次修改 [li,ri] 范围内的数,每个数加上一个 numberi,问 u
次修改后某个数是多少,可以使用差分来解决,这样每次修改的时间复杂度都是O(1)。
现在有一个问题,还是有一个由 n(n>1e5)
个数组成的数组,我们有 q(q>1e5)
操作,但每次操作操作有两种可能:
这样用之前前缀和的方法最大时间复杂度为 O(nq),因为每次修改的时间复杂度都是 O(n)。
而线段树则可以在可接受的时间复杂度内解决这个问题,下面为其树形结构图:
这里面每个 mid
表示 [l,r]
的中间值,即 (l+r)/2
,比如 mid1=(l+r)/2,mid2=(1+mid1)/2…,每个区间每次分为两个均匀的区间,直到每个区间只有一个数,这里的对于每个树中的 [l,r]
维护这个范围的数之和。
这个树形结构的树深度为 log2(n),那么每次对数组中的一个数的只需要寻找 log2(n) 次,即时间复杂度为 O(logn)。
而区间查询的时间复杂度也是 O(logn),我们可以考虑两种情况:
const int N = 4e5+5; //总节点数
struct node
{
int l/*区间左边界*/,r/*区间右边界*/,sum/*区间元素之和*/;
}tree[N];
void update(int k) // 更新节点k的sum;
{
tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
//一段区间的元素和等于它的子区间的元素和
}
因为我们每次修改一个元素的值,必定会对其上层的节点的 sum
产生影响,所以每次要重新计算一下节点 k
的 sum
。
void build(int k/*当前节点的编号*/,int l/*当前区间的左边界*/,int r/*当前区间的右边界*/) //初始化线段树
{
tree[k].l=l,tree[k].r=r;
if(l==r)//递归到叶节点
{
tree[k].sum=number[l];//其中number数组为给定的初值
return;
}
int mid=(l+r)>>1;//计算左右子节点的边界
build(k<<1,l,mid);//递归进左儿子[l,mid]
build(k<<1|1,mid+1,r);//递归进右儿子[mid+1,r]
update(k);//最后要用左右子区间的值更新该区间的值
}
当 [l,r]
的 l
与 r
相等时,表示递归了叶节点了,要返回了,当前节点的 sum=number[l]
。
void change(int k/*当前节点的编号*/,int x/*要修改节点的编号*/,int y/*要把编号为x的数字修改成y*/) // 单点修改
{
//如果当前区间只包含一个元素,那么该元素一定就是我们要修改的。
//由于该区间的sum一定等于编号为x的数字,所以直接修改sum就可以了。
if(tree[k].l==tree[k].r)
{
tree[k].sum=y;
return;
}
int mid=(tree[k].l+tree[k].r)>>1;//计算下一层子区间的左右边界
if(x<=mid)
change(k<<1,x,y);//递归进左儿子
else
change(k<<1|1,x,y);//递归进右儿子
update(k);//最后更新点k的值
}
int query(int k/*当前节点的编号*/,int l/*当前查询区间的左边界*/,int r/*当前查询区间的右边界*/) // 区间查询
//当前到了编号为k的节点,查询[l,r]范围的和
{
//如果当前区间是询问区间的子区间,可以直接返回
if(tree[k].l>=l&&tree[k].r<=r)
return tree[k].sum;
int mid=(tree[k].l+tree[k].r)>>1; //计算下一层子区间的左右边界
int res=0;
//如果询问区间包含左子区间的部分
if(l<=mid)
res += query(k<<1,l,r);
//如果询问区间包含右子区间的部分
if(r>mid)
res += query(k<<1|1,l,r);
return res;
}
区间修改与单点修改不同的是其每次是对一个区间[li,ri]进行修改。如果按照单点修改的写法,那么每次都更新到叶节点,那么时间复杂度最大为 O(nlogn),即 n
节点都递归 logn
层。这样的时间复杂度是不可接受的。
这时可以引入一个叫懒标记(lazy)的东西,我们每次区间,向下更新时,如果要更新的区间包含当前节点的区间,就不向下更新了,而是维护一个 lazy
,把要修改的值更新入 lazy
中。之后当需要进入当前节点下面节点时,再将 lazy
下传下去,去更新下面节点的区间和以及 lazy
,再将当前节点清零。
其实可以发现这和区间查询的方式很类似,所以其时间复杂度也为 O(logn)。
const int N = 4e5+5; //总节点数
struct node
{
int l/*区间左边界*/,r/*区间右边界*/,sum/*区间元素之和*/,lazy/*懒惰标记*/;
}tree[N];
相比于单点修改多了 lazy
懒标记。
void pushdown(int k) //懒标记下传
{
tree[k<<1].lazy += tree[k].lazy;
tree[k<<1|1].lazy += tree[k].lazy;
tree[k<<1].sum += tree[k].lazy * (tree[k<<1].r - tree[k<<1].l + 1); //左节点要加上懒标记的值*(左节点区间长)
tree[k<<1|1].sum += tree[k].lazy * (tree[k<<1|1].r - tree[k<<1|1].l + 1); //右节点要加上懒标记的值*(右节点区间长)
tree[k].lazy = 0; //下传到子节点之和当前点的懒标记要清零
}
先更新子节点的懒标记,再更新子节点的元素和,最后对懒标记清零。
void update(int k) // 更新节点k的sum;
{
tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
//一段区间的元素和等于它的子区间的元素和
}
void build(int k/*当前节点的编号*/,int l/*当前区间的左边界*/,int r/*当前区间的右边界*/) //初始化线段树
{
tree[k].l=l,tree[k].r=r;
lazy[k]=0; // 初始懒标记
if(l==r)//递归到叶节点
{
tree[k].sum=number[l];//其中number数组为给定的初值
return;
}
int mid=(l+r)>>1;//计算左右子节点的边界
build(k<<1,l,mid);//递归进左儿子[l,mid]
build(k<<1|1,mid+1,r);//递归进右儿子[mid+1,r]
update(k);//最后要用左右子区间的值更新该区间的值
}
void add(int k/*当前节点的编号*/, int l/*当前更新区间的左边界*/, int r/*当前更新区间的右边界*/, int val/*区间每个元素增加的值*/) // 区间更新
{
//如果当前区间是更新区间的子区间,更新sum与lazy
if(tree[k].l>=l&&tree[k].r<=r)
{
tree[k].sum += (tree[k].r - tree[k].l + 1)*val;
tree[k].lazy += val;
return ;
}
//如果当前节点被打上了懒惰标记,那么就把这个标记下传
if(tree[k].lazy)
pushdown(k);
int mid=(tree[k].l+tree[k].r)>>1;//计算下一层子区间的左右边界
//如果更新区间包含左子区间的部分
if(l<=mid)
add(k<<1,l,r,val);
if(r>mid) //如果更新区间包含右子区间的部分
add(k<<1|1,l,r,val);
update(k);//最后更新点k的值
}
int query(int k/*当前节点的编号*/,int l/*当前查询区间的左边界*/,int r/*当前查询区间的右边界*/) // 区间查询
//当前到了编号为k的节点,查询[l,r]范围的和
{
//如果当前区间是询问区间的子区间,可以直接返回
if(tree[k].l>=l&&tree[k].r<=r)
return tree[k].sum;
//如果当前节点被打上了懒惰标记,那么就把这个标记下传
if(tree[k].lazy)
pushdown(k);
int mid=(tree[k].l+tree[k].r)>>1; //计算下一层子区间的左右边界
int res=0;
//如果询问区间包含左子区间的部分
if(l<=mid)
res += query(k<<1,l,r);
//如果询问区间包含右子区间的部分
if(r>mid)
res += query(k<<1|1,l,r);
return res;
}
点我(^_^)
如题,已知一个数列,你需要进行下面两种操作:
第一行包含两个整数 n , m n, m n,m,分别表示该数列数字的个数和操作的总个数。
第二行包含 n n n 个用空格分隔的整数,其中第 i i i 个数字表示数列第 i i i 项的初始值。
接下来 m m m 行每行包含 3 3 3 或 4 4 4 个整数,表示一个操作,具体如下:
1 x y k
:将区间 [ x , y ] [x, y] [x,y] 内每个数加上 k k k。2 x y
:输出区间 [ x , y ] [x, y] [x,y] 内每个数的和。输出包含若干行整数,即为所有操作 2 的结果。
5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4
11
8
20
对于 30 % 30\% 30% 的数据: n ≤ 8 n \le 8 n≤8, m ≤ 10 m \le 10 m≤10。
对于 70 % 70\% 70% 的数据: n ≤ 10 3 n \le {10}^3 n≤103, m ≤ 10 4 m \le {10}^4 m≤104。
对于 100 % 100\% 100% 的数据: 1 ≤ n , m ≤ 10 5 1 \le n, m \le {10}^5 1≤n,m≤105。
保证任意时刻数列中所有元素的绝对值之和 ≤ 10 18 \le {10}^{18} ≤1018。
【样例解释】
这题就是区间修改与区间查询的模板题,需要注意的是,其元素之和的上限是 1e18,所以需要开 long long
。
#include
using namespace std;
const int N = 4e5+5; //总节点数
struct node
{
// int l/*区间左边界*/,r/*区间右边界*/,sum/*区间元素之和*/,lazy/*懒惰标记*/;
int l/*区间左边界*/,r/*区间右边界*/;
long long sum;
long long lazy;
}tree[N];
long long number[N];
void pushdown(int k) //懒标记下传
{
tree[k<<1].lazy += tree[k].lazy;
tree[k<<1|1].lazy += tree[k].lazy;
tree[k<<1].sum += tree[k].lazy * (tree[k<<1].r - tree[k<<1].l + 1); //左节点要加上懒标记的值*(左节点区间长)
tree[k<<1|1].sum += tree[k].lazy * (tree[k<<1|1].r - tree[k<<1|1].l + 1); //右节点要加上懒标记的值*(右节点区间长)
tree[k].lazy = 0; //下传到子节点之和当前点的懒标记要清零
}
void update(int k) // 更新节点k的sum;
{
tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
//一段区间的元素和等于它的子区间的元素和
}
void build(int k/*当前节点的编号*/,int l/*当前区间的左边界*/,int r/*当前区间的右边界*/) //初始化线段树
{
tree[k].l=l,tree[k].r=r;
if(l==r)//递归到叶节点
{
tree[k].sum=number[l];//其中number数组为给定的初值
return;
}
int mid=(l+r)>>1;//计算左右子节点的边界
build(k<<1,l,mid);//递归进左儿子[l,mid]
build(k<<1|1,mid+1,r);//递归进右儿子[mid+1,r]
update(k);//最后要用左右子区间的值更新该区间的值
}
void add(int k/*当前节点的编号*/, int l/*当前更新区间的左边界*/, int r/*当前更新区间的右边界*/, long long val/*区间每个元素增加的值*/) // 区间更新
{
//如果当前区间是更新区间的子区间,更新sum与lazy
if(tree[k].l>=l&&tree[k].r<=r)
{
tree[k].sum += (tree[k].r - tree[k].l + 1)*val;
tree[k].lazy += val;
return ;
}
//如果当前节点被打上了懒惰标记,那么就把这个标记下传
if(tree[k].lazy)
pushdown(k);
int mid=(tree[k].l+tree[k].r)>>1;//计算下一层子区间的左右边界
//如果更新区间包含左子区间的部分
if(l<=mid)
add(k<<1,l,r,val);
if(r>mid) //如果更新区间包含右子区间的部分
add(k<<1|1,l,r,val);
update(k);//最后更新点k的值
}
long long query(int k/*当前节点的编号*/,int l/*当前查询区间的左边界*/,int r/*当前查询区间的右边界*/) // 区间查询
//当前到了编号为k的节点,查询[l,r]范围的和
{
//如果当前区间是询问区间的子区间,可以直接返回
if(tree[k].l>=l&&tree[k].r<=r)
return tree[k].sum;
//如果当前节点被打上了懒惰标记,那么就把这个标记下传
if(tree[k].lazy)
pushdown(k);
int mid=(tree[k].l+tree[k].r)>>1; //计算下一层子区间的左右边界
long long res=0;
//如果询问区间包含左子区间的部分
if(l<=mid)
res += query(k<<1,l,r);
//如果询问区间包含右子区间的部分
if(r>mid)
res += query(k<<1|1,l,r);
return res;
}
int main()
{
int n,m;
cin>>n>>m;
for(int i=1; i<=n; ++i)
cin>>number[i];
build(1,1,n);
while(m--)
{
int opt;
cin>>opt;
if(opt == 1)
{
int x,y;
long long z;
cin>>x>>y>>z;
add(1,x,y,z);
}
else
{
int x,y;
cin>>x>>y;
cout<<query(1,x,y)<<endl;
}
}
return 0;
}
点我(^_^)
如题,已知一个数列,你需要进行下面三种操作:
将某区间每一个数乘上 x x x
将某区间每一个数加上 x x x
求出某区间每一个数的和
第一行包含三个整数 n , m , p n,m,p n,m,p,分别表示该数列数字的个数、操作的总个数和模数。
第二行包含 n n n 个用空格分隔的整数,其中第 i i i 个数字表示数列第 i i i 项的初始值。
接下来 m m m 行每行包含若干个整数,表示一个操作,具体如下:
操作 1 1 1: 格式:1 x y k
含义:将区间 [ x , y ] [x,y] [x,y] 内每个数乘上 k k k
操作 2 2 2: 格式:2 x y k
含义:将区间 [ x , y ] [x,y] [x,y] 内每个数加上 k k k
操作 3 3 3: 格式:3 x y
含义:输出区间 [ x , y ] [x,y] [x,y] 内每个数的和对 p p p 取模所得的结果
输出包含若干行整数,即为所有操作 3 3 3 的结果。
5 5 38
1 5 4 2 3
2 1 4 1
3 2 5
1 2 4 2
2 3 5 5
3 1 4
17
2
【数据范围】
对于 30 % 30\% 30% 的数据: n ≤ 8 n \le 8 n≤8, m ≤ 10 m \le 10 m≤10
对于 70 % 70\% 70% 的数据: n ≤ 1 0 3 n \le 10^3 n≤103, m ≤ 1 0 4 m \le 10^4 m≤104
对于 100 % 100\% 100% 的数据: n ≤ 1 0 5 n \le 10^5 n≤105, m ≤ 1 0 5 m \le 10^5 m≤105
除样例外, p = 571373 p = 571373 p=571373
(数据已经过加强_)
【样例说明】
故输出应为 17 17 17、 2 2 2( 40 m o d 38 = 2 40 \bmod 38 = 2 40mod38=2 )
主要探究懒标记的更新及下传,这里假定当前区间的和为 sum
,区间范围为 [l,r]
,下传下来的懒标记(即上一层的懒标记)为加法懒标记lazyk1
,减法懒标记lazyk2
,当前层的加法加法懒标记为 lazykk1
和 乘法懒标记 lazykk2
, 因为子区间的两个区间更新方法相同,所以这里就只考虑一种。
那么对于加法,还是和上一题一样。
更新加法
sum = sum + (r-l+1)*val;
lazykk1 = lazykk1+val;
更新乘法
因为是每个数乘上一个数val
,所以每个数乘val
之和相当于每个数之和乘val
。
lazykk1
要乘上 val
,这是因为 lazykk1*(r-l+1)*val = (lazykk1*val)*(r-l+1)
sum = sum*val
lazykk1 = lazykk1*val;
lazykk2 = lazykk2*val;
懒标记的下传
lazykk1 = lazykk1*lazyk2+lazykk1;
lazykk2 = lazykk2*lazyk2
sum = sum*lazyk2+lazyk1*(r-l+1);
lazyk1 = 0;
lazyk2 = 1;
因为乘法对加法的影响已经在懒标记计算时进行过了,所以这里只需要乘上 sum
就可以了。注意 lazyk2
要初始化为 1
,已经注意取模计算。
#include
using namespace std;
const int N = 4e5+10; //总节点数
long long mod;
struct node
{
int l/*区间左边界*/,r/*区间右边界*/;
long long lazy1/*加法懒惰标记*/,lazy2/*乘法懒惰标记*/,sum/*区间元素之和*/;
}tree[N];
long long number[N];
void pushdown(int k) //懒标记下传
{
tree[k<<1].lazy1 = (tree[k<<1].lazy1*tree[k].lazy2+tree[k].lazy1)%mod;
tree[k<<1|1].lazy1 = (tree[k<<1|1].lazy1*tree[k].lazy2+tree[k].lazy1)%mod;
tree[k<<1].lazy2 = tree[k<<1].lazy2*tree[k].lazy2%mod;
tree[k<<1|1].lazy2 = tree[k<<1|1].lazy2*tree[k].lazy2%mod;
tree[k<<1].sum = tree[k<<1].sum*tree[k].lazy2%mod; //先乘上lazy2
tree[k<<1|1].sum = tree[k<<1|1].sum*tree[k].lazy2%mod;
tree[k<<1].sum = (tree[k<<1].sum + tree[k].lazy1*(tree[k<<1].r-tree[k<<1].l+1))%mod; //再加上区间长度*lazy1
tree[k<<1|1].sum = (tree[k<<1|1].sum + tree[k].lazy1*(tree[k<<1|1].r-tree[k<<1|1].l+1))%mod;
tree[k].lazy1 = 0; //下传到子节点之和当前点的懒标记要清零
tree[k].lazy2 = 1;
}
void update(int k) // 更新节点k的sum;
{
tree[k].sum=(tree[k<<1].sum+tree[k<<1|1].sum)%mod;
//一段区间的元素和等于它的子区间的元素和
}
void build(int k/*当前节点的编号*/,int l/*当前区间的左边界*/,int r/*当前区间的右边界*/) //初始化线段树
{
tree[k].l=l,tree[k].r=r;
tree[k].lazy2 = 1;
tree[k].lazy1 = 0;
if(l==r)//递归到叶节点
{
tree[k].sum=number[l]%mod;//其中number数组为给定的初值
return;
}
int mid=(l+r)>>1;//计算左右子节点的边界
build(k<<1,l,mid);//递归进左儿子[l,mid]
build(k<<1|1,mid+1,r);//递归进右儿子[mid+1,r]
update(k);//最后要用左右子区间的值更新该区间的值
}
void add(int k/*当前节点的编号*/, int l/*当前更新区间的左边界*/, int r/*当前更新区间的右边界*/, long long val/*区间每个元素增加的值*/) // 区间更新
{
//如果当前区间是更新区间的子区间,更新sum与lazy
if(tree[k].l>=l&&tree[k].r<=r)
{
tree[k].sum = (tree[k].sum+(tree[k].r - tree[k].l + 1)*val)%mod;
tree[k].lazy1 = (tree[k].lazy1+val)%mod;
return ;
}
//如果当前节点被打上了懒惰标记,那么就把这个标记下传
if(tree[k].lazy1 || tree[k].lazy2!=1)
pushdown(k);
int mid=(tree[k].l+tree[k].r)>>1;//计算下一层子区间的左右边界
//如果更新区间包含左子区间的部分
if(l<=mid)
add(k<<1,l,r,val);
if(r>mid) //如果更新区间包含右子区间的部分
add(k<<1|1,l,r,val);
update(k);//最后更新点k的值
}
void mul(int k/*当前节点的编号*/, int l/*当前更新区间的左边界*/, int r/*当前更新区间的右边界*/, long long val/*区间每个元素要乘的值*/) // 区间更新
{
//如果当前区间是更新区间的子区间,更新sum与lazy
if(tree[k].l>=l&&tree[k].r<=r)
{
tree[k].sum = tree[k].sum*val%mod;
tree[k].lazy1 = tree[k].lazy1*val%mod;
tree[k].lazy2 = tree[k].lazy2*val%mod;
return ;
}
//如果当前节点被打上了懒惰标记,那么就把这个标记下传
if(tree[k].lazy1 || tree[k].lazy2!=1)
pushdown(k);
int mid=(tree[k].l+tree[k].r)>>1;//计算下一层子区间的左右边界
//如果更新区间包含左子区间的部分
if(l<=mid)
mul(k<<1,l,r,val);
if(r>mid) //如果更新区间包含右子区间的部分
mul(k<<1|1,l,r,val);
update(k);//最后更新点k的值
}
long long query(int k/*当前节点的编号*/,int l/*当前查询区间的左边界*/,int r/*当前查询区间的右边界*/) // 区间查询
//当前到了编号为k的节点,查询[l,r]范围的和
{
//如果当前区间是询问区间的子区间,可以直接返回
// if(tree[k].l>=l&&tree[k].r<=r)
// {
// cout<
// }
if(tree[k].l>=l&&tree[k].r<=r)
return tree[k].sum%mod;
//如果当前节点被打上了懒惰标记,那么就把这个标记下传
if(tree[k].lazy1 || tree[k].lazy2!=1)
pushdown(k);
int mid=(tree[k].l+tree[k].r)>>1; //计算下一层子区间的左右边界
long long res=0;
//如果询问区间包含左子区间的部分
if(l<=mid)
res = (res + query(k<<1,l,r))%mod;
//如果询问区间包含右子区间的部分
if(r>mid)
res = (res + query(k<<1|1,l,r))%mod;
return res%mod;
}
int main()
{
int n,m;
cin>>n>>m>>mod;
for(int i=1; i<=n; ++i)
cin>>number[i];
build(1,1,n);
while(m--)
{
int opt;
cin>>opt;
if(opt == 1)
{
int x,y;
long long z;
cin>>x>>y>>z;
mul(1,x,y,z);
}
else if(opt == 2)
{
int x,y;
long long z;
cin>>x>>y>>z;
add(1,x,y,z);
}
else
{
int x,y;
cin>>x>>y;
cout<<query(1,x,y)<<endl;
}
}
return 0;
}