题目大意
我们有两个国家 A 和 B,每个国家有若干人,每个人有一个友善值(整数)。朋友关系定义如下:
- A 国内部:两人友善值 (a, b) 满足 ((a \oplus b) \bmod 2 = 1)(即奇偶性不同)时是朋友。
- B 国内部:两人友善值 (a, b) 满足 ((a \oplus b) \bmod 2 = 0) 或 ((a \operatorname{or} b)) 的二进制表示有奇数个 1 时是朋友。
- A 与 B 之间:给出 M 对朋友关系(双向)。
“朋友圈”是一个点集,其中任意两人都是朋友。求最大的朋友圈大小。
多组数据,(T \leq 6)。
数据范围有两类:
- 第一类:(A \leq 200,\ B \leq 200)
- 第二类:(A \leq 10,\ B \leq 3000)
算法思路
关键性质
1. A 国内部的关系
A 国人之间,朋友关系仅当两人友善值奇偶性不同。这意味着:
- 任意两个奇数的 A 国人不是朋友。
- 任意两个偶数的 A 国人不是朋友。
- 任意一个奇数和任意一个偶数都是朋友。
因此,在 A 国人内部,最多只能选择两个人(一个奇数和一个偶数)构成团。否则,如果选了三个或以上,必然出现两个奇数或两个偶数,他们不是朋友。
所以,A 国人的选择只有三种情况:
- 不选任何 A 国人。
- 只选 1 个 A 国人(任意)。
- 选 2 个 A 国人(必须一个是奇数,一个是偶数)。
2. B 国内部的关系与补图转化
设原图为 (G),表示 B 国人之间的朋友关系。求最大团(完全子图)是 NP-Hard,但本题中 (G) 的补图 (\overline{G}) 具有特殊性质。
补图 (\overline{G}) 的定义:两个 B 国人 (i, j) 在 (\overline{G}) 中有边当且仅当他们在原图中 不是 朋友。
根据朋友条件,不是朋友的条件为:
注意到,若两人奇偶性相同,则一定是朋友(因为第一个条件已满足),所以补图中只可能存在奇偶性不同的点之间的边。因此,(\overline{G}) 是一个二分图,左部为奇数友善值的 B 国人,右部为偶数友善值的 B 国人。
重要结论:原图 (G) 中的团(任意两点相连)对应补图 (\overline{G}) 中的独立集(任意两点不相连)。因此,求 (G) 的最大团等价于求 (\overline{G}) 的最大独立集。
对于二分图,最大独立集大小 = 顶点总数 - 最小点覆盖大小 = 顶点总数 - 最大匹配大小(Kőnig 定理)。
因此,我们可以在 (\overline{G}) 上用最大匹配算法(如匈牙利或 Hopcroft–Karp)求得最大独立集,从而得到原图的最大团。
算法步骤
对于每组数据:
-
读入与预处理
- 读入 A, B, M,以及友善值。
- 记录每个 A 国人的奇偶性,分成奇数列表和偶数列表。
- 用邻接表或布尔数组存储 A 与 B 之间的朋友关系。
- 预处理补图 (\overline{G}) 的边:枚举所有奇数的 B 国人 (i) 和偶数的 B 国人 (j),若 (\operatorname{popcount}(b_i | b_j)) 为偶数,则在 (\overline{G}) 中添加边 (i \to j)。
-
枚举 A 国人的选择,对每种情况计算朋友圈大小,取最大值。
- 情况 0:不选 A 国人。
- 候选 B 国人集合 (B’ =) 所有 B 国人。
- 在 (B’) 上求 (\overline{G}) 的最大独立集大小 (mx),则朋友圈大小 = (mx)。
- 情况 1:选 1 个 A 国人 (x)。
- 候选 (B’ = { y \mid x \text{ 与 } y \text{ 是朋友} })。
- 在 (B’) 上求最大独立集大小 (mx),则朋友圈大小 = (mx + 1)。
- 情况 2:选 2 个 A 国人 (x)(奇数)、(y)(偶数)。
- 候选 (B’ = { z \mid x \text{ 与 } z \text{ 是朋友,且 } y \text{ 与 } z \text{ 是朋友} })。
- 在 (B’) 上求最大独立集大小 (mx),则朋友圈大小 = (mx + 2)。
- 情况 0:不选 A 国人。
-
对于每种情况,在候选集 (B’) 上求最大独立集:
- 从预处理的补图边中,只保留两端点都在 (B’) 中的边,得到诱导子图 (\overline{G}[B’])。
- 将 (B’) 中的点按奇偶性分为左部(奇数)和右部(偶数),重新编号。
- 在二分图 (\overline{G}[B’]) 上求最大匹配(根据数据规模选用匈牙利或 Hopcroft–Karp 算法)。
- 最大独立集大小 = (|B’|) − 最大匹配数。
-
剪枝优化:在枚举情况时,若当前候选集大小 (+) 已选 A 国人数 ≤ 当前答案,则直接跳过。
复杂度分析
- 预处理补图边:(O(B^2)),最多 (3000^2 = 9 \times 10^6),可接受。
- 枚举情况数:
- 情况 0:1 种。
- 情况 1:(A) 种,最多 200。
- 情况 2:((\text{A奇数个数}) \times (\text{A偶数个数})),最多约 (100 \times 100 = 10^4)(当 (A=200) 时)。
- 每种情况需要构建诱导子图并求最大匹配:
- 构建子图:遍历所有补图边(最多 (2.25 \times 10^6) 条),检查端点是否在 (B’) 中。
- 匹配算法:
- 若 (B \leq 200),使用匈牙利算法,单次 (O(|B’|^3)) 或 (O(|B’| \cdot E)),但实际很快。
- 若 (B) 较大(≤3000),使用 Hopcroft–Karp 算法,单次 (O(E \sqrt{V})),约 (2.25 \times 10^6 \times \sqrt{1500} \approx 8.8 \times 10^7)。
最坏情况下(第二类数据,(A=10, B=3000),枚举 36 次,每次边数满),总操作量约 (3.2 \times 10^9),但实际数据中边数较少,且剪枝有效,可以通过。
容易忽略的细节
- A 国人自己也可以构成朋友圈:即使不选 B 国人,只选 1 个或 2 个 A 国人也是合法的朋友圈,所以答案至少为 1。
- A 国人选两个时,必须是一个奇数一个偶数,否则他们之间不是朋友,不能同时选。
- 补图边的判定:必须严格满足两个条件(奇偶性不同且
popcount(b_i | b_j)为偶数)才会在补图中有边。计算popcount可以使用 GCC 内置函数__builtin_popcount(注意参数转换为无符号)。 - 多组数据:每次要清空数组,重新预处理。
- 朋友关系是双向的:存储时注意无向图等价于两条有向边,但 A-B 关系给出时已经是双向,只需存一次。
- 候选集 B’ 可能为空:此时最大独立集为 0,朋友圈大小就是已选 A 国人数。
示例与图解
以下用 Mermaid 图展示补图的构建与匹配过程(以样例为例)。
样例输入:
1
2 4 7
1 2
2 6 5 4
1 1
1 2
1 3
2 1
2 2
2 3
2 4
- A 国人:1(奇数)、2(偶数)。
- B 国人:b1=2(偶数)、b2=6(偶数)、b3=5(奇数)、b4=4(偶数)。
- A-B 朋友关系:全连接(共 7 对,只缺 A2-B4?实际上输入给出全部 8 条可能边中的 7 条,缺 A1-B4?检查输入:有 2 4 吗?输入最后一行是“2 4”,所以 A2-B4 是朋友,那么实际上所有 8 条边都给了?不对,M=7,所以缺一条。根据输入,给出的边是:
1 1, 1 2, 1 3, 2 1, 2 2, 2 3, 2 4。所以缺 A1-B4。因此:
- B1 的朋友:A1, A2
- B2 的朋友:A1, A2
- B3 的朋友:A1, A2
- B4 的朋友:A2
补图构建(B 国人之间):
计算 popcount(b_i | b_j)(仅奇偶不同时考虑):
- b3=5 (奇数) 与 b1=2 (偶数):5|2=7 (111b), popcount=3 → 奇数,所以原图是朋友,补图无边。
- b3 与 b2=6:5|6=7, popcount=3 → 奇数,补图无边。
- b3 与 b4=4:5|4=5, popcount=2 → 偶数,且奇偶不同,所以补图有边(表示原图中不是朋友)。
所以补图只有一条边:b3(奇数)— b4(偶数)。
情况枚举:
- 情况0:不选A,B’={B1,B2,B3,B4}。补图是二分图,左部={B3},右部={B1,B2,B4},只有一条边(B3,B4)。最大独立集 = 4 - 最大匹配。最大匹配为1(匹配B3-B4),所以独立集大小=3。朋友圈大小=3。
- 情况1:选A1,B’={B1,B2,B3}(因为B4不是A1的朋友)。补图中没有边(因为B3与B1,B2都没有补图边),所以最大独立集=3,朋友圈大小=3+1=4。
- 情况2:选A1和A2,B’={B1,B2,B3}(B4不是A1的朋友,所以不在)。同样补图无边,最大独立集=3,朋友圈大小=3+2=5。
最大值为5,与样例输出一致。
graph TD
subgraph "补图(二分图)"
L[B3 奇数] --> R[B4 偶数]
end
总结
本题的难点在于发现 A 国人选择的有限性,以及将 B 国人最大团问题转化为二分图最大独立集。通过枚举 A 国人的少量情况,并对每种情况在二分图上跑最大匹配,即可高效求解。注意根据数据规模选择合适的匹配算法,并利用剪枝优化。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github : https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* date: 2026-01-13 22:26:35
* Problem: P2423 [HEOI2012] 朋友圈
* Algorithm: Max Independent Set on Bipartite Graph (Maximum Clique in Complement)
* * 核心思路:
* 1. A国人之间: 只有奇偶性不同才能做朋友。这意味A国人中,朋友圈最多只能有2个人(一奇一偶),或者1个人,或者0人。
* -> 我们可以枚举A国人在朋友圈中的子集 S_A (大小为0, 1, 2)。
* * 2. B国人之间: 朋友条件复杂。我们看"非朋友"条件(补图):
* 非朋友 <=> 奇偶性不同 且 (a|b) 二进制由偶数个1。
* 这意味着 B国人的补图是一个【二分图】(左边奇数,右边偶数)。
* 原图的最大团(朋友圈) = 补图的最大独立集。
* 二分图最大独立集 = 总点数 - 二分图最大匹配。
* * 3. 算法流程:
* - 预处理 B 国人补图的边。
* - 枚举 A 国选出的集合 S_A (空, 1个, 2个一奇一偶)。
* - 对于确定的 S_A,筛选出 B 国中所有能与 S_A 中所有人做朋友的候选集合 B'。
* - 在 B' 构成的补图中求最大匹配,进而得到 B' 的最大独立集大小。
* - 答案 = |S_A| + |B'的最大独立集|。
*/
#include <bits/stdc++.h>
using namespace std;
// 调整数组大小以适应题目范围
// 第一类数据: A, B <= 200
// 第二类数据: A <= 10, B <= 3000
const int maxn = 3005 + 5; // B国人数最大3000,加上源汇点
const int maxe = 3000 * 3000 / 2 + 5; // B国补图边数可能较多
const long long INF = 1e18;
// 全局变量
int A_cnt, B_cnt, M_rel;
int A_val[205]; // A国人友善值
int B_val[3005]; // B国人友善值
bool AB_friend[205][3005]; // AB之间是否朋友
int s, t; // 源点 汇点
// 存图的模板
struct linkList {
typedef struct {int u,v,w,next;} edge;
edge e[maxe];
int h[maxn], edge_cnt=0;
linkList(){
reset();
}
void reset() {
edge_cnt=0;
memset(h, -1, sizeof(h)); // 注意:如果maxn很大,memset可能慢,需注意
}
void add(int u, int v, int w=0){
e[edge_cnt] = {u, v, w, h[u]};
h[u] = edge_cnt++;
}
edge& operator[](int i){ return e[i]; }
} e;
// Dinic算法最大流模板
struct Dinic {
vector<int> level, cur;
int n;
void init(int n) {
e.edge_cnt = 0;
// 这里为了效率,只重置 0 到 n 的 head
// memset(e.h, -1, sizeof(e.h)); // 全局重置太慢
for(int i = 0; i <= n; ++i) e.h[i] = -1;
level.assign(n + 5, 0);
cur.assign(n + 5, 0);
this->n = n;
}
void addEdge(int u, int v, int cap) {
e.add(u, v, cap); // 正向边
e.add(v, u, 0); // 反向边
}
bool bfs(int s, int t) {
fill(level.begin(), level.end(), -1);
queue<int> q;
level[s] = 0;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
for(int i = e.h[u]; ~i; i = e.e[i].next) {
int v = e.e[i].v;
int cap = e.e[i].w;
if (cap > 0 && level[v] < 0) {
level[v] = level[u] + 1;
q.push(v);
}
}
}
return level[t] >= 0;
}
long long dfs(int u, int t, long long preFlow) {
if (u == t || preFlow == 0) return preFlow;
long long flow = 0;
for (int& cid = cur[u]; cid != -1; cid = e.e[cid].next) {
int v = e.e[cid].v;
int cap = e.e[cid].w;
if (level[u] + 1 != level[v] || cap <= 0) continue;
long long tr = dfs(v, t, min(preFlow, (long long)cap));
e.e[cid].w -= tr;
e.e[cid ^ 1].w += tr;
flow += tr;
preFlow -= tr;
if (preFlow == 0) break;
}
if (flow == 0) level[u] = -1;
return flow;
}
long long maxFlow(int s, int t) {
long long flow = 0;
while (bfs(s, t)) {
for (int i = 0; i <= n; i++) cur[i] = e.h[i];
flow += dfs(s, t, LLONG_MAX);
}
return flow;
}
} dinic;
// --- 题目特定逻辑 ---
// B国补图的边:只有奇偶不同且 (a|b)有偶数个1 时才连边(代表冲突)
// 我们可以预处理这些关系
vector<int> B_comp_adj[3005]; // 存储B国人i在补图中的邻居
// 检查B国两人在补图中是否有边(即是否由于条件限制不能做朋友)
bool check_B_conflict(int i, int j) {
// 只有奇偶性不同才可能在补图中有边(否则一定是朋友)
// 题目: B国朋友条件: 奇偶相同 OR (a|b)有奇数个1
// 反之(非朋友): 奇偶不同 AND (a|b)有偶数个1
int val_i = B_val[i];
int val_j = B_val[j];
if ((val_i % 2) == (val_j % 2)) return false; // 奇偶相同一定是朋友,无冲突
unsigned int or_val = (unsigned int)(val_i | val_j);
int ones = __builtin_popcount(or_val);
return (ones % 2 == 0); // 偶数个1则不是朋友(有冲突)
}
// 求解当前情况下的最大朋友圈大小
// A_subset: 选中的A国人列表
// current_ans: 当前已知的最大答案(用于剪枝)
int solve_case(const vector<int>& A_subset, int current_ans) {
// 1. 筛选候选的 B 国人
// B'中的人必须和 A_subset 中所有人都互为朋友
vector<int> B_candidates;
for (int j = 1; j <= B_cnt; ++j) {
bool ok = true;
for (int a_idx : A_subset) {
if (!AB_friend[a_idx][j]) {
ok = false;
break;
}
}
if (ok) B_candidates.push_back(j);
}
// 2. 剪枝
// 如果 A选的人数 + B候选人数 <= 当前最优解,就算B全选也不可能更优
if ((int)A_subset.size() + (int)B_candidates.size() <= current_ans) {
return 0;
}
// 3. 构建二分图求最大匹配
// B_candidates 构成了补图的一个诱导子图
// 这个子图本身也是二分图(左:奇数, 右:偶数)
// 节点编号映射:
// S=0, T=B_cnt+1
// B国人保持原编号 1~B_cnt (虽然只有部分在图中)
s = 0;
t = B_cnt + 1;
dinic.init(t);
for (int u : B_candidates) {
if (B_val[u] % 2 != 0) { // 奇数: 源点 -> u
dinic.addEdge(s, u, 1);
// 连向偶数邻居
for (int v_neighbor : B_comp_adj[u]) {
// 注意: 邻居必须也在候选集中
// 为了快速判断,这里我们可以不做O(N)查找,而是构建图时只处理存在的点
// 但由于边表预处理了,这里需要确认 v_neighbor 是否在 B_candidates 中
// 一个简单方法是标记数组,或者直接在这里不做判断,
// 但为了正确性,必须保证流只在 candidates 之间流动。
// 更好的方法:只添加两端都在 candidates 中的边。
}
} else { // 偶数: v -> 汇点
dinic.addEdge(u, t, 1);
}
}
// 添加内部边 (只在 candidates 之间)
// 为了效率,我们可以用一个 bool 数组标记 candidates
static bool is_candidate[3005];
// 清空标记 (注意 B_cnt 可能较大,只清空用到的)
// 但为了安全,循环清空或者memset
for(int x : B_candidates) is_candidate[x] = true;
for (int u : B_candidates) {
if (B_val[u] % 2 != 0) { // 只处理左部点发出的边
for (int v : B_comp_adj[u]) {
if (is_candidate[v]) {
dinic.addEdge(u, v, 1);
}
}
}
}
// 恢复标记 (回溯)
for(int x : B_candidates) is_candidate[x] = false;
int max_match = dinic.maxFlow(s, t);
int max_independent_set_B = (int)B_candidates.size() - max_match;
return (int)A_subset.size() + max_independent_set_B;
}
void solve() {
// 读入 A, B, M
cin >> A_cnt >> B_cnt >> M_rel;
// 读入 A 值
vector<int> A_odds, A_evens;
for (int i = 1; i <= A_cnt; ++i) {
cin >> A_val[i];
if (A_val[i] % 2 != 0) A_odds.push_back(i);
else A_evens.push_back(i);
}
// 读入 B 值
for (int i = 1; i <= B_cnt; ++i) {
cin >> B_val[i];
}
// 初始化关系
// 1. A-B 关系
for (int i = 1; i <= A_cnt; ++i)
for (int j = 1; j <= B_cnt; ++j)
AB_friend[i][j] = false;
for (int i = 0; i < M_rel; ++i) {
int u, v;
cin >> u >> v;
AB_friend[u][v] = true;
}
// 2. B-B 补图关系预处理
for (int i = 1; i <= B_cnt; ++i) B_comp_adj[i].clear();
// 只需枚举奇数和偶数之间的对
// 为了方便,我们可以直接双重循环,或者分奇偶列表
vector<int> B_odds, B_evens;
for (int i = 1; i <= B_cnt; ++i) {
if (B_val[i] % 2 != 0) B_odds.push_back(i);
else B_evens.push_back(i);
}
for (int u : B_odds) {
for (int v : B_evens) {
if (check_B_conflict(u, v)) {
// 补图中有边 (u奇, v偶)
B_comp_adj[u].push_back(v);
// B_comp_adj[v].push_back(u); // Dinic建图只需要单向: 左->右
}
}
}
int ans = 0;
// --- 情况 0: 不选 A 国人 ---
ans = max(ans, solve_case({}, ans));
// --- 情况 1: 选 1 个 A 国人 ---
for (int i = 1; i <= A_cnt; ++i) {
ans = max(ans, solve_case({i}, ans));
}
// --- 情况 2: 选 2 个 A 国人 (必须一奇一偶) ---
// A国人朋友条件: (a^b)%2==1 => 奇偶性不同
for (int u : A_odds) {
for (int v : A_evens) {
// 这两个人必须互为朋友才能同时选
// 奇偶不同已经是前提,所以一定是朋友?
// 题目: A国人 (a^b)%2=1 是朋友。是的,只要奇偶不同就是朋友。
ans = max(ans, solve_case({u, v}, ans));
}
}
cout << ans << endl;
}
int main() {
// 优化 I/O
ios::sync_with_stdio(0); cin.tie(0);
int T_cases;
cin >> T_cases;
while (T_cases--) {
solve();
}
return 0;
}