蒟蒻讲解线段树+题目

线段树

  • 线段树
      • 啥是线段树
      • 线段树实现
        • 1创建线段树
        • 2查询线段树
        • 3单节点更新
        • 4区间更新
      • 那些关于线段的题
    • 资料来源

啥是线段树

线段树,类似区间树,它在各个节点保存一条线段(数组中的一段子数组),主要用于高效解决连续区间的动态查询问题,由于二叉结构的特性,它基本能保持每个操作的复杂度为O(logn)。

线段树的每个节点表示一个区间,子节点则分别表示父节点的左右半区间,例如父亲的区间是[a,b],那么(c=(a+b)/2)左儿子的区间是[a,c],右儿子的区间是[c+1,b]。

线段树实现


(1)创建线段树

const int MAXNUM = 1000;
struct SegTreeNode
{
    int val;
}segTree[MAXNUM];//定义线段树

/*
功能:构建线段树
root:当前线段树的根节点下标
arr: 用来构造线段树的数组
istart:数组的起始位置
iend:数组的结束位置
*/
void build(int root, int arr[], int istart, int iend)
{
    if(istart == iend)//叶子节点
        segTree[root].val = arr[istart];
    else
    {
        int mid = (istart + iend) / 2;
        build(root*2+1, arr, istart, mid);//递归构造左子树
        build(root*2+2, arr, mid+1, iend);//递归构造右子树
        //根据左右子树根节点的值,更新当前根节点的值
        segTree[root].val = min(segTree[root*2+1].val, segTree[root*2+2].val);
    }
}

(2)查询线段树

/*
功能:线段树的区间查询
root:当前线段树的根节点下标
[nstart, nend]: 当前节点所表示的区间
[qstart, qend]: 此次查询的区间
*/
int query(int root, int nstart, int nend, int qstart, int qend)
{
    //查询区间和当前节点区间没有交集
    if(qstart > nend || qend < nstart)
        return INFINITE;
    //当前节点区间包含在查询区间内
    if(qstart <= nstart && qend >= nend)
        return segTree[root].val;
    //分别从左右子树查询,返回两者查询结果的较小值
    int mid = (nstart + nend) / 2;
    return min(query(root*2+1, nstart, mid, qstart, qend),
               query(root*2+2, mid + 1, nend, qstart, qend));

}

(3)单节点更新

/*
功能:更新线段树中某个叶子节点的值
root:当前线段树的根节点下标
[nstart, nend]: 当前节点所表示的区间
index: 待更新节点在原始数组arr中的下标
addVal: 更新的值(原来的值加上addVal)
*/
void updateOne(int root, int nstart, int nend, int index, int addVal)
{
    if(nstart == nend)
    {
        if(index == nstart)//找到了相应的节点,更新之
            segTree[root].val += addVal;
        return;
    }
    int mid = (nstart + nend) / 2;
    if(index <= mid)//在左子树中更新
        updateOne(root*2+1, nstart, mid, index, addVal);
    else updateOne(root*2+2, mid+1, nend, index, addVal);//在右子树中更新
    //根据左右子树的值回溯更新当前节点的值
    segTree[root].val = min(segTree[root*2+1].val, segTree[root*2+2].val);
}

(4)区间更新

这里要提一个叫懒人标记的东西,也就是大家所说的延迟标记,是个很好用的东西,不废话了,上代码

const int INFINITE = INT_MAX;
const int MAXNUM = 1000;
struct SegTreeNode
{
    int val;
    int addMark;//延迟标记
}segTree[MAXNUM];//定义线段树

/*
功能:构建线段树
root:当前线段树的根节点下标
arr: 用来构造线段树的数组
istart:数组的起始位置
iend:数组的结束位置
*/
void build(int root, int arr[], int istart, int iend)
{
    segTree[root].addMark = 0;//----设置标延迟记域
    if(istart == iend)//叶子节点
        segTree[root].val = arr[istart];
    else
    {
        int mid = (istart + iend) / 2;
        build(root*2+1, arr, istart, mid);//递归构造左子树
        build(root*2+2, arr, mid+1, iend);//递归构造右子树
        //根据左右子树根节点的值,更新当前根节点的值
        segTree[root].val = min(segTree[root*2+1].val, segTree[root*2+2].val);
    }
}

/*
功能:当前节点的标志域向孩子节点传递
root: 当前线段树的根节点下标
*/
void pushDown(int root)
{
    if(segTree[root].addMark != 0)
    {
        //设置左右孩子节点的标志域,因为孩子节点可能被多次延迟标记又没有向下传递
        //所以是 “+=”
        segTree[root*2+1].addMark += segTree[root].addMark;
        segTree[root*2+2].addMark += segTree[root].addMark;
        //根据标志域设置孩子节点的值。因为我们是求区间最小值,因此当区间内每个元
        //素加上一个值时,区间的最小值也加上这个值
        segTree[root*2+1].val += segTree[root].addMark;
        segTree[root*2+2].val += segTree[root].addMark;
        //传递后,当前节点标记域清空
        segTree[root].addMark = 0;
    }
}

/*
功能:线段树的区间查询
root:当前线段树的根节点下标
[nstart, nend]: 当前节点所表示的区间
[qstart, qend]: 此次查询的区间
*/
int query(int root, int nstart, int nend, int qstart, int qend)
{
    //查询区间和当前节点区间没有交集
    if(qstart > nend || qend < nstart)
        return INFINITE;
    //当前节点区间包含在查询区间内
    if(qstart <= nstart && qend >= nend)
        return segTree[root].val;
    //分别从左右子树查询,返回两者查询结果的较小值
    pushDown(root); //----延迟标志域向下传递
    int mid = (nstart + nend) / 2;
    return min(query(root*2+1, nstart, mid, qstart, qend),
               query(root*2+2, mid + 1, nend, qstart, qend));

}

/*
功能:更新线段树中某个区间内叶子节点的值
root:当前线段树的根节点下标
[nstart, nend]: 当前节点所表示的区间
[ustart, uend]: 待更新的区间
addVal: 更新的值(原来的值加上addVal)
*/
void update(int root, int nstart, int nend, int ustart, int uend, int addVal)
{
    //更新区间和当前节点区间没有交集
    if(ustart > nend || uend < nstart)
        return ;
    //当前节点区间包含在更新区间内
    if(ustart <= nstart && uend >= nend)
    {
        segTree[root].addMark += addVal;
        segTree[root].val += addVal;
        return ;
    }
    pushDown(root); //延迟标记向下传递
    //更新左右孩子节点
    int mid = (nstart + nend) / 2;
    update(root*2+1, nstart, mid, ustart, uend, addVal);
    update(root*2+2, mid+1, nend, ustart, uend, addVal);
    //根据左右子树的值回溯更新当前节点的值
    segTree[root].val = min(segTree[root*2+1].val, segTree[root*2+2].val);
}

当你看到这里时,线段树大概已经了解一些了吧,希望对你有帮助!

那些关于线段的题

洛谷【P3372】(就是很简单的模板题)
题目描述:

如题,已知一个数列,你需要进行下面两种操作:

1.将某区间每一个数加上x

2.求出某区间每一个数的和
代码:

#include 
using namespace std;
const int N = 300000 + 10;
const int inf = 100000 + 10;
struct treepoint {
  int l, r, lson, rson;
} G[N];
int n, T, order, x, y, p, t, size;
long long val, z, sumv[N], addv[N], ans;
struct Segment_tree {
  void build(int& root, int L, int R) {
    if(!root) {
      root = ++size;
      G[size] = (treepoint) {L, R};
    }
    if(L == R) { sumv[root] = val; return; }
    int mid = (L+R)/2;
    if(p <= mid) build(G[root].lson, L, mid);
    else build(G[root].rson, mid+1, R);
    sumv[root] = sumv[G[root].lson] + sumv[G[root].rson];
  }
  void maintain(int root, int L, int R) {
    if(R > L) sumv[root] = sumv[G[root].lson] + sumv[G[root].rson];
    if(R == L) sumv[root] += z;
    else sumv[root] += addv[root] * (R-L+1);
  }
  void update(int root, int L, int R) {
    if(x <= L && y >= R) addv[root] += z;
    else {
      int mid = (L+R)/2;
      if(x <= mid) update(G[root].lson, L, mid);
      if(y > mid) update(G[root].rson, mid+1, R);
    }
    maintain(root, L, R);
  }
  void query(int root, int L, int R, long long add) {
    if(x <= L && y >= R) ans += sumv[root] + add * (R-L+1);
    else {
      int mid = (L+R)/2;
      if(x <= mid) query(G[root].lson, L, mid, add + addv[root]);
      if(y > mid) query(G[root].rson, mid+1, R, add + addv[root]);
    }
  }
};
Segment_tree S;
int main() {
  scanf("%d%d", &n, &T);
  for(p = 1; p <= n; p++) {
    scanf("%lld", &val);
    S.build(t, 1, inf);
  }
  while(T--) {
    scanf("%d", &order);
    if(order == 1) {
      scanf("%d%d%lld", &x, &y, &z);
      S.update(1, 1, inf);
    } else {
      scanf("%d%d", &x, &y);
      ans = 0;
      S.query(1, 1, inf, 0);
      printf("%lld\n", ans);
    }
  }
  return 0;
}

线段树经典例题:区间段颜色数

POJ[2777]Count Color

题目概述
有一个长板子,多次操作,有两种操作,第一种是给从a到b那段染一种颜色c,另一种是询问a到b有多少种不同的颜色。

分析:线段树,通过这题才真正理解了线段树的基本思想,无论是更新还是查询都要遵循一个原则,当线段恰好覆盖一个节点的区间时就直接对该节操作而不再向下操作。绝对不能把区间内所有节点全部一代到底,到叶子节点。

对于这种线段树,要在获得整块区间时停止并把该节点的end改为true。以后其他更新或询问需要向下走时再按照该节点的信息进行子节点的信息进行修改,然后向下再进行。

这题还学到一个重要的技巧,当数据范围较小时一定要考虑位操作。颜色最多30种,用位操作,一个整数可以表示一段的颜色状态。

上代码:

#include 
#include 
#include 
#include 
using namespace std;

#define maxn 100004

struct Node
{
    int color;
    int l, r;
    Node *pleft, *pright;
    bool end;
} tree[maxn * 3];

int n, t, o, ncount;

void buildtree(Node *proot, int l, int r)
{
    proot->l = l;
    proot->r = r;
    proot->color = 1;
    proot->end = true;
    if (l == r)
        return;
    int mid = (l + r) / 2;
    ncount++;
    proot->pleft = tree + ncount;
    ncount++;
    proot->pright = tree + ncount;
    buildtree(proot->pleft, l, mid);
    buildtree(proot->pright, mid + 1, r);
}

void paint(Node *proot, int l, int r, int color)
{
    if (proot->l == l && proot->r == r)
    {
        proot->end = true;
        proot->color = color;
        return;
    }
    if (proot->end)
    {
        proot->end = false;
        proot->pleft->color = proot->color;
        proot->pleft->end = true;
        proot->pright->color = proot->color;
        proot->pright->end = true;
    }
    int mid = (proot->l + proot->r) / 2;
    if (r <= mid)
        paint(proot->pleft, l, r, color);
    else if(l > mid)
    paint(proot->pright, l, r, color);
    else
    {
        paint(proot->pleft, l, mid, color);
        paint(proot->pright, mid +1, r, color);
    }
    proot->color = proot->pleft->color | proot->pright->color;
}

int query(Node *proot, int l, int r)
{
    if (proot->end)
        return proot->color;
    if (proot->l == l && proot->r == r)
        return proot->color;
    int mid = (proot->l + proot->r) / 2;
    if (r <= mid)
        return query(proot->pleft, l, r);
    else if(l > mid)
    return query(proot->pright, l, r);
    return query(proot->pleft, l, mid) | query(proot->pright, mid + 1, r);
}

int countbit(int a)
{
    int x = 1;
    int ret = 0;
    for (int i = 0; i < 32; i++, x <<= 1)
        if (x & a)
            ret++;
    return ret;
}

int main()
{
//    freopen("t.txt", "r", stdin);
    ncount = 0;
    scanf("%d%d%d", &n, &t, &o);
    getchar();
    buildtree(tree, 1, n);
    for (int i = 0; i < o; i++)
    {
        char order;
        int l, r, c;
        scanf("%c", &order);
        if (order == 'C')
        {
            scanf("%d%d%d", &l, &r, &c);
            if (l > r)
                swap(l, r);
            paint(tree, l, r, 1 << (c - 1));
        }
        else
        {
            scanf("%d%d", &l, &r);
            if (l > r)
                swap(l, r);
            printf("%d\n", countbit(query(tree, l, r)));
        }
        getchar();
    }
    return 0;
}

资料来源

一步一步理解线段树
POJ[2777]Count Color

你可能感兴趣的:(线段树)