题目描述
给出一个含有 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],我们会发现一些的性质:
- 由于
[
L
,
R
]
[L, R]
[L,R] 为
l
l
l 和
r
r
r 的
LCA
,所以 [ L , R ] [L, R] [L,R] 一定包含 [ l , r ] [l, r] [l,r]。 - [ 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));
}
}