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。
你觉得会是哪一种?很奇怪,并不是没优化的那种,而是只加上第九行的那种,也就是书上的那种。
那么问题来了:
- 为啥加上剪枝之后,反而会跑的更慢呢?
- 第 9 行和第 15 行,看起来完全一致,为啥跑起来却会天壤之别呢?
现在并不是很懂,只好把这些剪枝都加上去,反正不会导致题目出错......
补充:
lyd大神现身说法,Orz