[线段树(猫树)] 最大连续和

题目描述

给出一个含有 N N N 个结点的环,编号分别为 1 … N 1 \ldots N 1N,环上的点带有权值(可正可负),现要动态的修改某个点的权值,求每次修改后环上的最大连续和,但不能是整个序列的和。

输入格式

第一行为一个整数 N N N
第二行为 N N N 个用空格分开的整数。
第三行为一个整数 M ( 4 ≤ M ≤ 100000 ) M(4 \le M \le 100000) M(4M100000),表示修改的次数(绝对值小于等于 1000 1000 1000)。
接下来 M M M 行,每行两个整数 A A A B B B,表示将序列中的第 A A A 个数的值,修改为 B B B

输出格式

对于每个修改,输出修改后环上的最大连续和。

样例

样例输入1:

5
3 -2 1 2 -5
4
2 -2
5 -5
2 -4
5 -1

样例输出1:

4
4
3
5

数据范围

4 ≤ N ≤ 100000 4\le N \le 100000 4N100000
1 ≤ A ≤ N 1 \le A \le N 1AN
− 1000 ≤ B ≤ 1000 -1000 \le B \le 1000 1000B1000

题解

思路:

本题求序列的最大连续子段和(环形,不是整个序列),并且支持单点修改和区间查询。

考虑环形的最大连续字段和怎么求,有两种情况:

  1. 没有经过最左边和最右边。就是求普通的最大连续字段和。
  2. 经过最左边和最右边。用总和减去最小连续字段和求得最大。

接下来考虑如何让最大连续子段不是整个序列。

在查询时,分别查 ( 1 , n − 1 ) (1, n - 1) (1,n1) ( 2 , n ) (2, n) (2,n) 的值,再合并即可。


连续子段和的线段树求法:
采用合并的思想。

对于每段区间,记录 l s u m lsum lsum(包含左端点的最大值), r s u m rsum rsum(包含右端点的最大值), p s u m psum psum(整个区间的最大值), s u m sum sum(整个区间的和)。
将两个小区间 x , y x, y x,y 合并成大区间 a a a 时, a l s u m = max ⁡ ( x l s u m , x s u m + y l s u m ) a_{lsum} = \max(x_{lsum}, x_{sum} + y_{lsum}) alsum=max(xlsum,xsum+ylsum)(继承左边,或左边 + + + 右边的最左边最大值), a r s u m = max ⁡ ( y r s u m , y s u m + x r s u m ) a_{rsum} = \max(y_{rsum}, y_{sum} + x_{rsum}) arsum=max(yrsum,ysum+xrsum)(继承右边,或右边 + + + 左边的最右边最大值), a p s u m = max ⁡ ( a l s u m , a r s u m , x r s u m + y l s u m ) a_{psum} = \max(a_{lsum}, a_{rsum}, x_{rsum} + y_{lsum}) apsum=max(alsum,arsum,xrsum+ylsum)(可以为包含 l , r l, r l,r 的最大值,或者左右合并成的值)。

void updata(int bh){
	tr[bh].sum = tr[bh * 2].sum + tr[bh * 2 + 1].sum;
	tr[bh].lsum = max(tr[bh * 2].lsum, tr[bh * 2].sum + tr[bh * 2 + 1].lsum);
	tr[bh].rsum = max(tr[bh * 2 + 1].rsum, tr[bh * 2 + 1].sum + tr[bh * 2].rsum);
	tr[bh].psum = max(tr[bh * 2].psum, max(tr[bh * 2 + 1].psum, tr[bh * 2].rsum + tr[bh * 2 + 1].lsum));
}

连续字段和的猫树求法:
猫树比线段树快,但不支持修改,即一种静态的线段树。
构造猫树需要 O ( n log ⁡ n ) O(n \log n) O(nlogn),但是查询只需要 O ( 1 ) O(1) O(1)


1 ^1 1在查询 [ l , r ] [l, r] [l,r] 这段区间的信息和的时候,将线段树树上代表 [ l , l ] [l, l] [l,l] 的节点和代表 [ r , r ] [r, r] [r,r] 这段区间的节点在线段树上的 LCA 求出来,设这个节点 p p p 代表的区间为 [ L , R ] [L,R] [L,R],我们会发现一些的性质:

  1. 由于 [ L , R ] [L, R] [L,R] l l l r r rLCA,所以 [ L , R ] [L, R] [L,R] 一定包含 [ l , r ] [l, r] [l,r]
  2. [ l , r ] [l, r] [l,r] 一定横跨 [ L , R ] [L, R] [L,R] 的中点。如果 l l l r r r 都在一边,则那一边就已经为 l l l r r r 的祖先了。因此, l l l 一定在 [ L , m i d ] [L, mid] [L,mid] 这个区间内, r r r 一定在 ( m i d , R ] (mid, R] (mid,R] 这个区间内。

具体来说,我们建树的时候对于线段树树上的一个节点,设它代表的区间为 ( l , r ] (l, r] (l,r]
但是不像普通的线段树一样开结构体,直接开两个数组,前缀数组和后缀数组。

询问时,先将 [ l , l ] [l, l] [l,l] [ r , r ] [r, r] [r,r]LCA 求出来,根据性质 2 2 2,于是我们可以使用 p p p 里面的前缀和数组和后缀和数组,将 [ l , r ] [l, r] [l,r] 拆成 [ l , m i d ] + ( m i d , r ] [l, mid] + (mid, r] [l,mid]+(mid,r] 从而拼出来 [ l , r ] [l, r] [l,r] 这个区间。
但似乎处理起来很困难。

我们将这个序列补成 2 2 2 的整次幂,然后建树。
此时我们发现线段树上两个节点的 LCA 编号,就是两个节点二进制编号的最长公共前缀 LCP
我们可以发现在 x x x y y y 的二进制下 l c p ( x , y ) = x > > l o g [ x   x o r   y ] lcp(x, y) = x >> log[x\ xor\ y] lcp(x,y)=x>>log[x xor y]
所以我们预处理一个 l o g log log 数组即可轻松完成了任务。

int lg[400010], a[100010];
int pos[100010];
int f1[21][100010], f2[21][100010];
void build(int bh, int l, int r, int d){
	if(l == r){
		pos[l] = bh;
		return;
	}
	int mid = (l + r) >> 1;
	int s1 = a[mid], s2 = a[mid];
	f1[d][mid] = f2[d][mid] = a[mid];
	s2 = max(s2, 0);
	for(int i = mid - 1; i >= l; -- i){
		s1 += a[i], s2 += a[i];
		f1[d][i] = max(f1[d][i + 1], s1);
		f2[d][i] = max(f2[d][i + 1], s2);
		s2 = max(s2, 0);
	}
	s1 = s2 = a[mid + 1];
	f1[d][mid + 1] = f2[d][mid + 1] = a[mid + 1];
	s2 = max(s2, 0);
	for(int i = mid + 2; i <= r; ++ i){
		s1 += a[i], s2 += a[i];
		f1[d][i] = max(f1[d][i - 1], s1);
		f2[d][i] = max(f2[d][i - 1], s2);
		s2 = max(s2, 0);
	}
	build(bh * 2, l, mid, d + 1);
	build(bh * 2 + 1, mid + 1, r, d + 1); 
}
int query(int x, int y){
	if(x == y){
		return a[x];
	}
	int d = lg[pos[x]] - lg[pos[x] ^ pos[y]];
	return max(max(f2[d][x], f2[d][y]), f1[d][x] + f1[d][y]);
}
int main(){
	int l = 0, len;
	for(len = 2; len < n; len <<= 1){
	}
	l = len << 1;
	for(int i = 2; i <= l; ++ i){
		lg[i] = lg[i >> 1] + 1;
	}
	build(1, 1, len, 1);
	scanf("%d", &m);
	while(m --){
		int x, y;
		scanf("%d %d", &x, &y);
		printf("%d\n", query(x, y));
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值