浅谈线段树+模板

推荐两篇线段树博文:
线段树之一
线段树之二

小结:
线段树是一种二叉树,也可以说成是区间树,操作有:建树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;
}

会有很多种区间问题的思维题,套进线段树时注意思维,有的是剪枝,就的是暴力一部分,注意思维这一块的转换。。。

猜你喜欢

转载自blog.csdn.net/qq_36368339/article/details/79218106