P4249 [WC2007] 剪刀石头布(图论建模、费用流)题解简析
P4249 剪刀石头布(费用流差分建图解法)
问题描述
给定一张 \(n\) 个点的完全图,其中部分边的方向已确定,其余边的方向未确定。要求确定所有未定向边的方向,使得图中三元环的个数尽可能多(\(n \leq 100\))。
核心思路:正难则反 + 容斥计数 + 费用流建模
关键收获:图论计数套路口诀:正难则反、容斥反演
1. 三元环计数的容斥转化
直接计算 “合法三元环”(即每个点出度均为 1 的三元环)数量较困难,采用正难则反思想,通过 “总三元环数 - 非法三元环数” 求解,目标转化为最小化非法三元环数。
(1)总三元环数
完全图中任选 3 个点即可构成一个三元环,总数量为组合数:
$
\binom{n}{3} = \frac{n(n-1)(n-2)}{6}
$
(2)非法三元环的定义与计数
-
非法三元环:存在一个点的出度为 2 的三元环(即该点向另外两个点各连一条有向边)。
-
关键结论:设第 \(i\) 个点的最终出度为 \(deg_i\),则该点贡献的非法三元环数为 \(\binom{deg_i}{2}\)(从出边中任选 2 条,与对应两个点构成非法三元环)。
-
总非法三元环数:所有点贡献之和,即 \(\sum_{i=1}^n \binom{deg_i}{2}\)。
(3)目标函数转化
原问题 “最大化合法三元环数” 等价于:
因此只需最小化\(\sum_{i=1}^n \binom{deg_i}{2}\)即可。
2. 问题约束与费用流建模关联
(1)问题约束
-
每条边仅能选择一个方向(有向边从一个点指向另一个点,不重复、不遗漏)。
-
已确定方向的边:直接固定对应点的出度贡献(如边 \(i \to j\) 确定,则 \(deg_i\) 增加 1)。
-
未确定方向的边:二选一(要么 \(i \to j\) 使 \(deg_i\) 增加 1,要么 \(j \to i\) 使 \(deg_j\) 增加 1)。
(2)费用函数转化
需最小化的目标 \(\sum_{i=1}^n \binom{deg_i}{2}\) 可展开为:
由于 \(\sum_{i=1}^n \frac{1}{2}(-deg_i)\) 是常数(总边数固定,\(\sum deg_i = \binom{n}{2}\)),因此最小化 \(\sum \binom{deg_i}{2}\) 等价于最小化 \(\sum deg_i^2\)。
而 \(deg_i^2\) 是关于流量(\(deg_i\) 可视为流过该点的流量)的凸函数,可通过差分建图将二次费用转化为线性费用,适配最小费用最大流模型。
3. 费用流建图细节
(1)节点定义
| 节点类型 | 节点标识 / 范围 | 含义说明 |
|---|---|---|
| 源点 | \(S = 1\) | 流量起点,用于分配每条边的方向选择 |
| 边节点 | \(1 + 1 \sim 1 + m\)(\(m = \binom{n}{2}\)) | 每个节点对应完全图中的一条边,流量为 1 表示该边的方向已确定 |
| 点的差分节点 A | \(tox(i, 1) = 1 + m + i\) | 第 \(i\) 个点的差分入节点,用于累计出度流量 |
| 点的差分节点 B | \(tox(i, 2) = 1 + m + n + i\) | 第 \(i\) 个点的差分出节点,连接汇点 |
| 汇点 | \(T = 2 + m + n + n\) | 流量终点,所有流量最终汇入此处 |
其中 \(tox(i, type)\) 是自定义函数,用于计算第 \(i\) 个点的差分节点 A(\(type=1\))或 B(\(type=2\))的编号。
(2)边的构建规则
- 源点 → 边节点
-
对每条边(共 \(m\) 条),构建边 \(S \to (1 + ec)\)(\(ec\) 为边的序号,从 1 到 \(m\)),容量 \(w=1\),费用 \(c=0\)。
-
含义:每条边仅能选择一个方向(流量为 1,费用为 0 表示无额外开销)。
-
边节点 → 差分节点 A
根据边的方向状态分三种情况:
-
若边 \(i \leftrightarrow j\) 已确定为 \(i \to j\)(输入 \(mar[i][j] = 1\)):构建边 \((1 + ec) \to tox(i, 1)\),容量 \(w=1\),费用 \(c=0\)。
-
若边 \(i \leftrightarrow j\) 已确定为 \(j \to i\)(输入 \(mar[i][j] = 0\)):构建边 \((1 + ec) \to tox(j, 1)\),容量 \(w=1\),费用 \(c=0\)。
-
若边 \(i \leftrightarrow j\) 未确定(输入 \(mar[i][j] = 2\)):构建两条边 \((1 + ec) \to tox(i, 1)\) 和 \((1 + ec) \to tox(j, 1)\),容量均为 \(w=1\),费用均为 \(c=0\)。
-
含义:已确定方向的边固定贡献某点的出度,未确定的边二选一贡献出度。
-
差分节点 A → 差分节点 B(核心:二次费用转线性)
对每个点 \(i\),构建 \(n\) 条边 \(tox(i, 1) \to tox(i, 2)\),其中第 \(j\) 条边(\(j\) 从 1 到 \(n\))的容量 \(w=1\),费用 \(c=2j - 1\)。
- 推导:设流过该路径的流量为 \(deg_i\)(即点 \(i\) 的出度),则总费用为 \(\sum_{j=1}^{deg_i} (2j - 1) = deg_i^2\)(等差数列求和,恰好匹配需最小化的 \(\sum deg_i^2\))。
-
差分节点 B → 汇点
对每个点 \(i\),构建边 \(tox(i, 2) \to T\),容量 \(w=\text{INF}\)(足够大,确保流量不被限制),费用 \(c=0\)。
- 含义:差分节点 B 的流量(即出度)全部汇入汇点,无额外开销。
4. 代码实现
#include \<bits/stdc++.h>
const int N = 30000;
const int M = 1e5, INF = INT32\_MAX;
using namespace std;
struct Edge {
int to, next;
int w, c, nof = 0; // w:容量, c:费用, nof:已流流量
} e\[M];
int head\[N], idx = 0;
// 添加一条有向边 u->v,容量w,费用c
void addedge(int u, int v, int w, int c) {
e\[idx].to = v;
e\[idx].next = head\[u];
e\[idx].w = w;
e\[idx].c = c;
head\[u] = idx++;
}
// 添加双向边(正向+反向),反向边容量0,费用为负
void ade(int u, int v, int w, int c) {
addedge(u, v, w, c);
addedge(v, u, 0, -c);
}
int n, m, S, T;
// 计算第x个点的差分节点:type=1→A节点,type=2→B节点
int tox(int x, int type) {
int r = 1 + x + m;
if (type == 1)
return r;
else
return r + n;
}
bool ins\[N]; // 是否在队列中
int pre\[N]; // 前驱边索引
int dis\[N]; // 最小费用
// SPFA算法:寻找从S到T的最小费用路径
bool SPFA() {
memset(pre, -1, sizeof pre);
memset(dis, 127, sizeof dis); // 初始化无穷大
memset(ins, 0, sizeof ins);
queue\<int> q;
q.push(S);
ins\[S] = 1;
dis\[S] = 0;
while (!q.empty()) {
int x = q.front();
q.pop();
ins\[x] = 0;
for (int i = head\[x]; \~i; i = e\[i].next) {
// 容量已满,跳过
if (e\[i].nof >= e\[i].w)
continue;
int v = e\[i].to;
// 更新更优费用路径
if (dis\[v] > dis\[x] + e\[i].c) {
dis\[v] = dis\[x] + e\[i].c;
pre\[v] = i;
if (!ins\[v]) {
q.push(v);
ins\[v] = 1;
}
}
}
}
return pre\[T] != -1; // 是否存在到T的路径
}
// 最小费用最大流计算
int mcmf() {
int res = 0; // 总最小费用
int miad; // 当前路径的最小剩余容量
while (SPFA()) {
// 找当前路径的最小容量
miad = INF;
for (int i = pre\[T]; \~i; i = pre\[e\[i ^ 1].to]) {
miad = min(miad, e\[i].w - e\[i].nof);
}
// 更新流量
for (int i = pre\[T]; \~i; i = pre\[e\[i ^ 1].to]) {
e\[i].nof += miad;
e\[i ^ 1].nof -= miad;
}
// 累加费用
res += miad \* dis\[T];
}
return res;
}
int mar\[101]\[101]; // 存储边的方向:0→j→i,1→i→j,2→未确定
int ei\[M], ej\[M]; // 存储每条边的两个端点(i,j)
int main() {
memset(head, -1, sizeof head);
// 关闭同步,加速输入输出
std::ios::sync\_with\_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
cin >> n;
m = (n \* (n - 1)) / 2; // 完全图的总边数
// 初始化源点S和汇点T
S = 1;
T = 2 + m + n + n;
// 1. 构建差分节点A→B的边(每个点i)
for (int i = 1; i <= n; ++i) {
int u = tox(i, 1); // 差分A节点
int v = tox(i, 2); // 差分B节点
for (int j = 1; j <= n; ++j) {
ade(u, v, 1, 2 \* j - 1); // 第j条边:容量1,费用2j-1
}
// 2. 构建差分节点B→T的边
ade(v, T, INF, 0);
}
int ecnt = 0; // 边的序号(从1开始)
// 3. 处理所有边(i\<j,避免重复)
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
cin >> mar\[i]\[j];
if (j <= i) // 只处理i\<j的边,避免重复计算
continue;
ecnt++;
ei\[ecnt] = i; // 记录边的端点i
ej\[ecnt] = j; // 记录边的端点j
// 4. 构建S→边节点的边
ade(S, 1 + ecnt, 1, 0);
// 5. 构建边节点→差分A节点的边(根据边的方向状态)
if (mar\[i]\[j] == 1) {
// 已确定i→j,边节点连i的差分A节点
ade(1 + ecnt, tox(i, 1), 1, 0);
} else if (mar\[i]\[j] == 0) {
// 已确定j→i,边节点连j的差分A节点
ade(1 + ecnt, tox(j, 1), 1, 0);
} else {
// 未确定,边节点连i和j的差分A节点(二选一)
ade(1 + ecnt, tox(i, 1), 1, 0);
ade(1 + ecnt, tox(j, 1), 1, 0);
}
}
}
// 计算最终合法三元环数
// 公式推导:总三元环数 - 最小化的(∑deg\_i² - ∑deg\_i)/2 + 常数项(∑deg\_i固定为m,m=∑deg\_i → ∑deg\_i/2 = m/2 = n(n-1)/4)
int ans = n \* (n - 1) \* (n - 2) / 6 - mcmf() / 2 + n \* (n - 1) / 4;
cout << ans << endl;
// 还原未确定边的方向(mar\[i]\[j] == 2的边)
for (int ec = 1; ec <= ecnt; ++ec) {
int k = 1 + ec; // 边节点编号
int i = ei\[ec], j = ej\[ec];
if (mar\[i]\[j] != 2) // 只处理未确定的边
continue;
// 遍历边节点的出边,找到已流流量的边(确定方向)
for (int r = head\[k]; \~r; r = e\[r].next) {
if (e\[r].to != S && e\[r].nof > 0) {
if (e\[r].to == tox(i, 1)) {
// 流量流向i的差分A节点 → i→j
mar\[i]\[j] = 1;
mar\[j]\[i] = 0;
} else if (e\[r].to == tox(j, 1)) {
// 流量流向j的差分A节点 → j→i
mar\[i]\[j] = 0;
mar\[j]\[i] = 1;
}
break;
}
}
}
// 输出最终的边方向矩阵
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
cout << mar\[i]\[j] << " ";
}
cout << endl;
}
return 0;
}
5. 结果计算与边方向还原
(1)合法三元环数计算
根据容斥公式,结合费用流结果(最小化的 \(\sum deg_i^2\)),最终合法三元环数为:
- 解释:\(\text{mcmf()}\) 是最小化的 \(\sum deg_i^2\),除以 2 得到 $$\sum \frac{deg_i^2}{2}$$ $$\frac{n(n-1)}{4}$$ 是 $$\sum \frac{deg_i}{2}$$(因 $$\sum deg_i = \binom{n}{2} = \frac{n(n-1)}{2}$$),两者相减得到 $$\sum \binom{deg_i}{2}$$
(2)未确定边的方向还原
遍历所有未确定方向的边(\(mar[i][j] = 2\)),通过查询边节点的出边流量:
-
若边节点 \(1 + ec\) 到 \(tox(i, 1)\) 的流量为 1 → 边方向为 \(i \to j\),更新 \(mar[i][j] = 1\),\(mar[j][i] = 0\)。
-
若边节点 \(1 + ec\) 到 \(tox(j, 1)\) 的流量为 1 → 边方向为 \(j \to i\),更新 \(mar[i][j] = 0\),\(mar[j][i] = 1\)。
最后输出更新后的边方向矩阵。
(注:文档部分内容由 AI格式整理)

浙公网安备 33010602011771号