- 【Update - 2020/4/24】更新了倍增优化部分
Description
一个 (1) 到 (N) 的排列 (S),(S) 的近似有序的区间满足如下性质:
- 是 (S) 的连续子序列。
- 第一个数是最小的。
- 最后一个数是最大的。
例如:(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;
}