P1231 教辅的组成

题目链接

分析

很像是匹配题,只不过这次一组完整的匹配包含三个元素:书,习题,答案。

那么同样的,我们也可以朴素的建立一个网络流:源点连向习题,习题连向书,书再连向答案,答案连向汇点,边权均为 1,然后跑一边最大流。

过了样例,喜洋洋的去提交,然后......听取 WA 声一片(逃)。

没道理啊,照着书打的 Dinic 模板,不应该错啊?

其实 Dinic 没问题(后来发现还是有问题的),是建图的锅。

我们考虑这种情况:

示意图:错误的示范

按道理应该是 1,但实际上跑出的最大流是 2。

为啥出错呢?我们可以显然发现,问题在于:每本书实际上只可以用一次,但如果我们用上面的方法建图的话,每本书就可以被多次用,导致出错。

咋整呢?这时候我们就用到了一种奇妙的方法,也是图论中最常见的技巧:拆点(点边转换的一种)。

不多说,直接上图:

示意图:正确的示范

这样就成功加上了限定条件:每本书只能用一次。

这样建好图之后,便可以扔到 Dinic 里面跑一遍最大流了。

代码

#include<bits/stdc++.h>
using namespace std;
const int INF = 1 << 30, N = 40010;
int n1, n2, n3, m1, m2, s, t;
struct Edge {
    int from, to, flow;
    Edge(int u, int v, int f) : from(u), to(v), flow(f) {}
};
vector<Edge> edges;
vector<int> G[N];
void addEdge(int from, int to, int flow) {
    edges.push_back((Edge){from, to, flow});
    edges.push_back((Edge){to, from, 0});
    G[from].push_back(edges.size() - 2);
    G[ to ].push_back(edges.size() - 1);
}

int d[N];
queue<int>q;
bool bfs() {
    memset(d, 0, sizeof(d));
    while (!q.empty()) q.pop();
    q.push(s), d[s] = 1;
    while (!q.empty()) {
        int x = q.front(); q.pop();
        if (x == t) return true;
        for (int i = 0; i < G[x].size(); ++i) {
            Edge &e = edges[G[x][i]];
            int to = e.to;
            if (!d[to] && e.flow)
                q.push(to), d[to] = d[x] + 1;
        }
    }
    return false;
}
int Dinic (int x, int flow) {
    if (x == t) return flow;
    int rest = flow;
    for (int i = 0; i < G[x].size(); ++i) {
        Edge &e = edges[G[x][i]], &fe = edges[G[x][i]^1];
        int to = e.to;
        if (e.flow && d[to] == d[x] + 1) {
            int k = Dinic(to, min(rest, e.flow));
            if (!k) d[to] = 0;
            e.flow -= k, fe.flow += k;
            rest -= k;
            if (rest == 0) break;
        }
    }
    if (rest == flow) d[x] = 0;
    return flow - rest;
}
int main()
{
    scanf("%d%d%d", &n1, &n2, &n3);
    scanf("%d", &m1);
    for (int i = 1; i <= n1; ++i)
        addEdge(n2 + i, n1 + n2 + i, 1);
    for (int i = 1; i <= m1; ++i) {
        int u, v;
        scanf("%d%d",&u, &v);
        addEdge(v, n2 + u, 1);
    }
    scanf("%d", &m2);
    for (int i = 1; i <= m2; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        addEdge(n1 + n2 + u, 2 * n1 + n2 + v, 1);
    }
    s = 0, t = 2 * n1 + n2 + n3 + 1;

    for (int i = 1; i <= n2; ++i)
        addEdge(s, i, 1);
    for (int i = 1; i <= n3; ++i)
        addEdge(2 * n1 + n2 + i, t, 1);

    //solve
    int maxflow = 0;
    int flow;
    while (bfs())
        while (flow = Dinic(s, INF)) maxflow += flow;

    //output
	printf("%d\n", maxflow);
	return 0;
}

一些疑惑

关于 Dinic ,有一些疑惑点,折磨了我一个晚上:

int Dinic (int x, int flow) {
    if (x == t) return flow;
    int rest = flow;
    for (int i = 0; i < G[x].size(); ++i) {
        Edge &e = edges[G[x][i]], &fe = edges[G[x][i]^1];
        int to = e.to;
        if (e.flow && d[to] == d[x] + 1) {
            int k = Dinic(to, min(rest, e.flow));
            if (!k) d[to] = 0;
            e.flow -= k, fe.flow += k;
            rest -= k;
            if (rest == 0) break;
        }
    }
    if (rest == flow) d[x] = 0;
    return flow - rest;
}

除了多路增广,这段代码一共有三处剪枝:第 9 行,第 12 行, 第15 行

在《算法竞赛》上面,只加上了第 9 行的剪枝,在做这题前也没有出过锅。但是在这题就发生问题了:

3 处剪枝选或者不选,一共有 \(2^3=8\) 三种可能,其中 7 种可以 AC 此题,有一种则会 TLE。

你觉得会是哪一种?很奇怪,并不是没优化的那种,而是只加上第九行的那种,也就是书上的那种。

那么问题来了:

  1. 为啥加上剪枝之后,反而会跑的更慢呢?
  2. 第 9 行和第 15 行,看起来完全一致,为啥跑起来却会天壤之别呢?

现在并不是很懂,只好把这些剪枝都加上去,反正不会导致题目出错......

补充:

lyd大神现身说法,Orz

lyd大神现身说法

posted @ 2020-12-22 23:59  cyhforlight  阅读(81)  评论(0编辑  收藏  举报