数据结构-线段树

线段树是算法竞赛中常用的用来维护 区间信息 的数据结构。

线段树可以在O(log N)的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。线段树维护的信息,需要满足可加性,即能以可以接受的速度合并信息和修改信息,包括在使用懒惰标记时,标记也要满足可加性(例如取模就不满足可加性,对4取模然后对3取模,两个操作就不能合并在一起做)。

定义结构体

struct node {
    int l;      // 线段树的左端点
    int r;      // 线段树的右端点
    int value;  // 线段上的值
};

建树

对于每个结点,应该有两个指针来表示该结点的左右孩子,用一个数组tree[]来模拟一个树,那么,对于结点tree[v],它的左孩子结点为tree[v * 2],右孩子结点为tree[v * 2 + 1]。在线段树中,左右孩子分别是父结点的子区间,如父结点代表段l ~ r,令int mid = (l + r) / 2;则左孩子表示区间l ~ mid,右孩子表示区间为mid + 1 ~ r。叶结点x表示区间是[x, x]的一个值,即原数组a[]中的一个元素。

void build(int v, int l, int r)  // 对结点v进行建立,区间为l, r
{
    tree[v].l = l;
    tree[v].r = r;
    if (l == r) {
        // 进行结点的初始化
        tree[v].value = a[r];
        return;
    }
    int mid = (l + r) / 2;
    build(v * 2, l, mid);
    build(v * 2 + 1, mid + 1, r);
    // 根据左右儿子更新当前结点
    tree[v].value = tree[v * 2].value + tree[v * 2 + 1].value;
}

更新

在对某一段a[i] ~ a[j]上的所有元素都加上一个值c的时候,以一种记录增量的方法,即给每个结点再加上一个域int add;记录更新操作的增量c。初始时每个结点的add值均为0,当对2 ~ 5区间进行更新操作后,给该结点的add加上一个值c。再下次要对2 ~ 3结点进行更新或查询时,再将add传递到下面的孩子结点去。

void update(int v, int l, int r, int m)  // 更新区间 l ~ r 加上数 m
{
    if (tree[v].l == l && tree[v].r == r) {  // 找到了, 更新并记录增量
        tree[v].value += m * (r - l + 1);
        tree[v].add = m;
        return;  // 记得 return 实际儿子没有更新, 省时
    }

    if (tree[v].add) {                   // 上面没走掉, 传递能量
        tree[v * 2].add += tree[v].add;  // 儿子本身可能也有增量
        tree[v * 2 + 1].add += tree[v].add;
        tree[v].add = 0;  // 传递后, 清零增量
    }

    int mid = (tree[v].l + tree[v].r) / 2;
    if (r <= mid) {
        update(v * 2, l, r, m);  // 只对左孩子进行更新
    } else {
        if (l > mid) {
            update(v * 2 + 1, l, r, m);  // 只对右孩子进行更新
        } else {  // 区间横跨了左右儿子区间,对其两者均进行更新
            update(v * 2, l, mid, m);
            update(v * 2 + 1, mid + 1, r, m);
        }
    }
}

查询

即查询区间 l ~ r 上的value值

void query(int v, int l, int r)  // 当前查询结点为 v 要查询的区间是 l ~ r
{
    if (tree[v].l == l && tree[v].r == r) {
        ans += tree[v].value;
        return;
    }

    if (tree[v].add) {                   // 上面没走掉, 传递能量
        tree[v * 2].add += tree[v].add;  // 儿子本身可能也有增量
        tree[v * 2 + 1].add += tree[v].add;
        tree[v].add = 0;  // 传递后, 清零增量
    }

    int mid = (tree[v].l + tree[v].r) / 2;
    if (r <= mid) {
        query(v * 2, l, r);  // 要查询的区间都在左儿子
    } else {
        if (l > mid) {
            query(v * 2 + 1, l, r);  // 要查询的区间都在右儿子
        } else {                     // 要查询的区间分跨左右孩子
            query(v * 2, l, mid);
            query(v * 2 + 1, mid + 1, r);
        }
    }
}

线段树的用法非常灵活,离散化也是经常配合线段树使用的,因为线段树所需的空间是4 * MAX的,MAX为原数据个数。

上述内容为摘录书本,貌似有错误,以下示例代码

	/// <summary>
	/// 构建线段树
	/// </summary>
	/// <param name="v"></param>
	/// <param name="l"></param>
	/// <param name="r"></param>
	public void BuildTree(int v, int l, int r) {
		tree[v].l = l;
		tree[v].r = r;

		if (l == r) {
			tree[v].value = red[r];         // 1 or 0
			tree[v].add = 0;
			return;
		}
		int mid = (l + r) / 2;
		BuildTree(v * 2, l, mid);
		BuildTree(v * 2 + 1, mid + 1, r);
		tree[v].value = tree[v * 2].value + tree[v * 2 + 1].value;
	}


	// --------修改增量--------
	// 1.向下搜索, 修改区间值, 修改增量值(子结点需要)
	// 2.回溯更新父结点的区间值

	// --------增量清零--------
	// 清除增量前, a.修改子结点暂存的值 b.传递增量给子区间

	/// <summary>
	/// 更新线段树区间值
	/// </summary>
	/// <param name="v"></param>
	/// <param name="l"></param>
	/// <param name="r"></param>
	/// <param name="add"></param>
	public void UpdateTree(int v, int l, int r, int add) {
		if (tree[v].l == l && tree[v].r == r) {
			// 停止向下搜索, 更新该结点的增量值
			tree[v].value += (r - l + 1) * add;
			tree[v].add += add;
			return;
		}
		int mid = (tree[v].l + tree[v].r) / 2;
		if (tree[v].add != 0) {         // 父结点有增量, 需要清除增量
			tree[v * 2].value += (mid - tree[v].l + 1) * tree[v].add;       // a.更新区间值
			tree[v * 2 + 1].value += (tree[v].r - mid) * tree[v].add;

			tree[v * 2].add += tree[v].add;                     // b.传递增量
			tree[v * 2 + 1].add += tree[v].add;

			tree[v].add = 0;                                    // c.清除增量
		}

		if (r <= mid) {                         // 只更新左边
			UpdateTree(v * 2, l, r, add);
		} else if (mid < l) {                   // 只更新右边
			UpdateTree(v * 2 + 1, l, r, add);
		} else {                                // 二分, 更新左右边
			UpdateTree(v * 2, l, mid, add);
			UpdateTree(v * 2 + 1, mid + 1, r, add);
		}
		tree[v].value = tree[v * 2].value + tree[v * 2 + 1].value;
	}


	// --------查询区间--------
	// 当前结点值 = 暂存的值 + 增加值 (区间长度 * (父结点)增量)			// 须保证当前结点的祖先结点没有增量	!

	// --------增量清零--------
	// 清除增量前, a.修改子结点暂存的值 b.传递增量给子区间

	/// <summary>
	/// 查询线段树区间值
	/// </summary>
	/// <param name="v"></param>
	/// <param name="l"></param>
	/// <param name="r"></param>
	private int QueryTree(int v, int l, int r) {
		var tre = tree[v];
		if (tree[v].l == l && tree[v].r == r) {
			return tree[v].value;
		}

		int mid = (tree[v].l + tree[v].r) / 2;
		if (tree[v].add != 0) {         // 父结点有增量, 需要清除增量
			tree[v * 2].value += (mid - tree[v].l + 1) * tree[v].add;       // a.更新区间值
			tree[v * 2 + 1].value += (tree[v].r - mid) * tree[v].add;

			tree[v * 2].add += tree[v].add;                     // b.传递增量
			tree[v * 2 + 1].add += tree[v].add;

			tree[v].add = 0;                                    // c.清除增量
		}

		if (r <= mid) {                     // 查询区域全在mid左边
			return QueryTree(v * 2, l, r);
		} else if (mid < l) {               // 查询区域全在mid右边
			return QueryTree(v * 2 + 1, l, r);
		} else {                            // 查询区域在mid位于查询区域中间
			return QueryTree(v * 2, l, mid) + QueryTree(v * 2 + 1, mid + 1, r);
		}
	}

	public int QueryTree(int l, int r) {
		if (l > r) return 0;
		return QueryTree(1, l, r);
	}

	class node
	{
		public int add;     // 增量
		public int value;   // 区间值
		public int l, r;    // 树上的左右端点
	}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值