「线段树维护栈/单调栈」学习笔记

「线段树维护栈/单调栈」学习笔记

前言

俗话说的好:

线段树玩得好,暴踩某人陀螺Treap

线段树玩得六,暴碾聋跌随机数

用途

经常用于处理序列问题,这个序列经常会带一些性质,如:

  • 前面的会对后面的造成影响

  • 序列必须满足一些性质(递增、递减等等)

拥有线段树的优良性质:可支持单点/区间修改、单点/区间查询。

至于它怎么和栈扯上关系的,\(emmmm\),大概暴力可以用栈做。

特点

最为突出的特点是与平常线段树的 \(pushup\) 差别很大,根据不同问题的描述,\(pushup\) 也各有千秋。

例题

陶陶摘苹果

题目描述

陶陶家门前有一颗苹果树,树上有 \(n\) 个苹果,每个苹果有不同的高度 \(h_i\),形成了一个长度为 \(n\) 的序列,之后从左往右开始摘苹果。但是,陶陶只会摘高度严格递增的苹果,陶陶会摘多少个苹果呢?

陶陶眼瞎,看错了一个苹果的高度,可能有 \(m\) 种情况,并回答这 \(m\) 种情况的答案。

输入格式

第一行两个整数 \(n, m\)

第二行有 \(n\) 个整数表示 \(h_i(1\leq h_i\leq 10^9)\)

接下来 \(m\) 行,每行两个整数 \(p, h_p\),表示第 \(p\) 个位置的苹果的实际高度是 \(h_p\)

输出格式

输出共 \(m\) 行,每行一个整数,表示这种情况下的答案。

样例输入

5 3
1 2 3 4 4
1 5
5 5
2 3

样例输出

1
5
3

简化问题

实际上就是求一个最长的单调递增序列,并且要支持修改。

暴力

很好想吧,从第 \(1\) 个往后扫,记一个当前的最高高度,扫到一个比它高的,答案加 \(1\),并更新最高高度。

复杂度:\(\Theta (nm)\)

仔细剖析

我们会发现:

  • 一个序列左边的值越大,右边的值受到的限制也就越大,被统计到的元素也就越少,也就是:左边的元素会对右边的元素造成影响。

看一眼数据范围:\(1e5\)

很明显需要 \(nlog_n\) 做法,又是一个序列,又符合我们上面所说的一些性质,考虑打到线段树上。

线段树

既然我们将它打到了线段树上:

  • 单点修改,不考虑 \(lazy\) 标记和 \(pushdown\) 等操作。

现在我们需要思考的就是如何写这个 \(pushup\)


我们可以维护一个区间最大值 \(maxval\)

可以发现:我们只需要求出右区间大于左区间最大值的单调递增序列的最长长度即可。

这样我们可以维护一个最长递增序列长度 \(len\)

我们引用一个 \(Calc\) 函数,用来求出线段树上一个节点(序列上一个区间),比 \(val\) 大的最长递增序列长度。

分为三种情况:

  • 这个区间长度为 \(1\),只需判断这个值是不是大于 \(val\) 即可,返回长度为 \(1/0\)

  • 左区间的最大值大于了 \(val\),那么我们只需继续递归左区间,加上右儿子的 \(len\) 即可。

  • 左区间的最大值小于/等于 \(val\),左区间就没有用了,不可能会作出贡献,我们只需继续递归讨论右区间即可。

inline int Calc (register int rt, register int l, register int r, register int val) { // 分别对应上面的三种情况
	register int mid = (l + r) >> 1;
	if (l == r) return tree[rt].maxval > val;
	else if (tree[rt << 1].maxval > val) return Calc (rt << 1, l, mid, val) + tree[rt << 1 | 1].len;
	else return Calc (rt << 1 | 1, mid + 1, r, val);
}

由于只会是左区间会对右区间造成影响,\(pushup\) 时,只需更新右区间的 \(len\) 即可。

inline void Pushup (register int rt, register int l, register int r) {
	register int mid = (l + r) >> 1;
	tree[rt].maxval = max (tree[rt << 1].maxval, tree[rt << 1 | 1].maxval);
	tree[rt << 1 | 1].len = Calc (rt << 1 | 1, mid + 1, r, tree[rt << 1].maxval); // 以左区间的最大值为基准,递归右区间
}

考虑如何统计答案。

我们会发现,我们只记录了每个右区间的最长递增序列长度。

我们要求最长的递增序列长度,可以用一下我们那个 \(Calc\) 函数,直接查询 \(Calc (1, 1, n, -1)\) 即可。

代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int maxn = 1e5 + 50, INF = 0x3f3f3f3f;

inline int read () {
	register int x = 0, w = 1;
	register char ch = getchar ();
	for (; ch < '0' || ch > '9'; ch = getchar ()) if (ch == '-') w = -1;
	for (; ch >= '0' && ch <= '9'; ch = getchar ()) x = x * 10 + ch - '0';
	return x * w;
}

inline void write (register int x) {
	if (x / 10) write (x / 10);
	putchar (x % 10 + '0');
}

int n, m;
int a[maxn];

struct Tree {
	int maxval, len;
} tree[maxn << 2];

inline int Calc (register int rt, register int l, register int r, register int val) { 
	register int mid = (l + r) >> 1;
	if (l == r) return tree[rt].maxval > val;
	else if (tree[rt << 1].maxval > val) return Calc (rt << 1, l, mid, val) + tree[rt << 1 | 1].len;
	else return Calc (rt << 1 | 1, mid + 1, r, val);
}

inline void Pushup (register int rt, register int l, register int r) {
	register int mid = (l + r) >> 1;
	tree[rt].maxval = max (tree[rt << 1].maxval, tree[rt << 1 | 1].maxval);
	tree[rt << 1 | 1].len = Calc (rt << 1 | 1, mid + 1, r, tree[rt << 1].maxval); 
}

inline void Build (register int rt, register int l, register int r) {
	if (l == r) {
		tree[rt].maxval = a[l];
		return;
	}
	register int mid = (l + r) >> 1;
	Build (rt << 1, l, mid);
	Build (rt << 1 | 1, mid + 1, r);
	Pushup (rt, l, r);	
}

inline void Modify (register int rt, register int l, register int r, register int pos, register int val) {
	if (l == r) {
		tree[rt].maxval = val;
		return;
	}
	register int mid = (l + r) >> 1;
	if (pos <= mid) Modify (rt << 1, l, mid, pos, val);
	else Modify (rt << 1 | 1, mid + 1, r, pos, val);
	Pushup (rt, l, r);
}

int main () {
	freopen ("taopapp.in", "r", stdin);
	freopen ("taopapp.out", "w", stdout);
	n = read(), m = read();
	for (register int i = 1; i <= n; i ++) a[i] = read();
	Build (1, 1, n);
	while (m --) {
		register int pos = read(), val = read();
		Modify (1, 1, n, pos, val);
		printf ("%d\n", Calc (1, 1, n, -1));
		Modify (1, 1, n, pos, a[pos]);
	}
	return 0;
}

P4198 楼房重建

某谷P4198

思路

很简单,其实跟上个题一样。

要满足题目中的条件,可以发现:我们要求的就是斜率严格递增的最长序列。

直接套上面的板子即可。

代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int maxn = 1e5 + 50, INF = 0x3f3f3f3f;

inline int read () {
	register int x = 0, w = 1;
	register char ch = getchar ();
	for (; ch < '0' || ch > '9'; ch = getchar ()) if (ch == '-') w = -1;
	for (; ch >= '0' && ch <= '9'; ch = getchar ()) x = x * 10 + ch - '0';
	return x * w;
}

inline void write (register int x) {
	if (x / 10) write (x / 10);
	putchar (x % 10 + '0');
}

int n, m;

struct Tree {
	double maxk; // 斜率
	int len;
} tree[maxn << 2];

inline int Calc (register int rt, register int l, register int r, register double lastk) {
	register int mid = (l + r) >> 1;
	if (l == r) return tree[rt].maxk > lastk;
	else if (tree[rt << 1].maxk > lastk) return Calc (rt << 1, l, mid, lastk) + tree[rt << 1 | 1].len;
	else return Calc (rt << 1 | 1, mid + 1, r, lastk);
}

inline void Pushup (register int rt, register int l, register int r) {
	register int mid = (l + r) >> 1;
	tree[rt].maxk = max (tree[rt << 1].maxk, tree[rt << 1 | 1].maxk);
	tree[rt << 1 | 1].len = Calc (rt << 1 | 1, mid + 1, r, tree[rt << 1].maxk);
}

inline void Modify (register int rt, register int l, register int r, register int pos, register int val) {
	if (l == r) {
		tree[rt].maxk = (double) val / pos;
		return;
	}
	register int mid = (l + r) >> 1;
	if (pos <= mid) Modify (rt << 1, l, mid, pos, val);
	else Modify (rt << 1 | 1, mid + 1, r, pos, val);
	Pushup (rt, l, r);
}

int main () {
	n = read(), m = read();
	while (m --) {
		register int pos = read(), val = read();
		Modify (1, 1, n, pos, val);
		printf ("%d\n", Calc (1, 1, n, 0.0));
	}
	return 0;
}

总结

其实这个东西没什么难的,只是根据题意来写出相应的 \(pushup\) 操作,具体问题具体分析即可。

posted @ 2020-10-22 18:39  Rubyonlу  阅读(839)  评论(5编辑  收藏  举报