析合树学习笔记(线性建树方法

析合树是一类用于维护排列上连续段的结构。

其中连续段定义为一个区间 \([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();
}
posted @ 2026-07-21 20:46  CuteNess  阅读(1)  评论(1)    收藏  举报