淺談線段樹+模板

推薦兩篇線段樹博文:
線段樹之一
線段樹之二

小結:
線段樹是一種二叉樹,也可以說成是區間樹,操作有:建樹build,更新updata(單點+區間),查詢query(單點+區間)。單點操作時把區間不斷二分,用root指向數組下標;區間更新操作時,標記lazy,先對子樹的根節點做更新,當用到的這個子樹的時候,再把標記下推,同時遞歸時向上統計,更新區間;區間查詢時,會有遍歷的一個操作遇到layz時,把延遲的標記下推。查詢和更新複雜度估計O(logn);

用線段樹時,往往先用普通方法做(TLE),哪個地方需要優化時間,就放到線段樹上維護試試可不可行,最重要的是作圖,模擬一遍。線段樹針對的基本是區間操作,有線段樹單點問題,區間問題,區間和並,區間染色,離散化,掃描線。。。

單點操作:POJ 3264
求區間最大值最小值的差:

#include <cstdio> 
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;

typedef long long LL;
const int MAXN = 1e5 + 10;
LL dp_min[MAXN << 2], dp_max[MAXN << 2];
LL ans1, ans2;

void pushup(int root) {
    dp_min[root] = min(dp_min[root << 1], dp_min[root << 1 | 1]);
    dp_max[root] = max(dp_max[root << 1], dp_max[root << 1 | 1]);
}

void build(int root, int L, int R) {
    if(L == R) {
        scanf("%lld", &dp_max[root]);
        dp_min[root] = dp_max[root];
        return ;
    }
    int mid = (L + R) >> 1;
    build(root << 1, L, mid);
    build(root << 1 | 1, mid + 1, R);
    pushup(root);
}

void query(int root, int L, int R, int l, int r) {
    if(L >= l && R <= r) {
        ans1 = max(ans1, dp_max[root]);
        ans2 = min(ans2, dp_min[root]);
        return;
    }
    int mid = (L + R) >> 1;
    if(mid >= l) query(root << 1, L, mid, l, r);
    if(r > mid) query(root << 1 | 1, mid + 1, R, l, r);
}

int main() {
    int n, m;
    while(scanf("%d %d", &n, &m) != EOF) {
        build(1, 1, n);
        while(m--) {
            int x, y;
            scanf("%d %d", &x, &y);
            ans1 = 0;
            ans2 = 0x3f3f3f3f;
            query(1, 1, n, x, y);
            printf("%lld\n", ans1 - ans2);
        }
    }
    return 0;
}

區間操作(lazy): POJ 3468
Q是查詢區間和,C是使區間內的每個元素加上c;
解法:用lazy,區間更新,區間查詢;

#include <cstdio> 
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;

typedef long long LL;
const int mod = 24 * 60;
const int MAXN = 1e5 + 10;
LL dp[MAXN << 2], add[MAXN << 2];

void pushup(int root) {
    dp[root] = dp[root << 1] + dp[root << 1 | 1];
}

void pudown(int root, int L, int R) {
    if(add[root]) {//lazy具體步驟,之後會總體詳解
        add[root << 1] += add[root];
        add[root << 1 | 1] += add[root];
        dp[root << 1] += add[root] * L;
        dp[root << 1 | 1] += add[root] * R;
        add[root] = 0;
    }
}

void build(int root, int L, int R) {
    if(L == R) {
        scanf("%lld", &dp[root]);
        return ;
    }
    int mid = (L + R) >> 1;
    build(root << 1, L, mid);
    build(root << 1 | 1, mid + 1, R);
    pushup(root);
}

void updata(int root, int L, int R, int l, int r, int c) {
    if(L >= l && R <= r) {
        dp[root] += c * (R - L + 1);//區間更新
        add[root] += c;
        return ;
    }
    int mid = (L + R) >> 1;
    pudown(root, mid - L + 1, R - mid);//lazy操作
    if(l <= mid) updata(root << 1, L, mid, l, r, c);
    if(r > mid) updata(root << 1 | 1, mid + 1, R, l, r, c);
    pushup(root);
}

LL query(int root, int L, int R, int l, int r) {
    if(L >= l && R <= r) {
        return dp[root];
    }
    LL ans = 0;
    int mid = (L + R) >> 1;
    pudown(root, mid - L + 1, R - mid);//lazy操作
    if(l <= mid) ans += query(root << 1, L, mid, l, r);
    if(r > mid) ans += query(root << 1 | 1, mid + 1, R, l, r);
    return ans;
}

int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    build(1, 1, n);
    while(m--) {
        getchar();
        char ch;
        int x, y, c;
        scanf("%c", &ch);
        if(ch == 'Q') {
            scanf("%d %d", &x, &y);
            printf("%lld\n", query(1, 1, n, x, y));
        }
        else {
            scanf("%d %d %d", &x, &y, &c);
            updata(1, 1, n, x, y, c);//區間修改
        }
    }
    return 0;
}

區間染色:ZOJ 1610
給你區間[a, b]染成c色,最後統計區間內,顏色染了多少不連續的區間段;
解法:區間染色問題,自上而下更新,不用自下而上統計,線段樹單點查詢時,點是連續的;

#include <cstdio> 
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;

typedef long long LL;
const int mod = 24 * 60;
const int MAXN = 1e4 + 10;
int col[MAXN << 2], num[MAXN << 2];
int ncol = -1;

void down(int root) {
    if(col[root] != -1) { //狀態轉移,右根向左右孩子擴散,根節點不保存狀態
        col[root << 1] = col[root << 1 | 1] = col[root];
        col[root] = -1;
    }
}

void updata(int root, int L, int R, int l, int r, int c) {
    if(L >= l && R <= r) {
        col[root] = c; //把父節點染色
        return ;
    }
    int mid = (L + R) >> 1;
    down(root); //自上向下updata
    if(l <= mid) updata(root << 1, L, mid, l, r, c);
    if(r > mid) updata(root << 1 | 1, mid + 1, R, l, r, c);
}

void query(int root, int L, int R) {
    if(L == R) {
        if(col[root] != -1 && col[root] != ncol){
            num[col[root]]++; //更新顏色段
        }
        ncol = col[root]; //線段樹query時,區間是連續的,記錄此時的區間顏色,和下一個區間對比
        return;
    }
    int mid = (L + R) >> 1;
    down(root); //自上向下
    if(L <= mid) query(root << 1, L, mid);
    if(R > mid) query(root << 1 | 1, mid + 1, R);
}

int main() {
    int n;  
    while(scanf("%d", &n) != EOF) {
        memset(num, 0, sizeof(num));
        memset(col, -1, sizeof(col)); //初始化建樹
        while(n--) {
            int x, y, c;
            scanf("%d %d %d", &x, &y, &c);
            if(x < y) updata(1, 0, 8000, x, y - 1, c); //染的是區間段,不是[x, y]內的所有點
        }
        ncol = -1;
        query(1, 0, 8000); 
        for(int i = 0; i <= 8000; i++) {
            if(num[i]) printf("%d %d\n", i, num[i]);
        }
        puts("");
    }
    return 0;
}

離散化具體步驟:
線段樹離散化

區間和並:hdu 1540
線段樹維護區間連續長度時,用 l_sum和r_sum維護子樹的左端和右端長度,合併時判斷一下長度就行;

#include <cstdio> 
#include <algorithm>
#include <cstring>
#include <cmath>
#include <stack>
using namespace std;

typedef long long LL;
const int MAXN = 5e4 + 10;
int lsum[MAXN << 2], rsum[MAXN << 2]; 
bool vis[MAXN << 2];

void pushup(int root, int L, int R) {
    int mid = (L + R) >> 1; 
    lsum[root] = lsum[root << 1]; //狀態向上轉移,利於查詢
    rsum[root] = rsum[root << 1 | 1];
    if(lsum[root] == mid - L + 1) lsum[root] += lsum[root << 1 | 1]; //如果左子樹葉子全部連續,把右子樹的左端併到左子樹
    if(rsum[root] == R - mid) rsum[root] += rsum[root << 1]; //同上
}

void build(int root, int L, int R) {
    lsum[root] = rsum[root] = R - L + 1; //初始全部連續,區間值最大
    if(L == R) return ;
    int mid = (L + R) >> 1;
    build(root << 1, L, mid);
    build(root << 1 | 1, mid + 1, R);
    pushup(root, L, R);
}

void updata(int root, int L, int R, int x, bool flag) {
    if(L == R) { //單點更新,向上統計
        lsum[root] = rsum[root] = flag; //flag表示是否炸燬
        return ;
    }
    int mid = (L + R) >> 1;
    if(mid >= x) updata(root << 1, L, mid, x, flag);
    else updata(root << 1 | 1, mid + 1, R, x, flag);
    pushup(root, L, R);
}

int query(int root, int L, int R, int x) {
    if(L == R) { //單點查詢,但是複雜度會很低,因爲能查到的點會很少,都被剪枝了
    //  printf("%%%%%%%%%%%%%%\n");
        return lsum[root];
    }
    int mid = (L + R) >> 1;
    if(mid >= x) {
        if(mid - x + 1 <= rsum[root << 1]) { //如果x點在左子樹的右端範圍內,返回左子樹的右端值+右子樹的左端值
            return rsum[root << 1] + lsum[root << 1 | 1]; 
        }
        else { //否則繼續尋找
            return query(root << 1, L, mid, x);
        }
    }
    else if(x > mid) { //同上
        if(x - mid <= lsum[root << 1 | 1])
            return lsum[root << 1 | 1] + rsum[root << 1];
        else 
            return query(root << 1 | 1, mid + 1, R, x);
    }
}

int main() {
    int n, m, x;
    while(~scanf("%d %d", &n, &m)) {
        stack<int> s;
        build(1, 1, n);
        while(m--) {
            getchar();
            char ch;
            scanf("%c", &ch);
            if(ch == 'D') {
                scanf("%d", &x);
                s.push(x);
                updata(1, 1, n, x, 0);
            }
            else if(ch == 'R') { //修復過的也可再修復,正常寫就行,描述有點漏洞
                if(s.empty()) continue; //判斷是否有需要修復的了,坑點
                int y = s.top();
                s.pop();
                updata(1, 1, n, y, 1);
            }
            else if(ch == 'Q') {
                scanf("%d", &x);
                printf("%d\n", query(1, 1, n, x));
            }
        }
    }
    return 0;
}

會有很多種區間問題的思維題,套進線段樹時注意思維,有的是剪枝,就的是暴力一部分,注意思維這一塊的轉換。。。

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