题解:洛谷 P5782 和平委员会

【题目来源】

洛谷:P5782 [POI 2001 R2] 和平委员会

【题目描述】

根据宪法,Byteland 民主共和国的公众和平委员会应该在国会中通过立法程序来创立。不幸的是,由于某些党派代表之间的不和睦而使得这件事存在障碍。此委员会必须满足下列条件:

  • 每个党派都在委员会中恰有 \(1\) 个代表。
  • 如果 \(2\) 个代表彼此厌恶,则他们不能都属于委员会。

每个党在议会中有 \(2\) 个代表。代表从 \(1\) 编号到 \(2n\)。编号为 \(2i-1\)\(2i\) 的代表属于第 \(i\) 个党派。

任务:写一程序读入党派的数量和关系不友好的代表对,计算决定建立和平委员会是否可能,若行,则列出委员会的成员表。

【输入】

第一行有两个非负整数 \(n,m\)。他们各自表示:党派的数量 \(n\) 和不友好的代表对 \(m\)

接下来 \(m\) 行,每行为一对整数 \(a,b\),表示代表 \(a\)\(b\) 互相厌恶。

【输出】

如果不能创立委员会,则输出信息 NIE

若能够成立,则输出包括 \(n\) 个从区间 \(1\)\(2n\) 选出的整数,按升序输出,每行一个,这些数字为委员会中代表的编号。

如果委员会能以多种方法形成,程序可以只输出它们的某一个。

【输入样例】

3 2
1 3
2 4

【输出样例】

1
4
5

【核心思想】

  1. 问题分析:有 \(n\) 个党派,每个党派有 \(2\) 名代表,编号为 \(2i-1\)\(2i\)。需要在每个党派中恰好选择 \(1\) 名代表组成委员会,同时满足 \(m\) 个“不友好”约束:若 \(a\)\(b\) 互相厌恶,则它们不能同时被选入委员会。要求判断是否存在合法方案,若存在则输出任意一组。这等价于一个2-SAT问题:每个党派 \(i\) 对应一个布尔变量 \(x_i\)\(x_i\) 为真表示选择第 \(2i-1\) 号代表,\(x_i\) 为假表示选择第 \(2i\) 号代表。每个不友好约束 \(a, b\) 转化为两个蕴含关系:选 \(a\) 则不能选 \(b\)(即选 \(\neg b\)),选 \(b\) 则不能选 \(a\)(即选 \(\neg a\))。

  2. 算法选择

    • 2-SAT + Tarjan 强连通分量:将每个变量拆成两个节点(真/假),建立有向图(蕴含图)。对 \(2n\) 个节点运行 Tarjan 算法求出所有强连通分量。若存在某个变量 \(x_i\) 的真假两个节点在同一个 SCC 中,则无解;否则有解。构造解时,对于每个党派,比较其两个代表所属 SCC 的编号,选择编号较大的那个代表(因为 Tarjan 的 SCC 编号顺序是逆拓扑序,编号较大的分量在拓扑序中更靠后,取真值可满足所有蕴含关系)。
  3. 关键步骤

    • 读入与建图:读取 \(n, m\)。对每个党派,用 get(x) 返回同一党派中另一个代表的编号。对于每条厌恶关系 \((a, b)\),添加边 \(a \to \neg b\)(即 \(a \to get(b)\))和 \(b \to \neg a\)(即 \(b \to get(a)\)),表示“若 \(a\) 入选,则 \(b\) 不能入选;若 \(b\) 入选,则 \(a\) 不能入选”。
    • Tarjan 求 SCC:对 \(1 \sim 2n\) 的所有节点运行 Tarjan,得到每个节点的 SCC 编号 scc[id]
    • 矛盾检查:遍历每个党派 \(i\)\(1 \le i \le n\)),若 scc[2i-1] == scc[2i],则无解,输出 NIE
    • 构造解:若可行,对每个党派 \(i\),若 scc[2i-1] > scc[2i],则输出 2i-1(选择第一个代表);否则输出 2i(选择第二个代表)。这是因为 SCC 编号较大的分量在拓扑序中靠后,选择该分量中的文字(即该代表)可保证所有蕴含关系成立。
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n + m)\),Tarjan 算法线性。
    • 空间复杂度:\(O(n + m)\),存储邻接表和辅助数组。
  5. 2-SAT 的核心思想

    • 逻辑蕴含图:每个变量对应两个节点(真/假),每个约束转化为两条蕴含边,构成一个有向图。若一个文字能推出另一个文字,则存在路径。
    • 强连通分量与矛盾:若一个变量的真和假在同一个强连通分量中,则存在矛盾,无解。
    • 拓扑序赋值:Tarjan 的 SCC 编号顺序是反拓扑序,因此选择编号较大的分量中的文字作为真值,可以保证所有蕴含关系被满足。
    • 适用场景:适用于变量取布尔值、约束为析取式的可满足性问题,如排课、调度、电路设计等。

【算法标签】

提高 #2-SAT

【代码详解】

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

const int N = 8005 * 2;            // 最多代表数 2n,n 最大为 8000(根据原题范围)
const int M = 20005 * 2 * 2;       // 边数上限:m 最大 20000,每条厌恶对建两条边,再加反向边?实际建边数量为 2m

int n, m;                          // n: 党派数,m: 不友好代表对数
int h[N], e[M], ne[M], idx;        // 链式前向星邻接表
int dfn[N], low[N], tim;           // Tarjan 时间戳、追溯值、时间戳计数器
int stk[N], top;                   // 手动栈,用于 Tarjan
int scc[N], cnt;                   // scc[i]: 节点 i 所属的强连通分量编号,cnt: 分量总数

// 添加一条有向边 a -> b
void add(int a, int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx++;
}

// 获取代表 x 所在党派的另一个代表(即其对立面)
// 若 x 为奇数(2i-1),则返回 x+1(2i);若为偶数,则返回 x-1
int get(int x)
{
    return (x % 2) ? x + 1 : x - 1;
}

// Tarjan 算法求有向图的强连通分量
void tarjan(int x)
{
    dfn[x] = low[x] = ++tim;       // 初始化时间戳
    stk[++top] = x;                // 将当前节点入栈

    // 遍历所有出边
    for (int i = h[x]; i != -1; i = ne[i])
    {
        int y = e[i];
        if (!dfn[y])               // 若未访问,递归搜索
        {
            tarjan(y);
            low[x] = min(low[x], low[y]);
        }
        else if (!scc[y])          // 若已访问但未分配 SCC 编号,说明 y 在栈中
        {
            low[x] = min(low[x], dfn[y]);
        }
    }

    // 若 x 为当前 SCC 的根节点,则弹出栈中元素,构成一个 SCC
    if (low[x] == dfn[x])
    {
        ++cnt;                     // 新 SCC 编号
        for (int y = -1; y != x; )
        {
            y = stk[top--];        // 出栈
            scc[y] = cnt;          // 标记所属 SCC
        }
    }
}

int main()
{
    cin >> n >> m;                 // 输入党派数和厌恶关系数

    memset(h, -1, sizeof(h));      // 初始化邻接表头

    // 对于每一对厌恶关系 (a, b),添加蕴含边 a -> ¬b 和 b -> ¬a
    while (m--)
    {
        int a, b;
        cin >> a >> b;
        add(a, get(b));            // a 被选中 => b 不能被选中(即选 b 的对立面)
        add(b, get(a));            // b 被选中 => a 不能被选中
    }

    // 对每个节点求强连通分量
    for (int i = 1; i <= 2 * n; i++)
    {
        if (!dfn[i])
        {
            tarjan(i);
        }
    }

    // 检查是否存在矛盾:同一个党派的两个代表是否在同一个 SCC 中
    for (int i = 1; i <= 2 * n; i += 2)
    {
        if (scc[i] == scc[i + 1])  // 两个代表在同一个 SCC,则无解
        {
            cout << "NIE";
            return 0;
        }
    }

    // 输出一组可行解:按 SCC 编号顺序选择代表
    // 对于每个党派,选择 SCC 编号较大的那个代表(这是常见的构造方法)
    for (int i = 1; i <= 2 * n; i += 2)
    {
        if (scc[i] > scc[i + 1])
        {
            cout << i + 1 << endl; // 选择第 i+1 个代表(即 2i)
        }
        else
        {
            cout << i << endl;     // 选择第 i 个代表(即 2i-1)
        }
    }

    return 0;
}

【运行结果】

3 2
1 3
2 4
1
4
5
posted @ 2026-09-09 20:18  团爸讲算法  阅读(4)  评论(0)    收藏  举报