线段树

线段树

线段树概念
线段树和树状数组都是解决区间问题的数据结构,线段树的两个经典问题:区间求和,区间最值。
(1)区间最值:
1)求最值:给定i, j ≤ n,求区间[i, j]内的最值。
2)修改元素:给定k和x,把第k个元素a[k]改成x。
如果用普通数组存储数列,上面2个操作,求最值的复杂度是O(n),修改是O(1)。如果有m次“修改元素+查询最值”,那么总复杂度是O(mn)。如果m和n比较大,例如105,那么整个程序的复杂度是1010数量级,这个复杂度在算法竞赛中是不可接受的。
(2)区间求和:
给出一个数列,先修改一些值,然后选取[i,j]求区间和。如果用数组存数据,一次求和是O(n)的;如果更改和询问的操作总次数是m,那么整个程序的复杂度是O(mn)。这样的复杂度也是不行的。
对于这两类问题都可以用线段树在O(mlogn)的时间内完成。在上述两种基础的应用外,线段树还可以解决多种问题。

树状数组和线段树各有优点:
(1)逻辑结构。线段树基于二叉树,数据结构非常直观,更清晰易懂;另外,由于二叉树灵活而丰富,能用于更多场合,比树状数组适应面多。
(2)代码长度。线段树的编码需要维护二叉树,而树状数组只需简单地处理一个Tree数组,所以线段树的代码更长。不过,二叉树也可以用数组(满二叉树)来写,代码能稍微减少一点。

概况地说,线段树是“分治法思想 + 二叉树结构 + lazy-tag技术”。

线段树是分叉法和二叉树的结合
线段树的节点是一个区间,下图是一个包含10个线段的线段树,观察一下这棵树我们可以发现几个特点。
(1)树用分治法自上而下建立,每个节点把区间分为两半,左右子树各一半。
(2)每个节点表示一个线段,非子节点包含多个元素,叶子节点仅包含一个元素。
(3)除了最后一层其他都是满的,这种结构的树层数是最少的。

线段树_第1张图片
在查找一个区间[L,R]左端是L,右端是R。
当L==R时,说明这个区间仅包含一个元素,这个节点是叶子节点。
当L 线段树是二叉树,一个区间顺着节点往下分,包含n个元素的线段树,最多logn次就可以到达底层。搜索一个点或一个区间最多需要logn次就可以找到。

二叉树的实现

编码时,可以定义标准二叉树的编译模式,竞赛时一般用静态数组实现满二叉树,满二叉树虽然浪费空间但编码简单。父结点和子结点之间的访问非常简单,缺点是最后一行有大量结点被浪费。

//定义根结点是tree[1],即编号为1的结点是根
//(1)第一种方法:定义二叉树数据结构
struct{
     
    int L, R, data;             //用tree[i].data记录线段i的最值或区间和
}tree[MAXN*4];                  //分配静态数组,开4倍大
//(2)第二种方法:直接用数组表示二叉树,更节省空间
int tree[MAXN*4];	             //用tree[i]记录线段i的最值或区间和
//以上两种方式,都满足下面的父子关系。结点p是父,结点ls(p)是左儿子,rs(p)是右儿子
int ls(int x){
      return x<<1;  }     //左儿子,编号是 p*2
int rs(int x){
      return x<<1|1;}     //右儿子,编号是 p*2+1

注意,二叉树的空间需要开MAXN*4,即元素数量的4倍,下面说明原因。假设有一棵处理n个元素(叶子结点有n个)的线段树,且它的最后一层只有1个叶子,其他层都是满的;如果用满二叉树表示,它的结点总数是:最后一层有2n个结点(其中2n - 1个都浪费了没用到),前面所有的层有2n个结点,共4n个结点。空间的浪费是二叉树的本质决定的:它的每一层都按2倍递增。

void push_up(int p){
                                //从下往上传递区间值
    tree[p] = tree[ls(p)] + tree[rs(p)];      //区间和
    //tree[p] = min(tree[ls(p)], tree[rs(p)]);//求最小值
}
void build(int p,int pl,int pr){
                //结点编号p指向区间[pl, R]
    if(pl==pr){
     tree[p]=a[pl]; return; }    //最底层的叶子,存叶子的值
    int mid = (pl+pr) >> 1;                //分治:折半
    build(ls(p),pl,mid);                   //递归左儿子
    build(rs(p),mid+1,pr);                 //递归右儿子
    push_up(p);                            //从下往上传递区间值
}

单点修改+区间最值

以数列{1, 4, 5, 8, 6, 2, 3, 9, 10, 7}为例。首先建立一棵用满二叉树实现的线段树,用于查询任意子区间的最小值。如下图所示,每个结点上圆圈内的数字是这棵子树的最小值。圆圈旁边的数字,例如根结点的"1:[1,10]",1表示结点的编号,[1,10]是这个结点代表的元素范围,即第1到第10个元素。

线段树_第2张图片
查询任意区间[i, j]的最小值。例如查区间[4, 9]的最小值,递归查询到区间[4, 5]、[6, 8]、[9, 9],见图中画横线的线段,得最小值min{6, 2, 10} = 2。查询在O(logn)时间内完成。读者可以注意到,在这种情况下,线段树很像一个最小堆。
  m次“单点修改+区间查询”的总复杂度是O(mlogn)。对规模100万的问题,也能轻松解决。

单点修改+区间求和

首先建立一棵用于查询{1, 4, 5, 8, 6, 2, 3, 9, 10, 7}区间和的线段树,每个结点上圆圈内的数字是这棵子树的和。
线段树_第3张图片
例如查区间[4, 9]的和,递归查询到区间[4, 5]、[6, 8]、[9, 9],见图中画横线的线段,得sum{14, 14, 10} = 38。查询在O(logn)的时间内完成。

区间查询实现与代码
下面以查询[L,R]的和为例,查询递归到节点p时(p的区间是[pl,pr]),有三种情况。
(1)如果[L,R]包含[pl,pr],即L<=pl<=pr<=R,直接返回p的值即可。
(2)如果[L,R]与[pl,pr]完全不相交,返回0,退出。
(3)如果[L,R]与[pl,pr]部分重叠,分别搜索左右子节点,mid是p区间的中点,L<=mid递归左子节点,R>mid递归右节点。

int query(int L,int R,int p,int pl,int pr){
                  
    if(L<=pl && pr<=R) return tree[p];       //完全覆盖
    int mid = (pl+pr)>>1;
    if(L<=mid) res+=query(L,R,ls(p),pl,mid);   //L与左子节点有重叠  
    if(R>mid)  res+=query(L,R,rs(p),mid+1,pr); //R与右子节点有重叠
    return res;
}
//调用方式:query(L, R, 1, 1, n)

区间修改+区间求和

这里就用到了这节的核心算法“lazy-tag”。并给出了“区间修改+区间查询”的模板。
在上一节已经指出区间修改要比单点修改复杂的多。最普通区间修改,例如对一个数列的[L, R]区间内每个元素统一加上d,如果在线段树上,一个个地修改这些元素,那么m次区间修改的复杂度是O(mnlogn)的。
解决的办法很容易想到,还是利用线段树的特征:线段树的结点tree[i],记录了i这个区间的值。那么可以再定义一个tag[i],用它统一记录i这个区间的修改,而不是一个个地修改区间内的每个元素,这个办法被称为“lazy-tag”。
**“lazy-tag”**称懒惰标记也叫延迟标记。当修改一个区间时,可以将这个区间整体进行修改,其中的子节点先不改变,只有这个区间的整体性被破坏时,才去修改其中的子节点。每次区间修改的复杂度是O(logn)的,一共m次操作,总复杂度是O(mlogn)的。区间i的lazy操作,用tag[i]记录。
下面举例说明区间修改函数update()的具体步骤。例如把[4, 9]区间内的每个元素加3,执行步骤是:
  (1)左子树递归到结点5,即区间[4, 5],完全包含在[4, 9]内,打标记tag[5] = 3,更新tree[5]为20,不再继续深入;
  (2)左子树递归返回,更新tree[2]为30;
  (3)右子树递归到结点6,即区间[6, 8],完全包含在[4, 9]内,打标记tag[6]=3,更新tree[6]为23。
  (4)右子树递归到结点14,即区间[9, 9],打标记tag[14]=3,更新tree[14]=13;
  (5)右子树递归返回,更新tree[7]=20;继续返回,更新tree[3]=43;
  (6)返回到根节点,更新tree[1]=73。
  详情见下图。
  线段树_第4张图片
push_down()函数。在进行多次区间修改时,一个结点需要记录多个区间修改。而这些区间修改往往有冲突,例如做2次区间修改,一次是[4, 9],一次是[5, 8],它们都会影响5:[4, 5]这个结点。第一次修改[4, 9]覆盖了结点5,用tag[5]做了记录;而第二次修改[5, 8]不能覆盖结点5,需要再向下搜到结点11:[5, 5],从而破坏了tag[5],此时原tag[5]记录的区间统一修改就不得不往它的子结点传递和执行了,传递后tag[5]失去了意义,需要清空。所以lazy-tag的主要操作是解决多次区间修改,用push_down()函数完成。它首先检查结点p的tag[p],如果有值,说明前面做区间修改时给p打了tag标记,接下来就把tag[p]传给左右子树,然后把tag[p]清零。

#include
#include
using namespace std;

const int MAX = 1e5 + 10;

int tree[MAX], tag[MAX], a[MAX];

int ls(int v) {
      return v << 1; }
int rs(int v) {
      return v << 1 + 1; }

void push_up(int x)
{
     
	tree[x] = tree[ls(x)] + tree[rs(x)];
}

void build(int p, int pl, int pr)
{
     
	tag[p] = 0;
	if (pl == pr) {
      tree[p] = a[pl]; return; }
	int mid = (pl + pr) >> 1;
	build(ls(p), pl, mid);
	build(rs(p), mid + 1, pr);
	push_up(p);
}

int addtag(int p, int pl, int pr, int d)
{
     
	tag[p] += d;
	tree[p] += (pl - pr + 1)*d;
}

void push_down(int p, int pl, int pr)
{
     
	if (tag[p])
	{
     
		int mid = (pl + pr) >> 1;
		addtag(ls(p), pl, mid, tag[p]);
		addtag(rs(p), mid + 1, pr, tag[p]);
		tag[p] = 0;
	}
}

void update(int L, int R, int p, int pl, int pr, int d)
{
     
	if (L <= pl && pr <= R)
	{
     
		addtag(p, pl, pr, d);
		return;
	}
	push_down(p, pl, pr);
	int mid = (pl + pr) >> 1;
	if (L <= mid)update(L, R, ls(p), pl, mid, d);
	if (R > mid)update(L, R, rs(p), mid + 1, pr, d);
	push_up(p);
}

int query(int L, int R, int p, int pl, int pr)
{
     
	int ans = 0;
	if (L <= pl && pr <= R)
	{
     
		return tree[p];
	}
	push_down(p, pl, pr);
	int mid = (pl + pr) >> 1;
	if (L <= mid)ans += query(L, R, ls(p), pl, mid);
	if (R > mid)ans += query(L, R, rs(p), mid, pr);
	return ans;
}


int main()
{
     
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
	{
     
		cin >> a[i];
	}
	build(1, 1, n);
	while (m--)
	{
     
		int q, l, r, d;
		cin >> q;
		if (q == 1)
		{
     
			cin >> l >> r >> d;
			update(l, r, 1, 1, n, d);
		}
		else
		{
     
			cin >> l >> r;
			cout << query(l, r, 1, 1, n);
		}
	}
	system("pause");
	return 0;
}

具体的题目,可以根据情况选用树状数组和线段树。很多题目只能用线段树,如果两种都能用,建议先考虑用线段树。线段树的代码长很多,但是更容易理解、编码更清晰,做题时间更短。而树状数组的局限性很大,即使能用,也常常需要经过较难的思维转换,区间修改就是一个例子。

线段树二分操作例题

Vases and Flowers hdu 4614
题目描述:Alice有N个花瓶,编号0 ~ N-1,一个花瓶中只能插一朵花。Alice经常收到很多花并插到花瓶中,她也经常清理花瓶。
输入:第一行是整数T,表示测试样例数。对每个测试,第一行有两个整数N,M,1 1 A F 收到F朵花,从第A个花瓶开始插,如果花瓶中原来有花,就跳过去插下一个花瓶,如果插到最后的花瓶花还没插完就丢弃。
2 A B 清理从A到B的花瓶。
输出:对第1种操作,输出插入花瓶的第一个和最后一个位置,如果无法插入,输出“Can not put any one.”;对第2种操作,输出丢弃的花的数量。

**题解:**定义线段树,用线段树节点表示这一区间中花的数量和。
对于插入花这一操作需要三步
(1)在线段树上[0,n-1]二分查找到第一个0的位置pos1。
(2)在线段树上[pos1,n-1]二分查找到第F个0的位置pos2。
(3)将[pos1,pos2]所有的花瓶置1。
对于清理这一操作需要两步
(1)统计线段树[A,B]所有的1。
(2)将[A,B]所有的花瓶置0。
lazy-tag优化:如果一个节点所有的花瓶都已被填充,那么将tag置1标记。接下来在插入花时搜索到tag为1的节点就可以跳过。

#include
#include

using namespace std;

const int MAX = 5e4 + 10;

int tree[MAX], tag[MAX];

int ls(int x) {
      return x << 1; }
int rs(int x) {
      return (x << 1) + 1; }

int ans;
int pos(int n,int L, int p, int pl, int pr)
{
     
	int rnt = 0;
	if (n <= 0) return 0;
	if (tree[p] == 0)
	{
     
		int l = max(L, pl);
		if (n > pr - l + 1) {
      ans = pr; return pr - pl + 1; }
		ans = l + n - 1;
		return n;
	}
	if (tag[p]) return 0;
	if (pl == pr) return 0;
	int mid = (pl + pr) >> 1;
	if (L <= mid)rnt += pos(n, L, ls(p), pl, mid);
	pos(n - rnt, L, rs(p), mid + 1, pr);
	return rnt;
}

void push_up(int p)
{
     
	tree[p] = tree[ls(p)] + tree[rs(p)];
}

void update(int L, int R, int p, int pl, int pr)
{
     
	if (tag[p])return;
	if (pl == pr) {
      tree[p] = 1; return; }
	int mid = (pl + pr) >> 1;
	if (L <= mid)update(L, R, ls(p), pl, mid);
	if (R > mid)update(L, R, rs(p), mid + 1, pr);
	push_up(p);
	if (tree[p] >= pr - pl + 1)tag[p] = 1;
}

void put(int L,int num, int n)
{
     
	int pos1 = -1, pos2 = -1;
	ans = -1;
	pos(1, L, 1, 0, n - 1);
	pos1 = ans;
	if (pos1 == -1) {
      cout << "Can not put any one." << endl; return; }
	pos(num, pos1, 1, 0, n - 1);
	pos2 = ans;
	cout << pos1 << ' ' << pos2 << endl;
	update(pos1, pos2, 1, 0, n - 1);

}


int query(int L, int R, int p, int pl, int pr)
{
     
	int rnt = 0;
	if (tree[p] == 0)return 0;
	if (pl == pr) {
      tree[p] = 0; return 1; }
	int mid = (pl + pr) >> 1;
	if (L <= mid)rnt += query(L, R, ls(p), pl, mid);
	if (R > mid)rnt += query(L, R, rs(p), mid + 1, pr);
	push_up(p);
	tag[p] = 0;
	return rnt;
}

int main()
{
     
	memset(tree, 0, sizeof(tree));
	memset(tag, 0, sizeof(tag));
	int n, m;
	cin >> n >> m;
	int p, A, B;
	for (int i = 1; i <= m; i++)
	{
     
		cin >> p >> A >> B;
		if (p == 1) put(A, B, n);
		else cout << query(A, B, 1, 0, n - 1) << endl;
	}
	system("pause");
	return 0;
}

你可能感兴趣的:(算法笔记)