[HEOI2012] 朋友圈

OJ: luogu

题目 ID: P2423

标签: 二分图

日期: 2026-01-13 22:25

题目大意

我们有两个国家 A 和 B,每个国家有若干人,每个人有一个友善值(整数)。朋友关系定义如下:

  1. A 国内部:两人友善值 (a, b) 满足 ((a \oplus b) \bmod 2 = 1)(即奇偶性不同)时是朋友。
  2. B 国内部:两人友善值 (a, b) 满足 ((a \oplus b) \bmod 2 = 0) 或 ((a \operatorname{or} b)) 的二进制表示有奇数个 1 时是朋友。
  3. 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}) 中有边当且仅当他们在原图中 不是 朋友。

根据朋友条件,不是朋友的条件为:

(bibj)mod2=1popcount(biorbj)mod2=0 (b_i \oplus b_j) \bmod 2 = 1 \quad \text{且} \quad \operatorname{popcount}(b_i \operatorname{or} b_j) \bmod 2 = 0
即:两人奇偶性不同,且 (b_i \operatorname{or} b_j) 的二进制中 1 的个数为偶数。

注意到,若两人奇偶性相同,则一定是朋友(因为第一个条件已满足),所以补图中只可能存在奇偶性不同的点之间的边。因此,(\overline{G}) 是一个二分图,左部为奇数友善值的 B 国人,右部为偶数友善值的 B 国人。

重要结论:原图 (G) 中的团(任意两点相连)对应补图 (\overline{G}) 中的独立集(任意两点不相连)。因此,求 (G) 的最大团等价于求 (\overline{G}) 的最大独立集。

对于二分图,最大独立集大小 = 顶点总数 - 最小点覆盖大小 = 顶点总数 - 最大匹配大小(Kőnig 定理)。

因此,我们可以在 (\overline{G}) 上用最大匹配算法(如匈牙利或 Hopcroft–Karp)求得最大独立集,从而得到原图的最大团。

算法步骤

对于每组数据:

  1. 读入与预处理

    • 读入 A, B, M,以及友善值。
    • 记录每个 A 国人的奇偶性,分成奇数列表和偶数列表。
    • 用邻接表或布尔数组存储 A 与 B 之间的朋友关系。
    • 预处理补图 (\overline{G}) 的边:枚举所有奇数的 B 国人 (i) 和偶数的 B 国人 (j),若 (\operatorname{popcount}(b_i | b_j)) 为偶数,则在 (\overline{G}) 中添加边 (i \to j)。
  2. 枚举 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)。
  3. 对于每种情况,在候选集 (B’) 上求最大独立集

    • 从预处理的补图边中,只保留两端点都在 (B’) 中的边,得到诱导子图 (\overline{G}[B’])。
    • 将 (B’) 中的点按奇偶性分为左部(奇数)和右部(偶数),重新编号。
    • 在二分图 (\overline{G}[B’]) 上求最大匹配(根据数据规模选用匈牙利或 Hopcroft–Karp 算法)。
    • 最大独立集大小 = (|B’|) − 最大匹配数。
  4. 剪枝优化:在枚举情况时,若当前候选集大小 (+) 已选 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),但实际数据中边数较少,且剪枝有效,可以通过。

容易忽略的细节

  1. A 国人自己也可以构成朋友圈:即使不选 B 国人,只选 1 个或 2 个 A 国人也是合法的朋友圈,所以答案至少为 1。
  2. A 国人选两个时,必须是一个奇数一个偶数,否则他们之间不是朋友,不能同时选。
  3. 补图边的判定:必须严格满足两个条件(奇偶性不同且 popcount(b_i | b_j) 为偶数)才会在补图中有边。计算 popcount 可以使用 GCC 内置函数 __builtin_popcount(注意参数转换为无符号)。
  4. 多组数据:每次要清空数组,重新预处理。
  5. 朋友关系是双向的:存储时注意无向图等价于两条有向边,但 A-B 关系给出时已经是双向,只需存一次。
  6. 候选集 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 国人的少量情况,并对每种情况在二分图上跑最大匹配,即可高效求解。注意根据数据规模选择合适的匹配算法,并利用剪枝优化。

代码

cpp
/**
 * 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;
}