[CSP-S 2020] 动物园

GitHub跳转原题关系图返回列表

先用现有动物的按位或找出已出现过的二进制位,再把仍会引入新饲料的位置并成危险位,最后按自由位数量直接计数。

OJ: luogu

题目 ID: P7076

难度:普及+/提高-

标签:位运算二进制计数思维

创建: 2026-06-20 10:42

更新: 2026-10-07 10:45

形式化题目

有 2k2^k 种动物,编号为 0∼2k−10 \sim 2^k - 1。动物园已饲养 nn 种(编号 aia_i),另有 mm 条规则 (pj,qj)(p_j, q_j):若某种动物编号的第 pjp_j 个二进制位为 11,则必须购买第 qjq_j 种饲料。

当前动物园已根据现有动物购买了一份饲料清单。问:有多少种当前未饲养的动物,加入后饲料清单不发生变化。

解法路线

题面给出了四个真实的分数档位:

档位 真实约束 做法 瓶颈
20%20\% k⩽5, n⩽5k \leqslant 5,\ n \leqslant 5 枚举所有 2k2^k 只动物逐只判断 2k2^k 枚举
40%40\% k⩽20, n⩽15k \leqslant 20,\ n \leqslant 15 同上 220≈1062^{20} \approx 10^6,尚可
60%60\% k⩽30k \leqslant 30 统计禁止位,公式 2free−n2^{free} - n 无
100%100\% k⩽64k \leqslant 64 同上 + 处理 2642^{64} 溢出边界 无

20%20\% 和 40%40\% 共用暴力枚举,60%60\% 和 100%100\% 共用公式,区别仅在 2642^{64} 的溢出处理。每层代码都能独立提交、逐层拿分。

暴力解法:k⩽20k \leqslant 20

适用范围

20%20\% 和 40%40\% 的数据满足 k⩽20k \leqslant 20,此时 2k⩽1062^k \leqslant 10^6,可以枚举所有动物逐只判断。

思路

先根据当前 nn 只动物算出已购买的饲料集合,再枚举每一种未饲养的编号 xx,检查 xx 是否会触发某条"当前还没买"的饲料规则。如果不触发,xx 就可以加入。

代码

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-10-01 22:31
 * update_at: 2026-10-01 22:31
 */
// subtask_40.cpp:40% 数据 k ≤ 20,枚举所有 2^k 只动物逐只判断。
// 时间 O(2^k · m),空间 O(n + m)。覆盖 20% 和 40% 两档。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef unsigned long long ull;

ll n, m, c, k;
vector<ull> animals;
vector<int> rule_p;
vector<int> rule_q;
unordered_set<ull> exist_animal;
unordered_set<int> current_feed;

bool has_bit(ull x, int p) {
    return (x >> p) & 1ULL;
}

// 枚举所有动物和规则,算出当前已购买的饲料。
void build_current_feed() {
    current_feed.clear();
    for (ll i = 0; i < n; i++) {
        for (ll j = 0; j < m; j++) {
            if (has_bit(animals[i], rule_p[j])) {
                current_feed.insert(rule_q[j]);
            }
        }
    }
}

// 判断新动物 x 能否加入:不触发任何"当前还没买"的饲料规则。
bool can_add(ull x) {
    for (ll j = 0; j < m; j++) {
        if (has_bit(x, rule_p[j]) && current_feed.find(rule_q[j]) == current_feed.end()) {
            return false;
        }
    }
    return true;
}

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

    cin >> n >> m >> c >> k;

    animals.resize(n);
    for (ll i = 0; i < n; i++) {
        cin >> animals[i];
        exist_animal.insert(animals[i]);
    }

    rule_p.resize(m);
    rule_q.resize(m);
    for (ll i = 0; i < m; i++) {
        cin >> rule_p[i] >> rule_q[i];
    }

    build_current_feed();

    // 枚举所有编号,跳过已饲养的,统计能加入的。
    ull limit = 1ULL << k;
    ull ans = 0;
    for (ull x = 0; x < limit; x++) {
        if (exist_animal.find(x) != exist_animal.end()) {
            continue;
        }
        if (can_add(x)) {
            ans++;
        }
    }

    cout << ans << '\n';
    return 0;
}

复杂度与瓶颈

时间 O(2k⋅m)O(2^k \cdot m)。k=20k = 20 时 220≈1062^{20} \approx 10^6,m⩽20m \leqslant 20,尚可;但 k=30k = 30 时 230≈1092^{30} \approx 10^9 就超时了。

子任务解法:k⩽30k \leqslant 30

适用范围

60%60\% 的数据满足 k⩽30k \leqslant 30。2302^{30} 太大不能枚举,但可以用公式一步算出答案。

思路

真正的关键是把"会不会引入新饲料"翻译成对二进制位的限制。

先把所有当前动物做一个按位或:zoo_mask=a1∣a2∣⋯∣anzoo\_mask = a_1 | a_2 | \cdots | a_n。它的第 pp 位为 11 表示至少已有一种动物在第 pp 位上是 11。

对规则 (p,q)(p, q) 分两种情况:

情况 结论
zoo_maskzoo\_mask 第 pp 位已是 11 第 qq 种饲料早已买过
zoo_maskzoo\_mask 第 pp 位是 00,且第 qq 种饲料还没买 新动物不能把第 pp 位设成 11

把所有"会引入新饲料"的位置并成禁止位掩码 bad_maskbad\_mask。新动物合法当且仅当它的所有禁止位都是 00。若禁止位有 bad_cntbad\_cnt 个,自由位就有 free_cnt=k−bad_cntfree\_cnt = k - bad\_cnt 个,合法动物总数为 2free_cnt2^{free\_cnt},减去已饲养的 nn 种即得答案。

因为 k⩽30k \leqslant 30,free_cnt⩽30free\_cnt \leqslant 30,2302^{30} 远小于 unsigned long long 上限,无需溢出处理。

代码

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-10-01 22:31
 * update_at: 2026-10-01 22:31
 */
// subtask_60.cpp:60% 数据 k ≤ 30,用公式 2^free_cnt - n 直接算答案。
// 核心思路:统计"禁止位"(未买饲料对应的位),自由位数 = k - 禁止位数。
// 因为 k ≤ 30,free_cnt ≤ 30,2^30 远小于 ull 上限,无需溢出处理。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef unsigned long long ull;

const int MAXM = 1000005;

ll m, c, k;
ll n;
int rule_p[MAXM];
int rule_q[MAXM];
ull zoo_mask;
ull bad_mask;
unordered_set<int> bought_feed;

bool has_bit(ull x, int p) {
    return (x >> p) & 1ULL;
}

// 读入动物编号,合并为位掩码。
void read_animals() {
    zoo_mask = 0;
    for (ll i = 0; i < n; i++) {
        ull x;
        cin >> x;
        zoo_mask |= x;
    }
}

// 读入规则,标记已购买的饲料。
void read_rules() {
    bought_feed.clear();
    for (ll i = 1; i <= m; i++) {
        cin >> rule_p[i] >> rule_q[i];
        if (has_bit(zoo_mask, rule_p[i])) {
            bought_feed.insert(rule_q[i]);
        }
    }
}

// 构建禁止位掩码。
void build_bad_mask() {
    bad_mask = 0;
    for (ll i = 1; i <= m; i++) {
        if (bought_feed.find(rule_q[i]) == bought_feed.end()) {
            bad_mask |= (1ULL << rule_p[i]);
        }
    }
}

void solve() {
    read_animals();
    read_rules();
    build_bad_mask();

    int bad_cnt = __builtin_popcountll(bad_mask);
    int free_cnt = (int)k - bad_cnt;

    // k ≤ 30 保证 free_cnt ≤ 30,1ULL << 30 不会溢出。
    ull total = 1ULL << free_cnt;
    ull ans = total - n;
    cout << ans << '\n';
}

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

    cin >> n >> m >> c >> k;
    solve();

    return 0;
}

复杂度与瓶颈

时间 O(n+m)O(n + m),空间 O(m)O(m)。公式本身没问题,唯一需要注意的是 k=64k = 64 时 2642^{64} 的溢出。

正解

关键观察

公式 ans=2free_cnt−nans = 2^{free\_cnt} - n 对全部合法数据都成立。100%100\% 数据和 60%60\% 数据的算法完全相同,唯一区别是 k=64k = 64 时 free_cntfree\_cnt 可能等于 6464,此时 2642^{64} 超出 unsigned long long 的正向表示范围。

思路

  1. 读入动物编号,合并为 zoo_maskzoo\_mask;
  2. 读入规则,标记已购买的饲料,构建禁止位掩码 bad_maskbad\_mask;
  3. free_cnt=k−popcount⁡(bad_mask)free\_cnt = k - \operatorname{popcount}(bad\_mask);
  4. 若 free_cnt=64free\_cnt = 64,用无符号回绕计算 264−n2^{64} - n;否则直接 2free_cnt−n2^{free\_cnt} - n。

代码

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-10-01 22:31
 * update_at: 2026-10-01 22:31
 */
// main.cpp:满分做法,统计"禁止位"后用公式 2^free_cnt - n 得到答案。
// 核心思路:当前动物园已有动物的位掩码决定哪些规则的饲料已买,
// 未买饲料对应的位就是"禁止位",新动物这些位必须为 0。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef unsigned long long ull;

const int MAXM = 1000005;

ll m, c, k;          // 要求数、饲料种数、二进制位数
ull n;               // 动物数量(需要无符号做减法,见下方 2^64 边界处理)
int rule_p[MAXM];    // rule_p[j] = 第 j 条规则的二进制位编号(0 ~ k-1)
int rule_q[MAXM];    // rule_q[j] = 第 j 条规则对应的饲料编号(1 ~ c)
ull zoo_mask;        // 当前动物园里,哪些二进制位曾经出现过 1
ull bad_mask;        // 新动物这些位必须是 0,否则会引入新饲料
unordered_set<int> bought_feed; // 当前已购买的饲料集合

// 判断 x 的第 p 位是否为 1。
bool has_bit(ull x, int p) {
    return (x >> p) & 1ULL;
}

// 读入所有动物编号,合并为一个位掩码。
void read_animals() {
    zoo_mask = 0;
    for (ull i = 0; i < n; i++) {
        ull x;
        cin >> x;
        zoo_mask |= x;
    }
}

// 读入规则,同时标记哪些饲料已经被购买。
void read_rules() {
    bought_feed.clear();
    bought_feed.reserve((size_t)m * 2 + 5);
    bought_feed.max_load_factor(0.7f);

    for (ll i = 1; i <= m; i++) {
        cin >> rule_p[i] >> rule_q[i];

        // 如果当前动物园里已有动物在第 p 位上是 1,这条规则的饲料一定买过了。
        if (has_bit(zoo_mask, rule_p[i])) {
            bought_feed.insert(rule_q[i]);
        }
    }
}

// 构建禁止位掩码:未买饲料对应的位,新动物不能取 1。
void build_bad_mask() {
    bad_mask = 0;
    for (ll i = 1; i <= m; i++) {
        // 如果第 q 种饲料还没买,新动物一旦在第 p 位取 1 就会触发新饲料。
        if (bought_feed.find(rule_q[i]) == bought_feed.end()) {
            bad_mask |= (1ULL << rule_p[i]);
        }
    }
}

void solve() {
    read_animals();
    read_rules();
    build_bad_mask();

    int bad_cnt = __builtin_popcountll(bad_mask);
    int free_cnt = (int)k - bad_cnt; // 自由位数(不在禁止掩码中的位)

    // free_cnt == 64 只在 k == 64 且 bad_mask == 0 时出现。
    // 此时总方案数是 2^64,超出 ull 的正向表示范围,需要特殊处理。
    if (free_cnt == 64) {
        if (n == 0) {
            cout << "18446744073709551616\n"; // 2^64
        } else {
            ull ans = 0;
            ans -= n; // 等价于 2^64 - n(ull 回绕)
            cout << ans << '\n';
        }
        return;
    }

    ull total = 1ULL << free_cnt;
    ull ans = total - n;
    cout << ans << '\n';
}

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

    cin >> n >> m >> c >> k;
    solve();

    return 0;
}

复杂度

  • 时间复杂度:O(n+m)O(n + m)。
  • 空间复杂度:O(m)O(m)。

总结

这题的核心不是去模拟"买了哪些饲料",而是先看出:当前动物园真正提供的信息只有"哪些二进制位曾经出现过 11"。再进一步推出:所有会导致新饲料出现的位置必须固定为 00,其余位置全部自由。这样题目就从一个规则模拟题变成了一个直接的位运算计数题。

四个分数档位串起来是一条"从枚举到公式、从有符号到无符号"的路线:暴力枚举 2k2^k → 统计禁止位用公式 → 处理 2642^{64} 溢出边界。真正需要记住的只有 ans=2free_cnt−nans = 2^{free\_cnt} - n 和 k=64k = 64 时的溢出处理。

本文的 main.cpp 已用逐只枚举版 brute.cpp 进行 300 组随机对拍,覆盖 k⩽10k \leqslant 10 的随机数据。