数据结构专题-专项训练:线段树2(GSS1-5)

一些 update

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

回顾

数据结构专题-专项训练:线段树1 中我们见识了线段树的各种神奇应用,同时了解了线段树题目的五部曲:

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

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

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

题单:

GSS1

GSS1

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


  1. 我们需要维护什么?

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

那么还要维护什么呢?

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

在这里插入图片描述

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

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

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

在这里插入图片描述

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

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

于是我们就惊喜的发现我们只需要再维护一个 \(sum\) 就可以完美的解决问题了!

于是乎,我们最后定下来:维护 \(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(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 的进一步的升级。

这一道题控制了左右端点的范围,因此我们需要分类讨论。

第一种情况:\(r1 \leq l2\),如图:

在这里插入图片描述

那么从图上我们可以很清晰的看到:我们实质是要求 \([r1,l2]\) 的和加上 \([l1,r1]\) 的最大后缀加上 \([l2,r2]\) 的最大前缀然后减去 \(a_{r1},a_{l2}\)。这样做是为了防止 \(l1==r1\) 这种坑爹的情况干扰我们的判断(如果直接求 \([l1,r1-1]\) 的前缀就直接炸了)。

第二种情况:\(l2 < r1\),如图:

在这里插入图片描述

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

  1. \(x\)\([l1,l2)\) 中,\(y\)\([l2,r1]\) 中时,我们要求的是 \([l1,l2]\) 的最大后缀加上 \([l2,r1]\) 的前缀减去 \(a_{l2}\)
  2. \(x\)\([l1,l2)\) 中,\(y\)\((r1,l2]\) 中时,此时的询问就变成了前面 \(r1 \leq l2\) 的询问,此处不再讲解。
  3. \(x\)\([l2,r1]\) 中,\(y\)\([l2,r1]\) 中时,我们要求的是 \([l2,r1]\) 的最大子段和,模仿 GSS1 即可。
  4. \(x\)\([l2,r1]\) 中,\(y\)\((r1,l2]\) 中时,我们要求的是 \([l2,r1]\) 的最大后缀加上 \([r1,l2]\) 的最大前缀减去 \(a_{r1}\)

所以我们只需要按照上面的讨论解题即可。

代码:

#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

这道题 的双倍经验,在 数据结构专题-专项训练:线段树1 中已经讲过,此处不作讲解。唯一需要注意的是值域变大了。

GSS2

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

GSS2

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


  1. 我们需要维护什么?

显然的, GSS1 中前后缀维护已经变得不可行,所以我们需要另行他法。

而这类问题离线,又要去重的题目有一个固定的套路:离线询问,逐个击破。

啥意思?针对这道题,我们首先在 \(n\) 上建立一棵空树(除了 \(l(p),r(p)\) 啥都没有)。然后考虑去重。

为了去重,我们总需要知道在 \(a_i\) 前面且与它相等的数在哪个位置吧?于是我们需要预先处理出 \(pre_i\) 表示上一个与 \(a_i\) 相同的数的位置。

然后我们将所有询问离线,以右端点为关键字升序排序(左端点无所谓)。

这样做有什么好处吗?我们在离线处理询问的时候,如果以 \(i\) 为右端点的询问已经全部处理完了,那么我们后面就可以放心的去重了。

那么又如何处理答案呢?

首先,对于第 \(i\) 个位置 \(a_i\) ,我们针对 \([pre_i+1,a_i]\) 区间做一次区间加法。

比如现在有这样一个序列:1 2 3 4 5 6 5 7

那么前 6 个数加完之后线段树的区间变成了:21 20 18 15 11 6 0 0

此时 \(pre_7 = 5\)

然后我们对 \([5 + 1, 7]\) 做一次区间加法之后有 21 20 18 15 11 11 5 0

此时你会惊奇的发现 我们实际上是对序列进行了自动去重。

然后我们又要维护什么呢?

四个值:\(sum,maxn,lazy\_sum,lazy\_maxn\)。(简写为 \(s(p),m(p),ls(p),lm(p)\)

\(s(p)\) 为这个序列的最大子段和。

\(m(p)\)\(s(p)\) 出现过的最大和(历史最大和,注意这跟吉老师线段树没关系)。

\(ls(p)\)\(s(p)\) 的 lazy_tag。

\(lm(p)\)\(m(p)\) 的lazy_tag。

所以我们维护完了~


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

就是上文所述的区间


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

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


  1. 要不要重载运算符?

不需要。


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

重点!

先看 \(s(p)\)

如果是加法,那么直接加即可。

如果是合并左右两个区间,我们需要做的是求最大值而不是合并。

为什么不需要跟 GSS1 一样弄 \(a(p),p(p)\) ?原因很简单,因为我们左右儿子的区间是连续的。

\(m(p)\) 好维护,同样求最大值。

\(ls(p),lm(p)\) 在区间修改的时候维护,但是合并左右两个区间的时候不要动。

我们再看看怎么查询。

对于区间 \([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 的题目还是有一定的思维难度,更多的是看我们怎么想题目,思维性较强。

接下来,我们将介绍由线段树扩展而来的算法:可持久化线段树。

详情请见 数据结构专题-学习笔记:可持久化线段树

posted @ 2022-04-13 21:44  Plozia  阅读(99)  评论(0编辑  收藏  举报