给出一个含有 N N N 个结点的环,编号分别为 1 … N 1 \ldots N 1…N,环上的点带有权值(可正可负),现要动态的修改某个点的权值,求每次修改后环上的最大连续和,但不能是整个序列的和。
第一行为一个整数 N N N。
第二行为 N N N 个用空格分开的整数。
第三行为一个整数 M ( 4 ≤ M ≤ 100000 ) M(4 \le M \le 100000) M(4≤M≤100000),表示修改的次数(绝对值小于等于 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 4≤N≤100000
1 ≤ A ≤ N 1 \le A \le N 1≤A≤N
− 1000 ≤ B ≤ 1000 -1000 \le B \le 1000 −1000≤B≤1000
思路:
本题求序列的最大连续子段和(环形,不是整个序列),并且支持单点修改和区间查询。
考虑环形的最大连续字段和怎么求,有两种情况:
接下来考虑如何让最大连续子段不是整个序列。
在查询时,分别查 ( 1 , n − 1 ) (1, n - 1) (1,n−1) 和 ( 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],我们会发现一些的性质:
LCA
,所以 [ L , R ] [L, R] [L,R] 一定包含 [ l , r ] [l, r] [l,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));
}
}