目录
普通线段树
原理https://www.acwing.com/blog/content/514/
//区间修改,区间查询
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e6+10;
typedef long long ll;
ll A[N],tree[4*N],tag[4*N];//A为原数组
//更新当前节点 将处理好的左右儿子节点的值更新给父节点
void pushUp(int root) {
tree[root] = tree[root << 1] + tree[root << 1|1];
}
//建树 当前根节点root对应的原数组区间为[staroot,end]
void build(int root,int start,int end) {//通常在主函数中这么使用:build(1,1,N);
// 当前节点应该是一个叶子节点了
if(start == end)
tree[root] = A[start];
else {
// 将当前节点代表的区间拆分成两半,分给左子树(root << 1)和右子树(root << 1|1)
int mid = (start + end) >> 1;
build(root << 1,start,mid);
build(root << 1|1,mid + 1,end);
pushUp(root);
}
}
//单点修改 将A[pos]的值修改为val
//当前区间的根节点下标root,当前区间代表原数组的左右范围l,r
void update(int root,int l,int r,int pos,int val) {//通常在主函数这么使用update(1,1,N,2,5)//将A[pos] = 5;
if(l == r) {//递归边界 到达了叶子结点
A[pos] = val;
tree[root] = val;
return;
}
int mid = (l + r) >> 1;
if(pos <= mid)
update(root<<1,l,mid,pos,val);
else
update(root<<1 | 1,mid + 1,r,pos,val);
pushUp(root);
}
/* (不带区间修改的区间查询,通常不用)
//区间求和 当前根节点root所对应的区间为[l,r],我们希望查询的区间和是[start,end]
int rangeSum(int root,int l,int r,int start,int end) { //一般在主函数用 rangeSum(1,1,N,L,R)
// 如果当前节点范围不在目标查找区间内,即二者交集为空
// 如果当前节点对应区间恰好完整的在我们的目标区间中,即当前节点为查询区间的真子集,直接返回该节点的值
// 否则的话,分别在左右子树中查询,并完成累加和
if(start > r || end < l)//二者无交集,返回0,比如查询[2,5],递归到[0,1],返回0
return 0;
else if(start <= l && end >= r)//真子集,返回该节点值,比如查询[2,5],递归到[2,3],真子集返回值
return tree[root];
else {// 否则的话,分别在左右子树中查询,并完成累加和
int mid = (l + r) >> 1;
int sumLeft = rangeSum(root << 1,l,mid,start,end);
int sumRight = rangeSum(root << 1|1,mid + 1,r,start,end);
return sumLeft + sumRight;
}
}
*/
//区间修改
// pushDown函数的意义在于将父节点的tag信息传递给两个孩子节点。
// tree存储的是当前节点区间和,所以将tag传递给孩子节点时,孩子节点的值要加上区间长度乘以tag值
// tag信息传递给孩子之后,父节点的tag信息就清空啦
/*typedef long long ll;
ll A[N],tree[4*N],tag[4*N];*/
void pushDown(int root,int l,int r) {//传递延迟标记,更新子节点
if(tag[root] != 0) {
int mid = (l + r) >> 1;
ll lazy = tag[root];
int left = root << 1,right = root << 1|1;
tag[left] += lazy,tag[right] += lazy;
tree[left] += lazy * (mid - l + 1);// tree存储的是当前节点区间和,所以将tag传递给孩子节点时,孩子节点的值要加上区间长度乘以tag值
tree[right] += lazy * (r - mid);
tag[root] = 0;// tag信息传递给孩子之后,父节点的tag信息就清空啦
}
}
// 将区间[start,end]的值都加上delta
// 如果当前区间[l,r]是[start,end]的真子集,那么直接修改当前节点对应的区间不用递归向下修改了
// 记住,我们修改的值包括区间和(tree数组)和延迟标记(tag数组)
// 否则我们需要递归查找当前节点root的左右孩子,在进行递归查找之前我们需要先将当前节点已经存在的延迟标记传下去(pushDown函数)。
// 然后再根据左子树是否有目标区间的元素和右子树是否有目标区间的元素进行递归修改。
// 最后不要忘记更新当前节点的值
void updateRange(int root,int l,int r,int start,int end,ll delta) {// 将区间[start,end]的值都加上delta
if(start <= l && end >= r) {// 如果当前区间[l,r]是[start,end]的真子集,那么直接修改当前节点对应的区间不用递归向下修改了
tree[root] += (r - l + 1)*delta;
tag[root] += delta;// 记住,我们修改的值包括区间和(tree数组)和延迟标记(tag数组)
return;
}
pushDown(root,l,r);// 否则我们需要递归查找当前节点root的左右孩子,在进行递归查找之前我们需要先将当前节点已经存在的延迟标记传下去(pushDown函数)。
int mid = (l + r) >> 1;
if(start <= mid) updateRange(root << 1,l,mid,start,end,delta);// 然后再根据左子树是否有目标区间的元素和右子树是否有目标区间的元素进行递归修改。
if(end >= mid + 1)updateRange(root << 1|1,mid + 1,r,start,end,delta);
pushUp(root);// 最后不要忘记更新当前节点的值
}
//带区间修改的区间查询
// 查询区间[start,end]的和
// 如果当前区间节点root对应的区间[l,r]是[start,end]的真子集,那么直接返回当前节点的值就可以啦。
// 否则我们要将当前节点的延迟标记先传递给左右孩子,然后对左右子树进行查询。
ll rangeQuery(int root,int l,int r,int start,int end) {
if(start <= l && end >= r)// 如果当前区间节点root对应的区间[l,r]是[start,end]的真子集,那么直接返回当前节点的值就可以啦。
return tree[root];
pushDown(root,l,r);// 否则我们要将当前节点的延迟标记先传递给左右孩子,然后对左右子树进行查询。
int mid = (l + r) >> 1;
ll s = 0;
if(start <= mid) s += rangeQuery(root << 1,l,mid,start,end);
if(end >= mid + 1) s += rangeQuery(root << 1|1,mid + 1,r,start,end);
return s;
}
int main() {
build(1,1,N);
return 0;
}
zkw线段树
https://www.cnblogs.com/Judge/p/9514862.html
上面这篇文章虽然有些谬误,但总体很棒
先上一篇带解析的总代码
维护区间值模板
//zkw线段树 https://www.cnblogs.com/Judge/p/9514862.html (queryMax和queryMin有错误)
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1e6+10;
int sum[(N<<1)+10],mx[(N<<1)+10],mn[(N<<1)+10];//区间和,维护最大值,维护最小值 根据需要进行选择
int tag[(N<<1)+10];//区间修改的标记数组
int n,m;
inline int read() {
char c=getchar();
int s=0,w=1;
while(c<'0' || c>'9') {
if(c=='-') w*=-1;
c=getchar();
}
while(c>='0' && c<='9') s=(s<<3)+(s<<1)+c-'0',c=getchar();
}
inline void build() { //建树 这里我们假设要维护的信息有:区间和,区间最小值,区间最大值 下同
for(m=1; m<=n; m<<=1); //M为叶子结点和父节点的分水岭,m+i为第i个叶子结点
for(int i=m+1; i<=m+n; i++) //先写叶子结点
sum[i]=mn[i]=mx[i]=read();
for(int i=m-1; i; i--) { //在往上写父节点
sum[i]=sum[i<<1]+sum[i<<1|1];
mn[i]=min(mn[i<<1],mn[i<<1|1]);
mn[i<<1]-=mn[i],mn[i<<1|1]-=mn[i];//为了能修改,差分数组,查询时从叶子结点一路累加到根节点即为结果
mx[i]=max(mx[i<<1],mx[i<<1|1]);
mx[i<<1]-=mx[i],mx[i<<1|1]-=mx[i];//为了能修改,差分数组,查询时从叶子结点一路累加到根节点即为结果
}
}
inline void updateDot(int x,int delta) { //单点更新 将x增加delta 找到更新的节点所在的叶子结点,然后修改后一直向父节点更新即可
int A=0;
x+=m,mx[x]+=delta,mn[x]+=delta;//m+i为第i个叶子结点
for(; x>=1; x>>=1) {//每次往上寻找父节点 谬误:仅仅x>1是不对的
sum[x]+=delta;
A=min(mn[x],mn[x^1]);//^是异或运算,x^1:x若为奇数,x+=1 x若为偶数 x-=1 因此x和x^1是兄弟节点
mn[x]-=A,mn[x^1]-=A,mn[x>>1]+=A;//差分数组
A=max(mx[x],mx[x^1]),
mx[x]-=A,mx[x^1]-=A,mx[x>>1]+=A;//差分数组
}
sum[x>>1]+=delta;
}
/* 区间修改 这个东西...有点麻烦(你得稍微感性理解)
就是说...你每次要更新一段区间的时候,你要让左端点 -1 ,右端点 +1 。
然后你在更新权值的时候要判断 左端点当前所处的节点是否是它父节点的左孩子,
是的话就让该节点的兄弟(也就是它父节点的右孩子)得到更新,否则不做处理,
然后左节点再向右移一位(也就是跳到了父节点),重复迭代以上步骤。
那么右端点呢?其实也就是和左端点反着来了而已。
还有一点,循环的终止条件?这个简单,就是当左右端点所处的节点是兄弟节点的时候结束循环。
类似的,你更新一个节点时 同样可以用这种方法维护(只不过这样就更麻烦了啊)。
这样我们可以看到要被更新的区间都已经被染成黄色了。但是,zkw 没有下传标记啊!
那么我们查询的区间如果在染成黄色的节点的下部(也就是黄色节点的子树内)该怎么办?
我们可以这样...这样...没错!标记永久化!
因为我们已经将一个节点的标记永久化了,那么在该节点被访问到的时候,只要将当前查询到的、包含在该节点所管辖区间范围内的 区间长度乘上标记值,累加入答案即可。
*/
inline void updateRange(int l,int r,int delta) { //区间修改
int A=0,lc=0,rc=0,len=1; //len为须更新区间的长度,每次要更新一段区间的时候,你要让左端点 -1 ,右端点 +1 也就是l+=m-1,r+=m+1
//lc 代表左端点所处的节点下有多少长度的区间在更新区间内, rc 同理 ,通俗一点地说,就是 l 和 r 所分别走过的节点中包含的更新过的区间的总长
for(l+=m-1,r+=m+1; l^r^1; l>>=1,r>>=1,len<<=1) { //在这里的 tag 就是标记数组了 l^r^1表示l与r是兄弟节点时结束循环(l^r==1时式子为假,当r==l+1时l^r==1)
if(l&1^1) tag[l^1]+=delta,lc+=len, mn[l^1]+=delta,mx[l^1]+=delta;//l&1相当于l%2,当l位于左子树时0^1==1 让该节点的兄弟(l^1)得到更新,否则不做处理 然后左节点再向右移一位(也就是跳到了父节点),重复迭代以上步骤。
if(r&1) tag[r^1]+=delta,rc+=len, mn[r^1]+=delta,mx[r^1]+=delta;//标记永久化
//lc和rc
sum[l>>1]+=delta*lc, sum[r>>1]+=delta*rc;
A=min(mn[l],mn[l^1]),mn[l]-=A,mn[l^1]-=A,mn[l>>1]+=A;
A=min(mn[r],mn[r^1]),mn[r]-=A,mn[r^1]-=A,mn[r>>1]+=A;
A=max(mx[l],mx[l^1]),mx[l]-=A,mx[l^1]-=A,mx[l>>1]+=A;
A=max(mx[r],mx[r^1]),mx[r]-=A,mx[r^1]-=A,mx[r>>1]+=A;
}
for(lc+=rc; l>1; l>>=1) {
sum[l>>1]+=delta*lc;
A=min(mn[l],mn[l^1]),mn[l]-=A,mn[l^1]-=A,mn[l>>1]+=A;
A=max(mx[l],mx[l^1]),mx[l]-=A,mx[l^1]-=A,mx[l>>1]+=A;
}
}
/*仅仅维护区间和 实例
inline void updateRange(int l,int r,int delta) { //区间修改
int lc=0,rc=0,len=1;
for(l+=m-1,r+=m+1; l^r^1; l>>=1,r>>=1,len<<=1) {
if(l&1^1) tag[l^1]+=delta,lc+=len
if(r&1) tag[r^1]+=delta,rc+=len//标记永久化
sum[l>>1]+=delta*lc, sum[r>>1]+=delta*rc;
}
for(lc+=rc; l>1; l>>=1) {
sum[l>>1]+=delta*lc;
}
}
下面以图2为例考察第一个for循环
m=8
l=9
第一个for循环
l=4 tag[5]+=del lc=2 sum[2]+=del*2 黄色区域更新的是标记,修改数值的却是黄色的父节点,叶子结点纹丝不动
l=2 tag[3]+=del lc=6 sum[1]+=del*6
l=1
由此可见,lc和lr记录区间和以备更新父节点的值,由于子节点是待修改区域的真子集,所以只需要修改标记,父节点仅仅是交集,所以直接修改值,不能修改标记,叶子结点不做处理
接下来以图1为例考察第二个for循环 (图1是特殊情况的图,图2是非特殊情况的图)
n=6,m=8,查询(2,5),l=8,r=15
l=4 r=7 tag[5]=del lc=2 tag[6]=del rc=2 sum[2]=2*del sum[3]=2*del
l=2 r=3 第一个For结束
第二个for循环
lc=4 l=2 sum[1]+=del*4
由此可见,第二个for循环是为了更新结束第一个for循环的兄弟节点的父节点的值,并且一路向上更新到根节点*/
inline int queryDot(int x) { //单点查询 从叶子结点一直跳父节点,把途中节点的 mn (或者 mx )权值累加,最后得到的就是答案
int ans=0;
for(x+=m; x>0; x>>=1) ans+=mn[x];
return ans;
}
inline int querySum(int l,int r) { //区间查询
int lc=0,rc=0,len=1,ans=0;
for(l+=m-1,r+=m+1; l^r^1; l>>=1,r>>=1,len<<=1) {
if(l&1^1) ans+=sum[l^1]+len*tag[l^1],lc+=len;//加上当前节点*长度
if(r&1) ans+=sum[r^1]+len*tag[r^1],rc+=len;
if(tag[l>>1]) ans+=tag[l>>1]*lc;//父节点的标记作用于子节点 能这么做是因为父节点的小树囊括整个走过的路径!它的标记对路径的每个节点都能起到影响
if(tag[r>>1]) ans+=tag[r>>1]*rc;
}
for(lc+=rc,l>>=1; l; l>>=1) //l>>=1在for内声明了,这里在if声明等价
if(tag[l]) ans+=tag[l]*lc;//更新兄弟节点的父节点并一路向上
return ans;
}
/*模拟 如图
n=6 m=8 查询3,5
l=10 r=14 ans+=sum[11]+tag[11]*1 lc=1 if([tag5]) ans+=tag[5]*1
l=5 r=7 ans+=sum[6]+tag*2 rc=2 if(tag[3]) ans+=tag[3]*2
l=2 r=3 结束for
第二个for
lc=3 l=1 if(tag[1]) ans+==tag[1]*3
*/
inline int queryMin(int l,int r) {//这里询问时 s 和 t 不能 -1 或 +1 ,不然会查询到旁边不相干的节点。
int L=0, R=0, ans=0; //然后 s == t 的情况要特判一下,防止 s 和 t 一直都不是兄弟,陷入死循环。
if(l==r) return queryDot(l); // 单点要特判, 下同
for(l+=m,r+=m; l^r^1; l>>=1,r>>=1) { // 这里 l 和 r 直接加上 m
L+=mn[l],R+=mn[r];//差分数组要累加父节点
if(l&1^1) L=min(L,mn[l^1]);
if(r&1) R=min(R,mn[r^1]);
}
L+=mn[l],R+=mn[r];//易错!这个绝对不能忘记!兄弟节点的差分仍需累加!
for(ans=min(L,R),l>>=1; l; l>>=1) ans+=mn[l];
return ans;
}
/*
第一次模拟
n=6 m=8 查询(3,5)
l=11,r=13 L+=mn[11],R+=mn[13] R=min(R,mn[12])
l=5,r=6 L+=mn[5] ,R+=mn[6]
l=2 r=3 for结束
第二个for循环 ans=min(L,R) l=1,ans+=mn[1]
疑问:兄弟节点的差分不需要累加么?
第N次模拟找到的漏洞数据
6
1 2 3 4 5 6
l=11,r=13,L=1,R=0
l=5,r=6,L=3,R=4
l=1,ans=3
3
0
0 0
0 2 4 0
0 1 0 [1 0 1] 6 0
查询(1,2)
l=9,r=10,L=1,R=0
l=4 r=5 兄弟退出 第二个for循环
l=2,ans=0
l=1,ans=0
0
答案错误,说明算法漏洞确实是存在的
在第二个for前加上这样一行代码 L+=mn[l],R+=mn[r];//易错!这个绝对不能忘记!兄弟节点的差分仍需累加!
*/
inline int queryMax(int l,int r) {
int L=0, R=0, ans=0;
if(l==r) return queryDot(l);
for(l+=m,r+=m; l^r^1; l>>=1,r>>=1) {
L+=mx[l],R+=mx[r];
if(l&1^1) L=max(L,mx[l^1]);
if(r&1) R=max(R,mx[r^1]);
}
L+=mn[l],R+=mn[r];//易错!这个绝对不能忘记!兄弟节点的差分仍需累加!
for(ans=max(L,R),l>>=1; l; l>>=1) ans+=mx[l];
return ans;
}
int main() {
return 0;
}
区间修改的图2
图1
区间查询的图
维护最小值模板
#include<cstdio>//zkw维护最小值
#include<algorithm>
using namespace std;
const int N=1e6+10;
int mn[(N<<1)+10];
int n,m;
inline int read() {
char c=getchar();
int s=0,w=1;
while(c<'0' || c>'9') {
if(c=='-') w*=-1;
c=getchar();
}
while(c>='0' && c<='9') s=(s<<3)+(s<<1)+c-'0',c=getchar();
return s*w;
}
inline void build() { //建树 这里我们假设要维护的信息有:区间和,区间最小值,区间最大值 下同
for(m=1; m<=n; m<<=1); //M为叶子结点和父节点的分水岭,m+i为第i个叶子结点
for(int i=m+1; i<=m+n; i++) //先写叶子结点
mn[i]=read();
for(int i=m-1; i; i--) { //在往上写父节点
mn[i]=min(mn[i<<1],mn[i<<1|1]);
mn[i<<1]-=mn[i],mn[i<<1|1]-=mn[i];//为了能修改,差分数组,查询时从叶子结点一路累加到根节点即为结果
}
}
inline void updateDot(int x,int delta) { //单点更新 将x增加delta 找到更新的节点所在的叶子结点,然后修改后一直向父节点更新即可
int A=0;
x+=m,mn[x]+=delta;//m+i为第i个叶子结点
for(; x>1; x>>=1) {//每次往上寻找父节点
A=min(mn[x],mn[x^1]);//^是异或运算,x^1:x若为奇数,x+=1 x若为偶数 x-=1 因此x和x^1是兄弟节点
mn[x]-=A,mn[x^1]-=A,mn[x>>1]+=A;//差分数组
}
}
inline void updateRange(int l,int r,int delta) { //区间修改
int A=0,lc=0,rc=0; //len为须更新区间的长度,每次要更新一段区间的时候,你要让左端点 -1 ,右端点 +1 也就是l+=m-1,r+=m+1
for(l+=m-1,r+=m+1; l^r^1; l>>=1,r>>=1) { //在这里的 tag 就是标记数组了 l^r^1表示l与r是兄弟节点时结束循环(l^r==1时式子为假,当r==l+1时l^r==1)
if(l&1^1) mn[l^1]+=delta;//l&1相当于l%2,当l位于左子树时0^1==1 让该节点的兄弟(l^1)得到更新,否则不做处理 然后左节点再向右移一位(也就是跳到了父节点),重复迭代以上步骤。
if(r&1) mn[r^1]+=delta;//标记永久化
A=min(mn[l],mn[l^1]),mn[l]-=A,mn[l^1]-=A,mn[l>>1]+=A;
A=min(mn[r],mn[r^1]),mn[r]-=A,mn[r^1]-=A,mn[r>>1]+=A;
}
for(lc+=rc; l>1; l>>=1) {
A=min(mn[l],mn[l^1]),mn[l]-=A,mn[l^1]-=A,mn[l>>1]+=A;
}
}
inline int queryDot(int x) { //单点查询 从叶子结点一直跳父节点,把途中节点的 mn (或者 mx )权值累加,最后得到的就是答案
int ans=0;
for(x+=m; x>0; x>>=1) ans+=mn[x];
return ans;
}
inline int queryMin(int l,int r) {//这里询问时 s 和 t 不能 -1 或 +1 ,不然会查询到旁边不相干的节点。
int L=0, R=0, ans=0; //然后 s == t 的情况要特判一下,防止 s 和 t 一直都不是兄弟,陷入死循环。
if(l==r) return queryDot(l); // 单点要特判, 下同
for(l+=m,r+=m; l^r^1; l>>=1,r>>=1) { // 这里 l 和 r 直接加上 m
L+=mn[l],R+=mn[r];//差分数组要累加父节点
if(l&1^1) L=min(L,mn[l^1]);
if(r&1) R=min(R,mn[r^1]);
}
L+=mn[l],R+=mn[r];//易错!这个绝对不能忘记!兄弟节点的差分仍需累加!
for(ans=min(L,R),l>>=1; l; l>>=1) ans+=mn[l];
return ans;
}
int main() {
n=read();
build();
int t=queryMin(read(),read());
printf("%d",t);
return 0;
}
动态开点线段树
https://blog.csdn.net/u012972031/article/details/88751811 https://www.cnblogs.com/szbszb/p/11243728.html
动态开点是为了降低空间复杂度,防止爆内存,动态开点后空间复杂度为qlogn,q为访问节点数
动态开点线段树是一类特殊的线段树,与普通的线段树不同的是,每一个节点的左右儿子不是该点编号的两倍和两倍加一,而是现加出来的,可以类比链式前向星
既然如此,我们就需要开一个结构体或者多个数组来记录节点的左右儿子的地址,如果插入时访问到该节点地址为0,说明这个点还没开,我们就把这个点开辟(cnt++),如果访问时这个点地址为0,说明这个点不存在,返回值为0即可
以下为模板
#include<cstdio>//动态开点线段树是一类特殊的线段树,与普通的线段树不同的是,每一个节点的左右儿子不是该点编号的两倍和两倍加一,而是现加出来的
#include<iostream>//https://blog.csdn.net/u012972031/article/details/88751811 https://www.cnblogs.com/szbszb/p/11243728.html
#include<cstring>
using namespace std;
const int N=1e6+10;
struct node {
int ls,rs,lazy;//左儿子地址,右儿子地址,延迟标记
long long sum;//区间和
} tr[N<<1];
int root=0,cnt=0;//根据insert代码(11行),root从1开始
void init() {
memset(tr,0,sizeof(tr));
root=0,cnt=0;
}
void insert(int &root,int l,int r,int ll,int rr,int x) { //建树&&区间修改 root区间范围[l,r],修改范围[ll,rr],修改增量x root是人为规定的编号,涉及修改,传引用
if(!root)root=++cnt;//root还没开,开点
int len=min(r,rr)-max(l,ll)+1;//b为修改区间在当前区间的长度 例如真子集[1,N],修改[10,20],b=20-10+1=11 或者交集[50,100],修改[30,70],b=70-50+1 =21
if(len<0) return;//或者空集[50,100]修改[10,20] b=20-50+1 =-29 <0
tr[root].sum+=len*x;
if(l>=ll&&r<=rr) { //真子集
tr[root].lazy+=x;
return;
}
int mid=l+r>>1;
if(ll<=mid)insert(tr[root].ls,l,mid,ll,rr,x);
if(rr>mid)insert(tr[root].rs,mid+1,r,ll,rr,x);
}
long long query(int root,int l,int r,int ll,int rr) {//区间查询,在范围为[l,r]的root中查询[ll,rr]的区间和
if(l>=ll&&r<=rr)return tr[root].sum;//真子集
int mid=l+r>>1;
if(tr[root].lazy) {//下放延迟标记
if(!tr[root].ls) tr[root].ls=++cnt;
tr[tr[root].ls].lazy+=tr[root].lazy;
tr[tr[root].ls].sum+=tr[root].lazy*(mid-l+1);
if(!tr[root].rs) tr[root].rs=++cnt;
tr[tr[root].rs].lazy+=tr[root].lazy;
tr[tr[root].rs].sum+=tr[root].lazy*(r-mid);
tr[root].lazy=0;
}
long long ans=0;
if(ll<=mid) ans+=query(tr[root].ls,l,mid,ll,rr);
if(rr>mid) ans+=query(tr[root].rs,mid+1,r,ll,rr);
return ans;
}
int main() {
ios::sync_with_stdio(false);
int n,m;
cin>>n>>m;
for(int i=1; i<=n; i++) {
int temp;
cin>>temp;
insert(root,1,n,i,i,temp);
}
for(int i=1; i<=m; i++) {
int ta,tb,tc,td;
cin>>ta>>tb>>tc;
if(ta==1) {
cin>>td;
insert(root,1,n,tb,tc,td);
} else if(ta==2) {
cout<<query(root,1,n,tb,tc)<<endl;
}
}
return 0;
}
动态开点权值线段树
https://www.cnblogs.com/IzayoiMiku/p/13997750.html
例题https://www.luogu.com.cn/problem/P1908#submit
经过此题还让我学会了结构体数组在定义后不会立即分配内存,赋值时才会分配(不信你对结构体数组初始化会爆内存)
建树root传引用不会修改root,因为只有在节点没有开辟的时候才会开辟,引用修改的是子节点,不是根节点
#include<cstdio>//求逆序对(动态开点权值线段树)
#include<iostream>//https://www.cnblogs.com/IzayoiMiku/p/13997750.html
#include<cstring>
#define lnode tree[node].lson
#define rnode tree[node].rson
using namespace std;
typedef long long ll;
const int N=1e7+10;//动态树最大结点数
const int MAX=1e9;//题中给的值域
struct node {
ll sum;//该点的权值(出现的次数)
int lson,rson;//左子节点,右子节点
} tree[N];
int n,cnt=0,root;//cnt计数变量, 记录开了多少节点
inline int read() {
int s=0,w=1;
char c=getchar();
while(c<'0' || c>'9') {
if(c=='-') w*=-1;
c=getchar();
}
while(c>='0' && c<='9') {
s=(s<<3)+(s<<1)+c-'0';
c=getchar();
}
return s*w;
}
inline void push_up(int node) {//更新父节点
tree[node].sum=tree[lnode].sum+tree[rnode].sum;
}
void update(int &node,int start,int end,int k) { //node是人为规定的编号,所以传引用 k的权值+1
if(!node) { //新建节点
node=++cnt;
}
if(start==end) {
tree[node].sum++;
return;
}
int mid=(start+end)>>1;
if(k<=mid) update(lnode,start,mid,k);
else update(rnode,mid+1,end,k);
push_up(node);
}
ll query(int node,int start,int end,int l,int r) {//在以[start,end]为范围的node节点的子树下查询[l,r]出现的次数
if(!node) return 0;//这个节点未被创建, 返回0
if(start>=l && end<=r) return tree[node].sum;//真子集
ll sum=0;
int mid=start+end>>1;
if(l<=mid) sum+=query(lnode,start,mid,l,r);
if(r>mid) sum+=query(rnode,mid+1,end,l,r);
return sum;
}
int kth(int node,int start,int end,int k) { //查询第k大值是多少
if(start==end)
return start;
int mid=(start+end)>>1,s1=lnode,s2=rnode;//s1是左子树地址,s2是右子树地址
if(k<=tree[s2].sum)
return kth(rnode,mid+1,end,k);//向右子树搜索
else
return kth(lnode,start,mid,k-tree[s2].sum);//向左子树搜索
}
inline void init(){
for(int i=0;i<=cnt;i++){
tree[i].lson=0;
tree[i].rson=0;
tree[i].sum=0;
}
cnt=root=1;
head=0;
}
int main() {//求逆序对
n=read();//序列中有n个数
ll ans=0;
int x;
cnt=root=1;
for(int i=1; i<=n; i++) {
x=read();
if(x+1<=MAX) ans+=query(1,1,MAX,x+1,MAX);//查询当前树中比x大的数的数量,就是x所带来的逆序
update(root,1,MAX,x);
}
printf("%lld\n",ans);
return 0;
}
主席树(可持久化线段树)
原理https://dreamer.blue/blog/post/2018/03/01/aiabs_hjt.dream
这位姐姐的文章应该是全网讲的最好的了
模板
#include <cstdio>//https://dreamer.blue/blog/post/2018/03/01/aiabs_hjt.dream
#include <algorithm>
using namespace std;
const int MAXN = 1e5+1;
// 本代码中所有数据均按从下标 1 开始存放
// 主席树中的线段树结点,sum 表示此区间内元素个数
struct node {
int sum, l, r;
} hjt[MAXN*40];
int a[MAXN], sorted[MAXN], num; // sorted: 离散化后的数组 num: 离散化后的数组长度
int root[MAXN], cnt; // root: 主席树中用来保存每棵线段树树根的数组 cnt: 线段树结点数(用于数组方式动态开点)
// 查找离散化之后的下标
int GetIdx(int v) {
return lower_bound(sorted+1, sorted+1+num, v) - sorted;//lower_bound详解https://blog.csdn.net/qq_40160605/article/details/80150252
}
// 初始化
void Init() {
cnt = 0;
}
// 创建结点
inline int CreateNode(int sum, int l, int r) {
int idx = ++cnt;
hjt[idx].sum = sum;
hjt[idx].l = l;
hjt[idx].r = r;
return idx;
}
// 新建一棵线段树,只沿更新路径新建出较上个版本有修改的结点
// 调用参数
// root: 插入后新生成的线段树的根结点会赋值到 root 中存储
// pre_rt: 上一棵线段树的根
// pos: 本次要插入的数在线段树中的位置
// l, r: 递归参数。默认填写 1, num
void Insert(int &root, int pre_rt, int pos, int l, int r) {
// 动态创建结点,直接根据上一个版本复制对应的结点,sum+1
root = CreateNode(hjt[pre_rt].sum+1, hjt[pre_rt].l, hjt[pre_rt].r);
if(l == r) return;
int m = (l+r) >> 1;
if(pos <= m)
Insert(hjt[root].l, hjt[pre_rt].l, pos, l, m);
else Insert(hjt[root].r, hjt[pre_rt].r, pos, m+1, r);
}
// 本函数适用于查询区间 [l, r] 中的第 k 小。通常需要自行变通
// 调用参数
// s, e: 要查询区间所需的两个线段树的根,如要查询区间 [l, r],则传入 root[l-1], root[r]
// k: 要查询区间第几小
// l, r: 递归参数。默认填写 1, num
int Query(int s, int e, int k, int l, int r) {
if(l == r) return l;
int m = (l+r) >> 1;
int sum = hjt[hjt[e].l].sum - hjt[hjt[s].l].sum; // 计算左子树的元素数量
if(k <= sum) // 如果 k <= sum,则 k 在左子树,否则在右子树
return Query(hjt[s].l, hjt[e].l, k, l, m);
else return Query(hjt[s].r, hjt[e].r, k-sum, m+1, r);
}
int main(int argc, char const *argv[]) {
int n, m, l, r, k;
while(~ scanf("%d %d", &n, &m)) {
Init();
for(int i=1; i<=n; ++i) {
scanf("%d", &a[i]);
sorted[i] = a[i];
}
sort(sorted+1, sorted+1+n); // 按值排序
num = unique(sorted+1, sorted+1+n) - (sorted+1); // 去重,返回去重后的元素数量 unique详解https://www.cnblogs.com/wangkundentisy/p/9033782.html
for(int i=1; i<=n; ++i) { // 按顺序插入,建立 n 棵权值线段树
Insert(root[i], root[i-1], GetIdx(a[i]), 1, num);
}
while(m--) {
scanf("%d %d %d", &l, &r, &k);
printf("%d\n", sorted[Query(root[l-1], root[r], k, 1, num)]);
}
}
return 0;
}