「51Nod

  • 【Update - 2020/4/24】更新了倍增优化部分

Description

一个 (1)(N) 的排列 (S)(S) 的近似有序的区间满足如下性质:

  1. (S) 的连续子序列。
  2. 第一个数是最小的。
  3. 最后一个数是最大的。

例如:(S = {3, 1, 2, 5, 4})(S) 的所有近似有序区间为: ({3}, {1}, {1, 2}, {1, 2, 5}, {2}, {2, 5}, {5}, {4})

给出 (S),求 (S) 的近似有序区间的数量。

Hint

(1le Nle 5 imes 10^4)

Solution

这题,我们可以对于数列每一个位置 (i) 分别计算 (i) 为左端点的近似有序区间的个数,最后求和。

首先,可以明确的是,右端点可能出现的位置必定在 第一个小于第 (i) 个数的位置((operatorname{lower}(i)) 之前。

然后,先找到 第一个大于第 (i) 个数的位置((operatorname{upper}(i)),然后一直往 (operatorname{upper}) 的方向跳,边跳边使答案自增,超过 (operatorname{lower}(i)) 的时候停止。

为什么这么跳就可以找到答案?

题中规定最左边必须最小,那么限定在 (operatorname{lower}(i)) 之前即可,之间的不可能有比第 (i) 个更小的。

题中又规定最右边必须最大,那么下一个跳的位置不可能是比当前位置小的,并且第一个满足条件的就是 (operatorname{upper})

为什么这样不会超时?

显然这个算法会被形如 ({1,2,cdots,N}) 的数据卡成 (O(n^2))不好意思然而数据水。

如何优化?

肉眼可见瓶颈在 跳 (operatorname{upper}) 的地方可能会达到一次 (O(n)), 这就是超时的主要原因。因此可以试试优化这一部分。

这么实现快速的往上跳? 倍增!!

回忆一下 倍增 LCA 的做法,和这一个差不多。

首先建树。对于第 (i) 号结点,它的父结点为 (operatorname{upper}(i)) 。如果 (operatorname{upper}) 方向不存在就设成 (n + 1)。最后我们得到了一颗节点数为 (n + 1) 且根为第 (n+1) 号结点的树。

于是整个跳 (operatorname{upper}) 的过程变成了树上跳祖先——找到深度最小的且结点编号小于 (operatorname{lower}) 的祖先。

之后预处理父结点表,方法和 倍增 LCA 基本一致: (f(i,j)) 表示结点 (i) 的第 (2^j) 级祖先。然后显然 (f(i,j)=f(f(i,j-1),j-1)),如果不理解可以看看 倍增 LCA 的做法。

最后统计答案,对于一个结点 (i) ,倍增往上跳,设最终跳到了结点 (k),那么就将答案加上 (operatorname{dep}(i) - operatorname{dep}(i) + 1) 。((operatorname{dep}) 表示结点的深度)

复杂度是非常优秀的 (O(nlog n))

Code

优化前:

/*
 * Author : _Wallace_
 * Source : https://www.cnblogs.com/-Wallace-/
 * Problem : 51Nod - 1249 近似有序区间
 */
#include <iostream>
#include <stack>
using namespace std;

const int N = 5e4 + 5;
stack<int> st;
int lower[N], upper[N], A[N];
int n;

void calcBound() {
	while (!st.empty()) st.pop();
	for (register int i = n; i; i --) {
		while (!st.empty() && A[st.top()] > A[i]) st.pop();
		lower[i] = st.empty() ? n + 1 : st.top();
		st.push(i);
	}
	
	while (!st.empty()) st.pop();
	for (register int i = n; i; i --) {
		while (!st.empty() && A[st.top()] < A[i]) st.pop();
		upper[i] = st.empty() ? n + 1 : st.top();
		st.push(i);
	}
}

int calcPairs() {
	int ret = n;
	for (register int i = 1; i <= n; i ++)
		for (register int j = upper[i]; j < lower[i]; j = upper[j]) ret ++;
	return ret;
}

signed main() {
	cin >> n;
	for (register int i = 1; i <= n; i ++)
		cin >> A[i];
	calcBound();
	cout << calcPairs() << endl;
	return 0;
}

优化后:

/*
 * Author : _Wallace_
 * Source : https://www.cnblogs.com/-Wallace-/
 * Problem : 51Nod - 1249 近似有序区间
 */
#include <iostream>
#include <cstring>
#include <vector>
#include <stack>
using namespace std;

const int N = 5e4 + 5;
const int LogN = 25;

stack<int> st;
int lower[N], upper[N];
int n, A[N];

void calcBound() {
	while (!st.empty()) st.pop();
	for (register int i = n; i; i --) {
		while (!st.empty() && A[st.top()] > A[i]) st.pop();
		lower[i] = st.empty() ? n + 1 : st.top();
		st.push(i);
	}
	
	while (!st.empty()) st.pop();
	for (register int i = n; i; i --) {
		while (!st.empty() && A[st.top()] < A[i]) st.pop();
		upper[i] = st.empty() ? n + 1 : st.top();
		st.push(i);
	}
}

vector<int> G[N];
int f[N][LogN], dep[N];

void buildTable(int rt, int fa) {
	dep[rt] = dep[fa] + 1, f[rt][0] = fa;
	
	for (register int i = 1; (1 << i) <= dep[rt]; i ++)
		f[rt][i] = f[f[rt][i - 1]][i - 1];
	
	for (vector<int>::iterator it = G[rt].begin(); it != G[rt].end(); it ++)
		buildTable(*it, rt);
}

void buildTree() {
	for (register int i = 1; i <= n; i ++)
		G[upper[i]].push_back(i);
	buildTable(n + 1, 0);
}

long long calcPairs() {
	long long ret = 0ll;
	for (register int i = 1; i <= n; i ++) {
		int k = i;
		for (register int j = LogN - 1; j >= 0; j --) {
			if (!f[k][j] || f[k][j] >= lower[i]) continue;
			k = f[k][j];
		}
		ret += dep[i] - dep[k] + 1;
	}
	return ret;
}

signed main() {
	cin >> n;
	for (register int i = 1; i <= n; i ++) cin >> A[i];
	calcBound();
	buildTree();
	cout << calcPairs() << endl;
	return 0;
}

原文地址:https://www.cnblogs.com/-Wallace-/p/12731293.html