题解:洛谷 P5782 和平委员会
【题目来源】
【题目描述】
根据宪法,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
【核心思想】
-
问题分析:有 \(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-SAT + Tarjan 强连通分量:将每个变量拆成两个节点(真/假),建立有向图(蕴含图)。对 \(2n\) 个节点运行 Tarjan 算法求出所有强连通分量。若存在某个变量 \(x_i\) 的真假两个节点在同一个 SCC 中,则无解;否则有解。构造解时,对于每个党派,比较其两个代表所属 SCC 的编号,选择编号较大的那个代表(因为 Tarjan 的 SCC 编号顺序是逆拓扑序,编号较大的分量在拓扑序中更靠后,取真值可满足所有蕴含关系)。
-
关键步骤:
- 读入与建图:读取 \(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 编号较大的分量在拓扑序中靠后,选择该分量中的文字(即该代表)可保证所有蕴含关系成立。
- 读入与建图:读取 \(n, m\)。对每个党派,用
-
时间/空间复杂度:
- 时间复杂度:\(O(n + m)\),Tarjan 算法线性。
- 空间复杂度:\(O(n + m)\),存储邻接表和辅助数组。
-
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
浙公网安备 33010602011771号