一些 update
update 2020/12/29:感谢机房 hxh 大佬指出问题,GSS5 的分类讨论 1 有点问题,现在已经更正,对各位读者造成的影响深表歉意。
回顾
在 线段树算法总结&专题训练2 中我们见识了线段树的各种神奇应用,同时了解了线段树题目的五部曲:
- 我们需要维护什么?
- 线段树的每个叶子节点是什么?
- 需要 lazy_tag 吗?lazy_tag 又要维护什么呢?
- 要不要重载运算符?
- 最后又要怎么修改?怎么查询?
那么,我们来看看如何将这五部曲运用到 GSS1-5 上(之所以没有 GSS6-8 是因为他们不是线段树)。
这里对不知道 GSS 系列的题目的人做一个说明:GSS 系列题目都是数据结构题,而且都是基于树结构之上(比如线段树,平衡树),而这套题目当中出现次数最多的是求区间最大子段和问题。
题单:
GSS1
题目要求区间最大子段和,是 GSS 当中经典的一道题。
- 我们需要维护什么?
首先最显然的肯定要维护最大子段和 m a x n maxn maxn(记作 m ( p ) m(p) m(p))。
那么还要维护什么呢?
考虑如何合并最大子段和:
我们要合并上面两个黑色区间,有三种情况(如图):
- 最大子段和是左儿子的最大子段和,即 m ( p < < 1 ) m(p << 1) m(p<<1)。
- 最大子段和是右儿子的最大子段和,即 m ( p < < 1 ∣ 1 ) m(p << 1 | 1) m(p<<1∣1)。
- 最大子段和跨界了,左右都有,那么我们怎么取呢?
仔细考虑一下就会发现:我们本质上是需要求 左儿子的最大后缀和 和 右儿子的最大前缀和。这样我们就可以保证最后的总和最大。
于是一个问题解决了,此时我们又需要维护两个东西:最大前缀和 p r e pre pre (记作 p ( p ) p(p) p(p))和最大后缀和 a f t aft aft (记作 a ( p ) a(p) a(p))。但是这样以来,我们怎样维护最大前缀和和最大后缀和呢?
以最大前缀和为例,有两种情况:
- 就是左儿子的最大前缀和,为 a ( p < < 1 ) a(p << 1) a(p<<1)。
- 左儿子的总和与右儿子的最大前缀和,为 s u m ( p < < 1 ) + a ( p < < 1 ∣ 1 ) sum(p<<1)+a(p<<1|1) sum(p<<1)+a(p<<1∣1)。
于是我们就惊喜的发现我们只需要再维护一个 s u m sum sum 就可以完美的解决问题了!
于是乎,我们最后定下来:维护 s u m , m a x n , p r e , a f t sum,maxn,pre,aft sum,maxn,pre,aft,然后维护即可。
- 线段树的每个叶子节点是什么?
每个值的初始节点。
- 需要 lazy_tag 吗?lazy_tag 又要维护什么呢?
没有修改操作,不需要 lazy_tag。
- 要不要重载运算符?
这道题最好使用重载运算符,方便修改(虽然这道题没有,但是建树要用)与查询。
- 最后又要怎么修改?怎么查询?
首先考虑建树。
根据第 1 问所述,代码如下:
s(p) = s(p << 1) + s(p << 1 | 1);
p(p) = Max(p(p << 1), s(p << 1) + p(p << 1 | 1));
a(p) = Max(a(p << 1 | 1), s(p << 1 | 1) + a(p << 1));
m(p) = Max(m(p << 1), Max(m(p << 1 | 1), a(p << 1) + p(p << 1 | 1)));
然后修改呢?
如果修改区间 [ l , r ] [l,r] [l,r] 再当前节点 l ( p ) , r ( p ) l(p),r(p) l(p),r(p) 的左右儿子内都有区间,那么相应的求出每个区间的结果,使用 结构体 存储(而且是建树的结构体,我们需要知道每一个返回结果的相应变量),然后做一次加法(这就是为什么要用重载运算符);否则,单边寻找答案,返回结果结构体即可。
不理解?可以看看代码。
代码:
#include <bits/stdc++.h>
#define Max(a, b) ((a > b) ? a : b)
using namespace std;
const int MAXN = 5e4 + 10;
int n, m, a[MAXN];
typedef long long LL;
struct node
{
int l, r;
LL pre, aft, sum, maxn;//最大前缀和,最大后缀和,总和,最大子段和
#define l(p) tree[p].l
#define r(p) tree[p].r
#define p(p) tree[p].pre
#define a(p) tree[p].aft
#define s(p) tree[p].sum
#define m(p) tree[p].maxn
node operator + (const node &b)const
{
node c;
c.l = l; c.r = b.r;
c.pre = Max(pre, sum + b.pre);
c.aft = Max(b.sum + aft, b.aft);
c.sum = sum + b.sum;
c.maxn = Max(maxn, Max(b.maxn, aft + b.pre));
return c;
}
}tree[MAXN << 2];
int read()
{
int sum = 0, fh = 1; char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') fh = -1; ch = getchar();}
while (ch >= '0' && ch <= '9') {
sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
return sum * fh;
}
void build(int p, int l, int r)
{
l(p) = l, r(p) = r;
if (l == r) {
p(p) = a(p) = s(p) = m(p) = a[l]; return ;}
int mid = (l + r) >> 1;
build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
tree[p] = tree[p << 1] + tree[p << 1 | 1];
}
node ask(int p, int l, int r)
{
if (l(p) >= l && r(p) <= r) return tree[p];
int mid = (l(p) + r(p)) >> 1;
if (l <= mid && r > mid) return ask(p << 1, l, r) + ask(p << 1 | 1, l, r);
if (l <= mid) return ask(p << 1, l, r);
if (r >= mid) return ask(p << 1 | 1, l, r);
}
int main()
{
n = read();
for (int i = 1; i <= n; ++i) a[i] = read();
build(1, 1, n);
m = read();
for (int i = 1; i <= m; ++i)
{
int l = read(), r = read();
printf ("%lld\n", ask(1, l, r).maxn);
}
return 0;
}
GSS3
GSS3 只是在 GSS1 的基础上加了单点修改而已。
相信有了之前的基础,各位不难想到直接单线修改,返回时维护即可。别的与 GSS1 没有任何区别。
代码:
#include <bits/stdc++.h>
#define Max(a, b) ((a > b) ? a : b)
using namespace std;
typedef long long LL;
const int MAXN = 5e4 + 10;
int n, m, a[MAXN];
struct node
{
int l, r;
LL pre, aft, sum, maxn;
#define l(p) tree[p].l
#define r(p) tree[p].r
#define p(p) tree[p].pre
#define a(p) tree[p].aft
#define s(p) tree[p].sum
#define m(p) tree[p].maxn
node operator + (const node &b)
{
node c;
c.l = l; c.r = b.r;
c.sum = sum + b.sum;
c.pre = Max(pre, sum + b.pre);
c.aft = Max(b.aft, b.sum + aft);
c.maxn = Max(maxn, Max(b.maxn, aft + b.pre));
return c;
}
}tree[MAXN << 2];
int read()
{
int sum = 0, fh = 1; char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') fh = -1; ch = getchar();}
while (ch >= '0' && ch <= '9') {
sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
return sum * fh;
}
void build(int p, int l, int r)
{
l(p) = l, r(p) = r;
if (l == r) {
s(p) = a(p) = p(p) = m(p) = a[l]; return ;}
int mid = (l + r) >> 1;
build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
tree[p] = tree[p << 1] + tree[p << 1 | 1];
}
void change(int p, int loc, int val)
{
if (l(p) == r(p)) {
s(p) = a(p) = p(p) = m(p) = val; return ;}
int mid = (l(p) + r(p)) >> 1;
if (loc <= mid) change(p << 1, loc, val);
else change(p << 1 | 1, loc, val);
tree[p] = tree[p << 1] + tree[p << 1 | 1];
}
node ask(int p, int l, int r)
{
if (l(p) >= l && r(p) <= r) return tree[p];
int mid = (l(p) + r(p)) >> 1;
if (l <= mid && r > mid)
{
return ask(p << 1, l, r) + ask(p << 1 | 1, l, r);
}
else if (l <= mid) return ask(p << 1, l, r);
else return ask(p << 1 | 1, l, r);
}
int main()
{
n = read();
for (int i = 1; i <= n; ++i) a[i] = read();
build(1, 1, n);
m = read();
for (int i = 1; i <= m; ++i)
{
int opt = read();
if (opt == 0)
{
int x = read(), y = read();
change(1, x, y);
}
else
{
int l = read(), r = read();
printf("%lld\n", ask(1, l, r).maxn);
}
}
return 0;
}
GSS5
GSS5 是 GSS1 的进一步的升级。
这一道题控制了左右端点的范围,因此我们需要分类讨论。
第一种情况: r 1 ≤ l 2 r1 \leq l2 r1≤l2,如图:
那么从图上我们可以很清晰的看到:我们实质是要求 [ r 1 , l 2 ] [r1,l2] [r1,l2] 的和加上 [ l 1 , r 1 ] [l1,r1] [l1,r1] 的最大后缀加上 [ l 2 , r 2 ] [l2,r2] [l2,r2] 的最大前缀然后减去 a r 1 , a l 2 a_{r1},a_{l2} ar1,al2。这样做是为了防止 l 1 = = r 1 l1==r1 l1==r1 这种坑爹的情况干扰我们的判断(如果直接求 [ l 1 , r 1 − 1 ] [l1,r1-1] [l1,r1−1] 的前缀就直接炸了)。
第二种情况: l 2 < r 1 l2 < r1 l2<r1,如图:
那么此时我们需要进行分类讨论。设我们选取的最大子段和区间为 [ x , y ] [x,y] [x,y]。
- 当 x x x 在 [ l 1 , l 2 ) [l1,l2) [l1,l2) 中, y y y 在 [ l 2 , r 1 ] [l2,r1] [l2,r1] 中时,我们要求的是 [ l 1 , l 2 ] [l1,l2] [l1,l2] 的最大后缀加上 [ l 2 , r 1 ] [l2,r1] [l2,r1] 的前缀减去 a l 2 a_{l2} al2。
- 当 x x x 在 [ l 1 , l 2 ) [l1,l2) [l1,l2) 中, y y y 在 ( r 1 , l 2 ] (r1,l2] (r1,l2] 中时,此时的询问就变成了前面 r 1 ≤ l 2 r1 \leq l2 r1≤l2 的询问,此处不再讲解。
- 当 x x x 在 [ l 2 , r 1 ] [l2,r1] [l2,r1] 中, y y y 在 [ l 2 , r 1 ] [l2,r1] [l2,r1] 中时,我们要求的是 [ l 2 , r 1 ] [l2,r1] [l2,r1] 的最大子段和,模仿 GSS1 即可。
- 当 x x x 在 [ l 2 , r 1 ] [l2,r1] [l2,r1] 中, y y y 在 ( r 1 , l 2 ] (r1,l2] (r1,l2] 中时,我们要求的是 [ l 2 , r 1 ] [l2,r1] [l2,r1] 的最大后缀加上 [ r 1 , l 2 ] [r1,l2] [r1,l2] 的最大前缀减去 a r 1 a_{r1} ar1。
所以我们只需要按照上面的讨论解题即可。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int Max(LL fir, LL sec) {
return (fir > sec) ? fir : sec;}
int Min(LL fir, LL sec) {
return (fir < sec) ? fir : sec;}
const int MAXN = 1e4 + 10;
int t, n, m, a[MAXN];
struct node
{
int l, r;
LL sum, pre, aft, maxn;
#define l(p) tree[p].l
#define r(p) tree[p].r
#define s(p) tree[p].sum
#define p(p) tree[p].pre
#define a(p) tree[p].aft
#define m(p) tree[p].maxn
node operator + (const node &b)
{
node c;
c.l = l, c.r = b.r;
c.sum = sum + b.sum;
c.pre = Max(pre, sum + b.pre);
c.aft = Max(b.aft, b.sum + aft);
c.maxn = Max(maxn, Max(b.maxn, aft + b.pre));
return c;
}
}tree[MAXN << 2];
int read()
{
int sum = 0, fh = 1; char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') fh = -1; ch = getchar();}
while (ch >= '0' && ch <= '9') {
sum = (sum << 3) + (sum << 1) + (ch ^48); ch = getchar();}
return sum * fh;
}
void build(int p, int l, int r)
{
l(p) = l, r(p) = r;
if (l == r) {
s(p) = a(p) = p(p) = m(p) = a[l]; return ;}
int mid = (l(p) + r(p)) >> 1;
build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
tree[p] = tree[p << 1] + tree[p << 1 | 1];
}
node Ask(int p, int l, int r)
{
if (l(p) >= l && r(p) <= r) return tree[p];
int mid = (l(p) + r(p)) >> 1;
if (l <= mid && r > mid) return Ask(p << 1, l, r) + Ask(p << 1 | 1, l, r);
if (l <= mid) return Ask(p << 1, l, r);
if (r > mid) return Ask(p << 1 | 1, l, r);
}
int main()
{
t = read();
while (t--)
{
memset(a, 0, sizeof(a)); memset(tree, 0, sizeof(tree));
n = read();
for (int i = 1; i <= n; ++i) a[i] = read();
build(1, 1, n); m = read();
for (int i = 1; i <= m; ++i)
{
int l1 = read(), r1 = read(), l2 = read(), r2 = read(); LL ans;
if (r1 <= l2) ans = Ask(1, r1, l2).sum + Ask(1, l1, r1).aft + Ask(1, l2, r2).pre - a[r1] - a[l2];
else
{
ans = Ask(1, l1, l2).aft + Ask(1, l2, r1).pre - a[l2];
ans = Max(ans, Ask(1, l1, l2).aft + Ask(1, l2, r1).sum + Ask(1, r1, r2).pre - a[l2] - a[r1]);
ans = Max(ans, Ask(1, l2, r1).maxn);
ans = Max(ans, Ask(1, l2, r1).aft + Ask(1, r1, r2).pre - a[r1]);
}
printf("%lld\n", ans);
}
}
return 0;
}
GSS4
是 这道题 的双倍经验,在 线段树算法总结&专题训练2 中已经讲过,此处不作讲解。唯一需要注意的是值域变大了。
GSS2
这道题放在最后是因为它的难度是最大的。
求最大子段和是件容易事。去重之后再求就不是件容易事了。
- 我们需要维护什么?
显然的, GSS1 中前后缀维护已经变得不可行,所以我们需要另行他法。
而这类问题离线,又要去重的题目有一个固定的套路:离线询问,逐个击破。
啥意思?针对这道题,我们首先在 n n n 上建立一棵空树(除了 l ( p ) , r ( p ) l(p),r(p) l(p),r(p) 啥都没有)。然后考虑去重。
为了去重,我们总需要知道在 a i a_i ai 前面且与它相等的数在哪个位置吧?于是我们需要预先处理出 p r e i pre_i prei 表示上一个与 a i a_i ai 相同的数的位置。
然后我们将所有询问离线,以右端点为关键字升序排序(左端点无所谓)。
这样做有什么好处吗?我们在离线处理询问的时候,如果以 i i i 为右端点的询问已经全部处理完了,那么我们后面就可以放心的去重了。
那么又如何处理答案呢?
首先,对于第 i i i 个位置 a i a_i ai ,我们针对 [ p r e i + 1 , a i ] [pre_i+1,a_i] [prei+1,ai] 区间做一次区间加法。
比如现在有这样一个序列:1 2 3 4 5 6 5 7
那么前 6 个数加完之后线段树的区间变成了:21 20 18 15 11 6 0 0
此时 p r e 7 = 5 pre_7 = 5 pre7=5。
然后我们对 [ 5 + 1 , 7 ] [5 + 1, 7] [5+1,7] 做一次区间加法之后有 21 20 18 15 11 11 5 0
此时你会惊奇的发现 我们实际上是对序列进行了自动去重。
然后我们又要维护什么呢?
四个值: s u m , m a x n , l a z y _ s u m , l a z y _ m a x n sum,maxn,lazy\_sum,lazy\_maxn sum,maxn,lazy_sum,lazy_maxn。(简写为 s ( p ) , m ( p ) , l s ( p ) , l m ( p ) s(p),m(p),ls(p),lm(p) s(p),m(p),ls(p),lm(p))
s ( p ) s(p) s(p) 为这个序列的最大子段和。
m ( p ) m(p) m(p) 为 s ( p ) s(p) s(p) 出现过的最大和(历史最大和,注意这跟吉老师线段树没关系)。
l s ( p ) ls(p) ls(p) 是 s ( p ) s(p) s(p) 的 lazy_tag。
l m ( p ) lm(p) lm(p) 是 m ( p ) m(p) m(p) 的lazy_tag。
所以我们维护完了~
- 线段树的每个叶子节点是什么?
就是上文所述的区间
- 需要 lazy_tag 吗?lazy_tag 又要维护什么呢?
需要一个加法 lazy_tag,一个最大子段和 lazy_tag。
- 要不要重载运算符?
不需要。
- 最后又要怎么修改?怎么查询?
重点!
先看 s ( p ) s(p) s(p) 。
如果是加法,那么直接加即可。
如果是合并左右两个区间,我们需要做的是求最大值而不是合并。
为什么不需要跟 GSS1 一样弄 a ( p ) , p ( p ) a(p),p(p) a(p),p(p) ?原因很简单,因为我们左右儿子的区间是连续的。
m ( p ) m(p) m(p) 好维护,同样求最大值。
l s ( p ) , l m ( p ) ls(p),lm(p) ls(p),lm(p) 在区间修改的时候维护,但是合并左右两个区间的时候不要动。
我们再看看怎么查询。
对于区间 [ l , r ] [l,r] [l,r] 的询问,我们直接求 [ l , r ] [l,r] [l,r] 的最大子段和即可。为什么不需要做处理?还是因为左右儿子的区间是连续的。
代码:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 1e5 + 10;
int n, m, a[MAXN], ans[MAXN], pre[MAXN], las[MAXN << 2];
struct node
{
int l, r, sum, maxn, lazy_sum, lazy_maxn;
#define l(p) tree[p].l
#define r(p) tree[p].r
#define s(p) tree[p].sum
#define m(p) tree[p].maxn
#define ls(p) tree[p].lazy_sum
#define lm(p) tree[p].lazy_maxn
}tree[MAXN << 2];
struct query
{
int l, r, id;
}q[MAXN];
int Max(int fir, int sec) {
return (fir > sec) ? fir : sec;}
int read()
{
int sum = 0, fh = 1; char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') fh = -1; ch = getchar();}
while (ch >= '0' && ch <= '9') {
sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
return sum * fh;
}
bool cmp(const query &fir, const query &sec)
{
if (fir.r ^ sec.r) return fir.r < sec.r;
return fir.l < sec.l;
}
void build(int p, int l, int r)
{
l(p) = l, r(p) = r;
if (l == r) return ;
int mid = (l + r) >> 1;
build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
return ;
}
void spread(int p)
{
m(p << 1) = Max(m(p << 1), s(p << 1) + lm(p));
s(p << 1) += ls(p);
lm(p << 1) = Max(lm(p << 1), ls(p << 1) + lm(p));
ls(p << 1) += ls(p);
m(p << 1 | 1) = Max(m(p << 1 | 1), s(p << 1 | 1) + lm(p));
s(p << 1 | 1) += ls(p);
lm(p << 1 | 1) = Max(lm(p << 1 | 1), ls(p << 1 | 1) + lm(p));
ls(p << 1 | 1) += ls(p);
lm(p) = ls(p) = 0;
}
void change(int p, int l, int r, int k)
{
if (l(p) >= l && r(p) <= r)
{
s(p) += k; m(p) = Max(m(p), s(p));
ls(p) += k; lm(p) = Max(lm(p), ls(p));
return ;
}
spread(p);
int mid = (l(p) + r(p)) >> 1;
if (l <= mid) change(p << 1, l, r, k);
if (r > mid) change(p << 1 | 1, l, r, k);
s(p) = Max(s(p << 1), s(p << 1 | 1));
m(p) = Max(m(p << 1), m(p << 1 | 1));
}
int ask(int p, int l, int r)
{
if (l(p) >= l && r(p) <= r) return m(p);
spread(p);
int mid = (l(p) + r(p)) >> 1; int val = -0x7f7f7f7f;
if (l <= mid) val = Max(val, ask(p << 1, l, r));
if (r > mid) val = Max(val, ask(p << 1 | 1, l, r));
return val;
}
signed main()
{
n = read();
for (int i = 1; i <= n; ++i) a[i] = read();
for (int i = 1; i <= n; ++i)
{
pre[i] = las[a[i] + 100000];
las[a[i] + 100000] = i;
}
build(1, 1, n);
m = read();
for (int i = 1; i <= m; ++i) {
q[i].l = read(); q[i].r = read(); q[i].id = i;}
sort(q + 1, q + m + 1, cmp);
for (int i = 1, j = 1; i <= n; ++i)
{
change(1, pre[i] + 1, i, a[i]);
for (; j <= m && q[j].r == i; ++j)
ans[q[j].id] = ask(1, q[j].l, q[j].r);
}
for (int i = 1; i <= m; ++i) printf("%lld\n", ans[i]);
return 0;
}
总结:
GSS 的题目还是有一定的思维难度,更多的是看我们怎么想题目,思维性较强。
接下来,我们将介绍由线段树扩展而来的算法:可持久化线段树/主席树。
详情请见 线段树算法总结&专题训练4。