算法提高篇(3)线段树(下)

一、线段树+分治

线段树本身就是基于分治思想的二叉树。那么对于许多可以通过分治解决的问题,查询起来也可以通过线段树来维护。其中,最经典的就是最大子段和问题

1.1 小白逛公园

题目解析:

首先我们先来回忆一下如何用分治解决最大子段和问题。我们把区间[l, r]从中间一分为二,最大子段和可能出现在三处位置:全部在左半边、全部在右半边和包含左右两边。其中,包含左右两边的这一部分我们可以这样看:从mid位置往左最大能延伸到多大的子段和 + 从mid + 1位置往右最大能延伸到多大的子段和。(可以参考下图)

因此,最大子段和 = max(max1,max2,rmax + lmax)。

所以,根据上面所说的来看,node结构体中就应该维护如下信息:l,r,max(最大子段和),lmax(从左端点开始的最大子段和),rmax(从右端点开始的最大子段和)。

那么,是不是就维护这五个信息就足够了呢?我们来看pushup函数该如何实现,在pushup函数中负责合并计算p这一节点的信息,p.max根据最大子段和的求解思想就等于max(lc.max,rc.max,lc.rmax + rc.lmax)。那么p.lmax和p.rmax该如何修改呢?以p.lmax为例,我们发现,p.lmax有两种可能:一种可能就是刚好等于lc.lmax,还有一种可能就是lc的区间总和再加上rc.lmax。(如下图)

所以,在node结构体中,我们还需要再维护一个信息sum,也就是区间和!

最后,我们再来分析一下查询函数query。按照我们之前线段树的模板,query函数都是返回一个数,那么在查询最大子段和的时候,我们还是简简单单的返回一个数max吗?我们回归到query函数的原理上,query函数是把要查询的区间拆分成一个个的小区间,再从这些小区间中找出我们想要的区间,再向上依次合并结果,这不刚好是我们用分治思想解决最大子段和一样的套路吗?所以,像之前模板那样简单地将小区间加起来求得的值肯定是有错误的,因为这里只考虑了左右两段区间的最大子段和,并没有考虑中间包含两段区间的那个情况!因此,我们要返回的不能单单是一个数,而是应该返回一个结构体!!!

代码实现:

cpp 复制代码
#include <iostream>
#include <algorithm>
using namespace std;

#define lc (p * 2)
#define rc (p * 2 + 1)

const int N = 5e5 + 10;
typedef long long LL;

int n, m;
LL a[N];

struct node
{
	int l, r;
	LL sum, max, lmax, rmax;
}tr[N * 4];

void pushup(node& p, node l, node r)
{
	p.max = max(max(l.max, r.max), l.rmax + r.lmax);
	p.lmax = max(l.lmax, l.sum + r.lmax);
	p.rmax = max(r.rmax, r.sum + l.rmax);
	p.sum = l.sum + r.sum;
}

void build(int p, int l, int r)
{
	tr[p] = { l,r,a[l],a[l],a[l],a[l] };
	if (l == r)
		return;

	int mid = (l + r) / 2;
	build(lc, l, mid);
	build(rc, mid + 1, r);
	pushup(tr[p], tr[lc], tr[rc]);
}

void modify(int p, int x, LL k)
{
	int l = tr[p].l, r = tr[p].r;
	if (x == l && x == r)
	{
		tr[p].sum = tr[p].max = tr[p].lmax = tr[p].rmax = k;
		return;
	}

	int mid = (l + r) / 2;
	if (x <= mid)
		modify(lc, x, k);
	else
		modify(rc, x, k);
	pushup(tr[p], tr[lc], tr[rc]);
}

node query(int p, int x, int y)
{
	int l = tr[p].l, r = tr[p].r;
	if (x <= l && r <= y)
		return tr[p];

	int mid = (l + r) / 2;
	if (y <= mid)
		return query(lc, x, y);
	if (x > mid)
		return query(rc, x, y);

	node ret;
	node L = query(lc, x, y), R = query(rc, x, y);
	pushup(ret, L, R);

	return ret;
}

int main()
{
	cin >> n >> m;
	for (int i = 1;i <= n;i++)
		cin >> a[i];

	build(1, 1, n);

	while (m--)
	{
		int op, a, b;
		cin >> op >> a >> b;
		if (op == 1)
		{
			if (a > b)
				swap(a, b);

			cout << query(1, a, b).max << endl;
		}
		else
		{
			modify(1, a, b);
		}
	}

	return 0;
}

1.2 序列操作

题目解析:

这道题目十分综合,先一个一个来看。这道题的区间修改操作涉及到重置和翻转两个操作,那么首先我们要做的是确定区间修改的优先级。根据上一章的结论,想要线段树中同时维护好重置和翻转两个操作的话,那么必然是先重置,再翻转

接着是区间查询。首先是查询区间内总共有多少个1,又因为该数组只包含0和1,所以相当于就是查询区间和。因此,node结构体中要维护的就是左端点l ,右端点r ,区间和 s1 。第二个是查询区间最多有多少个连续的1,因此结构体中还需要维护一个变量m1 ,表示区间内最长的连续的1。根据上一题查询最大子段和的结论,我们还需要再维护两个变量l1r1,分别表示从左 / 右端点开始最长连续的1。

最后再来分析一下本题最特殊的操作------翻转操作。因为反转之后1变为0,0变为1,那么我们在查询反转之后数组的时候,最长连续1就和上一次数组的最长连续0有关,总共的1的个数就和上一次数组的0的个数有关。因此对于线段树而言,正因为有了这个翻转操作,我们仅仅维护一套1的信息还不够,还要在维护一套0的信息(s0l0r0m0)。

除此之外,由于本题涉及区间修改,我们还要维护两个懒标记:f = -1(f=0表示全部置为0,f=1表示全部置为1),rev = -1(rev = 2表示执行翻转操作)

代码实现:

cpp 复制代码
#include <iostream>
#include <algorithm>
using namespace std;

#define lc (p * 2)
#define rc (p * 2 + 1)

const int N = 1e5 + 10;
typedef long long LL;
int n, m;

int a[N];

struct node
{
	int l, r;
	LL s0, l0, r0, m0;
	LL s1, l1, r1, m1;
	int f, rev;
}tr[N * 4];

void pushup(node& p, node& l, node& r)
{
	p.s0 = l.s0 + r.s0;
	p.l0 = l.s1 == 0 ? (l.s0 + r.l0) : l.l0;
	p.r0 = r.s1 == 0 ? (r.s0 + l.r0) : r.r0;
	p.m0 = max(max(l.m0, r.m0), l.r0 + r.l0);

	p.s1 = l.s1 + r.s1;
	p.l1 = l.s0 == 0 ? (l.s1 + r.l1) : l.l1;
	p.r1 = r.s0 == 0 ? (r.s1 + l.r1) : r.r1;
	p.m1 = max(max(l.m1, r.m1), l.r1 + r.l1);
}

void build(int p, int l, int r)
{
	tr[p] = { l, r,
		a[l] ^ 1, a[l] ^ 1, a[l] ^ 1, a[l] ^ 1,
		a[l], a[l], a[l], a[l],
		-1, -1};

	if (l == r)
		return;

	int mid = (l + r) / 2;
	build(lc, l, mid);
	build(rc, mid + 1, r);
	pushup(tr[p], tr[lc], tr[rc]);
}

void lazy(int p, int op)
{
	int l = tr[p].l, r = tr[p].r;
	if (op == 0)
	{
		tr[p].l0 = tr[p].r0 = tr[p].m0 = tr[p].s0 = r - l + 1;
		tr[p].l1 = tr[p].r1 = tr[p].m1 = tr[p].s1 = 0;
		tr[p].f = 0;
		tr[p].rev = -1;
	}

	if (op == 1)
	{
		tr[p].l1 = tr[p].r1 = tr[p].m1 = tr[p].s1 = r - l + 1;
		tr[p].l0 = tr[p].r0 = tr[p].m0 = tr[p].s0 = 0;
		tr[p].f = 1;
		tr[p].rev = -1;
	}

	if (op == 2)
	{
		swap(tr[p].l0, tr[p].l1);
		swap(tr[p].r0, tr[p].r1);
		swap(tr[p].m0, tr[p].m1);
		swap(tr[p].s0, tr[p].s1);
		tr[p].rev = tr[p].rev == 2 ? -1 : 2;
	}
}

void pushdown(int p)
{
	lazy(lc, tr[p].f);
	lazy(rc, tr[p].f);
	lazy(lc, tr[p].rev);
	lazy(rc, tr[p].rev);

	tr[p].f = tr[p].rev = -1;
}

void modify(int p, int x, int y, int op)
{
	int l = tr[p].l, r = tr[p].r;
	if (x <= l && r <= y)
	{
		lazy(p, op);
		return;
	}

	pushdown(p);
	int mid = (l + r) / 2;
	if (x <= mid)
		modify(lc, x, y, op);
	if (y >= mid + 1)
		modify(rc, x, y, op);
	pushup(tr[p], tr[lc], tr[rc]);
}

node query(int p, int x, int y)
{
	int l = tr[p].l, r = tr[p].r;
	if (x <= l && r <= y)
	{
		return tr[p];
	}

	pushdown(p);
	int mid = (l + r) / 2;
	if (y <= mid)
		return query(lc, x, y);
	if (x >= mid + 1)
		return query(rc, x, y);

	node ret;
	node L = query(lc, x, y);
	node R = query(rc, x, y);
	pushup(ret, L, R);

	return ret;
}

int main()
{
	cin >> n >> m;
	for (int i = 1;i <= n;i++)
	{
		cin >> a[i];
	}

	build(1, 1, n);

	while (m--)
	{
		int op, l, r;
		cin >> op >> l >> r;
		l++;  r++;

		if (op < 3)
		{
			modify(1, l, r, op);
		}
		else if (op == 3)
		{
			cout << query(1, l, r).s1 << endl;
		}
		else
		{
			cout << query(1, l, r).m1 << endl;
		}
	}

	return 0;
}

二、线段树+剪枝

线段树在维护区间修改时,有些操作是无法"懒"下来的,比如对某个区间里的每一个数进行开平方的操作,因为我们根据原来存储的sum是无法直接得到新的sum'的,此时只能从上到下把所有的叶子结点修改完毕之后再向上返回。

但是,如果在修改的过程中发现整个区间修改到一定程度之后就无需修改了。那么可以通过剪枝操作优化区间修改。

这样的线段树也叫做势能线段树

2.1 上帝造题的七分钟

题目描述:

题目解析:

这道题乍一看是一个区间修改+区间查询的问题,但是仔细分析之后就会发现:区间修改操作是无法被"懒"下来的。这时如果再盲目使用线段树的话,单次区间修改,时间复杂度就会来到惊人的O(NlogN),甚至还不如暴力解法。

但是,这是不是意味着线段树就不能用了呢?我们再来仔细思考一下,对于1这个数而言,还有继续开根号下去的必要吗?显然没有!因为 = 1,继续开根号下去只是无效操作。那么对于某一个区间而言,它的区间最大值为1,那么从根节点修改到该区间所在的节点时,还有必要继续向下递归吗?完全没必要!因为区间最大值为1,那么该节点下面所有的分支节点都为1,此时就可以直接向上返回了!

因此,线段树要维护的信息除了l,r,sum之外,还需维护一个区间最大值max。

代码实现:

cpp 复制代码
#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;

#define lc (p * 2)
#define rc (p * 2 + 1)

const int N = 1e5 + 10;
typedef long long LL;

int n, m;
LL a[N];

struct node
{
	int l, r;
	LL sum, max;
}tr[N * 4];

void pushup(int p)
{
	tr[p].sum = tr[lc].sum + tr[rc].sum;
	tr[p].max = max(tr[lc].max, tr[rc].max);
}

void build(int p, int l, int r)
{
	tr[p] = { l,r,a[l],a[l] };
	if (l == r)
		return;

	int mid = (l + r) / 2;
	build(lc, l, mid);
	build(rc, mid + 1, r);
	pushup(p);
}

void modify(int p, int x, int y)
{
	// 剪枝
	if (tr[p].max == 1)
		return;

	int l = tr[p].l, r = tr[p].r;
	if (l == r)
	{
		tr[p].sum = sqrt(tr[p].sum);
		tr[p].max = sqrt(tr[p].max);
		return;
	}

	int mid = (l + r) / 2;
	if (x <= mid)
		modify(lc, x, y);
	if (y >= mid + 1)
		modify(rc, x, y);
	pushup(p);
}

LL query(int p, int x, int y)
{
	int l = tr[p].l, r = tr[p].r;
	if (x <= l && r <= y)
		return tr[p].sum;

	int mid = (l + r) / 2;
	LL ret = 0;
	if (x <= mid)
		ret += query(lc, x, y);
	if (y >= mid + 1)
		ret += query(rc, x, y);

	return ret;
}

int main()
{
	cin >> n;
	for (int i = 1;i <= n;i++)
		cin >> a[i];

	build(1, 1, n);

	cin >> m;
	while (m--)
	{
		int k, l, r;
		cin >> k >> l >> r;

		if (l > r)
			swap(l, r);

		if (k == 0)
			modify(1, l, r);
		else
			cout << query(1, l, r) << endl;
	}

	return 0;
}

2.2 The Child and Sequence

题目解析:

取模操作和开根号操作一样,区间修改是无法"懒"下来的。但是,当某个区间的最大值小于模数时,整个区间的数都没有必要再取模下去了,因此可以维护区间最大值进行剪枝。

代码实现:

cpp 复制代码
#include <iostream>
using namespace std;

#define lc (p * 2)
#define rc (p * 2 + 1)

const int N = 1e5 + 10;
typedef long long LL;

int n, m;
LL a[N];

struct node
{
	int l, r;
	LL sum, max;
}tr[N * 4];

void pushup(int p)
{
	tr[p].sum = tr[lc].sum + tr[rc].sum;
	tr[p].max = max(tr[lc].max, tr[rc].max);
}

void build(int p, int l, int r)
{
	tr[p] = { l,r,a[l],a[l] };
	if (l == r)
		return;

	int mid = (l + r) / 2;
	build(lc, l, mid);
	build(rc, mid + 1, r);
	pushup(p);
}

void modify_1(int p, int x, int y, LL k)
{
	if (tr[p].max < k)
		return;

	int l = tr[p].l, r = tr[p].r;
	if (l == r)
	{
		tr[p].sum %= k;
		tr[p].max %= k;
		return;
	}

	int mid = (l + r) / 2;
	if (x <= mid)
		modify_1(lc, x, y, k);
	if (y >= mid + 1)
		modify_1(rc, x, y, k);
	pushup(p);
}

void modify_2(int p, int x, LL k)
{
	int l = tr[p].l, r = tr[p].r;
	if (l == r)
	{
		tr[p].max = tr[p].sum = k;
		return;
	}

	int mid = (l + r) / 2;
	if (x <= mid)
		modify_2(lc, x, k);
	else
		modify_2(rc, x, k);

	pushup(p);
}

LL query(int p, int x, int y)
{
	int l = tr[p].l, r = tr[p].r;
	if (x <= l && r <= y)
	{
		return tr[p].sum;
	}

	int mid = (l + r) / 2;
	LL ret = 0;
	if (x <= mid)
		ret += query(lc, x, y);
	if (y > mid)
		ret += query(rc, x, y);

	return ret;
}

int main()
{
	cin >> n >> m;
	for (int i = 1;i <= n;i++)
		cin >> a[i];

	build(1, 1, n);

	while (m--)
	{
		int op;
		cin >> op;
		if (op == 1)
		{
			int l, r;
			cin >> l >> r;
			cout << query(1, l, r) << endl;
		}
		else if (op == 2)
		{
			int l, r;
			LL x;
			cin >> l >> r >> x;
			modify_1(1, l, r, x);
		}
		else
		{
			int k;
			LL x;
			cin >> k >> x;
			modify_2(1, k, x);
		}
	}

	return 0;
}

三、权值线段树+离散化

【引入】

对于一组数据,查询区间[x, y]之间每个数出现次数的和。

针对这样的问题,就可以使用权值线段树来解决。

【权值线段树】

相较于普通线段树,权值线段树维护区间内的数出现的次数。

节点的取件信息表示:数据的值域

节点的权值信息表示:这些数据一共出现的次数

注:实际做题中,数据的值域一般很大,如果仅仅考虑数的大小而不考虑具体的值,常常会先把原始数据进行离散化。

3.1 逆序对

题目描述:

题目解析:

我们以题目中的数组为例,数组的值域是1~6,如果现在要查找以数字3为结尾的逆序对,我们只需要关心数字4~6的个数即可,这时就可以用权值线段树来解决。

代码实现:

cpp 复制代码
#include <iostream>
#include <algorithm>
#include <unordered_map>
using namespace std;

#define lc (p * 2)
#define rc (p * 2 + 1)

const int N = 5e5 + 10;
typedef long long LL;

int n;
LL a[N], t[N];

int cnt;
unordered_map<int, int> mp;

struct node
{
	int l, r;
	LL cnt;
}tr[N];

void pushup(int p)
{
	tr[p].cnt = tr[lc].cnt + tr[rc].cnt;
}

void build(int p, int l, int r)
{
	tr[p] = { l,r,0 };
	if (l == r)
	{
		return;
	}

	int mid = (l + r) / 2;
	build(lc, l, mid);
	build(rc, mid + 1, r);
}

void modify(int p, int x)
{
	int l = tr[p].l, r = tr[p].r;
	if (l == r)
	{
		tr[p].cnt++;
		return;
	}

	int mid = (l + r) / 2;
	if (x <= mid)
		modify(lc, x);
	else
		modify(rc, x);

	pushup(p);
}

LL query(int p, int x, int y)
{
	int l = tr[p].l, r = tr[p].r;
	if (x <= l && r <= y)
	{
		return tr[p].cnt;
	}

	int mid = (l + r) / 2;
	LL ret = 0;
	if (x <= mid)
		ret += query(lc, x, y);
	if (y >= mid + 1)
		ret += query(rc, x, y);

	return ret;
}

int main()
{
	cin >> n;
	for (int i = 1;i <= n;i++)
	{
		cin >> a[i];
		t[i] = a[i];
	}

	sort(t + 1, t + 1 + n);
	for (int i = 1;i <= n;i++)
	{
		int x = t[i];
		if (mp.count(x))
			continue;

		mp[x] = ++cnt;
	}

	build(1, 1, cnt);

	LL ret = 0;
	for (int i = 1;i <= n;i++)
	{
		int x = mp[a[i]];
		ret += query(1, x + 1, cnt);
		modify(1, x);
	}
	cout << ret << endl;

	return 0;
}
相关推荐
嘻嘻哈哈樱桃2 小时前
牛客经典101题题解集--二叉树
java·数据结构·python·算法·leetcode·职场和发展
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 98. 验证二叉搜索树 | C++ 指针边界法
c++·算法·leetcode
AI科技星2 小时前
算子数学|独立完整学科章节(百条原创公式· ROOT传世定稿)
大数据·算法·机器学习·数学建模·数据挖掘·量子计算
斯维赤2 小时前
每天学习一个小算法:堆排序
学习·算法·排序算法
ncj3934379062 小时前
Canvas 图形开发高频算法面试题
算法·canvas
MediaTea2 小时前
AI 术语通俗词典:F1 值(分类)
人工智能·算法·机器学习·分类·数据挖掘
望舒3292 小时前
KMP算法
数据结构·算法
潇楠Web3哨兵2 小时前
桌面级Web3交易终端的底层炼狱:自研多源报价引擎、移除重型依赖、跨进程钱包桥接与强制安全拦截
算法·web3
贾斯汀玛尔斯2 小时前
每天学一个算法--回溯算法(Backtracking)
算法