0x43.數據結構進階 - 線段樹

聲明:
本系列博客是《算法競賽進階指南》+《算法競賽入門經典》+《挑戰程序設計競賽》的學習筆記,主要是因爲我三本都買了 按照《算法競賽進階指南》的目錄順序學習,包含書中的少部分重要知識點、例題解題報告及我個人的學習心得和對該算法的補充拓展,僅用於學習交流和複習,無任何商業用途。博客中部分內容來源於書本和網絡(我儘量減少書中引用),由我個人整理總結(習題和代碼可全都是我自己敲噠)部分內容由我個人編寫而成,如果想要有更好的學習體驗或者希望學習到更全面的知識,請於京東搜索購買正版圖書:《算法競賽進階指南》——作者李煜東,強烈安利,好書不火系列,謝謝配合。


下方鏈接爲學習筆記目錄鏈接(中轉站)

學習筆記目錄鏈接


ACM-ICPC在線模板


好久沒有寫線段樹了。線段樹可好玩了呢!正好這次從基礎再過一遍
具體的基礎知識可以看《算法競賽進階指南》喲

一、基礎線段樹

線段樹要保證數組長度不小於4N

首先定義左右兒子

#define ls (p<<1)
#define rs (p<<1|1)

線段樹的建樹

struct SegmentTree{
    int l,r;
    int dat;//最大值
    int sum;//和
}t[N<<2];

void build(int p,int l,int r){
    t[p].l = l;t[p].r = r;
    if(l == r){
        t[p].dat = a[r];
        return ;
    }
    int mid = (l + r) / 2;
    build(ls,l,mid);
    build(rs,mid+1,r);
    t[p].dat = max(t[ls].dat,t[rs].dat);
    t[p].sum = t[ls].sum + t[rs].sum;
}
int n;
int main(){
    build(1,1,n);
}

線段樹的單點修改

void change(int p,int x,int val){
    if(t[p].l == t[p].r){
        t[p].dat = t[p].sum = val;
        return ;
    }
    int mid = (t[p].l + t[p].r) / 2;
    if(x <= mid)change(ls,x,val);
    else change(rs,x,val);
    t[p].dat = max(t[ls].dat,t[rs].dat);
    t[p].sum = t[ls].sum + t[rs].sum;
}

線段樹的區間查詢


int ask_max(int p,int l,int r){
    if(l <= t[p].l && r >= t[p].r)
        return t[p].dat;
    int mid = (t[p].l + t[p].r) / 2;
    int val = -(1<<30);
    if(l <= mid)val = max(val,t[ls].dat);
    if(r > mid)val = max(val,t[rs].dat);
    return val;
}

線段樹的延遲標記(懶惰標記)

1.POJ3486 A Simple Problem with IntegersA\ Simple\ Problem\ with\ Integers

The sums may exceed the range of 32-bit integers.
Source

題意:一行序列,每次操作把一個區間裏的每個數都加上一個數,或者查詢一個區間的和。

模板題,
注意數據範圍會爆int,記得開long long 。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<math.h>
#include<cstring>
#include<bitset>
#include<vector>
#include<queue>

#define ls (p<<1)
#define rs (p<<1|1)

#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;
const int INF = 0x3f3f3f3f;
const int N = 1e5+7;
const int M = 2007;

struct SegmentTree{
    int l,r;
    ll sum;//總和
    ll lz_add;//增量延遲標記
    #define tl(x) tree[x].l
    #define tr(x) tree[x].r
    #define tsum(x) tree[x].sum
    #define lza(x) tree[x].lz_add
}tree[N<<2];

int a[N],n,m;

void build(int p,int l,int r){
    tl(p) = l,tr(p) = r;
    if(l == r) {
        tsum(p) = a[r];
        return ;
    }
    int mid = (l + r) / 2;
    build(ls,l,mid);
    build(rs,mid+1,r);
    tsum(p) = tsum(ls) + tsum(rs);
}

void push_down(int p){//向下更新一層
    if(lza(p)){
        tsum(ls) += lza(p)*(tr(ls) - tl(ls) + 1);
        tsum(rs) += lza(p)*(tr(rs) - tl(rs) + 1);
        lza(ls) += lza(p);//注意是+=
        lza(rs) += lza(p);
        lza(p) = 0;
    }
}

void change(int p,int l,int r,int d){
    if(l <= tl(p) && r >= tr(p)){
        tsum(p) += (ll)d * (tr(p) - tl(p) + 1);//差多少補多少
        lza(p) += d;
        return ;
    }
    push_down(p);
    int mid = (tl(p) + tr(p)) / 2;
    if(l <= mid)change(ls,l,r,d);
    if(r > mid)change(rs,l,r,d);
    tsum(p) = tsum(ls) + tsum(rs);
}

ll ask(int p,int l,int r){//ask和change的時候都要push_down,因爲要直接用下一層
    if(l <= tl(p) && r >= tr(p))
        return tsum(p);
    push_down(p);
    int mid = (tl(p) + tr(p)) / 2;
    ll val = 0;
    if(l <= mid)val += ask(ls,l,r);
    if(r > mid)val += ask(rs,l,r);
    return val;
}

int main()
{
    cin>>n>>m;
    over(i,1,n)
    scanf("%d",&a[i]);
    build(1,1,n);
    while(m--){
        char ch[2];
        int l,r,d;
        scanf("%s%d%d",ch,&l,&r);
        if(ch[0] == 'C'){
            scanf("%d",&d);
            change(1,l,r,d);
        }
        else printf("%lld\n",ask(1,l,r));
    }
    return 0;
}

二、掃描線法

2.POJ1151 AtlantisAtlantis

在這裏插入圖片描述
線段樹掃描線的應用

我們嘗試設想有一條無限高的豎線左往右掃過這個並集圖形,按照每一個矩形的的左右邊界,我們可以將這個並集圖形分爲2n 段,對於兩兩相鄰的部分,我們可以分別計算面積,這樣就得到了整個並集圖形的面積。

如圖,我們就是把每個矩形的左右邊界提了出來,就變成了這樣一些線段。
那麼我們需要這些量化記錄下來:每個四元組(x,y1,y2,1/−1)分別代表了一條線段,x是線段的橫座標,(y1,y2)是線段上下端點的縱座標,1/−1代表了這條線段是矩形的左邊界還是右邊界。

顯然,我們只需要把這些線段按照橫座標排序,對於一次遍歷來說,兩兩線段之間的距離是已知的。那麼我們需要解決的問題就是縱座標的影響範圍。
我們不妨把縱座標都取出來,離散化映射到[1,T]之間的T個整數值,並將這些縱座標表示爲T−1段,其中第i段代表了第i個縱座標和第i+1個縱座標之間的部分,然後,我們設立數組ci代表第i段被覆蓋的次數。

這樣,我們就可以用如下的算法流程計算矩形的面積:

  1. 對於每一個線段,將其的k值累加到這個線段對應的若干個縱座標區間
  2. 計算面積:所有T−1個縱座標區間對應的c值大於零的就說明這些部分的區間還存在,將存在的區間的長度累加起來,乘上當前線段與下一條線段之間的橫座標之差就是這兩條線段之間的面積。

顯然,這裏需要我們維護一個區間內的區間加法,區間求和,這個就是線段樹的事情了。

由於本題中的區間修改成對出現,互相抵消,所以我們可以不寫帶有lazytag的線段樹。我們在線段樹的每一個節點上維護兩個值cnt和len,cnt代表這段區間被覆蓋的次數,如果cnt>0則當前區間的len等於當前區間的縱座標長度,反之lenp=lenp∗2+lenp∗2+1。那麼對於每一次區間修改,我們直接在線段樹上改cnt的值即可,並沿路更新len值即可。
在這裏插入圖片描述

/*POJ 1151 Atlantis*/
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<math.h>
#include<cstring>
#include<bitset>
#include<vector>
#include<queue>

#define ls (p<<1)
#define rs (p<<1|1)

#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;
const int INF = 0x3f3f3f3f;
const int N = 1e5+7;
const int M = 2007;

struct line{
    double x,y1,y2;
    int f;
    bool operator<(const line &t)const{
        return x < t.x;
    }
}a[N<<1];

struct SegmentTree{
    int l,r;
    double cnt;
    double len;
    #define tl(x) tree[x].l
    #define tr(x) tree[x].r
    #define tcnt(x) tree[x].cnt
    #define tlen(x) tree[x].len
}tree[N<<3];

int n,T,tot,val[N<<1][2];
double raw[N<<1],ans;

inline void build(int p,int l,int r){
    tl(p) = l;
    tr(p) = r;
    if(l == r)return ;
    int mid = (l+r) / 2;
    build(ls,l,mid);
    build(rs,mid+1,r);
}

inline void push_up(int p){
    if(tcnt(p) > 0)tlen(p) = raw[tr(p) + 1] - raw[tl(p)];
    else if(tl(p) == tr(p))tlen(p) = 0;
    else tlen(p) = tlen(ls) + tlen(rs);
}

void updata(int p,int l,int r,int v){
    if(l <= tl(p) && r >= tr(p)){
        tcnt(p) += v;
        push_up(p);
        return ;
    }
    int mid = (tl(p) + tr(p)) / 2;
    if(l <= mid)updata(ls,l,r,v);
    if(r > mid)updata(rs,l,r,v);
    push_up(p);
}

int main()
{
    while(cin>>n && n){
        ans = tot = 0;
        over(i,1,n){
            double x1,x2,y1,y2;
            scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2);
            a[2*i-1] = (line){x1,y1,y2,1};//奇和偶,左和右
            a[2*i] = (line){x2,y1,y2,-1};
            raw[++tot] = y1;
            raw[++tot] = y2;
        }
        sort(a+1,a+1+2*n);
        sort(raw+1,raw+1+tot);
        tot = unique(raw+1,raw+1+tot) - (raw+1);//注意+1因爲從1開始的
        over(i,1,2*n){
            val[i][0] = lower_bound(raw+1,raw+1+tot,a[i].y1) - raw;
            val[i][1] = lower_bound(raw+1,raw+1+tot,a[i].y2) - raw;
        }
        build(1,1,tot);
        over(i,1,2*n){
            updata(1,val[i][0],val[i][1] - 1,a[i].f);
            ans += (a[i+1].x - a[i].x) * tlen(1);
        }
        printf("Test case #%d\nTotal explored area: %.2f\n\n",++T,ans);
    }
    return 0;
}

POJ 2482-Stars in Your Window(經典掃描線變式)

題目大意:給出n個星星的座標,每個星星有一個亮度,給出一個矩形的長和寬,問矩形能包括的星星的最大亮度和(不包括邊框)。

https://fanfansann.blog.csdn.net/article/details/106725096

三、動態開點和線段樹合併

動態開點

在一些計數問題中,線段樹用於維護值域(一段權值範圍),這樣的線段樹被稱之爲權值線段樹。爲了降低空間複雜度,我們可以不建出整棵樹的結構,而是在最初只建立一個根結點,代表整個區間,當需要訪問線段樹的某棵子樹(某個子區間)時,在建立代表這個區間的結點,這種方法就是動態開點的線段樹。

這裏使用了指針來代表子結點,並在遞歸訪問的過程中區間作爲參數來傳遞。

int tot;
struct Seg{
    int lc,rc;
    int dat;//區間最大值
    #define trl(x) tree[x].lc
    #define trr(x) tree[x].rc
    #define tdat(x) tree[x].dat
}tree[N<<1];

int build(){
    tot++;
    trl(tot) = trr(tot) = tdat(tot) = 0;
    return tot;
}
//將val位置上的值加上delta
void Insert(int p,int l,int r,int val,int delta){
    if(l == r){
        tdat(p) += delta;
        return ;
    }
    int mid = (l + r) / 2;
    if(val <= mid){//要往左邊走
        if(!trl(p))//但是左邊沒有
            trl(p) = build();//動態開點//返回的是tot,也就是指針
        Insert(trl(p),l,mid,val,delta);
    }
    else {
        if(!trr(p))//同上
            trr(p) = build();
        Insert(trr(p),mid+1,r,val,delta);
    }
    tdat(p) = max(tdat(trl(p)),tdat(trr(p)));
}
int root,n;
int delta,val;
int main()
{
    tot = 0;
    root = build();//根節點
    Insert(root,1,n,val,delta);
}

線段樹合併

int Merge(int p,int q,int l,int r){
    if(!p)return q;//一旦有一個爲空就return
    if(!q)return p;
    if(l == r){//到達葉子結點
        tdat(p) += tdat(q);
        return p;//規定合併到p處並返回p
    }
    int mid = (l + r) / 2;
    trl(p) = Merge(trl(p),trl(q),l,mid);
    trr(p) = Merge(trr(p),trr(q),mid+1,r);
    tdat(p) = max(tdat(trl(p)),tdat(trr(p)));
    return p;
}

一個寫的很棒的線段樹教程
cppblog.com/menjitianya/archive/2016/02/25/212891.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章