线段树算法总结&专题训练3

线段树算法总结&专题训练3

一些 update

update 2020/12/29:感谢机房 hxh 大佬指出问题,GSS5 的分类讨论 1 有点问题,现在已经更正,对各位读者造成的影响深表歉意。

回顾

线段树算法总结&专题训练2 中我们见识了线段树的各种神奇应用,同时了解了线段树题目的五部曲:

  1. 我们需要维护什么?
  2. 线段树的每个叶子节点是什么?
  3. 需要 lazy_tag 吗?lazy_tag 又要维护什么呢?
  4. 要不要重载运算符?
  5. 最后又要怎么修改?怎么查询?

那么,我们来看看如何将这五部曲运用到 GSS1-5 上(之所以没有 GSS6-8 是因为他们不是线段树)。

这里对不知道 GSS 系列的题目的人做一个说明:GSS 系列题目都是数据结构题,而且都是基于树结构之上(比如线段树,平衡树),而这套题目当中出现次数最多的是求区间最大子段和问题。

题单:

GSS1

GSS1

题目要求区间最大子段和,是 GSS 当中经典的一道题。


  1. 我们需要维护什么?

首先最显然的肯定要维护最大子段和 m a x n maxn maxn(记作 m ( p ) m(p) m(p))。

那么还要维护什么呢?

考虑如何合并最大子段和:

在这里插入图片描述

我们要合并上面两个黑色区间,有三种情况(如图):

  1. 最大子段和是左儿子的最大子段和,即 m ( p < < 1 ) m(p << 1) m(p<<1)
  2. 最大子段和是右儿子的最大子段和,即 m ( p < < 1 ∣ 1 ) m(p << 1 | 1) m(p<<11)
  3. 最大子段和跨界了,左右都有,那么我们怎么取呢?
    仔细考虑一下就会发现:我们本质上是需要求 左儿子的最大后缀和右儿子的最大前缀和。这样我们就可以保证最后的总和最大。

于是一个问题解决了,此时我们又需要维护两个东西:最大前缀和 p r e pre pre (记作 p ( p ) p(p) p(p))和最大后缀和 a f t aft aft (记作 a ( p ) a(p) a(p))。但是这样以来,我们怎样维护最大前缀和和最大后缀和呢?

在这里插入图片描述

以最大前缀和为例,有两种情况:

  1. 就是左儿子的最大前缀和,为 a ( p < < 1 ) a(p << 1) a(p<<1)
  2. 左儿子的总和与右儿子的最大前缀和,为 s u m ( p < < 1 ) + a ( p < < 1 ∣ 1 ) sum(p<<1)+a(p<<1|1) sum(p<<1)+a(p<<11)

于是我们就惊喜的发现我们只需要再维护一个 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,然后维护即可。


  1. 线段树的每个叶子节点是什么?

每个值的初始节点。


  1. 需要 lazy_tag 吗?lazy_tag 又要维护什么呢?

没有修改操作,不需要 lazy_tag。


  1. 要不要重载运算符?

这道题最好使用重载运算符,方便修改(虽然这道题没有,但是建树要用)与查询。


  1. 最后又要怎么修改?怎么查询?

首先考虑建树。

根据第 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

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 r1l2,如图:

在这里插入图片描述

那么从图上我们可以很清晰的看到:我们实质是要求 [ 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,r11] 的前缀就直接炸了)。

第二种情况: l 2 < r 1 l2 < r1 l2<r1,如图:

在这里插入图片描述

那么此时我们需要进行分类讨论。设我们选取的最大子段和区间为 [ x , y ] [x,y] [x,y]

  1. 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
  2. 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 r1l2 的询问,此处不再讲解。
  3. 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 即可。
  4. 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

这道题放在最后是因为它的难度是最大的。

GSS2

求最大子段和是件容易事。去重之后再求就不是件容易事了。


  1. 我们需要维护什么?

显然的, 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。

所以我们维护完了~


  1. 线段树的每个叶子节点是什么?

就是上文所述的区间


  1. 需要 lazy_tag 吗?lazy_tag 又要维护什么呢?

需要一个加法 lazy_tag,一个最大子段和 lazy_tag。


  1. 要不要重载运算符?

不需要。


  1. 最后又要怎么修改?怎么查询?

重点!

先看 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

猜你喜欢

转载自blog.csdn.net/BWzhuzehao/article/details/111566557
今日推荐