嘘~ 正在从服务器偷取页面 . . .

CF103E Buying Sets 题解


CF103E Buying Sets 题解

题目链接:Buying Sets

题意

有一个大小为 \(n~(1 \le n \le 300)\) 的全集,每个元素是一个数

现在有 \(n\) 个子集,大小分别为 \(m_i\) ,题目保证任意 \(k\) 个子集的并的大小 \(\ge k\)

每个子集有一个可正可负的权值,你需要选出一些子集使得这些子集并的大小等于子集个数,

且所选子集的权值和最小。可以为空集。保证 \(\sum m_i \le 10^6\)

输入格式

第一行 \(n\) ,接下来 \(n\) 行:每行第一个数是 \(m_i\) ,接下来 \(m_i\) 个数是集合中的数。

最后一行 \(n\) 个数,表示每个子集

输出格式

一行,即权值和的最小值。

考虑将权值取反,这样只需要求最大权值。

注意到,如果我们将集合看作一些点,\(n\) 个数看作 \(n\) 个点,将集合与其中的点连边

那么,不考虑「子集并的大小等于子集个数」这个限制的话,这就是一个经典的最大权闭合子图问题。

最大权闭合子图问题,就是给定一张有向图,每个点有点权(可能为负)。

现在要找到一个点权和最大的子图,满足如果 \(u\) 在这个子图中,那么所有 \(u\) 的后继都在子图中。

这个问题可以用最小割解决。

  • 对于点权 \(w_i > 0\) 的点,连边 \(S\to i\) ,容量为 \(w_i\)
  • 对于点权 \(w_i < 0\) 的点,连边 \(i \to T\) ,容量为 \(-w_i\) (显然是正数)。
  • 对于原图中的边 \(e \in E\) ,连边 \(e\) 即可,容量为 \(+\infty\)

那么答案就是所有正点权的权值之和 减去 最小割。证明略。

现在考虑「子集并的大小等于子集个数」。

\(\mathrm{INF}\) 为一个极大值(正数),\(w_i\) 为取反后的集合的权值。

考虑以下最小割模型:

  • \(S\) 连向集合,容量为 \(\mathrm{INF} + w_i\) (割掉这条边相当于不选这个集合)。
  • 集合连向集合内的元素,容量为 \(\rm INF\)
  • 元素向 \(T\) 连边,容量为 \(\rm INF\) (割掉这条边相当于这个元素)。

不难发现,在最小割方案中,割掉集合连向集合内的数字的边是不优的。

由于题目保证任意 \(k\) 个子集的并的大小 \(\ge k\) ,根据 Hall 定理(如下)

对于一个二分图 \(G\sim (X,Y)\)\(X\) 存在一个匹配的充分必要条件为:

对于 \(X\) 的任意子集 \(S\)\(S\) 的邻居的个数 \(|N(S)|\) 满足 \(|N(S)| \ge |S|\)

换句话说,左侧任选 \(i\) 个点,右侧都至少有 \(i\) 个点跟它对应。

即这个图是一定存在完美匹配的,且满足 \(|N(S)| \ge |S|\) ,所以至少会割掉 \(n\) 条边

又因为边权最少为 \(\mathrm{INF}\) ,所以不会选超过 \(n\) 条边,否则一定不优。

那么设最小割选了集合 \(S\) 包括的(表示元素的)点,那么有 \[ |S| = |N(S)| \] 时间复杂度 \(\mathcal{O}\left(|V|^2|E|\right)\) ,其中 \(|V| = \mathcal{O}(n),|E| = \mathcal{O}(n^2)\)

代码:

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define INF 0x3f3f3f3f
void up(int &x, int y) { x < y ? x = y : 0; }
void down(int &x, int y) { x > y ? x = y : 0; }
#define rep(i, a, b) for(int i = (a), i##END = (b); i <= i##END; i++)
#define Rep(i, a, b) for(int i = (a), i##END = (b); i >= i##END; i--)
#define N ((int)(666))
#define M ((int)(5e5 + 15))

int S, T, tot, pos = 1, head[N], now[N], dep[N];
struct Edge { int v, w, next; } e[M];
void addEdge(int u, int v, int w) { e[++pos] = {v, w, head[u]}; head[u] = pos; }
void add(int u, int v, int w) { addEdge(u, v, w); addEdge(v, u, 0); }
bool bfs()
{
    static queue<int> q; rep(i, 1, tot) dep[i] = INF;
    q.push(S); dep[S] = 1; now[S] = head[S];
    while(!q.empty())
    {
        int u = q.front(); q.pop();
        for(int i = head[u]; i; i = e[i].next)
        {
            int v = e[i].v;
            if(e[i].w > 0 && dep[v] == INF)
            {
                dep[v] = dep[u] + 1;
                now[v] = head[v]; q.push(v);
            }
        }
    }
    return dep[T] != INF;
}
int dfs(int u, int in)
{
    if(u == T) return in;
    int out = 0;
    for(int i = now[u]; i; i = e[i].next)
    {
        if(!in) break; int v = e[i].v; now[u] = i;
        if(e[i].w > 0 && dep[v] == dep[u] + 1)
        {
            int res = dfs(v, min(in, e[i].w));
            e[i].w -= res; e[i ^ 1].w += res;
            in -= res; out += res;
        }
    }
    if(!out) dep[u] = INF;
    return out;
}
int Dinic()
{
    int res = 0;
    while(bfs()) res += dfs(S, INF);
    return res;
}
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    // freopen("check.in","r",stdin);
    // freopen("check.out","w",stdout);
    int n, x, res = 0; cin >> n; S = n * 2 + 1; T = S + 1; tot = T;
    rep(i, 1, n) { cin >> x; rep(j, 1, x){ cin >> x; add(i, x + n, INF); } }
    rep(i, 1, n) { cin >> x; res -= x; add(S, i, INF - x); add(i + n, T, INF); }
    res -= Dinic() - INF * n; cout << -res << '\n';
    return 0;
}

双倍经验:LOJ6045 「雅礼集训 2017 Day8」价


参考文献

[1] https://www.luogu.com.cn/article/xd366l3z


题外话

然后 cxy 填了通信工程,据说是填反了。难绷。

通信工程应该……也要学 C++ 的罢。😡😡😡。


文章作者: q779
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 q779 !
评论
  目录