【Coel.学习笔记】最小割进阶:最大权闭合图与最大密度子图

又要学定义了,有点糟心……

新知详解

包括最大权闭合图与最大密度子图。

最大权闭合图

闭合图是有向图的一个点集,满足任何一个点的出边指向的点都属于这个点集中,也就是说出边不能跨集合。点集与点所连的边合称为闭合子图

下图展示的例子中, \((1,3),(2,3),(1,2,3)\) 都不能构成一个闭合图,但 \((3,4)\) 可以构成一个闭合图。
image
最大权闭合图是点权和最大的闭合图。

求最大权闭合图,需要把这个问题对应到流网络的割集之中。

我们先试着把原图变成流网络。源点与所有点权为正的点连边,容量等于权值;汇点与所有点权为负的点连边,容量等于权值的相反数。对于原图存在的边,容量设为正无穷。

为了做法方便,我们定义一种特殊的割:
简单割:所有割边只为与源点和汇点所连边,不包含原图的边。由于中间的边容量已经为正无穷,所以最小割一定不会包含原图边。因此,最小割一定是一个简单割

接下来证明闭合图能够和简单割一一对应,这样也就可以用最小割模型求解了。首先,对于原图的任何一个闭合点集,构造流网络的割集,使得割的 \(S\) 集合为源点加上闭合子图点,\(T\) 集合为流网络去掉 \(S\) 集合点。因为对于闭合子图任意一个点,能走到的点都处在这个点集之中,所以 \(S\) 集合必然是闭合的,\(T\) 集合同理,那么闭合图对应简单割;反过来,对于一个简单割,它对应的闭合子图是 \(S\) 减去源点,由于 \(S\) 是简单割,所以不存在从 \(S\)\(T\) 的直接连边,因此简单割可以对应闭合图。

可以证明,最大权值和等于正权值之和减去最小割。

最大密度子图

给出一个无权值的有向图,从中选择一个点集 \(V^\prime\) 与边集 \(E^\prime\),使得边集中边所连点在点集之中。
最大密度子图就是找到一种选择方式,使得边集大小与点集大小之比最大。

怎么做?联系上次的 0/1 分数规划问题,转化为找到一个最大的 \(\lambda=\dfrac{|E^\prime|}{|V^\prime|}\),等价于 \(|E^\prime|-\lambda|V^\prime|=0\)。求解左边式子最大值,若大于 \(0\) 则二分右半区间,反之二分左半区间。

怎么把问题划归为流网络模型?经过非常复杂的一番推导(这里就不写了)可以知道,这个问题可以转化为求 \(c_{S,T}\) 最小值,即最小割。

建图方式为:对于每次二分得到的 \(\lambda\),统计每点的度数 \(deg_i\),给源点与所有点连上容量等于总边数 \(m\) 的边,汇点连上容量等于 \(m+\lambda * 2 - deg_i\) 的边,最后再给每个点之间两两连边。

例题讲解

[NOI2006]最大获利(最大权闭合图做法)

洛谷传送门
有若干的地址可以建造通讯站,在某个点建造通讯站的成本为 \(P_i\)。若干个用户群要在两个通讯站 \(A_i,B_i\) 沟通,公司可以获利 \(C_i\)。求出最大利润(获利总和减去建通讯站花费)。

解析:把建站看做负权点,用户群看做正权点,这个问题就转化为最大权闭合图问题。源点与用户群连边,汇点与通讯站连边,再在用户群与对应的通讯站连边,求正权值之和减去最小割即可。
实际上,这题的点具有特殊性(每个用户群只会与两个通讯站相连),所以用最大权闭合图模型不是最优解。使用最大密度子图模型可以实现更优解。
代码如下(缩短一下篇幅,以后只放主函数内容了,反正最大流算法都一样):

// Problem: P4174 [NOI2006] 最大获利
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P4174
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// Author:Coel
// 
// Powered by CP Editor (https://cpeditor.org)

/*省略了加边函数、dinic 的实现*/

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m;
    S = 0, T = n + m + 1;
    memset(head, -1, sizeof(head));
    for (int i = 1, p; i <= n; i++) {
        cin >> p;
        add(m + i, T, p);
    }
    for (int i = 1; i <= m; i++) {
        int a, b, c;
        cin >> a >> b >> c;
        add(S, i, c), add(i, m + a, inf), add(i, m + b, inf);
        sum += c;
    }
    cout << sum - dinic();
    return 0;
}

POJ3155 Hard Life

洛谷传送门
已知公司中一共有 \(n\) 名员工,员工之间共有 \(m\) 对两两矛盾关系,团队的管理难度系数等于团队中的矛盾关系对数除以团队总人数。找出一个安排方案,使得管理难度系数最大。
解析:最大密度子图的模板题,不过要输出方案。
输出方案的话,只要从源点跑一遍 dfs 就行了。
代码如下(这题用到了二分,dinic 实现有些许不同,所以完整放上):

// Problem: UVA1389 Hard Life
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/UVA1389
// Memory Limit: 0 MB
// Time Limit: 3000 ms
// Author:Coel
//
// Powered by CP Editor (https://cpeditor.org)

#include <cstdlib>
#include <cstring>
#include <iostream>
#include <queue>

using namespace std;

const int maxn = 1e5 + 10, inf = 1e8;
const double eps = 1e-8;

int n, m, S, T;
int head[maxn], nxt[maxn], to[maxn], cnt;
double c[maxn];
int d[maxn], cur[maxn], deg[maxn];
int ans;
bool vis[maxn];

struct node {
    int u, v;
} e[maxn];

void add(int u, int v, double w1, double w2) {
    nxt[cnt] = head[u], to[cnt] = v, c[cnt] = w1, head[u] = cnt++;
    nxt[cnt] = head[v], to[cnt] = u, c[cnt] = w2, head[v] = cnt++;
}

bool bfs() {
    queue<int> Q;
    memset(d, -1, sizeof(d));
    Q.push(S), d[S] = 0, cur[S] = head[S];
    while (!Q.empty()) {
        int u = Q.front();
        Q.pop();
        for (int i = head[u]; ~i; i = nxt[i]) {
            int v = to[i];
            if (d[v] == -1 && c[i] > 0) {
                d[v] = d[u] + 1;
                cur[v] = head[v];
                if (v == T) return true;
                Q.push(v);
            }
        }
    }
    return false;
}

double find(int u, double limit) {
    if (u == T) return limit;
    double flow = 0;
    for (int i = cur[u]; ~i && flow < limit; i = nxt[i]) {
        int v = to[i];
        cur[u] = i;
        if (d[v] == d[u] + 1 && c[i] > 0) {
            double t = find(v, min(c[i], limit - flow));
            if (t <= 0) d[v] = -1;
            c[i] -= t, c[i ^ 1] += t, flow += t;
        }
    }
    return flow;
}

void dfs(int u) {
    vis[u] = true;
    if (u != S) ans++;
    for (int i = head[u]; ~i; i = nxt[i]) {
        int v = to[i];
        if (!vis[v] && c[i] > 0) dfs(v);
    }
}

void init_k(double k) {
    memset(head, -1, sizeof(head));
    cnt = 0;
    for (int i = 0; i < m; i++) add(e[i].u, e[i].v, 1, 1);
    for (int i = 1; i <= n; i++) {
        add(S, i, m, 0);
        add(i, T, m + k * 2 - deg[i], 0);
    }
}

double dinic(double k) {
    init_k(k);
    double res = 0, flow;
    while (bfs())
        while ((flow = find(S, inf))) res += flow;
    return res;
}

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    while (cin >> n >> m) {
        memset(deg, 0, sizeof(deg));
        ans = 0;
        S = 0, T = n + 1;
        for (int i = 0; i < m; i++) {
            int u, v;
            cin >> u >> v;
            deg[u]++, deg[v]++;
            e[i].u = u, e[i].v = v;
        }
        double l = 0, r = m;
        while (r - l > eps) {
            double mid = (l + r) / 2;
            double t = dinic(mid);
            if (m * n - t > 0)
                l = mid;
            else
                r = mid;
        }
        dinic(l);
        dfs(S);
        if (!ans) {
            cout << 1 << '\n' << 1 << '\n';
            continue;
        }
        cout << ans << '\n';
        for (int i = 1; i <= n; i++)
            if (vis[i]) cout << i << '\n';
        cout << '\n';
    }
    return 0;
}
posted @ 2022-07-13 08:02  秋泉こあい  阅读(43)  评论(0编辑  收藏  举报