Divide and Conquer

Divide and Conquerhttps://qoj.ac/contest/4113/problem/20239

来源: 2026 icpc网络预选赛2 D题

题目概述

给定一棵深度为 \(n\) 的完美二叉树(线段树),叶子节点数量为 \(M = 2^n\),节点总数 \(N = 2^{n+1} - 1\)。叶子节点的值必须是 \(1,2,\dots,M\) 的一个排列。每个内部节点的值等于其两个子节点值的最大值。现在有 \(q\) 个约束 \((u_i, x_i)\),要求节点 \(u_i\) 的值恰好为 \(x_i\)。求满足所有约束的叶子排列数量,对 \(998244353\) 取模。

关键观察

  1. 叶子值是一个排列:每个值 \(1 \dots M\) 恰好出现一次。
  2. 内部节点的值是其子树中叶子值的最大值。
  3. 若节点 \(u\) 被约束为 \(x\),则:
    • \(u\) 子树中所有叶子值 \(\le x\);
    • \(u\) 子树中至少有一个叶子值恰好为 \(x\);
    • \(u\) 的祖先节点值 \(\ge x\)。

预处理与合法性检查

1. 根节点约束

根节点(节点 1)的值必然是 \(M\)(所有叶子中的最大值)。若题目已给根节点约束且不是 \(M\),直接无解。我们将根节点的约束设为 \(M\)。

2. 计算子树叶子数 \(sz[u]\)

  • 叶子节点(\(u \ge M\)):\(sz[u] = 1\);
  • 内部节点(\(u < M\)):\(sz[u] = sz[2u] + sz[2u+1]\)。

3. 计算每个节点的值上限 \(up[u]\)

从根向下传递:

  • \(up[1] = M\);
  • 若节点 \(u\) 有约束 \(x\),则 \(up[u] = x\),并且必须满足 \(x \le up[\text{parent}]\);
  • 若无约束,则 \(up[u] = up[\text{parent}]\)。

同时检查:对每个有约束的节点 \(u\),必须满足 \(sz[u] \le x \le M\)。

4. 同一节点多个约束

若同一节点给出不同的 \(x\),无解。

5. 相同约束值的节点必须形成祖先-后代链

因为值 \(x\) 只能出现一次,如果多个节点都约束为 \(x\),它们必须位于同一条从祖先到后代的路径上。否则,两个不相交的子树都要求最大值为 \(x\),矛盾。

对于每个值 \(v\),记录约束为 \(v\) 的最深节点 \(deepest[v]\)。

计数原理:从大到小分配值

我们按值从大到小(\(v = M\) 到 \(1\))依次为每个值选择一个叶子来放置。每一步的选择数相乘,即为总方案数。

1. 计算 \(free[u]\)

\(free[u]\) 表示节点 \(u\) 子树中,\(up\) 值恰好等于 \(up[u]\) 的叶子数量。

计算方法:

  • 叶子节点:\(free[u] = 1\);
  • 内部节点:\(free[u] = \sum_{v \in \{2u, 2u+1\}} free[v] \cdot [up[v] == up[u]]\)。

用途:当某个约束要求节点 \(u\) 的值为 \(x\)(即 \(up[u] = x\))时,值 \(x\) 必须由 \(u\) 子树中某个叶子承担,而只有 \(up\) 恰好等于 \(x\) 的叶子才能取到 \(x\)。所以 \(free[u]\) 就是能承担这个约束值 \(x\) 的候选叶子数。

2. 计算全局 \(cnt[v]\) 与后缀和 \(suf[v]\)

  • \(cnt[v]\):整棵树中 \(up\) 值等于 \(v\) 的叶子数量。
  • \(suf[v] = \sum_{k \ge v} cnt[k]\):所有 \(up \ge v\) 的叶子总数,即有能力承担值 \(v\) 的叶子数量。

3. 从大到小分配

维护变量 \(used\),表示已经分配了值的叶子数量。

对于当前值 \(v\):

  • 情况 A:\(v\) 是某个约束节点的值(即 \(deepest[v] \ne -1\))

    • 值 \(v\) 必须分配给 \(deepest[v]\) 子树中 \(up == v\) 的叶子。
    • 可选叶子数为 \(free[deepest[v]]\)。
    • 方案数乘上 \(free[deepest[v]]\)。
    • \(used \gets used + 1\)。
  • 情况 B:\(v\) 不是任何约束节点的值

    • 值 \(v\) 可以分配给任何 \(up \ge v\) 且尚未被分配值的叶子。
    • 这些叶子的总数是 \(suf[v]\)。
    • 已经分配了 \(used\) 个叶子,它们都满足 \(up \ge v\),所以剩余可用叶子数为 \(suf[v] - used\)。
    • 方案数乘上 \((suf[v] - used)\)。
    • \(used \gets used + 1\)。

最后将所有方案数相乘,并对 \(998244353\) 取模。

正确性说明

  • 从大到小分配,保证大值优先占用那些“只有大 \(up\) 才能承担”的叶子。
  • 对于约束值 \(v\),只有 \(up\) 恰好等于 \(v\) 的叶子能取 \(v\),因此必须从 \(free[deepest[v]]\) 中选一个。
  • 对于非约束值 \(v\),任何 \(up \ge v\) 的未分配叶子都可以取 \(v\),因为它们的上限允许,且不会影响其他约束。
  • 每一步的选择都是独立的,直接相乘即可。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 998244353;

void solve() {
    int n, q; cin >> n >> q;
    int N = (1 << (n + 1)) - 1;//总数
    int M = (1 << n);//叶子结点数量
    vector<int> limit(N + 1, -1); limit[1] = M;
    bool repeat = false;
    for(int i = 0; i < q; i ++) {
        int u, x; cin >> u >> x;
        if(limit[u] != -1 && limit[u] != x) repeat = true;
        limit[u] = x;
    }
    if(repeat) {cout << "0\n"; return;}

    vector<int> sz(N + 1, 1);
    vector<int> dep(N + 1, 0);
    for(int i = M - 1; i >= 1; i --) sz[i] = sz[i * 2] + sz[i * 2 + 1];
    for(int i = 2; i <= N; i ++) dep[i] = dep[i / 2] + 1;
    //合法性检查
    vector<int> up(N + 1, 0); up[1] = limit[1];
    for(int u = 2; u <= N; u ++) {
        int p = u / 2;
        up[u] = up[p];
        if(limit[u] != -1) {
            if(limit[u] > up[p]) {cout << "0\n"; return;}
            if(sz[u] > limit[u]) {cout << "0\n"; return;}
            up[u] = limit[u];
        }
    }

    vector<vector<int>> group(N + 1);
    for(int i = 1; i <= N; i ++) {
        if(limit[i] != -1) group[limit[i]].push_back(i);
    }
    vector<int> deepest(M + 2, -1);//每个值的最深出现位置
    for(int i = 1; i <= M; i ++) {
        if(group[i].empty()) continue;
        sort(group[i].begin(), group[i].end());
        for(size_t j = 0; j < group[i].size() - 1; j ++) {
            int anc = group[i][j];
            int des = group[i][j + 1];
            while(des > anc) des /= 2;
            if(des != anc) {cout << "0\n"; return;}
        }
        deepest[i] = group[i].back();
    }

    vector<int> free(N + 1, 0);//每个up[u]有多少个叶子承担
    for(int i = M; i <= N; i ++) free[i] = 1;
    for(int i = M - 1; i >= 1; i --) {
        if(up[i] == up[i * 2]) free[i] += free[i * 2];
        if(up[i] == up[i * 2 + 1]) free[i] += free[i * 2 + 1];
    }
    vector<int> cnt(M + 1, 0);//每个up值的叶子数量
    for(int i = M; i <= N; i ++) cnt[up[i]] ++;
    vector<int> suf(M + 2, 0);//计算后缀up值
    for(int i = M; i >= 1; i --) suf[i] = suf[i+1] + cnt[i];

    ll ans = 1;
    ll used = 0;
    for(int v = M; v >= 1; v --) {
        if(deepest[v] != -1) {
            int d = deepest[v];
            if(free[d] == 0) {//没有多余叶子可以承载
                ans = 0; break;
            }
            ans = ans * free[d] % MOD;
            used ++;
        }
        else {//v只能放到up[u]>=v的叶子上
            int avail = suf[v] - used;
            if(avail <= 0) {
                ans = 0; break;
            }
            ans = ans * avail % MOD;
            used ++;
        }
    }
    cout << ans << "\n";
}

int main() {
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    int _ = 1; //cin >> _;
    while(_--) solve();
    return 0;
}
posted @ 2026-10-03 09:34  thenrify  阅读(17)  评论(0)    收藏  举报