一:线段树
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(log2N)。
线段树的每个节点都表示一个区间[L, R],对于一个线段树的区间:
若L < R,则必能被分为[L, M]和[M+1, R],其中M = (L + R) / 2。
若L = R,则为叶子节点。
实现方法:
数组实现:节点T的左儿子是2T,代表[L, M]区间,右儿子是2T+1,代表[M+1,R]区间。
结构体指针实现:左右子树为*l,*r。
三个重点:
1.线段树的构建
int create_tree(int h,int x,int y)
{
tree[h].l=x;tree[h].r=y;//当前节点的区间赋值为[x,y];
if(x==y)//若当前节点为叶子节点,则更新该点权值,返回给父亲节点。
{
tree[h].s=a[x];
return tree[h].s;
}
int mid=(x+y)/2;//向下
int x1=create_tree(h*2,x,mid);//更新当前节点;
int x2=create_tree(h*2+1,mid+1,y);
tree[h].s=max(x1,x2);//更新权值
return tree[h].s;
}
2线段树的查询
int query(int 当前节点,int L,int R)
{
如果[L,R]与当前节点区间无交集,则返回;
若[L,R]包含当前节点区间,则返回所求值;
搜索左右子树;
返回值;
}
3.线段树的更新
void update(int 当前节点,int L,int R)
{
如果[L,R]与当前节点区间无交集,则返回;
若[L,R]包含当前节点区间,则返回所求值,停止递归;
搜索左右子树;
重新计算本节点信息;
返回;
}
下面有道例题:
例1 I hate it(hdu 1754)
题目描述:
很多学校流行一种比较的习惯。老师们很喜欢询问,从某某到某某当中,分数最高的是多少。
这让很多学生很反感。不管你喜不喜欢,现在需要你做的是,就是按照老师的要求,写一个程序,模拟老师的询问。当然,老师有时候需要更新某位同学的成绩。
本题目包含多组测试
在每个测试的第一行,有两个正整数 N 和 M ( 0~N<=200000,0~M<5000 ),分别代表学生的数目和操作的数目。学生ID编号分别从1编到N。第二行包含N个整数,代表这N个学生的初始成绩,其中第i个数代表ID为i的学生的成绩。接下来有M行。每一行有一个字符 C (只取’Q’或’U’) ,和两个正整数A,B。
当C为’Q’的时候,表示这是一条询问操作,它询问ID从A到B(包括A,B)的学生当中,成绩最高的是多少。
当C为’U’的时候,表示这是一条更新操作,要求把ID为A的学生的成绩更改为B。
对于每一次询问操作,在一行里面输出最高成绩。
输入
本题目包含多组测试,请处理到文件结束。
在每个测试的第一行,有两个正整数 N 和 M 分别代表学生的数目和操作的数目。
学生ID编号分别从1编到N。
第二行包含N个整数,代表这N个学生的初始成绩,其中第i个数代表ID为i的学生的成绩。
接下来有M行。每一行有一个字符 C (只取’Q’或’U’) ,和两个正整数A,B。
当C为’Q’的时候,表示这是一条询问操作,它询问ID从A到B(包括A,B)的学生当中,成绩最高的是多少。
当C为’U’的时候,表示这是一条更新操作,要求把ID为A的学生的成绩更改为B。
输出
对于每一次询问操作,在一行里面输出最高成绩。
样例输入
5 6
1 2 3 4 5
Q 1 5
U 3 6
Q 3 4
Q 4 5
U 2 9
Q 1 5
样例输出
5
6
5
9
分析
最容易想到的算法是将成绩存到数组里,然后对于每一条查询,遍历数组的每一个元素。总时间复杂度是O(NM),实在是太大了。根据题目,我们可以用线段树来存储[x,y]区间中成绩的最大值,这样做的时间复杂度只有O(MlogN)。
参考代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=200000+10;
struct node//定义线段树
{
int s;//权值
int l,r;//左右子树权值
};
struct node tree[maxn*10];
int a[maxn];
int create_tree(int h,int x,int y)//建树(h为树编号)
{
tree[h].l=x;tree[h].r=y;//记录区间[l,r]
if(x==y)//叶子结点
{
tree[h].s=a[x];//记录权值
return tree[h].s;//返回权值
}
int mid=(x+y)/2;//取中点(int自动取整)
int x1=create_tree(h*2,x,mid);//左子树权值
int x2=create_tree(h*2+1,mid+1,y);//右子树权值
tree[h].s=max(x1,x2);//取更大值
return tree[h].s;//返回权值
}
int query(int h,int x,int y)//查询
{
if(y<tree[h].l||x>tree[h].r)//...x2---y2...l——r...x1---y1...
return 0;
if(x<=tree[h].l&&tree[h].r<=y)//达到范围...x---l——r---y...
return tree[h].s;//返回权值
int x1=query(2*h,x,y);//左子树
int x2=query(2*h+1,x,y);//右子树
return max(x1,x2);//返回权值
}
int update(int h,int x)//维护线段树
{
if(x<tree[h].l || x>tree[h].r)//超过范围...x1...l——r...x2...
return tree[h].s;//返回权值
if(tree[h].l==tree[h].r)//左右子树相同
{
tree[h].s=a[tree[h].l];//改权值
return tree[h].s;//返回权值
}
int x1=update(2*h,x);//左子树
int x2=update(2*h+1,x);//右子树
tree[h].s=max(x1,x2);//改权值
return tree[h].s;//返回权值
}
int main()
{
int i,j,k,m,n;int x,y;char c;
scanf("%d%d",&n,&m);
for(i=1;i<=n;i++) scanf("%d",&a[i]);
create_tree(1,1,n);
for(i=1;i<=m;i++)
{
getchar();//过滤换行
scanf("%c%d%d",&c,&x,&y);//取得指令
if(c=='Q')
{printf("%d
",query(1,x,y));}
else
{a[x]=y;update(1,x);}
}
return 0;
}
二:Lazy-Tag
lazy-tag思想,记录每一个线段树节点的变化值,当这部分线段的一致性被破坏我们就将这个变化值传递给子区间,大大增加了线段树的效率。
在此通俗的解释我理解的Lazy意思:
现在需要对[a,b]区间值进行加c操作,那么就从根节点[1,n]开始调用update函数进行更新操作;如果刚好执行到一个rt节点,而且tree[rt].l == a && tree[rt].r == b,这时我们就应该一步更新此时rt节点的sum[rt]的值(sum[rt]+=c* (tree[rt].r - tree[rt].l + 1))。
关键来了,如果此时按照常规的线段树的update操作,这时候还应该更新rt子节点的sum[]值,而Lazy思想恰恰是暂时不更新rt子节点的sum[]值,而是在这里打一个tag,直接return。直到下次需要用到rt子节点的值的时候才去更新,这样避免许多可能无用的操作,从而节省时间 。
另外我们经常在树里面用到位运算,简单介绍一下:
(i<<n)==(i*2n) (i>>n)==(⌊i/2n⌋)
在找子树的时候,若父亲结点编号为i,则左右子结点分别表示为2i,2i+1,而树中就直接写为i<<1和i<<1|1(“|”详细自行百度),而寻找子节点可以表示为i>>1;
申请结构体的时候,要开到四倍长度空间,直接表示为i<<2;
这里再说明一下为什么要开四倍空间
假设我们用一个数组来头轻脚重地存储一个线段树,根节点是1,孩子节点分别是2n, 2n+1, 那么,设线段长为L(即[1..L+1))
设树的高度为H,对H,有:H(L)={1,1+H(⌈L2⌉)L>=1;
这是一个很简单的递归式,并用公式逐次代换,就等到
H(L)=k+H(⌈L2k⌉),其中 k 是满足2k≥L的最小值,所以H(L)=⌈lgL⌉+1.
所以显然所需空间为
2^H−1=2^(⌈lgL⌉+1)−1
=2×2^(⌈lgL⌉)−1
=2×2(L−1)−1
=4L−5,L≥2
来看一道题:
例2:一个简单的问题与整数 [POJ 3468]
题目描述
你有N个整数,A1,A2,…,AN。 你需要处理两种操作。 一种类型的操作是在给定间隔中向每个数字添加一些给定数目。 另一个是要求给定间隔内的数字之和。
输入
第一行包含两个数字N和Q (1≤N,Q≤100000)
第二行包含N个数字,即A1,A2,…,AN的初始值。(-1000000000≤Ai≤1000000000)。
接下来的Q行中的每一行表示操作。
“C a b c”意味着把Aa,Aa+1,…,Ab中的每一个都加上C(-10000≤c≤10000)。
“Q a b”表示查询Aa,Aa+1,…,Ab的和。
输出
按顺序回答所有的“Q”命令。 一行中有一个答案。
样例输入
10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4
样例输出
4
55
9
15
*提示:可能超出int范围
参考代码
#include<cstdio>
using namespace std;
#define maxn 100000+10
typedef long long LL;
struct node{
int l,r,m;//左右中点
LL sum,mark;//权值、tag
}T[maxn<<2];
int a[maxn];
void build(int id,int l,int r){
T[id].l=l;//左端点
T[id].r=r;//右端点
T[id].m=(l+r)>>1;//中点
T[id].mark=0;//初始化标记
if(l==r)//达到端点
{T[id].sum=a[l];return;}//标记和,停止递归并返回
build(id<<1,l,T[id].m);//递归左子树
build(id<<1|1,T[id].m+1,r);//递归右子树
T[id].sum=(T[id<<1].sum+T[id<<1|1].sum);//记录和
}
void update(int id,int l,int r,int val){
if(T[id].l==l&&T[id].r==r)//确定是这一段了
{T[id].mark+=val;return;}//不必递归到叶子结点,打tag
T[id].sum+=(LL)val*(r-l+1);//更新权值
if(T[id].m>=r)//只要更新左子树
update(id<<1,l,r,val);
else if(T[id].m<l)
update((id<<1)+1,l,r,val);//只要更新右子树
else
{
update(id<<1,l,T[id].m,val);//更新左右子树
update(id<<1|1,T[id].m+1,r,val);
}
}
LL query(int id,int l,int r){
if(T[id].l==l&&T[id].r==r)//找到结点
return T[id].sum+T[id].mark*(LL)(r-l+1);//权值+tag
if(T[id].mark)//原来更新到这里的时候没有继续更新下去了(有tag)
{
T[id<<1].mark+=T[id].mark;//tag下传
T[id<<1|1].mark+=T[id].mark;
T[id].sum+=(LL)(T[id].r-T[id].l+1)*T[id].mark;//把tag加回sum
T[id].mark=0;//去掉tag
}
if(T[id].m>=r){
return query(id<<1,l,r);//只有左子树
}
else if(T[id].m<l){
return query(id<<1|1,l,r);//只有左子树
}
else{
return query(id<<1,l,T[id].m)+query((id<<1)+1,T[id].m+1,r);//左右子树都有
}
}
int main(){
int n,Q;
char str[8];
int b,c,d;
scanf("%d%d",&n,&Q);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
build(1,1,n);//建树
for(int i=0;i<Q;i++)
{
scanf("%s",str);
if(str[0]=='Q')
{
scanf("%d%d",&b,&c);
printf("%lld
",query(1,b,c));//查询
}
else
{
scanf("%d%d%d",&b,&c,&d);
update(1,b,c,d);//更新
}
}
return 0;
}
更大的挑战:
例3 Count color[POJ 2777]
题目描述
有一个非常长的板,长度L厘米,L是一个正整数,所以我们可以均匀地划分为L段,他们从左到右标记为1,2,… L,每个是1厘米长。现在我们必须着色板 - 一段只有一种颜色。我们可以在板上进行以下两个操作:
1.“C A B C”使板材从板材A到板材C着色C.
2.“P A B”输出在段A和段B(包括)之间绘制的不同颜色的数量。
在我们的日常生活中,我们有很少的词来描述一种颜色(红色,绿色,蓝色,黄色…),所以你可以假设不同颜色T的总数是非常小的。为了简单起见,我们将颜色的名称表示为颜色1,颜色2,…颜色T.在开始时,板子以颜色1绘制。现在剩下的问题留给你。
输入
第一行输入包含L(1≤L≤100000),T(1≤T≤30)和O(1≤O≤100000)。这里O表示操作的数量。在O行之后,每个包含“C A B C”或“P A B”(这里A,B,C是整数,A可以大于B)作为先前定义的操作。
输出
输出结果按顺序输出操作,每行包含一个数字。
样例输入
2 2 4
C 1 1 2
P 1 2
C 2 2 2
P 1 2
样例输出
2
1
分析
根据题目的数据规模,暴力求解显然超时。所以就考虑用线段树做。
说明
本题运用了线段树中“区间修改”的思想,只修改目标区间而不再继续修改其子节点(lazy)
参考代码:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=100010;
#define L(rt) (rt<<1)
#define R(rt) (rt<<1|1)
struct Tree{
int l,r;
int col; //用一个32位的int型,每一位对应一种颜色,用位运算代替bool col[32]
bool cover; //表示这个区间都被涂上同一种颜色提高效率
}tree[N<<2];
void build(int L,int R,int rt){
tree[rt].l=L;//左区间
tree[rt].r=R;//右区间
tree[rt].col=1; //开始时都为涂有颜色1
tree[rt].cover=1;//当然只有一种颜色
if(tree[rt].l==tree[rt].r)
return ;//叶节点直接返回
int mid=(L+R)>>1;//取中点
build(L,mid,L(rt));//建左子树
build(mid+1,R,R(rt));//建右子数
}
void PushDown(int rt){//下推
tree[L(rt)].col=tree[rt].col;
tree[L(rt)].cover=1;
tree[R(rt)].col=tree[rt].col;
tree[R(rt)].cover=1;
tree[rt].cover=0;//标记取消
}
void PushUp(int rt){//最后递归回来再更改父节点的颜色
tree[rt].col=tree[L(rt)].col | tree[R(rt)].col;//相加
}
void update(int val,int L,int R,int rt){
if(L<=tree[rt].l && R>=tree[rt].r){//区间在要求范围内
tree[rt].col=val;//刷颜色
tree[rt].cover=1;//打标记
return;//不需要更新子树了
}
if(tree[rt].col==val)//剪枝
return;//不需要更新子树了
if(tree[rt].cover)//这里面只有一种颜色
PushDown(rt);//下推
int mid=(tree[rt].l+tree[rt].r)>>1;
if(R<=mid)
update(val,L,R,L(rt));
else if(L>=mid+1)
update(val,L,R,R(rt));
else{
update(val,L,mid,L(rt));
update(val,mid+1,R,R(rt));
}
PushUp(rt); //上载
}
int sum;
void query(int L,int R,int rt)
{
if(L<=tree[rt].l && R>=tree[rt].r){
sum |= tree[rt].col;//把颜色加进和里
return;
}
if(tree[rt].cover){//这个区间全部为1种颜色,就没有继续分割区间的必要了
sum |= tree[rt].col;//颜色种类相加的位运算代码
return;
}
int mid=(tree[rt].l+tree[rt].r)>>1;
if(R<=mid)
query(L,R,L(rt));
else if(L>=mid+1)
query(L,R,R(rt));
else
{
query(L,mid,L(rt));
query(mid+1,R,R(rt));
}
}
int solve()//位运算
{
int ans=0;
while(sum)
{
if(sum&1)
ans++;
sum>>=1;
}
return ans;
}
void swap(int &a,int &b)
{
int tmp=a;a=b;b=tmp;
}
int main()
{
int n,t,m;
scanf("%d%d%d",&n,&t,&m);
build(1,n,1);//建树
char op[3];
int a,b,c;
while(m--)
{
scanf("%s",op);
if(op[0]=='C')
{
scanf("%d%d%d",&a,&b,&c);
if(a>b)
swap(a,b);
update(1<<(c-1),a,b,1); // int型的右起第c位变为1,即2的c-1次方。
}
else
{
scanf("%d%d",&a,&b);
if(a>b)
swap(a,b);
sum=0;
query(a,b,1);
printf("%d
",solve());
}
}
return 0;
}