P8600 [蓝桥杯 2013 省 B] 连号区间数 题解
析合树是一种连续段数据结构。
引入:
给定排列 $\{P_n\}$,求值域连续的段的个数。
概念
排列 是排列。
连续段 是值域连续的段,满足集合运算。
值域区间 是连续段值域的区间。
本原段 是不与其他连续段部分相交的连续段。
连续段集 $I_p$,本原段集 $M_p$。
形式化的,
定义序列 $P$ 是 $n$ 阶排列 $\Leftrightarrow|P|=n$ 且 $\forall i\in[1,n],i\in P$。
定义 $(P,[l,r])$ 是连续段 $\Leftrightarrow\nexists x,z\in[l,r],y\notin[l,r],P_x<P_y<P_z$。
连续段 $(P,[l,r])$ 满足 $[l,r]$ 上的集合运算。
定义 $(P,[l,r])$ 的值域区间为 $[\min\limits_{i=l}^rP_i,\max\limits_{i=l}^rP_i]$。
定义连续段集 $I_p=\{A|A\ \text{是连续段}\}$。
定义连续段 $X$ 是本原段 $\Leftrightarrow\forall A\in I_p,X\cap A=(P,\varnothing)$ 或 $X\subseteq A$ 或 $A\subseteq X$。
定义本原段集 $M_p=\{A|A\ \text{是本原段}\}$。
析点与合点
析合树的点集是 $M_p$。
概念
节点 $u$ 的 值域区间 为 $[u_l,u_r]$。
节点 $u$ 的 子节点序列 $S_u$ 是 $u$ 的极长真子连续段序列。
节点 $u$ 的 子节点排列 $P_u$ 是 $S_u$ 按值域区间左端点离散化的结果。
节点 $u$ 是 合点 当且仅当 $P_u$ 有序。
节点 $u$ 是 析点 当且仅当 $u$ 不是合点。
形式化的,
定义节点 $u$ 的值域区间为 $[u_l,u_r]$。
定义节点 $u$ 的子节点序列 $S_u=\{v|v\subsetneqq u,\nexists w\subsetneqq u,v\subsetneqq w\}$,且按 $v$ 的左端点排序。
注意,$v$ 的左端点不是 $v$ 的值域区间左端点。
定义节点 $u$ 的子节点排列 $P_{u,i}=|\{v_l\le{S_{u,i}}_l|v\in S_u\}|$。
定义节点 $u$ 是合点 $\Leftrightarrow\forall i\in[1,|S_u|],P_{u,i}=i$ 或 $\forall i\in[1,|S_u|],P_{u,i}=|S_u|-i+1$。
定义节点 $u$ 是析点 $\Leftrightarrow u$ 不是合点。
性质
$u$ 的子节点的并是 $u$。显然。
$u$ 是合点当且仅当 $S_u$ 的子段构成连续段。显然。
$u$ 是析点当且仅当 $S_u$ 的多元素真子段不构成连续段。
证明:若命题不成立,则 $S_u$ 上构成连续段的极长真子段构成本原段,而此本原段未在析合树中出现。
形式化的,
$\bigcup\limits_{v\in S_u}v=u$。
$u$ 是合点 $\Leftrightarrow\forall [l,r]\subseteq[1,|S_u|],\bigcup\limits_{i=l}^rS_{u,i}\in I_p$。
$u$ 是析点 $\Leftrightarrow\forall [l,r]\subseteq[1,|S_u|],l<r,\bigcup\limits_{i=l}^rS_{u,i}\notin I_p$。
建树
可以用 增量法 $O(n\log n)$ 建析合树。
将 $P_i$ 依次加入析合树,用一个栈维护 $P_j|j\in[1,i)$ 形成的析合森林。
策略
维护当前节点 $u$(初值为 $(P,[i,i])$),考虑 $u$ 与栈内节点的合并情况。
- $u$ 能成为栈顶节点的子节点 $\Leftrightarrow$ 栈顶节点是合点且 $u$ 与栈顶节点的最右子节点能合并成连续段,令 $u$ 成为栈顶节点的子节点,然后令 $u\gets$ 栈顶节点。
- $u$ 不能成为栈顶节点的子节点,令 $u$ 与栈顶若干节点合并成连续段 $v$ 且 $|S_v|$ 最小。考虑 $v$ 的类型,容易发现此时 $v$ 是合点 $\Leftrightarrow |S_v|=2$。令 $u\gets v$。
- 重复上述方案直到不能进行(即找不到 2. 中合法的 $v$),将 $u$ 入栈。
(来自 CF,对 $\{9,1,10,3,2,5,7,6,8,4\}$ 建析合树)
根据上述策略,我们需要快速判断 $u$ 与一些点能否合并成连续段,即判断任意后缀 $[j,i]|j\in[1,i)$ 是否连续段。
子问题
考虑如何快速判断 $[j,i]|j\in[1,i)$ 是否连续段。
$P$ 是排列,所以 $[j,i]$ 是连续段当且仅当 $\max\limits_{j\le k\le i}-\min\limits_{j\le k\le i}=i-j$。
维护 $Q_j=\max\limits_{j\le k\le i}-\min\limits_{j\le k\le i}-i+j|j\in[1,i)$,则 $[j,i]$ 是连续段当且仅当 $Q_j=0$。
考虑更新 $i$ 时如何快速更新 $Q$。
在 $\max\limits_{j\le k\le i}-\min\limits_{j\le k\le i}-i+j$ 中,$j$ 是不变的,每次更新 $i$ 对 $Q_j$ 的贡献减一,最值的贡献不好维护。
设 $P_i$ 更新了 $B_i$ 个后缀最值的位置,注意到 $\sum\limits_{i=1}^nB_i=O(n)$(单调栈结论)。
用单调栈维护后缀最值的位置,以后缀最小值为例。
维护单调递增的栈 $x$。不难发现 $P_{x_i}$ 是后缀 $[j,i]|j\in(x_{i-1},x_i]$ 的最小值。
$x_i$ 出栈时失去对 $Q_j|j\in(x_{i-1},x_i]$ 的贡献,所以 $Q_j|j\in(x_{i-1},x_i]\gets Q_j+P_{x_i}$。
$i$ 入栈时获得对 $Q_j|j\in(x_{top},i]$ 的贡献,所以 $Q_j|j\in(x_{top},i]\gets Q_j-P_i$。
后缀最大值同理。
关于本题
考虑析合树内每个点的贡献。
由合点的性质,若 $u$ 是合点,则 $S_u$ 的任意子段构成连续段,即 $u$ 的贡献为 $|S_u|\choose2$。
由析点的性质,若 $u$ 是析点,则 $S_u$ 的任意多元素子段不构成连续段,即 $u$ 的贡献为 $1$。
注意到 $|M_p|=O(n)$,所以时间复杂度 $O(n\log n)$。
#include <cstdio>
#include <vector>
#define A(u, v) u->c.push_back(v)
using namespace std;
int n, L, X, Y, a[50050], x[50050], y[50050];
long long q;
struct S
{
int l, r;
bool b;
vector<S *> c;
} * u, *f, *s[50050];
struct T
{
T *l, *r;
int s, t, v, x;
T(int p, int q) : s(p), t(q), v(0), x(0) {}
void f(int p)
{
v += p;
x += p;
}
void u() { v = min(l->v, r->v); }
void d()
{
l->f(x);
r->f(x);
x = 0;
}
} * r;
void D(S *p)
{
q += p->b ? 1ll * p->c.size() * (p->c.size() - 1) >> 1 : 1;
for (auto c : p->c)
D(c);
}
void B(int s, int t, T *&p)
{
p = new T(s, t);
if (s != t)
{
int m = p->s + p->t >> 1;
B(s, m, p->l);
B(m + 1, t, p->r);
p->u();
}
}
void M(int l, int r, int x, T *p)
{
if (l <= p->s && p->t <= r)
return p->f(x);
p->d();
int m = p->s + p->t >> 1;
if (l <= m)
M(l, r, x, p->l);
if (r > m)
M(l, r, x, p->r);
p->u();
}
bool Q(int l, T *p)
{
if (p->s == p->t)
return !p->v;
p->d();
int m = p->s + p->t >> 1;
return l <= m ? Q(l, p->l) : Q(l, p->r);
}
int Z(T *p)
{
if (p->s == p->t)
return p->s;
p->d();
int m = p->s + p->t >> 1;
return p->l->v ? Z(p->r) : Z(p->l);
}
int main()
{
scanf("%d", &n);
B(1, n, r);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = 1, p, o; i <= n; ++i)
{
while (X && a[i] <= a[x[X]])
M(x[X - 1] + 1, x[X], a[x[X]], r), --X;
while (Y && a[i] >= a[y[Y]])
M(y[Y - 1] + 1, y[Y], -a[y[Y]], r), --Y;
M(x[X] + 1, i, -a[i], r);
M(y[Y] + 1, i, a[i], r);
x[++X] = y[++Y] = i;
u = new S{i, i, 0};
p = Z(r);
while (L && s[L]->l >= p)
if (s[L]->b && Q(s[L]->c.back()->l, r))
s[L]->r = i, A(s[L], u), u = s[L--];
else
{
f = new S{0, i, Q(s[o = L]->l, r)};
while (!Q(s[L]->l, r))
--L;
for (int j = L; j <= o; ++j)
A(f, s[j]);
f->l = s[L--]->l;
A(f, u);
u = f;
}
s[++L] = u;
M(1, i, -1, r);
}
return D(s[1]), printf("%lld", q), 0;
}