析合树学习笔记(线性建树方法
析合树是一类用于维护排列上连续段的结构。
其中连续段定义为一个区间 \([l,r]\),其中的值在值域上连续,也即满足 \(\max-\min=r-l\)。
一个序列中最多有 \(n^2\) 个连续段,而析合树可以将其用 \(O(n)\) 个节点表示出来。
析合树中每个点都代表一个原序列上的连续段。点分为四类:
- 叶子。
- 析点:不存在一些儿子,其并是连续段。
- 顺点:任意连续的儿子都能组成连续段,且其值域递增。
- 逆点:任意连续的儿子都能组成连续段,且其值域递减。
其中顺点与逆点统称为合点。
我们不难证明任意一类点的儿子必为另外三类之一,否则可以与父亲合并。
儿子数为 \(k\) 的合点代表了原序列上 \(k^2\) 个连续段。
以下是一种线性建树方法:
我们增量构造,从左到右加入每一个位置 \(r\),并维护当前所有段。
过程中维护一个候选集,表示可能成为新段左端点的位置 \(q\)。
同时在值域上用并查集 \(T\) 维护已经被加入的所有数由相邻关系构成的连通块。
当我们准备压栈一个新的段 \(P[l,r]\) 时,从大往小枚举候选列表中的候选左端点 \(q\):
- 若 \(P\) 与 \(v_q\) 在 \(T\) 上连通,假如 \([q,r]\) 为一个合法的段,则合并为新段 \(N[q,r]\),作为新的 \(P\) 重复这个过程。否则我们从候选列表中删去 \(q\),下次不再枚举。正确性可以考虑:由于已经在 \(T\) 上连通,之后加入的任何 \(v_{r'}\) 都不在其值域内,若 \(r\) 不合法,则不存在更大的 \(r'\) 令其合法。
- 否则不联通,则任何 \(q'\le q\) 均不能使 \([q',r]\) 合法,我们直接结束当前过程。
每个段只会被压入和弹出候选列表 \(1\) 次,复杂度 \(\mathcal O(n)\)。
在过程中不难直接维护 \([q,r]\) 的值域,即可绕开四毛子做到总复杂度线性。
直接讲可能有点难懂,这里有代码,可以参考一下(x
code
const int N = 2e5 + 7;
#define rep(i,a,b) for(int i(a);i<=(b);++i)
enum TYPE {
LEAF, INCR, DECR, PRIM
};
int n, v[N], m;
struct node { TYPE t; int st, ed, x, y, fa; } w[N];
struct info { int q, x, y; };
std::stack<info> ca; std::stack<int> st;
std::basic_string<int> g[N];
struct DSU {
int fa[N];
int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }
void merge(int x, int y) { fa[find(x)] = find(y); }
bool check(int x, int y) { return find(x) == find(y); }
} dsu;
int root, rt[N], active[N];
void build() {
rep(i, 1, n) dsu.fa[i] = i;
rep(i, 1, n) {
int a = v[i], X = a, Y = a, p = ++m;
w[p] = {LEAF, i, i, a, a, 0}, rt[i] = p, active[a] = 1;
if(a > 1 && active[a-1]) dsu.merge(a, a-1);
if(a < n && active[a+1]) dsu.merge(a, a+1);
while(!st.empty()) {
int pos = 0;
while(!ca.empty()) {
auto& [q, x, y] = ca.top();
x = X = std::min(x, X), y = Y = std::max(y, Y);
if(!dsu.check(v[q], a)) break;
ca.pop();
if(i - q == Y - X) { pos = q; break; }
}
if(!pos) break;
int t = st.top();
if(w[t].t == DECR && w[p].y + 1 == w[t].x || w[t].t == INCR && w[t].y + 1 == w[p].x) {
w[p].fa = t, w[t].ed = w[p].ed;
if(w[t].t == DECR) w[t].x = w[p].x;
else w[t].y = w[p].y;
st.pop(), p = t;
} else { // new
int F = ++m; w[p].fa = F;
if(pos == w[t].st) w[F].t = w[t].y > w[p].y ? DECR : INCR;
else w[F].t = PRIM;
for(; w[F].st != pos; st.pop())
t = st.top(), w[F].st = w[t].st, w[t].fa = F;
w[F].ed = w[p].ed, w[F].x = X, w[F].y = Y, p = F;
}
}
ca.push({w[p].st, w[p].x, w[p].y}), st.push(p);
}
root = st.top();
rep(i, 1, m) g[w[i].fa] += i;
}
P4747 [CERC2017] Intrinsic Interval
我们直接在析合树上找到两个点的 LCA,析点直接输出,合点就二分一下儿子即可。
CF997E Good Subsegments
我们考虑合点贡献 \(k^2\) 种方案,当包含 \(k\) 个儿子时。析点只在完全被包含时贡献一种。
那么我们直接倍增,维护跳到 lca 的过程上的贡献,lca 处额外处理一下即可。
CF526F Pudding Monsters
上一题的全局弱化版(?
CF1205F Beauty of a Permutation
这个题要我们构造一颗析合树。
我们考虑枚举每个点是合点/析点,以及其儿子数 \(d\)。
若为合点,有 \(d\ge 2\),贡献 \({d\choose 2}\) 种方案;若为析点,有 \(d\ge 4\),贡献 \(d\) 种方案。
由于方案数只和每个点的儿子数有关,树的结构是无关紧要的,那么我们考虑构造一条链,在每个点下额外挂 \(d_i-1\) 个儿子即可构造一种合法方案。
那么可以直接背包 dp 即可求出是否可行以及状态之间的转移路径。构造考虑从上往下构造。
如果要求合点,直接放连续 \(k\) 个数;如果要求析点,考虑间隔放置,类似 9 1 8 2 7 3 [4-6]。
但是最后一个数可能和后面连起来,我们交换倒数第一和第三个即可 9 1 8 3 7 2 [4-6]。
时间复杂度 \(\mathcal O(n^2d)\)
code
#include <algorithm>
#include <iostream>
const int N = 107, M = 5057;
int n, k;
std::pair<int, int> dp[N][M]; // used n nodes & m ranges
int ans[N], m;
void construct(int n, int k, int l, int r, int d) {
// d: last is r+1(1) / l-1(0)
if(n == 1) {
ans[++m] = l;
return ;
}
auto [w, dt] = dp[n][k];
if(w > 0) { // join
if(d) {
for(int i = l; i <= l+w-1; ++i)
ans[++m] = i;
construct(n - w, dt, l+w, r, 0);
} else {
for(int i = r; i >= r-w+1; --i)
ans[++m] = i;
construct(n - w, dt, l, r-w, 1);
}
} else { // cut
w = -w; int fw = w / 2;
if(d) {
for(int i = 1; i <= fw; ++i) {
ans[++m] = l + fw - i;
ans[++m] = r - fw + i;
}
if(w & 1) ans[++m] = l + fw, std::swap(ans[m], ans[m-2]);
construct(n - w, dt, l + fw + (w & 1), r - fw, 1);
} else {
for(int i = 1; i <= fw; ++i) {
ans[++m] = r - fw + i;
ans[++m] = l + fw - i;
}
if(w & 1) ans[++m] = r - fw, std::swap(ans[m], ans[m-2]);
construct(n - w, dt, l + fw, r - fw - (w & 1), 1);
}
}
}
inline void solve() {
std::cin >> n >> k;
m = 0;
if(dp[n][k].first) {
std::cout << "YES\n";
construct(n, k, 1, n, 0);
for(int i = 1; i <= n; ++i)
std::cout << ans[i] << " ";
std::cout << "\n";
} else std::cout << "NO\n";
}
int main() {
std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
dp[1][1] = {1, 0};
int n = 100, k = 5050;
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= k; ++j) {
if(!dp[i][j].first) continue;
for(int w = 1; i + w <= n; ++w) {
int n1 = j + (w + 1) * (w + 2) / 2 - 1;
if(w >= 1 && n1 <= k)
dp[i+w][n1] = {w, j}; // join
int n2 = j + w + 1;
if(w >= 3 && n2 <= k)
dp[i+w][n2] = {-w, j}; // cut
}
}
}
int q; std::cin >> q; while(q--) solve();
}
本文来自博客园,作者:CuteNess,转载请注明原文链接:https://www.cnblogs.com/CuteNess/p/21751073
浙公网安备 33010602011771号