线段树

    xiaoxiao2021-03-25  126

    主要写写自己的想法吧,网上的前几篇博文写的都很详细,线段树主要目的主要是用来高效地解决单点或连续区间的查询或操作。

    线段树的叶子节点记录的是原数组的值,然后每个非叶节点都有一个左儿子和右儿子。比方说要求某区间内的最小值,已知叶子节点记录的是原数组值,那么它的父节点存储的值则是它和另一个同父兄弟节点两者间的最小值,即父节点表示的区间范围内的最小值,依照此种思路,不断向上更新,线段树的节点会存储相应的值,然后进行查询,时间复杂度为O(logN),对,线段树和树状数组的功能主要就是这么两个方面:修改某点或区间的值,查询区间内相应的值。

    假设区间[a, b],那么它的左儿子代表的区间是[a, (a+b)/2], 右儿子区间为[(a+b)/2+1, b].

    在进行建树前,需要做一些准备工作,我们假设问题是求任意某区间和

    #define lson l, m, rt << 1 #define rson m+1, r, rt << 1 | 1 int jk[MAXN], sum[MAXN << 2];建树的数组要开原数组的4倍去进行构建树。

    对了,当我对父节点的儿子节点们进行操作完成后,需要相应地更新父节点的值。

    void pushUp(int rt){//rt代表结点的编号 sum[rt] = sum[rt<<1] + sum[rt<<1|1]; }

    那么建树的代码如下,r-l表示有多少个叶节点

    void build(int l, int r, int rt){//rt代表结点的编号 if(l == r){ scanf("%d", &jk[l]); sum[rt] = jk[l]; return; } int m = (l+r) >> 1; build(lson); build(rson); pushUp(rt); }单点更新如下,

    void update(int c, int v, int l, int r, int rt){ //c:要更新的点,v:要更新的值,l:当前节点的区间l,r:当前节点的区间r,rt:结点的编号 if(l == r){ 当缩小至一个点时其实就是找到了c点,此时l=r=c sum[rt] = v; return; } int m = (l+r) >> 1; if(m >= c)//如果该点在中点的左边或包含中点,进入左子树更新 update(c, v, lson); else update(c, v, rson);//否则进入右子树更新 pushUp(rt); }

    接下来便只剩查询了

    int query(int L, int R, int l, int r, int rt){ //L、R:进行查询的区间左端点,右端点,l、r:当前节点的区间l和r,rt:节点编号 if(l >= L && r <= R){ //当满足,l-r区间内包含的值为L-R内部分值,返回即可 return sum[rt]; } int m = (l+r) >> 1, ans = 0; if(m >= L) //如果中点左边及中点存在查询区间内的点,将查询下发至左儿子 ans += query(L, R, lson); if(m < R) //如果中点右边存在查询区间内的点,将查询下发至右儿子 ans += query(L, R, rson); return ans; } 该问题的整篇代码为,

    #include <iostream> #include <cstdio> #define MAXN 50005 #define lson l, m, rt << 1 #define rson m+1, r, rt << 1 | 1 using namespace std; int jk[MAXN], sum[MAXN << 2]; void pushUp(int rt){//rt代表结点的编号 sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } void build(int l, int r, int rt){//rt代表结点的编号 if(l == r){ scanf("%d", &jk[l]); sum[rt] = jk[l]; return; } int m = (l+r) >> 1; build(lson); build(rson); pushUp(rt); } int query(int L, int R, int l, int r, int rt){ //L、R:进行查询的区间左端点,右端点,l、r:当前节点的区间l和r,rt:节点编号 if(l >= L && r <= R){ //当满足,l-r区间内包含的值为L-R内部分值,返回即可 return sum[rt]; } int m = (l+r) >> 1, ans = 0; if(m >= L) //如果中点左边及中点存在查询区间内的点,将查询下发至左儿子 ans += query(L, R, lson); if(m < R) //如果中点右边存在查询区间内的点,将查询下发至右儿子 ans += query(L, R, rson); return ans; } void update(int c, int v, int l, int r, int rt){ //c:要更新的点,v:要更新的值,l:当前节点的区间l,r:当前节点的区间r,rt:结点的编号 if(l == r){ 当缩小至一个点时其实就是找到了c点,此时l=r=c sum[rt] = v; return; } int m = (l+r) >> 1; if(m >= c)//如果该点在中点的左边或包含中点,进入左子树更新 update(c, v, lson); else update(c, v, rson);//否则进入右子树更新 pushUp(rt); } int main(){ int n, k; cin >> n; build(1, n, 1); update(1, 5, 1, n, 1); while(cin >> k && k){ printf("%d\n", query(1, k, 1, n, 1)); } return 0; } 当然 ,线段树除了单点查询之外,也可进行区间查询和区间操作,

    区间查询是通过再开一个数组用来进行延迟标记,为什么进行延迟标记呢,因为当进行区间更新的时候,当遇到范围内区间就停止向下更新,由此会产生问题,叶子在进行更新或者查询的时候,如果涉及到该区间内某几个点,而更新并没有影响到这几个叶子节点,所以便会产生错误。

    怎么使用延迟标记呢?

    延迟标记是用来记录该点是否进行某种修改,所以当进行该节点的子节点的操作时,我们需要把这些修改下放至子节点,然后取消标记,并且给子节点加上标记。

    下面新增一个pushDown()进行标记下放

    void pushDown(int rt, int k){ //rt:结点的编号,k:该结点表示的区间范围 if(ks[rt]){ ks[rt<<1] += ks[rt]; //子节点加标记 ks[rt<<1|1] += ks[rt]; c[rt<<1] += (k-k/2)*ks[rt]; c[rt<<1|1] += (k/2)*ks[rt]; ks[rt] = 0; //清除标记 } }建树,

    void build(int l, int r, int rt){ ks[rt] = 0; //建树使把每个节点的标记清0 if(l == r){ scanf("%d", &c[rt]); return; } int m = (l+r) >> 1; build(lson); build(rson); pushUp(rt); }

    更新,

    void update(int L, int R, int va, int l, int r, int rt){ //L、R:进行更新的区间l,r,va:要更新的值 if(L <= l && R >= r){ ks[rt] += va; c[rt] += (r-l+1)*va; return; } pushDown(rt, r-l+1); //每次要访问该节点的子节点前,进行处理标记 int m = (l+r) >> 1; if(L <= m) update(L, R, va, lson); if(R > m) update(L, R, va, rson); pushUp(rt); }

    查询,

    int query(int L, int R, int l, int r, int rt){ if(L <= l && R >= r){ return c[rt]; } pushDown(rt, r-l+1); //查询时也许进行标记处理 int sum = 0, m = (l+r)>>1; if(m >= L) sum += query(L, R, lson); if(m < R) sum += query(L, R, rson); pushUp(rt); return sum; } 当然,当L=R时,就相当于单点更新了。

    总的代码如下,

    #include <iostream> #include <cstdio> #define MAXN 666 #define lson l, m, rt << 1 #define rson m+1, r, rt << 1 | 1 using namespace std; int c[MAXN<<2], ks[MAXN<<2]; void pushUp(int rt){ c[rt] = c[rt<<1] + c[rt<<1|1]; } void pushDown(int rt, int k){ //rt:结点的编号,k:该结点表示的区间范围 if(ks[rt]){ ks[rt<<1] += ks[rt]; //子节点加标记 ks[rt<<1|1] += ks[rt]; c[rt<<1] += (k-k/2)*ks[rt]; c[rt<<1|1] += (k/2)*ks[rt]; ks[rt] = 0; //清除标记 } } void build(int l, int r, int rt){ ks[rt] = 0; //建树使把每个节点的标记清0 if(l == r){ scanf("%d", &c[rt]); return; } int m = (l+r) >> 1; build(lson); build(rson); pushUp(rt); } int query(int L, int R, int l, int r, int rt){ if(L <= l && R >= r){ return c[rt]; } pushDown(rt, r-l+1); //查询时也许进行标记处理 int sum = 0, m = (l+r)>>1; if(m >= L) sum += query(L, R, lson); if(m < R) sum += query(L, R, rson); pushUp(rt); return sum; } void update(int L, int R, int va, int l, int r, int rt){ //L、R:进行更新的区间l,r,va:要更新的值 if(L <= l && R >= r){ ks[rt] += va; c[rt] += (r-l+1)*va; return; } pushDown(rt, r-l+1); //每次要访问该节点的子节点前,进行处理标记 int m = (l+r) >> 1; if(L <= m) update(L, R, va, lson); if(R > m) update(L, R, va, rson); pushUp(rt); } int main(){ int n, k, a, b; cin >> n; build(1, n, 1); while(cin >> k >> a >> b){ printf("%d\n", query(k, a, 1, n, 1)); if(b != 0) update(k, a, b, 1, n, 1); printf("%d\n", query(1, n, 1, n, 1)); } return 0; }

    继续加油~

    转载请注明原文地址: https://ju.6miu.com/read-12932.html

    最新回复(0)