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

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$ 包括的(表示元素的)点,那么有

时间复杂度 $\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 !
评论
  目录