【数据结构】线段树专辑

线段树是一种高效的数据结构。它可以将一段区间组织为二叉树的结构,比如比如将[1,10]划分为两个子树[1,5],[6,10],再分别划分为[1,3],[4,5],[6,8],[9,10]……直至为左右两端相同的叶子节点。线段树具有二叉树的性质,它的操作都是nlgn的时间复杂度。下面我们通过代码来看一下线段树的应用。


单点更新

//线段树
//hdu1166 敌兵布阵  单点增删,区间求和 
#define lson l,m,rt<<1
#define rson m,r,rt<<1|1
const int maxn=55555;
int sum[maxn<<2];
void PushUp(int rt)
{
	sum[rt]=sum[rt<<1|1+sum[rt<<1|1];
} 
void build(int l,int r,int rt)
{
	if(l==r)
	{
		scanf("%d",&sum[rt]);//创建线段树,当l==r时为叶子节点 
		return;
	}
	int m=(l+r)<<1;
	build(lson);//递归创建左子树 
	build(rson);//递归创建右子树 
	PushUp(rt);//左右子树创建完之后更新他们的父节点 
} 
void update(int p,int add,int l,int r,int rt)//单点更新
{
	if(l==r)//当lS ==r时找到该点  
	{
		sum[rt]+=add;
		return;
	}
	int m=(l+r)<<1;
	if(p<=m) update(p,add,lson);//如果p小于中点m,则往左子树更新 
	else update(p,add,rson);//否则往右子树 
	PushUp(rt);//更新父节点 
}
int query(int L,int R,int l,int r,int rt)
{
	if(L<=l&&r<=R)
	{
		return sum[rt];
	}
	int m=(l+r)<<1;
	int ret=0;
	if(L<=m)ret+=query(L,R,lson);
	if(R>m) ret+=qeery(L,R,rson);
	return ret;
}

//hdu 1754,单点替换,区间最值 
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
const int maxn=222222;
int MAX[maxn<<2];
void PushUp(int rt)
{
	MAX[rt]=max(MAX[rt<<1],MAX[rt<<1|1]);
}
void build(int l,int r,int rt)
{
	if(l==r)
	{
		scanf("%d",&MAX[rt]);
		return ;
	}
	int m=(l+r)<<1;
	build(lson);
	build(rson);
	PushUp(rt);
}
void update(int p,int sc,int l,int r,int rt)
{
	if(l==r)
	{
		MAX[rt]=sc;
		return ;
	}
	int m=(l+r)<<1;
	if(p<=m)update(p,sc,lson);
	else update(p,sc,rson);
	PushUp(rt);
}
int query(int L,int R,int l,int r,int rt)
{
	if(L<=l&&r<=R)//当 [l,r]落在[l,R]区间时,直接返回总值 
	{
		return MAX[rt]; 
	}
	int m=(l+r)<<1;
	int ret=0;
	if(L<=m) ret=max(ret,query(L,R,lson));//如果L小于中点m,则继续查找左子树 
	if(R>m) ret=max(ret,query(L,R,rson));//如果R大于中点,则继续查找右子树 
	return ret; 
}

//hdu1394  求逆序数,就是给出一串数,当依次在将第一个数变为最后一个数的过程中,要你求它的最小逆序数。
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
const int maxn =5555;
int sum[maxn<<2];
void PushUp(int rt)
{
	sum[rt]=sum[rt<<1]+sum[rt<<1|1];
}
void build(int l,int r,int rt)
{
	sum[rt]=0;
	if(l==r) return ;
	int m=(l+r)>>1;
	build(lson);
	build(rson);
} 
void update(int p,int l,int r,int rt)
{
	if(l==r)
	{
		sum[rt]++;
		return ;
	}
	int m=(l+r)>>1;
	if(p<=m)update(p,lson);
	else update(p,rson);
	PushUp(rt);
}
int query(int L,int R,int l,int r,int rt)
{
	if(L<=l&&r<=R)
	{
		return sum[rt];
	}
	int m=(l+r)>>1;
	int ret=0;
	if(L<=m) ret+=query(L,R,lson);
	if(R>m) ret+=query(L,R,rson);
	return ret;
}
int x[maxn];
int main()
{
	int n;
	while(~scanf("%d",&n))
	{
		build(0,n-1,1);
		int sum=0;
		for(int i=0;i<n;i++)
		{
			scanf("%d",&x[i]);
			sum+=query(x[i],n-1,0,n-1,1);//边插入边统计逆序数,逆序数为比当前值x[i]先插入,又落在[x[i],n-1]的数的数量. 
			update(x[i],0,n-1,1); //将x[i]更新到线段树上. 
		}
		int ret=sum;
		for(int i=0;i<n;i++)
		{
			sum+=n-x[i]-x[i]-1;//如果是0到n的排列,那么如果把第一个数放到最后,对于这个数列,逆序数是减少a[i],而增加n-1-a[i]的。
			ret=min(ret,sum);
		}
		printf("%d\n",ret);
	}
	return 0;
}

成段更新

Lazy思想成段更新的时候,有时候会遇到这样一种情况,比如我对1到10这个区间全部加上10,当找到[1,10]这个节点时,正常来说它所有的子节点全部都要加上10。假如这时候我又来一个操作:对1到10这个区间全部减去10,那么我们又要对[1,10]这个区间的所有节点减去10。如果有大量这样的操作,那么算法效率将大大降低。Lazy思想就在这里体现作用了。对[1,10]整体加10,当我们找到[1,10]这个节点时,我们在它身上设置一个标记,表示它整个区间加10,但并不更新它的子树。这时[1,10]减10的操作来的时候,我们只要是把标记抵消了,没有进行多余的操作。但是采用了lazy思想,当我们想要更新左右子树之前,就必须先查看当前的节点是否存在 这样的标记,把它往下一层传,这样才能保证不出错的情况下,效率也得到了提高。


 HDU1698

给你一些牌子(铜,银,金)分别用1,2,3表示,一开始这些都是铜牌。现在对这些区间区间进行操作,比如说将1到5的牌子涂成银牌等等。最后统计这些牌子的总价值。

#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
const int maxn=11111;
int col[maxn<<2];
int sum[maxn<<2];
void PushUp(int rt)
{	
	sum[rt]=sum[rt<<1]+sum[rt<<1|1];
}
void PushDown(int rt,int m)//基于Lazy思想的向下更新操作 
{
	if(col[rt])//如果当前节点被标记 
	{
		col[rt<<1]=col[rt<<1|1]=col[rt];//更新左右子树的标记 
		sum[rt<<1]=(m-(m>>1))*col[rt];//更新左右子树的值 
		sum[rt<<1|1]=m>>1*col[rt];
		col[rt]=0;//完成后将当前结点的标记清空。 
	}
}
void build(int l,int r,int rt)
{
	col[rt]=0;
	sum[rt]=1;
	if(l==r)return ;
	int m=(l+r)>>1;
	build(lson);
	build(rson);
	PushUp(rt);
}
void update(int L,int R,int c,int l,int r,int rt)
{
	if(L<=l&&r<=R)//当[l,r]落到[l,R]区域时,标记该区域,更新该区域的值,但不对子节点进行操作。 
	{
		col[rt]=c;
		sum[rt]=c*(r-l+1);
		return ;
	}
	PushDown(rt,r-l+1);//每次更新当前结点前,都要查看当前结点有无标记 
	int m=(l+r)>>1;
	if(L<=m)update(L,R,c,lson);
	if(R>m)update(L,R,c,rson);
	PushUp(rt);//更新完左右子树之后,更新父节点的标记 
}


//poj3468题意:给你N个数,Q个操作,操作有两种,‘Q a b ’是询问a~b这段数的和,‘C a b c’是把a~b这段数都加上c。update成段增减 query 区间求和 
#define lson l,m,rt<<1
#define rson m+1,r.rt<<1|1
#define LL long long
const int maxn=11111;
LL add[maxn<<2];
LL sum[maxn<<2];
void PushUp(int rt)
{
	sum[rt]=sum[rt<<1]+sum[rt<<1|1];
}
void PushDown(int rt,int m)
{
	if(add[rt])
	{
		add[rt<<1]+=add[rt];
		add[rt<<1|1]+=add[rt];
		sum[rt<<1]+=add[rt]*(m-(m>>1));
		sum[rt<<1|1]+=add[rt]*(m>>1);
		add[rt]=0;
	}
}
void build(int l,int r,int rt)
{
	add[rt]=0;
	if(l==r)
	{
		scanf("%lld",&sum[rt]);
		return ;
	}
	int m=(l+r)>>1;
	build(lson);
	build(rson);
	PushUp(rt);
}
void update(int L,int R,int c,int l,int r,int rt)
{
	if(L<=l&&r<=R)
	{
		add[rt]+=c;
		sum[rt]+=(LL)c*(r-l+1);
		return ;
	}
	PushDown(rt,r-l+1);
	int m=(l+r)>>1;
	if(L<=m) update(L,R,c,lson);
	if(R>m) update(L,R,rson);
	PushUp(rt); 
}
LL query(int L,int R,int l,int r)
{
	if(L<=l&&r<=R)
	{
		return sum[rt];
	}
	PushDown(rt,r-l+1);
	int m=(l+r)>>1;
	LL ret=0;
	if(L<=m)ret+=query(L,R,lson);
	if(R>m)ret+=query(L,R,rson);
	return ret;
}


离散化:使用离散化可以对线段树的长度进行压缩,将一个很长的区间映射到一个较小的区间之中。比如我们有3组线段,[1,1000],[2,2000],[3,3000]。正常情况下我们需要将[1,3000]组织为线段树。但是通过离散化,我们令x[1]=1,x[2]=2,x[3]=1000,x[4]=1000,x[5]=2000,x[6]=3000(已排序),更新[1,1000]也就是将区间[1,4]更新,以此类推,只需要将[1,6]组织为线段树,大大节省了空间。

我们看一下poj2528这道题。

题意:n(n<=10000)个人依次贴海报,给出每张海报所贴的范围li,ri(1<=li<=ri<=10000000)。求出最后还能看见多少张海报。

这道题并不能简单地使用离散化,下面借用一下别人的例子。

如三张海报为:1~10 1~4 6~10

离散化时 X[ 1 ] =1, X[ 2 ] = 4, X[ 3 ] = 6, X[ 4 ] = 10
第一张海报时:墙的1~4被染为1
第二张海报时:墙的1~2被染为23~4仍为1
第三张海报时:墙的3~4被染为31~2仍为2
最终,第一张海报就显示被完全覆盖了,于是输出2,但实际上明显不是这样,正确输出为3

新的离散方法为:在相差大于1的数间加一个数,例如在上面1 4 6 10中间加5算法中实际上14之间,610之间都新增了数的)

X[ 1 ] = 1, X[ 2 ] = 4, X[ 3 ] = 5, X[ 4 ] = 6 X[ 5 ] =10

这样之后,第一次是1~5被染成1;第二次1~2被染成2;第三次4~5被染成3

最终,1~22314~53,于是输出正确结果3


//POJ2528 贴报纸 离散化
#define lson l,m,rt<<1
#define rson m+1,r.rt<<1|1
const int maxn=11111;
bool hash[maxn];
int li[maxn],ri[maxn];
int X[maxn*3];
int col[maxn<<4];
int cnt;
void PushDown(int rt)
{
	if(col[rt]!=-1)
	{
		col[rt<<1]=col[rt<<1|1]=col[rt];
		cik[rt]=-1;
	}
} 
void update(int L,int R,int l,int r,int rt)
{
	if(L<=l&&r<=R)
	{
		col[rt]=c;
		return ;
	}
	PushDown(rt);
	int m=(l+r)>>1;
	if(L<=m) update(L,R,c,lson);
	if(m<R) update(L,R,c,rson);
}
void query(int l,int r,int rt)
{
	if(col[rt]!=-1)
	{
		if(!hash[col[rt]]) cnt++;
		hash[col[rt]]=true;
		return ;
	}
	if(l==r) return ;
	int m=(l+r)>>1;
	query(lson);
	query(rson);
}
int Bin(int key,int n,int X[])//二分查找 
{
	int l=0;,r=n-1;
	while(l<=r)
	{
		int m=(l+r)>>1;
		if(X[m]==key) return m;
		if(X[m]<key) l=m+1;
		else r=m-1;
	}
	return -1;
}
int main()
{
	int T,n;
	scanf("%d,",&T);
	while(T--)
	{
		scanf("%d",&n);
		int nn=0;
		for(int i=0;i<n;i++)//将区间离散化,将一个很大的区间映射到一个较小的区间之中 
		{
			scanf("%d%d",&li[i],&ri[i]);
			X[nn++]=li[i];
			X[nn++]=ri[i];
		}
		sort(X,X+nn);//从小到大排序 
		int m=1;
		for(int i=1;i<nn;i++)//去除区间重复值 
		{
			if(X[i]!=X[i-1])X[m++]=[i];
		}
		for(int i=m-1;i>0;i--)//在相差大于1的数间加一个数
		{
			if(X[i]!=X[i-1]+1)X[m++]=X[i-1]+1;
		}
		sort(X,X+m);//再次排序 
		memset(col,-1,sizeof(col));
		for(int i=0;i<n;i++)
		{
			int l=Bin(li[i],m,X);//用二分查找方法找到报纸的左右端点所在下标 
			int r=Bin(ri[i],m,X);
			update(l,r,i,0,m,1);//更新线段树 
		}
		cnt=0;
		memset(hash,false,sizeof(hash));
		query(0,m,1);
		printf("%d\n",cnt);
	}
	return 0;
}

区间合并

特点:update区间替换 query询问满足条件的最左断点 ,需要在PushUp的时候对左右儿子的区间进行合并 

poj3667  题意 1 a:询问是不是有连续长度为a的空房间,有的话住进最左边 2 a b:将[a,a+b-1]的房间清空 

 #define lson l,m,rt<<1
 #define rson m+1,rt<<1|1
 const int maxn=55555;

 int lsum[maxn<<2],rsum[maxn<<2],msum[maxn<<2];//lsum区间左边数连续空房间的数目,rsum从区间右边数连续空房间的数目,msum该区间中连续空房间的总数目 
 int cover[maxn<<2];
 void PushDown(int rt,int m)
 {
 	if(cover[rt]!=-1)
 	{
 		cover[rt<<1]=cover[rt<<1|1]=cover[rt];
 		msum[rt<<1]=lsum[rt<<1]=rsum[rt<<1]=cover[rt]?0:m-(m>>1);
 		msum[rt<<1|1]=lsum[rt<<1|1]=rsum[rt<<1|1]=cover[rt]?0:(m>>1);
 		cover[rt]=-1;
	 }
 }
 void PushUp(int rt,int m)
 {
 	lsum[rt]=lsum[rt<<1];//当前节点的lsum为左子树的lsum 
 	rsum[rt]=rsum[rt<<1|1];//当前结点的rsum为右子树的rsum 
 	if(lsum[rt]==m-(m>>1))lsum[rt]+=lsum[rt<<1|1];//如果当前结点的lsum等于左子树的区间长度,则再加上右子树的 lsum 
 	if(rsum[rt]==(m>>1))rsum[rt]+=rsum[rt>>1];//如果当前结点的rsum 等于右子树的区间长度,则再加上左子树的rsum 
 	msum[rt]=max(lsum[rt<<1|1]+rsum[rt<<1],max(msum[rt<<1],msum[rt<<1|1]));//当前结点的连续空房间总数目为三者的最大值 
 }
void build(int l,int r,int rt)
{
	msum[rt]=lsum[rt]=rsum[rt]=r-l+1;
	cover[rt]=-1;
	if(l==r) return ;
	int m=(l+r)>>1;
	build(lson);
	build(rson);
}
void update(int L,int R,int c,int l,int r,int rt)
{
	if(L<=l&&r<=R)
	{
		msum[rt]=lsum[rt]=rsum[rt]=c?0:r-l+1;
		cover[rt]=c;
		return ;
	}
	PushDown(rt,r-l+1);
	int m=(l+r)>>1;
	if(L<=m) update(L,R,c,lson);
	if(m<R) update(L,R,c,rson);
	PushUp(rt,r-l+1);
}
int query(int w,int l,int r,int rt)
{
	if(l==r)	return l;
	PushDown(rt,r-l+1);
	int m=(l+r)>>1;
	if(msum[rt<<1]>=w)return query(w,lson);
	else if(rsum[rt<<1]+lsum[rt<<1|1]>=w) return m-rsum[rt<<1]+1;
	return query(w,rson);
}
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	build(1,n,1);
	while(m--)
	{
		int op,a,b;
		scanf("%d",&op);
		if(op==1)
		{
			scanf("%d",&a);
			if(msum[1]<a) puts("0");
			else 
			{
				int p=query(a,1,n,1);
				printf("%d\n",p);
				update(p,p+a-1,1,1,n,1);
			}
		}
		else 
		{
			scanf("%d%d",&a,&b);
			update(a,a+b-1,0,1,n,1);
		}
	}
	return 0;
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值