HDU 3911 Black and White 线段树(区间合并) 模板题

Problem Description

There are a bunch of stones on the beach; Stone color is white or black. Little Sheep has a magic brush, she can change the color of a continuous stone, black to white, white to black. Little Sheep like black very much, so she want to know the longest period of consecutive black stones in a range [i, j].

Input

  There are multiple cases, the first line of each case is an integer n(1<= n <= 10^5), followed by n integer 1 or 0(1 indicates black stone and 0 indicates white stone), then is an integer M(1<=M<=10^5) followed by M operations formatted as x i j(x = 0 or 1) , x=1 means change the color of stones in range[i,j], and x=0 means ask the longest period of consecutive black stones in range[i,j]

Output

When x=0 output a number means the longest length of black stones in range [i,j].

Sample Input

4 1 0 1 0 5 0 1 4 1 2 3 0 1 4 1 3 3 0 4 4

Sample Output

1 2 0


题意:

输入n 后面n个数,代表着1-n区间为哪些数。

接着输入m表示有m个操作。

输入0就是查询区间内的1的和;输入1,就把区间内的值0变成1,1变成0(区间内连续0的长度和连续1的长度,数值交换)


参考博客和代码:

线段树区间合并详解 介绍为什么这种问题选择线段树和一些套路

参考代码1

参考代码2

因为自己之前整理的的线段树模板单点,区间 修改和查询,并没有将线段树的节点封装成结构体,所以这次阅读的代码,也是使用了很多很多数组的。


第一次碰到区间合并的问题,需要详细记录一下。

  • 数组定义

针对这一题,询问在指定的区间内,长度为1的连续序列的长度是多少?

数组tree1[ i],代表线段树上第i个结点 代表的区间内,连续的1的长度是多少;

由于在更新过程中,一旦对区间的数进行翻转更新,连续0的长度就变成了连续1的长度,连续1的长度变成连续0的长度,所以我们同样定义一个数组tree0[i],代表线段树上第i个结点 代表的区间内,连续0的长度是多少。

在对  孩子节点 Pushup( )得到父节点时,父节点代表的区间内,连续1的长度可能是:

①左孩子中连续1的长度

②右孩子中连续1的长度

③可能跨越左孩子和右孩子。(两个孩子手牵手的赶脚)

在第③种情况下,我们就得知道,左孩子代表的区间,从右向左数有多少个连续1?右孩子代表的区间,从左向右数有多少个连续1?

所以定义数组:

left0[i] 表示,线段树上第i个结点 代表的区间内,从左向右数有几个连续的0。姑且称之为“左连续0”

left1[i]表示,线段树上第i个结点 代表的区间内,从左向右数有几个连续的1。“左连续1”

right0[i] 表示,线段树上第i个结点 代表的区间内,从右向左数有几个连续的0。“右连续0”

right1[i]表示,线段树上第i个结点 代表的区间内,从右向左数有几个连续的1。“右连续1”

还有个num[i]表示原数组。lazy[i] 是懒惰标记。

const int maxn=1e5+5;
int n;
int num[maxn];//存储原数组
 
int tree0[maxn<<2];//当前节点所代表的区间,连续0的长度 
int tree1[maxn<<2];//...1的长度 

int left0[maxn<<2];//当前节点代表的区间,从左往右数连续0的长度 
int left1[maxn<<2];//...1的长度 

int right0[maxn<<2];//当前节点代表的区间,从右往左数连续0的长度 
int right1[maxn<<2];//...1的长度 

int lazy[maxn<<2];//懒惰标记
  • pushup( ) 函数

void pushup(int root,int left,int right)
{
	int mid=(left+right)/2;
	int ln=mid-left+1;//左边叶子节点的个数 
	int rn=right-mid;
	
	left0[root]=left0[2*root];
	left1[root]=left1[2*root];
	if(left0[root]==ln)
		left0[root]+=left0[2*root+1];
	if(left1[root]==ln)
		left1[root]+=left1[2*root+1];
		
	right0[root]=right0[2*root+1];
	right1[root]=right1[2*root+1];
	if(right0[root]==rn)
		right0[root]+=right0[2*root];
	if(right1[root]==rn)
		right1[root]+=right1[2*root];
	
	tree0[root]=max( max(tree0[2*root],tree0[2*root+1]), right0[2*root]+left0[2*root+1] );
	tree1[root]=max( max(tree1[2*root],tree1[2*root+1]), right1[2*root]+left1[2*root+1] );
	
	return;
}

pushup( ) 函数,利用孩子节点向上推出父节点的各项数据。 

我们线段树上的每个结点都有6个属性,对应了6个数组。(哦,还有一个懒惰标记,就算7个叭)。

对于left0[ i] ,父节点一定可以先更新为左孩子的“左连续0”。但是父节点的“左连续0”一定等于左孩子的“左连续0”吗?如果左孩子全部都是0(left0[root]==ln),右孩子的区间开始也有0怎么办?这个时候,就要加上右孩子的“左连续0”了。

1也同理。right0/1也同理。

最后,更新tree0[root]。如上面所说的三种情况,左孩子中连续0的个数tree[2*root],右孩子中连续0的个数tree[2*root+1],左右孩子联手right0[2*root]+left0[2*root+1](左孩子的“右连续”+右孩子的“左连续”)。取最大值。

  • build( ) 函数

build( ) 函数用于建立线段树。有了Pushup函数之后,build函数与之前的线段树建立并无太大差别。

void build(int root,int left,int right)
{
	lazy[root]=0; 
	left0[root]=right0[root]=tree0[root]=0;
	left1[root]=right1[root]=tree1[root]=0;
	if(left==right)
	{
		if(num[left]==1)
		{
			left1[root]=right1[root]=tree1[root]=1;
			left0[root]=right0[root]=tree0[root]=0;
		}
		else
		{
			left1[root]=right1[root]=tree1[root]=0;
			left0[root]=right0[root]=tree0[root]=1;
		}
		return;
	}
	int mid=(left+right)/2;
	build(root*2,left,mid);
	build(root*2+1,mid+1,right);
	pushup(root,left,right);
}

开始,该节点的所有属性初始化为0。

当函数走到叶子节点的时候,也就是(left==right)的时候,通过原数组的数值,更新连续区间的6个“连续”属性。

也有简便的写法:

if(left==right)
	{
		int j;
		scanf("%d",&j);
		left1[rt]=right1[rt]=tree1[rt]= j;//简便写法. 
		left0[rt]=right0[rt]=tree0[root]= !j;
		return;
    }
  • pushdown( ) 函数

与lazy[ ]懒惰标记有关,父节点的更新,在必要时传给子节点。

void Swap(int root)
{
	swap(tree0[root],tree1[root]);
	swap(left0[root],left1[root]);
	swap(right0[root],right1[root]);
}

void pushdown(int root)
{
	if(lazy[root])
	{
		lazy[2*root] ^=1;
		lazy[2*root+1] ^=1;
		lazy[root]=0;
		
		Swap(2*root);
		Swap(2*root+1);
	}
}

lazy[ ]数组,其实只需要0和1来表示就可以了。因为翻转了一次(lazy[i]=1)之后,再翻转一次(一共翻转2次),等于翻转0次。 与1进行^,异或运算,就是取反了。

如果父节点有懒惰标记为1(0说明翻转了偶数次,跟不翻的结果相同),(如果接下来涉及到子节点查询或者更新了,就是不能再懒了的时候),将我自己的父节点的标记传下去。父节点包含了左孩子和右孩子代表的区间,这两个区间懒惰标记取反,意思是孩子们有这个任务区间查询或翻转的任务了,紧接着进行孩子们的交换操作。最后,清除自己的懒惰标记,因为我已经把事情完成了,左孩子交换了,右孩子交换了,也就是我自己父节点代表的区间完成了交换。

  • update( )函数

update( )函数,用于更新。uleft,uright代表更新的左右区间。

void update(int root,int left,int right,int uleft,int uright)
{
	if(left>=uleft && right<=uright)
	{
		lazy[root] ^=1;
		Swap(root);
		return;
	}
	int mid=(left+right)/2;
	pushdown(root);
	
	if(mid>=uleft)
		update(2*root,left,mid,uleft,uright);
	if(mid<uright)
		update(2*root+1,mid+1,right,uleft,uright);
	
	pushup(root,left,right);
}

如果(当前区间)[left,right]\subseteq [uleft,uright](查询区间),进行操作。操作包括:

①更新懒惰标记(标记一下我这里交换过一次了,孩子我还懒得不想动。。23333)

②交换0和1的3个连续区间。这里是我root的交换,,虽然其实已经到整个线段树的最下面一层叶子节点了。

 

否则,如果[left,right]\nsubseteq [uleft,uright],这个时候我必须得动一下自己的孩子节点了。先Pushdown( ),告诉孩子们我刚刚经历了什么,然后递归更新左边区间,递归更新右边区间。更新完了之后,从新的孩子节点中,得到我自己父节点的新的数值。

  • query( )函数

查询[qleft,qright]区间内,最大的连续1的长度。

int query(int root,int left,int right,int qleft,int qright)
{
	if(left>=qleft && right<=qright)
	{
		return tree1[root];
	}
	int mid=(left+right)/2;
	pushdown(root);
	if(qright<=mid)//全在左子树
		return query(2*root,left,mid,qleft,qright); 
	if(qleft>mid)//全在右子树 
		return query(2*root+1,mid+1,right,qleft,qright);
	
	int ll=query(2*root,left,mid,qleft,qright);
	int rr=query(2*root+1,mid+1,right,qleft,qright);
	int lr=min(right1[2*root],mid-qleft+1)+min(left1[2*root+1],qright-mid);
	
	return max(max(ll,rr),lr);
}

如果(当前区间)[left,right]\subseteq [qleft,qright](查询区间), 直接返回这个区间内的连续1的长度tree1[root]

否则,说明还没到底嘛。

如果查询区间全部在左子树,递归查询。

查询区间全在右子树上,递归查询。

最怕的呢,就是横跨两个子树的区间,取最大值。左孩子连续1的长度(ll),右孩子上连续1的长度(rr),联手的两个孩子连续1的长度( lr),都有可能最大。根据我们pushup()函数的思想,联手的两个孩子连续1的长度=左孩子的“右连续1”+右孩子的“左连续1”。

哦,是吗?

并不!

左孩子的“右连续1”,如果有3个,但是我要查询的区间就包括了左孩子的一个点,那不就错了吗?

也即,左子树的“右连续1”个数,不能大于他要           查询的左区间(qleft)---- 左孩子代表的区间的右端点   中所有结点个数(mid-qleft+1个)。两者取较小值。

  • 最后的代码

//AC
//HDU 3911
#include<iostream>
#include<algorithm>
#include<set>
#include<cstring>
#include<queue>
#include<stack>
#include<vector>
#include<stdio.h>
using namespace std;

const int maxn=1e5+5;
int n;
int num[maxn];//存储原数组
 
int tree0[maxn<<2];//当前节点所代表的区间,连续0的长度 
int tree1[maxn<<2];//...1的长度 

int left0[maxn<<2];//当前节点代表的区间,从左往右数连续0的长度 
int left1[maxn<<2];//...1的长度 

int right0[maxn<<2];//当前节点代表的区间,从右往左数连续0的长度 
int right1[maxn<<2];//...1的长度 

int lazy[maxn<<2];//懒惰标记

void pushup(int root,int left,int right)
{
	int mid=(left+right)/2;
	int ln=mid-left+1;//左边叶子节点的个数 
	int rn=right-mid;
	
	left0[root]=left0[2*root];
	left1[root]=left1[2*root];
	if(left0[root]==ln)
		left0[root]+=left0[2*root+1];
	if(left1[root]==ln)
		left1[root]+=left1[2*root+1];
		
	right0[root]=right0[2*root+1];
	right1[root]=right1[2*root+1];
	if(right0[root]==rn)
		right0[root]+=right0[2*root];
	if(right1[root]==rn)
		right1[root]+=right1[2*root];
	
	tree0[root]=max( max(tree0[2*root],tree0[2*root+1]), right0[2*root]+left0[2*root+1] );
	tree1[root]=max( max(tree1[2*root],tree1[2*root+1]), right1[2*root]+left1[2*root+1] );
	
	return;
}

void build(int root,int left,int right)
{
	lazy[root]=0; 
	left0[root]=right0[root]=tree0[root]=0;
	left1[root]=right1[root]=tree1[root]=0;
	if(left==right)
	{
		if(num[left]==1)
		{
			left1[root]=right1[root]=tree1[root]=1;
			left0[root]=right0[root]=tree0[root]=0;
		}
		else
		{
			left1[root]=right1[root]=tree1[root]=0;
			left0[root]=right0[root]=tree0[root]=1;
		}
		return;
	}
	int mid=(left+right)/2;
	build(root*2,left,mid);
	build(root*2+1,mid+1,right);
	pushup(root,left,right);
}

void Swap(int root)
{
	swap(tree0[root],tree1[root]);
	swap(left0[root],left1[root]);
	swap(right0[root],right1[root]);
}


void pushdown(int root)
{
	if(lazy[root])
	{
		//lazy[2*root] ^=lazy[root];//lazy[root]必然等于1 
	//	lazy[2*root+1] ^=lazy[root];
		lazy[2*root] ^=1;
		lazy[2*root+1] ^=1;
		lazy[root]=0;
		
		Swap(2*root);
		Swap(2*root+1);
	}
}

void update(int root,int left,int right,int uleft,int uright)
{
	if(left>=uleft && right<=uright)
	{
		lazy[root] ^=1;
		Swap(root);
		return;
	}
	int mid=(left+right)/2;
	pushdown(root);
	
	if(mid>=uleft)
		update(2*root,left,mid,uleft,uright);
	if(mid<uright)
		update(2*root+1,mid+1,right,uleft,uright);
	
	pushup(root,left,right);
}

int query(int root,int left,int right,int qleft,int qright)
{
	if(left>=qleft && right<=qright)
	{
		return tree1[root];
	}
	int mid=(left+right)/2;
	pushdown(root);
	if(qright<=mid)//全在左子树
		return query(2*root,left,mid,qleft,qright); 
	if(qleft>mid)//全在右子树 
		return query(2*root+1,mid+1,right,qleft,qright);
	
	int ll=query(2*root,left,mid,qleft,qright);
	int rr=query(2*root+1,mid+1,right,qleft,qright);
	int lr=min(right1[2*root],mid-qleft+1)+min(left1[2*root+1],qright-mid);
	
	return max(max(ll,rr),lr);
}


int main()
{
	while(scanf("%d",&n)!=EOF)
	{
		for(int i=1;i<=n;i++)
			scanf("%d",&num[i]);
		build(1,1,n);
		int m;
		scanf("%d",&m);
		while(m--)
		{
			int choice,left,right;
			scanf("%d%d%d",&choice,&left,&right);
			if(choice==0)
			{
				int res=query(1,1,n,left,right);
				printf("%d\n",res);
			}
			else
			{
				update(1,1,n,left,right);
			}
		}
	}
}

希望自己这样理解是没毛病的。恳请大佬们指正。


最后,转一下国立老师养的锦鲤。国立老师怎么养个鱼,都能拿全国冠军??


我常常因为爱豆的努力和敬业,而感到非常的羞愧。ε=(´ο`*)))唉

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值