线段树(Segment Tree)几乎是算法竞赛最常用的数据结构了,它主要用于维护区间信息(要求满足结合律)。与树状数组相比它,普通线段树不仅可以进行加法区间修改还可以进行乘法区间修改和维护区间的最大值和最小值
(权值线段树可以维护一个区间数出现的个数也就可以维护第k大的数是多少 我们只能对给定数列解决整个数列的第k大/小,并不能解决数列的子区间的第k大/小。这种问题可以通过主席树解决)
线段树时间复杂度为O (logN) 最大空间<4n(n为原数组大小)
建树
void build(int s, int t, int p) {
// 对 [s,t] 区间建立线段树,当前根的编号为 p
if (s == t) {
d[p] = a[s];
return;
}
int m = s + ((t - s) >> 1);
// 移位运算符的优先级小于加减法,所以加上括号
// 如果写成 (s + t) >> 1 可能会时间超限
build(s, m, p * 2), build(m + 1, t, p * 2 + 1);
// 递归对左右区间建树
d[p] = d[p * 2] + d[(p * 2) + 1];
}
单点修改
void updata(int l,int r,int pos,int p,int v) //pos是修改位置,v修改的值,p树状数组编号
{
if(r<pos||l>pos) return;
if(l==r&&l==pos)
{
d[p]=v;
return;
}
int mid=(l+r)/2;
updata(l,mid,pos,p*2,v);
updata(mid+1,r,pos,p*2+1,v);
d[p]=d[p*2]+d[p*2+1];
}
单点修改后的区间查询
int getsum(int l, int r, int s, int t, int p) {
// [l,r] 为查询区间,[s,t] 为当前节点包含的区间,p 为当前节点的编号
if (l <= s && t <= r)
return d[p]; // 当前区间为询问区间的子集时直接返回当前区间的和
int m = s + ((t - s) >> 1), sum = 0;
if (l <= m) sum += getsum(l, r, s, m, p * 2);
// 如果左儿子代表的区间 [l,m] 与询问区间有交集,则递归查询左儿子
if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
// 如果右儿子代表的区间 [m+1,r] 与询问区间有交集,则递归查询右儿子
return sum;
}
求区间最大值及其下标(下面代码是相同大小尽量往右)
pair<int,int>querymax(int l,int r,int s,int t,int p){
if(s>=l&&t<=r){
return {d1[p],pos1[p]};
}
int m=(s+t)>>1;
int maxx=0;
int pos;
if(m>=l){
pair<int,int>now=querymax(l,r,s,m,p*2);
if(now.first>=maxx){
maxx=now.first;
pos=now.second;
}
}
if(m<r){
pair<int,int>now=querymax(l,r,m+1,t,p*2+1);
if(now.first>=maxx){
maxx=now.first;
pos=now.second;
}
}
return {maxx,pos};
}
线段树的区间修改与懒惰标记
如果要求修改区间 ,把所有包含在区间 中的节点都遍历一次、修改一次,时间复杂度无法承受。我们这里要引入一个叫做 「懒惰标记」 的东西。
懒惰标记,简单来说,就是通过延迟对节点信息的更改,从而减少可能不必要的操作次数。每次执行修改时,我们通过打标记的方法表明该节点对应的区间在某一次操作中被更改,但不更新该节点的子节点的信息。实质性的修改则在下一次访问带有标记的节点时才进行。
区间修改后区间查询(区间加减)
void update(int l, int r, int c, int s, int t, int p) {
// [l,r] 为修改区间,c 为被修改的元素的变化量,[s,t] 为当前节点包含的区间,p
// 为当前节点的编号
if (l <= s && t <= r) {
d[p] += (t - s + 1) * c, b[p] += c;
return;
} // 当前区间为修改区间的子集时直接修改当前节点的值,然后打标记,结束修改
int m = s + ((t - s) >> 1);
if (b[p] && s != t) {
// 如果当前节点的懒标记非空,则更新当前节点两个子节点的值和懒标记值
d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m);
b[p * 2] += b[p], b[p * 2 + 1] += b[p]; // 将标记下传给子节点
b[p] = 0; // 清空当前节点的标记
}
if (l <= m) update(l, r, c, s, m, p * 2);
if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);
d[p] = d[p * 2] + d[p * 2 + 1];
}
int getsum(int l, int r, int s, int t, int p) {
// [l,r] 为查询区间,[s,t] 为当前节点包含的区间,p为当前节点的编号
if (l <= s && t <= r) return d[p];
// 当前区间为询问区间的子集时直接返回当前区间的和
int m = s + ((t - s) >> 1);
if (b[p]) {
// 如果当前节点的懒标记非空,则更新当前节点两个子节点的值和懒标记值
d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m),
b[p * 2] += b[p], b[p * 2 + 1] += b[p]; // 将标记下传给子节点
b[p] = 0; // 清空当前节点的标记
}
int sum = 0;
if (l <= m) sum = getsum(l, r, s, m, p * 2);
if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
return sum;
}
区间修改后区间查询(区间修改某一个数)
void update(int l, int r, int c, int s, int t, int p) {
if (l <= s && t <= r) {
d[p] = (t - s + 1) * c, // <-区别于整体加减的区别
b[p] = c;
return;
}
int m = s + ((t - s) >> 1);
if (b[p]) {
d[p * 2] = b[p] * (m - s + 1), d[p * 2 + 1] = b[p] * (t - m),
b[p * 2] = b[p * 2 + 1] = b[p]; // <-区别于整体加减的区别
b[p] = 0;
}
if (l <= m) update(l, r, c, s, m, p * 2);
if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);
d[p] = d[p * 2] + d[p * 2 + 1];
}
int getsum(int l, int r, int s, int t, int p) {
if (l <= s && t <= r) return d[p];
int m = s + ((t - s) >> 1);
if (b[p]) {
d[p * 2] = b[p] * (m - s + 1), d[p * 2 + 1] = b[p] * (t - m),
b[p * 2] = b[p * 2 + 1] = b[p];
b[p] = 0;
}
int sum = 0;
if (l <= m) sum = getsum(l, r, s, m, p * 2);
if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
return sum;
}
区间修改后区间查询(区间乘法)
void pushdown(int p, int l, int r) {
int mid = (l + r) >> 1;
sum[p << 1] = sum[p << 1] * tag[p] % mod;
sum[p << 1 | 1] = sum[p << 1 | 1] * tag[p] % mod;
tag[p << 1] = tag[p << 1] * tag[p] % mod;
tag[p << 1 | 1] = tag[p << 1 | 1] * tag[p] % mod;
tag[p] = 1;
}
void update(int p, int l, int r, int x, int y, int k) {
if (x <= l && r <= y) {
sum[p] = sum[p] * k % mod;
tag[p] = tag[p] * k % mod;
return;
}
pushdown(p, l, r);
int mid = (l + r) >> 1;
if (x <= mid) update(p << 1, l, mid, x, y, k);
if (y > mid) update(p << 1 | 1, mid + 1, r, x, y, k);
pushup(p);
}
int query(int p, int l, int r, int x, int y) {
int res = 0;
if (x <= l && r <= y) return sum[p];
int mid = (l + r) >> 1;
pushdown(p, l, r);
if (x <= mid) res += query(p << 1, l, mid, x, y);
if (y > mid) res += query(p << 1 | 1, mid + 1, r, x, y);
return res;
}
zkw线段树板子
inline void Maintain(int x) {
tree[x] = tree[lson(x)] + tree[rson(x)];
}
inline void Build() {
for(M=1;M<N;M<<=1);
for(int i=M+1;i<=M+N;++i) scanf("%d", &tree[i]);
for(int i=M-1;i;--i) Maintain(i);
}
inline void Update(int pos,int v) {
pos += M;
tree[pos] = v;
for(pos>>=1;pos;pos>>=1) Maintain(pos);
}
inline int Sum(int l,int r) {
int ans = 0;
// l=l+M-1->将查询区间改为L-1,r=r+M+1->将查询区间改为R+1
// l^r^1 -> 相当于判断l与r是否是兄弟节点
for(l=l+M-1,r=r+M+1;l^r^1;l>>=1,r>>=1) {
if(~l&1) // l % 2 == 0 即l是l/2的左儿子
ans += tree[l^1];
if(r&1) // r % 2 == 1 即r是r/2的右儿子
ans += tree[r^1];
}
return ans;
}
权值线段树板子:
权值线段树较普通线段树不同地方在于:权值线段树的数组范围 n 是它的值域大小,储存的内容是这个值的出现次数。 由于权值线段树的特性,因此权值线段树可以用来实现一些普通平衡树的功能,如: 但权值线段树也有一些弊端,因为权值线段树的数组大小是值域大小,因此当值域非常大时(如 10 9 ),需要进行离散化 当左子树内权值大于等于剩余查询个数,则递归进入左子树进行查询,否则递归进入右子树进行查询。但权值线段树也有一些弊端,因为权值线段树的数组大小是值域大小,因此当值域非常大时,需要进行离散化
可以用于:求整个区间的整体第k大的值
板子题
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int a[maxn],b[maxn],d[maxn*4];
void build(int l,int r,int pos){ //建树
if(l==r){
d[pos]=l;
return;
}
int mid=(l+r)/2;
build(l,mid,pos*2);
build(mid+1,r,pos*2+1);
d[pos]=d[pos*2]+d[pos*2+1];
}
void add(int l,int r,int k,int cnt,int pos){ //单点修改(增删数字) 将大小为K的值增减
if(l==r){
d[pos]+=cnt;
return;
}
int mid=(l+r)/2;
if(k<=mid){
add(l,mid,k,cnt,pos*2);
}
if(k>mid){
add(mid+1,r,k,cnt,pos*2+1);
}
d[pos]=d[pos*2]+d[pos*2+1];
}
int find1(int l,int r,int k,int pos){ //询问数K有多少个
if(l==r){
return d[k];
}
int mid=(l+r)/2;
if(mid>=k){
return find1(l,mid,k,pos*2);
}
else{
return find1(mid+1,r,k,pos*2+1);
}
}
int find2(int l,int r,int k,int pos){ //询问第K大的数是多少
if(l==r) return l;
int mid=(l+r)/2;
if(d[pos*2]>=k){
return find2(l,mid,k,pos*2);
}
else{
return find2(mid+1,r,k-d[pos*2],pos*2+1);
}
}
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
b[i]=a[i];
}
//离散化
sort(a+1,a+1+n);
int cnt=unique(a+1,a+1+n)-a-1;
for(int i=1;i<=n;i++){
b[i]=lower_bound(a+1,a+1+cnt,b[i])-a;
}
//
for(int i=1;i<=n;i++){
add(1,cnt,b[i],1,1);
if(i&1){
cout<<a[find2(1,cnt,(i+1)/2,1)]<<endl;
}
}
}
可持久化线段树求区间第k大板子:
需要注意的是可持久化线段树里面的l和r代表的是now这个线段树所指向的区间范围为l到r
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int a[maxn];
vector<int>vec;
int getid(int x){
return lower_bound(vec.begin(),vec.end(),x)-vec.begin()+1;
}
struct node{
int l,r,sum;
}hjt[maxn*40];
int cnt,root[maxn]; //内存计数器和根节点编号
void insert(int l,int r,int pre,int &now,int p){ //int &now是因为now代表的是pre主席树的左右儿子之一,插入的时候向下递归儿子会跟着变
hjt[++cnt]=hjt[pre];
now=cnt;
hjt[now].sum++;
if(l==r){
return;
}
int m=(l+r)>>1;
if(p<=m){
insert(l,m,hjt[pre].l,hjt[now].l,p);
}
else{
insert(m+1,r,hjt[pre].r,hjt[now].r,p);
}
}
int query(int l,int r,int ll,int rr,int k){//ll代表 询问的左边-1的版本 rr询问的右边的版本
if(l==r){
return l;
}
int mid=(l+r)>>1;
int tmp=hjt[hjt[rr].l].sum-hjt[hjt[ll].l].sum;
if(k<=tmp){
return query(l,mid,hjt[ll].l,hjt[rr].l,k);
}
else{
return query(mid+1,r,hjt[ll].r,hjt[rr].r,k-tmp);
}
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
vec.push_back(a[i]);
}
sort(vec.begin(),vec.end());
vec.erase(unique(vec.begin(),vec.end()),vec.end());
for(int i=1;i<=n;i++){
insert(1,n,root[i-1],root[i],getid(a[i]));
}
while(m--){
int l,r,k;
cin>>l>>r>>k;
cout<<vec[query(1,n,root[l-1],root[r],k)-1]<<"\n";
}
}
可持久化线段树求区间小于等于X的数的个数
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int a[maxn],b[maxn];
struct node{
int l,r,sum;
}hjt[maxn*40];
int cnt,root[maxn];
void insert(int l,int r,int pre,int&now,int p){
hjt[++cnt]=hjt[pre];
now=cnt;
hjt[now].sum++;
if(l==r){
return;
}
int m=(l+r)>>1;
if(p<=m){
insert(l,m,hjt[pre].l,hjt[now].l,p);
}
else{
insert(m+1,r,hjt[pre].r,hjt[now].r,p);
}
}
int query(int l,int r,int now,int k){ //now代表版本 val代表值
if(k>=b[r]){ //hjt[now].sum代表b[l]到b[r]的值加起来是多少
return hjt[now].sum;
}
else if(k<b[l]){
return 0;
}
int res=0;
int m=(l+r)>>1;
if(k<b[m]){
res+=query(l,m,hjt[now].l,k);
}
else{
res+=hjt[hjt[now].l].sum;
res+=query(m+1,r,hjt[now].r,k);
}
return res;
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
b[i]=a[i];
}
sort(b+1,b+1+n);
int nn=unique(b+1,b+1+n)-b-1;
for(int i=1;i<=n;i++){
int x=lower_bound(b+1,b+1+nn,a[i])-b;
insert(1,nn,root[i-1],root[i],x);
}
while(m--){
int l,r,val;
cin>>l>>r>>val;
int qian=query(1,nn,root[r],val);
cout<<qian-query(1,nn,root[l-1],val)<<"\n";
}
}
可持久化数组板子 luogu P3919 可持久化数组还可以用c++内置的stl rope实现 链接
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
struct node{
int l,r,v;
};
int a[maxn];
node hjt[maxn*40];
int cnt,root[maxn];
void build(int l,int r,int &now)
{
now=++cnt;
if(l==r)
{
hjt[now].v=a[l];
return;
}
int m = (l+r)>>1;
build(l,m,hjt[now].l);
build(m+1,r,hjt[now].r);
}
void modify(int l,int r,int ver,int &now,int pos,int num)
{
hjt[now=++cnt]=hjt[ver];
if(l==r)
{
hjt[now].v=num;
return;
}
int m = (l+r)>>1;
if(pos<=m) modify(l,m,hjt[ver].l,hjt[now].l,pos,num);
else modify(m+1,r,hjt[ver].r,hjt[now].r,pos,num);
}
int query(int l,int r,int ver,int &pos)
{
if(l==r) return hjt[ver].v;
int m = (l+r)>>1;
if(pos<=m) return query(l,m,hjt[ver].l,pos);
else return query(m+1,r,hjt[ver].r,pos);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
build(1,n,root[0]);
for(int i=1;i<=m;i++){
int pre,opt;
cin>>pre>>opt;
if(opt==1){
int x,y;
cin>>x>>y;
modify(1,n,root[pre],root[i],x,y);
}
else{
int x;
cin>>x;
cout<<query(1,n,root[pre],x)<<"\n";
root[i]=root[cnt];
}
}
}
线段树查找第一个小于等于x的数的下标(该线段树维护的是最小值)
求最后一个大于,小于......同理
int quert_first_xiaoyu(int l,int r,int s,int t,int val,int p){
if(s==t){
if(d[p]<=val){
return s;
}
return -1;
}
if(s>=l&&t<=r){
if(d[p]>val){
return -1;
}
}
int mid=(s+t)/2;
if(l<=mid){
int tmp=query(l,r,s,mid,val,p*2);
if(tmp!=-1){
return tmp;
}
}
if(r>mid){
int tmp=query(l,r,mid+1,t,val,p*2+1);
return tmp;
}
}
线段树维护区间修改gcd
利用差分数组 gcd(a,b,c,d)=gcd(a,b-a,c-b,d-c) 注意到 b-a,d-b,d-c类似于差分数组所以可以建立差分数组然后a的值可以用差分数组区间和来求,差分数组区间修改只需要修改两个值,所以gcd(l,r)==gcd(sum(1,l),gcdd(l+1,r))其中红色部分就是我们维护的差分数组的区间gcd和差分数组的区间和所以如果要修改某一个区间的数,那么gcdd和sum变化的只有l和r+1这两个地方
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int a[maxn];
int d1[maxn*4],d2[maxn*4];
int gcd(int x,int y){
if(y==0){
return (abs)(x);
}
else{
return (abs)(gcd(y,x%y));
}
}
void build(int l,int r,int p){
if(l==r){
d1[p]=a[l];
d2[p]=a[l];
return;
}
int mid=l+r>>1;
build(l,mid,p*2);
build(mid+1,r,p*2+1);
d1[p]=d1[p*2]+d1[p*2+1];
d2[p]=gcd(d2[p*2],d2[p*2+1]);
}
int querysum(int l,int r,int s,int t,int p){
if(s>=l&&t<=r){
return d1[p];
}
int mid=s+t>>1;
int ans=0;
if(l<=mid){
ans+=querysum(l,r,s,mid,p*2);
}
if(r>mid){
ans+=querysum(l,r,mid+1,t,p*2+1);
}
return ans;
}
int querygcd(int l,int r,int s,int t,int p){
if(s>=l&&t<=r){
return d2[p];
}
int ans=0;
int mid=s+t>>1;
if(l<=mid){
int tmp=querygcd(l,r,s,mid,p*2);
ans=gcd(tmp,ans);
}
if(r>mid){
int tmp=querygcd(l,r,mid+1,t,p*2+1);
ans=gcd(tmp,ans);
}
return ans;
}
void updata(int l,int r,int pos,int x,int p){
if(pos<l||pos>r){
return;
}
if(l==r&&l==pos){
d1[p]+=x;
d2[p]+=x;
return;
}
int mid=l+r>>1;
updata(l,mid,pos,x,p*2);
updata(mid+1,r,pos,x,p*2+1);
d1[p]=d1[p*2]+d1[p*2+1];
d2[p]=gcd(d2[p*2],d2[p*2+1]);
}
int gcdd(int l,int r,int s,int t,int p){
return gcd(querysum(1,l,s,t,1),querygcd(l+1,r,s,t,1));
}
void solve(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=n;i>=2;i--){
a[i]=a[i]-a[i-1];
}
build(1,n,1);
char now;
while(cin>>now){
if(now=='Q'){
int l,r;
cin>>l>>r;
cout<<gcdd(l,r,1,n,1)<<"\n";
}
if(now=='C'){
int l,r,x;
cin>>l>>r>>x;
updata(1,n,l,x,1);
if(r+1<=n){
updata(1,n,r+1,-1*x,1);
}
}
}
}
int main(){
int t;
cin>>t;
while(t--){
solve();
}
}
线段树统计区间交集大小(修改还有清空)
给定区间长度为n,有m次操作每次把L-R区间增加颜色X(1-3),并且中间有操作清空之前的操作,问同时涂了1-3颜色的区间长度为多少
#include<bits/stdc++.h>
#define int long long
#define io ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
const int maxn=2e5+5;
const int inf=1e9+7;
const int mod=1e9+7;
int d[maxn<<2][4]; //d[p][x]记录这个区间有x个不同数相交的个数有几个
int tag[maxn<<2][4]; //tag[p][x]记录这个区间加过x这个数了没有加过了就不加了
int lazy[maxn<<2][4]; //lazy[p][x]懒惰标记
int qingkong[maxn<<2];
void build(int l,int r,int p){
if(l==r){
d[p][0]=1;
return;
}
int m=(l+r)>>1;
build(l,m,p*2);
build(m+1,r,p*2+1);
for(int i=0;i<=3;i++){
d[p][i]=d[p*2][i]+d[p*2+1][i];
}
}
void update(int l,int r,int s,int t,int p,int x){ //s到t加上x
if(qingkong[p]){
for(int i=0;i<=3;i++){
d[p][i]=0;
tag[p][i]=0;
lazy[p][i]=0;
}
d[p][0]=t-s+1;
qingkong[p*2]=1;
qingkong[p*2+1]=1;
qingkong[p]=0;
}
if(l<=s&&t<=r){
if(l==2&&r==2&&x==2){
}
if(tag[p][x]){
return;
}
else{
tag[p][x]=1;
lazy[p][x]=1;
for(int i=3;i>=1;i--){
d[p][i]=d[p][i-1];
}
d[p][0]=0;
}
return;
}
for(int ii=1;ii<=3;ii++){
int x=ii;
if(lazy[p][x]){
if(!tag[p*2][x]){
tag[p*2][x]=1;
lazy[p*2][x]=1;
for(int i=3;i>=1;i--){
d[p*2][i]=d[p*2][i-1];
}
d[p*2][0]=0;
}
if(!tag[p*2+1][x]){
tag[p*2+1][x]=1;
lazy[p*2+1][x]=1;
for(int i=3;i>=1;i--){
d[p*2+1][i]=d[p*2+1][i-1];
}
d[p*2+1][0]=0;
}
lazy[p][x]=0;
}
}
int m=(s+t)>>1;
if(m>=l){
update(l,r,s,m,p*2,x);
}
if(m+1<=r){
update(l,r,m+1,t,p*2+1,x);
}
for(int i=0;i<=3;i++){
d[p][i]=d[p*2][i]+d[p*2+1][i];
}
}
void solve(){
int n,m;
cin>>n>>m;
build(1,n,1);
for(int i=1;i<=m;i++){
int l,r,x;
cin>>l>>r>>x;
update(l,r,1,n,1,x);
}
cout<<d[1][3]<<"\n"; //输出a,b,c相交的个数
qingkong[1]=1; //清空,在下次修改的时候起效
}
signed main(){
int t=1;
// cin>>t;
while(t--){
solve();
}
}