[CSP-S 2020] 动物园

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

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

OJ: luogu

题目 ID: P7076

难度:普及+/提高

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

日期: 2026-06-20 10:42

题意

2^k 种动物,编号是 0 ~ 2^k - 1

现在动物园里已经养了 n 种动物,编号分别是 a_i
还有 m 条规则,每条规则是 (p, q),表示:

  • 如果动物编号的第 p 个二进制位是 1
  • 就必须购买第 q 种饲料

当前动物园已经根据现有动物买好了一份饲料清单。

现在问:有多少种 当前还没养 的动物,可以直接加入动物园,而且加入后饲料清单不会发生变化。

思路

先看一个可以直接验证定义的小数据暴力:

cpp
#include <bits/stdc++.h>
using namespace std;

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

// 小数据直接判断二进制位。
bool has_bit(unsigned long long x, int p) {
    return (x >> p) & 1ULL;
}

void build_current_feed() {
    current_feed.clear();

    // 朴素做法:枚举每只当前动物,再检查每一条规则是否触发。
    for (int i = 0; i < n; i++) {
        unsigned long long x = animals[i];
        for (int j = 0; j < m; j++) {
            if (has_bit(x, rule_p[j])) {
                current_feed.insert(rule_q[j]);
            }
        }
    }
}

bool can_add(unsigned long long x) {
    // 直接模拟:如果 x 会触发某条“当前还没买过”的饲料规则,
    // 那么加入它后清单就会变化,这种动物不能算。
    for (int i = 0; i < m; i++) {
        if (has_bit(x, rule_p[i]) && current_feed.find(rule_q[i]) == 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 (int i = 0; i < n; i++) {
        cin >> animals[i];
        exist_animal.insert(animals[i]);
    }

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

    build_current_feed();

    unsigned long long limit = 1ULL << k;
    unsigned long long ans = 0;
    for (unsigned long long x = 0; x < limit; x++) {
        if (exist_animal.find(x) != exist_animal.end()) {
            continue;
        }
        if (can_add(x)) {
            ans++;
        }
    }

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

brute.cpp 先朴素求出当前饲料清单,再枚举每一种还没养的动物,检查它会不会触发新的饲料。这个做法很直观,但只有在 k 很小时才能用。

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

先把所有当前动物做一个按位或:

zoo_mask = a_1 | a_2 | ... | a_n

它的含义是:

  • 如果 zoo_mask 的第 p 位是 1
  • 说明当前动物园里,至少已经有一种动物在第 p 位上是 1

于是对一条规则 (p, q) 来说,会分成两种情况:

情况 结论
zoo_mask 的第 p 位已经是 1 q 种饲料早就买过了
zoo_mask 的第 p 位是 0,且第 q 种饲料当前还没买 新动物不能把第 p 位设成 1

所以我们只需要关心那些 会引入新饲料 的位置,把它们记成一个集合 bad_mask

  • 如果某条规则 (p, q) 对应的第 q 种饲料现在还没买
  • 那么第 p 位就是危险位,必须为 0

最后一个新动物 x 合法,当且仅当:

  • x 的所有危险位都是 0

也就是说:

  • 危险位不能选
  • 其它位都可以自由取 0/1

若危险位有 bad_cnt 个,那么自由位就有:

free_cnt = k - bad_cnt

所以满足条件的动物总数是:

2^{free_cnt}

但题目问的是“当前还没养”的动物种数,因此还要减去已经存在的 n 种动物:

ans = 2^{free_cnt} - n

实现时还有一个细节:

  • k = 64 且没有危险位时,总数是 2^64
  • 这个值已经超出了 unsigned long long 的正常正向表示范围

所以代码里要把这个边界单独处理。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXM = 1000005;

int n, m, c, k;
int rule_p[MAXM];
int rule_q[MAXM];
unsigned long long zoo_mask;   // 当前动物园里,哪些二进制位曾经出现过 1
unsigned long long bad_mask;   // 新动物这些位必须是 0,否则会引入新饲料
unordered_set<int> bought_feed;

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

void read_animals() {
    zoo_mask = 0;
    for (int i = 1; i <= n; i++) {
        unsigned long long 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 (int 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]);
        }
    }
}

void build_bad_mask() {
    bad_mask = 0;

    for (int i = 1; i <= m; i++) {
        // 如果第 q 种饲料现在还没买,那么新动物一旦在第 p 位取 1,
        // 就会触发一份新的饲料清单,于是这种位必须保持为 0。
        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 = k - bad_cnt;

    // free_cnt == 64 只会在 k == 64 且 bad_mask == 0 时出现。
    // 此时总方案数是 2^64,已经超出 unsigned long long 的正向表示范围。
    if (free_cnt == 64) {
        if (n == 0) {
            cout << "18446744073709551616\n";
        } else {
            unsigned long long ans = 0;
            ans -= (unsigned long long)n;  // 等价于 2^64 - n
            cout << ans << '\n';
        }
        return;
    }

    unsigned long long total = 1ULL << free_cnt;
    unsigned long long ans = total - (unsigned long long)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)

总结

这题的核心不是去模拟“买了哪些饲料”,而是先看出:

  • 当前动物园真正提供的信息,只有“哪些二进制位曾经出现过 1”

再进一步推出:

  • 所有会导致新饲料出现的位置必须固定为 0
  • 其余位置全部自由

这样题目就从一个看起来很长的规则模拟题,变成了一个非常直接的位运算计数题。