数据抢修

在二进制 Trie 上递归计算异或阈值冲突图的最大团,并缓存跨子 Trie 状态支持合并。

OJ: shumeng

题目 ID: CSP202512E

难度:提高+/省选-

标签:Trie二分图动态合并

日期: 2026-07-31 16:22

形式化题目

每个数据包是可重集。若两元素权值 x,yx,y 满足 xy<Wx \oplus y < W,则它们不能放在同一个稳定子包中。一个数据包的维修代价是能将其划分成稳定子包的最小数量。维护多个数据包,支持插入元素、合并数据包、查询所有激活数据包维修代价之和。

思路

先看朴素做法:把元素建成冲突图并回溯染色,验证答案的含义。

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-31 16:22
 * update_at: 2026-08-17 23:10
 */
// brute.cpp:把元素建成冲突图,回溯求最少染色数,适合小数据验证。
#include <bits/stdc++.h>
using namespace std;

int element_count;
int best_color;
vector<int> order_vertices;
vector<int> vertex_color;
vector<int> conflict_mask;
vector<int> vertex_degree;

bool degree_compare(int x, int y) {
    return vertex_degree[x] > vertex_degree[y];
}

void color_dfs(int position, int used_color) {
    if (used_color >= best_color) {
        return;
    }
    if (position == element_count) {
        best_color = used_color;
        return;
    }
    int u = order_vertices[position];
    int forbidden = 0;
    for (int i = 0; i < position; i++) {
        int v = order_vertices[i];
        if ((conflict_mask[u] >> v) & 1) {
            forbidden |= 1 << vertex_color[v];
        }
    }
    for (int color = 0; color < used_color; color++) {
        if (((forbidden >> color) & 1) == 0) {
            vertex_color[u] = color;
            color_dfs(position + 1, used_color);
        }
    }
    vertex_color[u] = used_color;
    color_dfs(position + 1, used_color + 1);
    vertex_color[u] = -1;
}

int repair_cost(const vector<int> &values, int w) {
    element_count = (int)values.size();
    if (element_count == 0) {
        return 0;
    }
    if (w >= (1 << 30)) {
        return element_count;
    }
    conflict_mask.assign(element_count, 0);
    vertex_degree.assign(element_count, 0);
    for (int i = 0; i < element_count; i++) {
        for (int j = 0; j < i; j++) {
            if ((values[i] ^ values[j]) < w) {
                conflict_mask[i] |= 1 << j;
                conflict_mask[j] |= 1 << i;
                vertex_degree[i]++;
                vertex_degree[j]++;
            }
        }
    }
    order_vertices.resize(element_count);
    for (int i = 0; i < element_count; i++) {
        order_vertices[i] = i;
    }
    sort(order_vertices.begin(), order_vertices.end(), degree_compare);
    vertex_color.assign(element_count, -1);
    best_color = element_count;
    color_dfs(0, 0);
    return best_color;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, w;
    cin >> n >> w;
    vector<vector<int> > packets(n + 1);
    for (int i = 1; i <= n; i++) {
        int count;
        cin >> count;
        packets[i].resize(count);
        for (int j = 0; j < count; j++) {
            cin >> packets[i][j];
        }
    }
    int q;
    cin >> q;
    while (q--) {
        int type;
        cin >> type;
        if (type == 1) {
            int u, x;
            cin >> u >> x;
            packets[u].push_back(x);
        } else if (type == 2) {
            int u, v;
            cin >> u >> v;
            packets[u].insert(packets[u].end(),
                              packets[v].begin(), packets[v].end());
            packets[v].clear();
        } else {
            long long answer = 0;
            for (int i = 1; i <= n; i++) {
                answer += repair_cost(packets[i], w);
            }
            cout << answer << '\n';
        }
    }
    return 0;
}

转成图论模型

把每个元素看成顶点,异或值小于 WW 的两顶点连边。稳定子包就是冲突图的独立集,维修代价等于最小染色数。这个异或阈值图具有按最高位递归的完美图结构,最小染色数恰好等于最大团大小。

在 Trie 上递归求最大团

hhWW 的最高位。高于 hh 的位一旦不同,异或值至少为 2h+1W2^{h+1} \ge W,不会冲突,所以最大团只能来自同一个高位前缀。在第 hh 位时,同侧元素两两冲突;若同时选左右两侧,就要递归检查低位限制 W2hW - 2^h

定义 cross(u, v, b, L) 为从两棵低位 Trie 中选取元素,使任意跨侧异或小于 LL 时能选出的最大总数:

  • L=0L=0:只能选一侧,答案为 max(cnt(u),cnt(v))\max(cnt(u), cnt(v))
  • LL 当前位为 0:只能递归同位分支,取最大值;
  • LL 当前位为 1:两个反向分支互不影响,答案是两者之和。

动态维护

每个跨子树状态用节点版本号缓存,避免重复计算。合并数据包时启发式合并两棵 Trie 并刷新受影响的 best;插入只沿一条路径更新。根节点的 best 就是该数据包的维修代价。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-31 16:22
 * update_at: 2026-08-17 23:10
 */
#include <bits/stdc++.h>
using namespace std;

const int MAX_VALUE_BIT = 29;          // 值域 [0,10^9] 的二进制最高位
const int MAX_TOTAL_ELEMENT = 500000;  // 所有数据包元素总数上限
const int MAX_TRIE_NODE = MAX_TOTAL_ELEMENT * (MAX_VALUE_BIT + 1) + 5;

struct TrieNode {
    int child[2]; // 0/1 两个子节点下标
    int count;    // 子树内元素个数
    int best;     // 该子树对应冲突图的最大团大小(即该子树的维修代价)
    int version;  // 版本号,用于判断跨子树缓存是否过期
};

struct CrossCache {
    int version_left;  // 左子树版本
    int version_right; // 右子树版本
    int value;         // 缓存的跨子树结果
};

int n;
long long threshold_w;   // 稳定阈值 W
int highest_bit;         // W 的最高位所在位置
int low_threshold;       // W 去掉最高位后的低位部分
bool all_pairs_conflict; // W >= 2^30 时任意两元素异或都小于 W,全部互斥
TrieNode trie[MAX_TRIE_NODE];
int trie_nodes;
unordered_map<unsigned long long, CrossCache> cross_cache; // 跨子树状态缓存

// 两个节点下标拼成一个 key,用于跨子树缓存
unsigned long long pair_key(int x, int y) {
    if (x > y) {
        swap(x, y);
    }
    return (unsigned long long)(unsigned int)x << 32
           | (unsigned int)y;
}

int get_count(int u) {
    return u == 0 ? 0 : trie[u].count;
}

int new_node() {
    trie_nodes++;
    trie[trie_nodes].child[0] = 0;
    trie[trie_nodes].child[1] = 0;
    trie[trie_nodes].count = 0;
    trie[trie_nodes].best = 0;
    trie[trie_nodes].version = 0;
    return trie_nodes;
}

// 从两个低位 Trie 中选取元素,使任意跨侧异或小于 limit 时能选出的最大总数。
// 这是冲突图最大团的递归计算核心
int cross_value(int left, int right, int bit, int limit) {
    if (left == 0 && right == 0) {
        return 0;
    }
    if (left == 0) {
        return get_count(right);
    }
    if (right == 0) {
        return get_count(left);
    }
    if (limit == 0) {
        return max(get_count(left), get_count(right));
    }
    if (bit < 0) {
        return get_count(left) + get_count(right);
    }

    // 用版本号判断缓存是否仍有效
    unsigned long long key = pair_key(left, right);
    unordered_map<unsigned long long, CrossCache>::iterator it;
    it = cross_cache.find(key);
    if (it != cross_cache.end()
        && it->second.version_left == trie[left].version
        && it->second.version_right == trie[right].version) {
        return it->second.value;
    }

    int result;
    if (((limit >> bit) & 1) == 0) {
        // 低位限制的当前位为 0:只能选同一位分支,取最大值
        int same_zero = cross_value(trie[left].child[0],
                                    trie[right].child[0], bit - 1, limit);
        int same_one = cross_value(trie[left].child[1],
                                   trie[right].child[1], bit - 1, limit);
        result = max(max(get_count(left), get_count(right)),
                     max(same_zero, same_one));
    } else {
        // 当前位为 1:两个反向分支互不影响,可以同时选
        int lower_limit = limit ^ (1 << bit);
        int different_zero = cross_value(trie[left].child[0],
                                         trie[right].child[1],
                                         bit - 1, lower_limit);
        int different_one = cross_value(trie[left].child[1],
                                        trie[right].child[0],
                                        bit - 1, lower_limit);
        result = different_zero + different_one;
    }

    CrossCache cache;
    cache.version_left = trie[left].version;
    cache.version_right = trie[right].version;
    cache.value = result;
    cross_cache[key] = cache;
    return result;
}

// 重新计算节点 u 的 best:看左右子树的单侧最大团与跨侧最大团
void update_best(int u, int bit) {
    if (bit > highest_bit) {
        trie[u].best = max(trie[trie[u].child[0]].best,
                           trie[trie[u].child[1]].best);
    } else if (bit == highest_bit) {
        int left_count = get_count(trie[u].child[0]);
        int right_count = get_count(trie[u].child[1]);
        int cross = cross_value(trie[u].child[0], trie[u].child[1],
                                highest_bit - 1, low_threshold);
        trie[u].best = max(max(left_count, right_count), cross);
    }
}

// 向 Trie 插入一个值,沿路径更新计数并刷新各节点的 best
void insert_value(int &u, int bit, int value) {
    if (u == 0) {
        u = new_node();
    }
    trie[u].count++;
    trie[u].version++;
    if (bit < 0) {
        return;
    }
    int direction = (value >> bit) & 1;
    insert_value(trie[u].child[direction], bit - 1, value);
    update_best(u, bit);
}

// 合并两棵 Trie(按大小启发式),返回合并后的根
int merge_trie(int left, int right, int bit) {
    if (left == 0) {
        return right;
    }
    if (right == 0) {
        return left;
    }
    if (trie[left].count < trie[right].count) {
        swap(left, right);
    }
    trie[left].count += trie[right].count;
    trie[left].version++;
    if (bit >= 0) {
        trie[left].child[0] = merge_trie(trie[left].child[0],
                                          trie[right].child[0], bit - 1);
        trie[left].child[1] = merge_trie(trie[left].child[1],
                                          trie[right].child[1], bit - 1);
        update_best(left, bit);
    }
    return left;
}

int packet_cost(int root) {
    if (root == 0) {
        return 0;
    }
    if (all_pairs_conflict) {
        return trie[root].count;
    }
    return trie[root].best;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> threshold_w;
    highest_bit = 0;
    long long temp_w = threshold_w;
    while (temp_w > 1) {
        temp_w >>= 1;
        highest_bit++;
    }
    all_pairs_conflict = threshold_w >= (1LL << 30);
    if (all_pairs_conflict) {
        highest_bit = MAX_VALUE_BIT;
        low_threshold = 0;
    } else {
        low_threshold = (int)(threshold_w - (1LL << highest_bit));
    }
    cross_cache.reserve(1 << 20);

    vector<int> packet_root(n + 1, 0);
    vector<int> packet_size(n + 1, 0);
    long long total_cost = 0;
    for (int i = 1; i <= n; i++) {
        int count;
        cin >> count;
        packet_size[i] = count;
        for (int j = 0; j < count; j++) {
            int value;
            cin >> value;
            insert_value(packet_root[i], MAX_VALUE_BIT, value);
        }
        total_cost += packet_cost(packet_root[i]);
    }

    int q;
    cin >> q;
    while (q--) {
        int type;
        cin >> type;
        if (type == 1) {
            int u, value;
            cin >> u >> value;
            total_cost -= packet_cost(packet_root[u]);
            insert_value(packet_root[u], MAX_VALUE_BIT, value);
            packet_size[u]++;
            total_cost += packet_cost(packet_root[u]);
        } else if (type == 2) {
            int u, v;
            cin >> u >> v;
            total_cost -= packet_cost(packet_root[u]);
            total_cost -= packet_cost(packet_root[v]);
            packet_root[u] = merge_trie(packet_root[u], packet_root[v],
                                        MAX_VALUE_BIT);
            packet_size[u] += packet_size[v];
            packet_root[v] = 0;
            packet_size[v] = 0;
            total_cost += packet_cost(packet_root[u]);
        } else {
            cout << total_cost << '\n';
        }
    }
    return 0;
}

复杂度

Trie 深度为 B30B \le 30。插入沿一条路径更新 O(B)O(B);合并访问较小 Trie 的节点,配合缓存均摊 O(nB)O(nB);空间复杂度为 Trie 节点与访问过的跨状态数量。

总结

异或阈值关系不是普通数值区间,但它在二进制最高位上具有清晰的递归结构。把分组问题转成冲突图染色,再利用完美图性质把最大团递归落实到 Trie,就能同时高效处理插入与数据包合并。