树和树结构(4): 线段树(部分转载)

原文来自http://blog.csdn.net/metalseed/article/details/8039326, 有改动
使用tyvj1039_忠诚2 作为测试题目: http://www.tyvj.cn/p/1039
源码下载 Tyvj1039_忠诚2.cpp

一:线段树基本概念
0: 图片

1:概述
线段树,类似区间树,是一个完全二叉树,它在各个节点保存一条线段(数组中的一段子数组),主要用于高效解决连续区间的动态查询问题,由于二叉结构的特性,它基本能保持每个操作的复杂度为O(lgN)!
性质:父亲的区间是[a,b],(c=(a+b)/2)左儿子的区间是[a,c],右儿子的区间是[c+1,b],线段树需要的空间为数组大小的四倍
个人感觉: 其实是将区间二分, 预处理出一部分区间值. 这样在查询某一个区间最小值时, 可以直接调用其中一部分. 例如在1..10的区间内求2..6的最小值, 实质上可以变为求2..(1+10)/2最小值和(1+10)/2+1..6的最小值. 前者又可以划分为2..3和4..5等等等, 以此类推. 读者可以用手算下1..100区间内求31..78的最小值过程, 可以理解线段树高效的原因.

首先建立线段树的数据结构:

    #include <iostream>
    #include <cstring>
    #include <cstdio>
    #define INF 100000001
    #define half(x) ((x)>>(1))
    using namespace std;

    int nums[100005];
    int n, q;

    typedef struct node;
    typedef node *tree;
    int init(tree &t, int from, int to);
    struct node {
    tree lc, rc;
        //左右子树
        int left, right, value;
        //线段
        node() {
                lc = rc = NULL;
                left = right = value = 0;
        }
    }*root;

2:基本操作(demo用的是查询区间最小值)
线段树的主要操作有:
(1):线段树的构造 void init(node, begin, end);
主要思想是递归构造,如果当前节点记录的区间只有一个值,则直接赋值,否则递归构造左右子树,最后回溯的时候给当前节点赋值
注意: 为了节约空间, 这里给出的代码只将区间划分为i..i+1.

    int init(tree &t, int from, int to)
    {
        if (to < from) return INF;
        if (from == to) return nums[from];
        //到达边界
        if (t == NULL)
            t = new node;
        t->left = from;    
        t->right = to;
        return t->value = min(
        init(t->lc, from, half(from+to)),
        init(t->rc, half(from+to)+1, to)
        );
        //递归构建左右子树
    }

(2):区间查询int ask_min(tree n, int from, int to)
(其中n为当前查询节点,from, to为此次query所要查询的区间)
主要思想是把所要查询的区间[a,b]划分为线段树上的节点,然后将这些节点代表的区间合并起来得到所需信息
比如前面一个图中所示的树,如果询问区间是[0,2],或者询问的区间是[3,3],不难直接找到对应的节点回答这一问题。但并不是所有的提问都这么容易回答.

询问有且只有五种情况:
0 线段长度为0
1 和此节点重合
2 被此节点的左子树包含
3 被此节点的右子树包含
4 可分成左右两部分

据此很容易写出子程序

    int ask_min(tree n, int from, int to)
    {
        if (from == to) return nums[from];           //0
        if (to < from || !n || (n->left > from && n->right < to)) return INF;
        //剪去无意义的
        if (n->left == from && n->right == to)
            return n->value;                     //1
        if (n->left <= from && half(n->left+n->right) >= to)
            return ask_min(n->lc, from, to);     //2
        if (half(n->left+n->right)+1 <= from && n->right >= to)
            return ask_min(n->rc, from, to);     //3
        return min(
        ask_min(n->lc, from, half(n->left+n->right)),
        ask_min(n->rc, half(n->left+n->right)+1, to)
        );                                           //4
    }

(3):节点的更新 动态维护void update(tree &n, int num)
由顶自下更新, 再返回来向上更新. 先写一个获取此节点值的函数:

    int value(tree n) {
        if (!n->lc)
            return min(nums[n->left], nums[n->right]);
        //左子树为空, 说明左右都为空
        if (!n->rc)
            return min(n->lc->value, nums[n->right]);
        //右子树为空, 左子树不为空
        return min(n->lc->value, n->rc->value);
        //都不为空
    }

再写更新(这个需要好好理解递归构树):

    void update(tree &n, int num)
    {
        if (!n)
            return; //到底部了
        if (num <= half(n->left+n->right)) {
            update(n->lc, num);
            n->value = value(n);
            //向左子树更新
        } else {
            update(n->rc, num);
            n->value = value(n);
            //向右子树更新
        }
    }

最后呼之欲出的main!

    int main()
    {
        scanf ("%d%d", &n, &q);
        for (int i=1; i<=n; i++)
            scanf ("%d", &nums[i]);
        init(root, 1, n);
        for (int i=1; i<=q; i++) {
            int order, f, t;
            scanf ("%d%d%d", &order, &f, &t);
            if (order == 1)
                printf ("%d ", ask_min(root,f,t));
            if (order == 2) {
                nums[f] = t;
                update (root, f);
            }
        }
        return 0;
    }

通过这个练习, 对树结构有了更深刻的理解. 十分欣赏树结构的简洁, 美观, 高效!

明天就要期中考了, 还是忍不住来发个文. 对原文做了一些NOIp难度的改动, 希望对大家有些帮助.
发技术文好累23333

posted @ 2015-12-27 13:07  ljt12138  阅读(144)  评论(0编辑  收藏  举报