先用现有动物的按位或找出已出现过的二进制位,再把仍会引入新饲料的位置并成危险位,最后按自由位数量直接计数。
OJ: luogu
题目 ID: P7076
难度:普及+/提高-
标签:位运算二进制计数思维
创建: 2026-06-20 10:42
更新: 2026-10-07 10:45
形式化题目
有
当前动物园已根据现有动物购买了一份饲料清单。问:有多少种当前未饲养的动物,加入后饲料清单不发生变化。
解法路线
题面给出了四个真实的分数档位:
| 档位 | 真实约束 | 做法 | 瓶颈 |
|---|---|---|---|
| 枚举所有 |
|||
| 同上 | |||
| 统计禁止位,公式 |
无 | ||
| 同上 + 处理 |
无 |
暴力解法:
适用范围
思路
先根据当前
代码
/**
* 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;
}复杂度与瓶颈
时间
子任务解法:
适用范围
思路
真正的关键是把"会不会引入新饲料"翻译成对二进制位的限制。
先把所有当前动物做一个按位或:
对规则
| 情况 | 结论 |
|---|---|
| 第 |
|
| 新动物不能把第 |
把所有"会引入新饲料"的位置并成禁止位掩码
因为 unsigned long long 上限,无需溢出处理。
代码
/**
* 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;
}复杂度与瓶颈
时间
正解
关键观察
公式 unsigned long long 的正向表示范围。
思路
- 读入动物编号,合并为
; - 读入规则,标记已购买的饲料,构建禁止位掩码
; ; - 若
,用无符号回绕计算 ;否则直接 。
代码
/**
* 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;
}复杂度
- 时间复杂度:
。 - 空间复杂度:
。
总结
这题的核心不是去模拟"买了哪些饲料",而是先看出:当前动物园真正提供的信息只有"哪些二进制位曾经出现过
四个分数档位串起来是一条"从枚举到公式、从有符号到无符号"的路线:暴力枚举
本文的 main.cpp 已用逐只枚举版 brute.cpp 进行 300 组随机对拍,覆盖