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)目标函数转化

原问题 “最大化合法三元环数” 等价于:

\[ANS = \binom{n}{3} - \sum_{i=1}^n \binom{deg_i}{2} \]

因此只需最小化\(\sum_{i=1}^n \binom{deg_i}{2}\)即可。

2. 问题约束与费用流建模关联

(1)问题约束

  1. 每条边仅能选择一个方向(有向边从一个点指向另一个点,不重复、不遗漏)。

  2. 已确定方向的边:直接固定对应点的出度贡献(如边 \(i \to j\) 确定,则 \(deg_i\) 增加 1)。

  3. 未确定方向的边:二选一(要么 \(i \to j\) 使 \(deg_i\) 增加 1,要么 \(j \to i\) 使 \(deg_j\) 增加 1)。

(2)费用函数转化

需最小化的目标 \(\sum_{i=1}^n \binom{deg_i}{2}\) 可展开为:

\[\binom{deg_i}{2} = \frac{deg_i(deg_i - 1)}{2} = \frac{1}{2}(deg_i^2 - deg_i) \]

由于 \(\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)边的构建规则

  1. 源点 → 边节点
  • 对每条边(共 \(m\) 条),构建边 \(S \to (1 + ec)\)\(ec\) 为边的序号,从 1 到 \(m\)),容量 \(w=1\),费用 \(c=0\)

  • 含义:每条边仅能选择一个方向(流量为 1,费用为 0 表示无额外开销)。

  1. 边节点 → 差分节点 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\)

  • 含义:已确定方向的边固定贡献某点的出度,未确定的边二选一贡献出度。

  1. 差分节点 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\))。
  1. 差分节点 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{ans} = \binom{n}{3} - \frac{\text{mcmf()}}{2} + \frac{n(n-1)}{4} \]

  • 解释:\(\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格式整理)

posted @ 2025-08-26 20:07  Director_Ni  阅读(6)  评论(0)    收藏  举报