POJ 2828: 线段树

8 篇文章 0 订阅
2 篇文章 0 订阅


——线段树
原题传送门

Description

Railway tickets were difficult to buy around the Lunar New Year in China, so we must get up early and join a long queue…

The Lunar New Year was approaching, but unluckily the Little Cat still had schedules going here and there.
Now, he had to travel by train to Mianyang, Sichuan Province for the winter camp selection of the national team of Olympiad in Informatics(NOI).

It was one o’clock a.m. and dark outside.
Chill wind from the northwest did not scare off the people in the queue.
The cold night gave the Little Cat a shiver. Why not find a problem to think about?
That was none the less better than freezing to death!

People kept jumping the queue.
Since it was too dark around, such moves would not be discovered even by the people adjacent to the queue-jumpers.
“If every person in the queue is assigned an integral value and all the information about those who have jumped the queue and where they stand after queue-jumping is given, can I find out the final order of people in the queue?” Thought the Little Cat.

Data

Input
There will be several test cases in the input. Each test case consists of N + 1 lines where N is given in the first line of the test case. The next N lines contain the pairs of values Posi and Vali in the increasing order of i (1 ≤ i ≤ N). For each i, the ranges and meanings of Posi and Vali are as follows:

(Posi) P o s i ∈ [ 0 , i − 1 ] Posi ∈ [0, i − 1] Posi[0,i1] — The i i i-th person came to the queue and stood right behind the Posith person in the queue. The booking office was considered the 0th person and the person at the front of the queue was considered the first person in the queue.
(Vali) V a l i ∈ [ 0 , 32767 ] Vali ∈ [0, 32767] Vali[0,32767] — The i i i-th person was assigned the value Vali.
There no blank lines between test cases. Proceed to the end of input.

Output
For each test cases, output a single line of space-separated integers which are the values of people in the order they stand in the queue.

	Sample Input
	4
	0 77
	1 51
	1 33
	2 69
	4
	0 20523
	1 19243
	1 3890
	0 31492
	Sample Output
	77 33 69 51
	31492 20523 3890 19243

1 ≤ N ≤ 200,000

思路

这道题,在学习线段树之前,有两种不同的BF算法.

  1. 顺序存储,查询 O ( 1 ) O(1) O(1),插入最坏是 O ( n ) O(n) O(n).
  2. 链式存储,查询 O ( n ) O(n) O(n),插入最坏是 O ( n ) O(n) O(n)

总的复杂度能达到 O ( n 2 ) O(n^{2}) O(n2),这是不能接受的.
所以,我们需要更好的数据结构.

线段树

线段树是一种二叉树结构,不过其节点都是由区间和数据域组成:
一张无辜的图片
对于每一个区间,都会被剖分成两个部分,两个部分的区间大小差值不超过1.
也就是说,这种划分和二分算法十分相似.
实际上,线段树结构单点修改和查询都是 O ( n l o g n ) O(nlogn) O(nlogn) 的.

线段树框架

对于普通线段树结点,通常会有区间域 [ l . . r ] [l..r] [l..r],用 l l l r r r表示.
同时,作为一棵树,还会有维护父亲孩子之间的信息.
但是,可以发现,线段树的结构近似于一棵完全二叉树,且深度在 l o g ( n ) log(n) log(n)左右(可证明,可以自行思考 ).
所以,对于结点 i i i,可以 2 i 2i 2i表示左孩子, 2 i + 1 2i+1 2i+1表示右孩子.
而线段树的最后一层深度可能会超过 O ( l o g n ) O(logn) O(logn),所以需要准备 4 4 4倍的空间.
初始化时,将原数组复制到线段树中.

  • 如果是叶节点,将 S u m Sum Sum赋值为 A [ ] A[] A[]当中的值.
  • 否则, S u m Sum Sum值是左孩子和右孩子 S u m Sum Sum的和.

代码如下:

void build(ll L,ll R,ll p)
{
	tree[p].L=L,tree[p].R=R;
	if (L==R)
	{
		tree[p].add=0;
		tree[p].sum=A[L];
		return;
	} 
	ll mid=(L+R)>>1;
	build(L,mid,p<<1);
	build(mid+1,R,p<<1|1);
	up(p);
}

1

线段树的更新

这里以A Simple Problem with Integers为例.
这是一道区间查询,区间修改的题目 //树状数组会很难受
这里,我们在每个结点上维护一个总和Sum.
有递归定义:

  • 对于叶节点 S u m Sum Sum,其值为原数组 A [ L ] A[L] A[L].
  • 对于其他结点 S u m Sum Sum,其值为左孩子 S u m Sum Sum+右孩子 S u m Sum Sum.

所以,一般可以有如下更新代码:

void update(ll L,ll R,ll x,ll p)
{
	if (tree[p].L==L&&tree[p].R==R)// if leaf
	{
		tree[p].add+=x;
		tree[p].sum+=x*(R-L+1);
		return;
	}
	//others
	ll mid=(tree[p].L+tree[p].R)>>1;
	if (R<=mid) update(L,R,x,p<<1);
	else if (L>mid) update(L,R,x,p<<1|1);
	else
	{
		update(L,mid,x,p<<1);
		update(mid+1,R,x,p<<1|1);
	}
	tree[p].sum=tree[p<<1].sum+tree[p<<1|1].sum;
}//TLE :-)

线段树支持单点更新,单点更新的时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)
对于区间修改,可以拆分成若干个单点进行修改.
但是,如果这样进行修改,可能会遍历到不同的子树,导致最终的复杂度会增长至 O ( n ) O(n) O(n).
这和直接修改好像没什么区别,并且常数好像还大一些. ?
所以,单纯地修改是不现实的.

懒标记

考虑最简的遍历方法,我们至少需要按照如下遍历,才能保证更新地准确:
//我们暂且称这样的结点为"必需节点 "

那么,如何修改已修改结点的孩子呢?
答案是,我们不修改!
在修改必需节点之后,我们判断是否还要更新必需结点:

  • 是,则继续按照原顺序更新
  • 否,则不必更新,改为打上标记.

这个标记代表本节点已更新,但是其孩子未更新.
这个标记就叫懒标记.

当我们询问或再次更新时,为了保证必需结点的准确性,还是要下放懒标记.
但是我们下放的孩子结点很快就会被遍历到,所以不会影响到时间复杂度.
所以,我们已经把时间复杂度优秀优化到 O ( n l o g n ) O(nlogn) O(nlogn)2 ?

在例题中可以实现如下的下放懒标记代码:

void down(ll p)
{
	ll &t=tree[p].add;
	if (t==0) return;
	tree[p<<1].add+=t;
	tree[p<<1].sum+=tree[p<<1].len()*t;
	tree[p<<1|1].add+=t;
	tree[p<<1|1].sum+=tree[p<<1|1].len()*t; 
	t=0;
}

实现更新代码:

void update(ll L,ll R,ll x,ll p)
{
	if (tree[p].L==L&&tree[p].R==R)
	{
		tree[p].add+=x;
		tree[p].sum+=x*(R-L+1);
		return;
	}
	down(p);
	ll mid=(tree[p].L+tree[p].R)>>1;
	if (R<=mid) update(L,R,x,p<<1);
	else if (L>mid) update(L,R,x,p<<1|1);
	else
	{
		update(L,mid,x,p<<1);
		update(mid+1,R,x,p<<1|1);
	}
	up(p); 
}

线段树的询问

区间询问不需要更新懒标记,但需要下放懒标记.
很简单,为了保证非必需节点的准确性.
那么,有递归性查询:

  • 如果询问区间正好是当前结点的区间域 ,返回 S u m Sum Sum.
  • 如果询问区间被左孩子区间域完全覆盖,递归到左子树.
  • 如果询问区间被右孩子区间域完全覆盖,递归到右子树.
  • 否则,将询问区间拆成两半,分别进入两个孩子询问.

//询问之前需要下放懒标记
代码如下:

ll query(ll L,ll R,ll p)
{
	if (L==tree[p].L&&R==tree[p].R)
	{
		return tree[p].sum;
	}
	down(p);
	ll mid=(tree[p].L+tree[p].R)>>1;
	if (R<=mid) return query(L,R,p<<1);
	else if (L>mid) return query(L,R,p<<1|1);
	else
	{
		return query(L,mid,p<<1)+query(mid+1,R,p<<1|1); 
	}
}

扩展

对于这道题,线段树只是一个数据结构本来也只是数据结构,对算法不造成任何影响.
在这里主要把算法讲一下,配套的线段树基本不变.
我们逆序处理每个人,对于每个人:

在第 N N N个位置插入,可以转化为在 N N N个空位后插入.
这其实不难理解.
POJ 盗图
如果逆序的话,一开始只有一个数 69 69 69.
如果要插入 51 51 51,那么原本是在 2 2 2个空位后插入的,是第 3 3 3位.
69 69 69插入以后,前面空位不变,但 51 51 51往后跑了 1 1 1格.
所以,我们将线段树的 S u m Sum Sum变成结点中空位的个数.
然后,改变线段树查询更新的规则即可:
//这里更新查询一块写

  • 如果左子树的空位 &gt; &gt; >询问的空位数,递归左子树.
  • 否则递归右子树.
  • 因为单点更新,不需要懒标记. ?

下面是Van美的代码.

Code

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define lc root<<1
#define rc root<<1|1
using namespace std;
const int N=2e5+5;
struct node
{
	int l,r;
	int space,val;
}tree[4*N];
struct per
{
	int val,pos;
}a[N];
int n;
int ans[N];
void up(int root)
{
	tree[root].space=tree[lc].space+tree[rc].space;
}
void build(int root,int L,int R)
{
	tree[root].l=L,tree[root].r=R;
	if (L==R)
	{
		tree[root].space=1;
		tree[root].val=0; 
		return;
	}
	int mid=(L+R)>>1;
	build (lc,L,mid);
	build (rc,mid+1,R);
	up(root);
}
void update(int root,int x,int d)
{
	int L=tree[root].l,R=tree[root].r;
	if (L==R) 
	{
		tree[root].space=0;
		tree[root].val=d;
		return;
	}
	int mid=(L+R)>>1;
	if (tree[lc].space>x) update(lc,x,d);
	else update(rc,x-tree[lc].space,d);
	up(root);
}
void query(int root)
{
	int L=tree[root].l,R=tree[root].r;
	if (L==R)
	{
		ans[L]=tree[root].val;
		return;
	}
	query(lc);
	query(rc);
}
int main()
{
	while (~scanf("%d",&n))
	{
		build(1,1,n);
		for (int i=1;i<=n;i++) scanf("%d%d",&a[i].pos,&a[i].val);
		for (int i=n;i>=1;i--)
			update(1,a[i].pos,a[i].val);
		query(1);
		for (int i=1;i<=n;i++) printf("%d ",ans[i]);
		puts("");
	}
	return 0;
}

感谢奆老关注 qwq ?


  1. ll 表示 long long 长整型,下文同理. ↩︎

  2. 具体证明自行思考,提示:下放懒标记只有四种方式. ↩︎

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值