很早以前就想写博客记录一下自己做过的各种有趣的网络流题目,然而总是忘了写。那么今天就在知乎专栏开一个坑,慢慢把各种值得记录的题目搬上去。
对于每一道可以补的题目,都会给出题解和AC代码。也许后面会出现一些找不到oj收录的题目,那么就只提供思路了。
为了节省版面,代码中将省去最大流或者费用流的主体部分,而仅仅保留api的调用。
希望本文能长期更新...
CodeForces1288F
https://codeforces.com/contest/1288/problem/F
本题大致题意为,给你一张二分图,图中每个顶点都被标为三种状态之一:红色、蓝色、未染色。要求你对二分图中的边进行染色(红色或蓝色)使得对于所有的红色点,与之相连接的红边数量严格大于蓝边数量;对于所有的蓝色点,与之相连接的蓝边数量严格大于红边数量;未染色的顶点无限制条件。注意允许保持某一条边未染色的状态。
染一条红边的代价为R,染一条蓝边的代价为B。需要求出最小代价以及对应的染色方案,或者说明这是不可能的。
本题在满足了一个约束条件的情况下要求最小化代价,因此很容易联想到费用流模型。约束条件作用在顶点上,可以将已被染色的边定为入边或出边,一条边上是否有流量代表着原图对应的边是否被染色。这样红边和蓝边的约束就被转化为流量的约束。这一步是容易想到的。
但是网络流模型中流量的约束是流入量等于流出量,不能直接应用在本题的不等约束上。因此除了原图中的边以外还需要引入额外的边以产生更多的流量。
在解释清楚这一步之前,先要对原图中对应的边定义方向。因为原图是二分图,所以我们可以定义染成某种颜色的边全都是从左部指向右部的,而另外一种颜色是右部指向左部。这里我们定义红边是从左到右而蓝边是从右到左。
原图中的被染色顶点被分为了四种情况。左部的顶点是红边流出而蓝边流入,因此左部的红点需要流出量大于流入量(多余的流量通过额外边流入,下同),左部的蓝点需要流入量大于流出量;右部的红点需要流入量大于流出量,右部的蓝点需要流出量大于流入量。而未染色的顶点因为不存在约束,因此连向它的边可以完全由另一侧的顶点决定性质。
于是原图中的边于费用流模型中的边存在了以下对应关系:如果是红点与红点相连,显然我们只会希望它是红边(或是不染色)。蓝点与蓝点同理。如果已染色的点和未染色的点之间存在边,我们同样也只会希望边的颜色与已染色点的颜色相同,而边的方向则取决于这个已染色点属于上述四种情况的哪一种(如,左部的红点应该让边为自己的入边)。未染色的点之间的边可以直接忽略。如果边的两端分别是红点和蓝点,那么我们需要建两条反向平行边使得它们分别是红边和蓝边。这些边的容量量全部都是1。
直觉上,我们应该从源点向左部的红点和右部的蓝点连边,从汇点向左部的蓝边和右部的红边连边。为了弄清楚这些边应该具有多少容量,我们要先来考察流网络中的流量守恒关系。所有的已染色的点都满足流入量大于流出量或者流出量大于流入量(这里说的是忽视额外边的情况)。一条边上如果存在流量,则必然会同时对两个顶点的流量产生影响:一个点流入量增加1,另一个点流出量增加1。因此原图中的不等约束关系可以被转换成等于关系:我们将流量差定义为流入量减去流出量,或者反之。所有顶点的流量差之和应该为零。换而言之,所有的顶点的多余流量是守恒的,这些多余流量需要引入额外边处理,这些额外边显然应该都连接同一个顶点。
事实上,因为在之前的处理中存在一个顶点用于表示未染色的边(尽管会存在多个分布在二分图不同侧的未染色点,但其实都可以映射到流网络的同一个顶点上),所以我们可以将额外边连接的顶点和这个顶点合并。
这样一来,本题的费用流模型就基本建立完毕了。剩下的工作是设置与源汇点相连的边的容量以初始化流网络。显然对于每一个已染色的点,都需要1单位的流量流入或流出。需要流入的点和需要流出的点数量不一定是相等的,而存在的差值则应该通过额外边相连接的顶点所抵消。于是我们就计算出了当流网络存在预期的流量守恒关系时的流量大小。如果最大流的值不等于它,则说明无解。
为了输出方案,我们需要记录流网络中与原图对应的边的对应序号。遵循如果有流量则意味着选中的原则。值得注意的是在建图的过程中是存在反向平行边的情况的,事实上网络流中会出现反向平行边同时存在流量的情况的,这种情况下两条边的流量相互抵消,实则意味着该边没有被选中。
AC代码(仅保留关键部分):
注:代码中的add_edge()方法会返回对应边在边集数组中的下标。
int n1, n2, m, r, b;
cin >> n1 >> n2 >> m >> r >> b;
string A, B;
cin >> A >> B;
vector<int> edge;
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
u--, v--;
if (A[u] == 'R') {
if (B[v] == 'R')
edge.push_back(cost_flow.add_edge(u + 1, n1 + v + 1, 1, r, i, 'R'));
else if (B[v] == 'B') {
edge.push_back(cost_flow.add_edge(u + 1, v + 1 + n1, 1, r, i, 'R'));
edge.push_back(cost_flow.add_edge(v + 1 + n1, u + 1, 1, b, i, 'B'));
} else
edge.push_back(cost_flow.add_edge(u + 1, 0, 1, r, i, 'R'));
} else if (A[u] == 'B') {
if (B[v] == 'R') {
edge.push_back(cost_flow.add_edge(u + 1, v + 1 + n1, 1, r, i, 'R'));
edge.push_back(cost_flow.add_edge(v + 1 + n1, u + 1, 1, b, i, 'B'));
} else if (B[v] == 'B')
edge.push_back(cost_flow.add_edge(v + 1 + n1, u + 1, 1, b, i, 'B'));
else
edge.push_back(cost_flow.add_edge(0, u + 1, 1, b, i, 'B'));
} else {
if (B[v] == 'R')
edge.push_back(cost_flow.add_edge(0, v + 1 + n1, 1, r, i, 'R'));
else if (B[v] == 'B')
edge.push_back(cost_flow.add_edge(v + n1 + 1, 0, 1, b, i, 'B'));
}
}
vector<int> in(n1 + n2 + 1);
for (int i = 0; i < n1; i++) {
if (A[i] == 'R') {
in[i + 1]++, in[0]--;
cost_flow.add_edge(0, i + 1, 1e9, 0);
} else if (A[i] == 'B') {
in[i + 1]--, in[0]++;
cost_flow.add_edge(i + 1, 0, 1e9, 0);
}
}
for (int i = 0; i < n2; i++) {
if (B[i] == 'R') {
in[i + n1 + 1]--, in[0]++;
cost_flow.add_edge(i + n1 + 1, 0, 1e9, 0);
} else if (B[i] == 'B') {
in[i + n1 + 1]++, in[0]--;
cost_flow.add_edge(0, i + n1 + 1, 1e9, 0);
}
}
int tot = 0, S = n1 + n2 + 1, T = n1 + n2 + 2;
for (int i = 0; i < n1 + n2 + 1; i++) {
if (in[i] > 0) tot += in[i];
if (in[i] > 0) cost_flow.add_edge(S, i, in[i], 0);
if (in[i] < 0) cost_flow.add_edge(i, T, -in[i], 0);
}
auto ans = cost_flow.get_mcmf(S, T);
if (ans.second != tot) {
cout << "-1n";
} else {
cout << ans.first << endl;
string _ans(m, 'U');
for (auto& e : edge) {
auto& E = cost_flow.E[e];
if (E.cap) continue;
if (_ans[E.id] != 'U')
_ans[E.id] = 'U';
else
_ans[E.id] = (char)E.color;
}
cout << _ans << endl;
}
CCPC Qinhuangdao 2019 E
https://codeforces.com/gym/102361/problem/E
其实本题说不上“有趣”,记录的原因仅仅是这题现场赛通过的人很少但其实是个傻逼题。。。
大致题意为,一个n*m的矩形区域,顶部有a个入口,底部有b个出口。这个矩形区域中可能有部分格子是无法通行的。现在从每个入口都放一个机器人,让它们穿过矩形区域从底部的出口出去。这些机器人只能走直线(一开始机器人的方向是向下的),但是可以在某一个格子上放置转向器使得走到该格子上的机器人改变方向,转向器分为四种:
1.允许上-右或者右-上转向。即从上往下走的机器人转向右或者从右往左的机器人转向上,以下类似。
2.允许上-左或者左-上转向。
3.允许左-下或者下-左转向。
4.允许右-下或者下-右转向。
每个格子上可以同时经过多个机器人,但是每个格子上只能放置一个转向器,且放置转向器后该格子就不能接受除转向以外的其它进入方向(如允许上-右或右-上的转向器就不能允许从下往上或者从左往右的方向进入)。问是否存在一种放置转向器的方式使得所有的机器人都能穿过矩形区域并离开?
本题的坑点可能就在于题面中的“每个格子上可以同时经过多个机器人”,但事实上一种合法的机器人行走路线中,任意两个机器人的路线都是不可能出现重叠的(可以允许交叉),这里省去证明过程,只需要自己画一画图想一想就能明白为什么。
于是这题变成了最大流傻逼题,所有边都连流量为1的边即可。当然少不了拆点:把一个点拆成横点和竖点,横点横连,竖点竖连。同时每个点的横点和竖点之间同样连边,表示放置转向器转向。注意对于所有的点对应的横点和竖点之间都需要连出反平行的两条边以表示可以双向通过。
然后判断一下最大流量是否等于机器人的数量即可。
AC代码(仅保留关键部分):
int T;
cin >> T;
int n, m, a, b;
auto vertpos = [&](int x, int y) { return m * x + y; };
auto horzpos = [&](int x, int y) { return m * x + y + n * m; };
while (T--) {
cin >> n >> m >> a >> b;
vector<vector<int>> G;
max_flow<int> flow(n * m * 2 + 2, G);
vector<string> mat(n);
for (auto& i : mat) cin >> i;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (mat[i][j] == '1') continue;
if (i && mat[i - 1][j] == '0')
flow.add_edge(vertpos(i, j), vertpos(i - 1, j), 1);
if (j && mat[i][j - 1] == '0')
flow.add_edge(horzpos(i, j), horzpos(i, j - 1), 1);
if (i + 1 < n && mat[i + 1][j] == '0')
flow.add_edge(vertpos(i, j), vertpos(i + 1, j), 1);
if (j + 1 < m && mat[i][j + 1] == '0')
flow.add_edge(horzpos(i, j), horzpos(i, j + 1), 1);
flow.add_edge(vertpos(i, j), horzpos(i, j), 1);
flow.add_edge(horzpos(i, j), vertpos(i, j), 1);
}
}
int S = n * m * 2, T = n * m * 2 + 1;
for (int i = 0; i < a; i++) {
int p;
cin >> p;
flow.add_edge(S, vertpos(0, p - 1), 1);
}
for (int i = 0; i < b; i++) {
int p;
cin >> p;
flow.add_edge(vertpos(n - 1, p - 1), T, 1);
}
if (flow.get_maxflow(S, T) == a)
cout << "Yesn";
else
cout << "Non";
}
EC-final 2016 J
https://codeforces.com/gym/101194
2016年EC-final的J题。大致题意为,给你一个n*m的矩形区域,可以在区域的每个格子上放置管道,管道有以下四种类型:
1.下和右连接。2.左和上连接。3.上和右连接。4.下和左连接。
如下图所示:
乍一看上去似乎和2019年秦皇岛E题很相似?
对于每一个格子,如果格子上的管道连到了与它相邻的上下左右四个格子,那么都会分别获得四种不同的收益。但是有两个限制条件:一是区域中所有的管道都必须形成环(可以分别属于不同的环),二是区域中有些格子上必须被放置管道。
下图中展示了三种放置管道的情况,灰色的格子表示必须被放置管道的格子。可见下图中只有中间的情况是合法的,最右边的情况里,管道没有形成环。(注意此处原题面有错误,我当时做这题就被坑了好久。)
请输出合法的放置管道能获得的最大收益,或者说明这是不可能的。
这一题同时具备网格图和横竖的不同连接性质这两大特征,因此对每个格子拆点是没的跑了。但拆完点以后怎么做?一个很符合直觉的想法就是让流网络中的流像题目中管道的放置的那样流动,但如果基于这样的想法的话下一步的考虑会困难重重:首先我们怎么保证流形成一个环?其次怎么保证某些格子上一定有流量?再其次我们怎么保证流不会一直流出一条直线?(根据题意,因为所有的管道都是拐弯的,所以合法的管道放置一定是呈锯齿状的,唯一的可行的矩形管道就是上图中的正方形管道放置。)
这一道题能否做出来取决于是否能否定掉一开始的直觉,而多就能做出来则取决于有多快能下定决心完全放弃之前的思考来重新构思模型。
幸运的是,熟练的网络流选手在面对网格图的时候应该还会迅速联想到另一个关键词:二分图。(回顾一下:如果网格图中每个点只与它上下左右四个相邻的点连接,那么该图是二分图。)
先来回想一下我们最初决定拆点的时候:不难想到,横点与相邻的竖点相连,竖点与相邻的横点相连。当开始从二分图作为切入点思考时,很自然地联想到让一个点和它相邻的点匹配起来,而且匹配具有排他性,我们恰好也不希望已经流入横向的流的节点再流出横向的流。这时,我们发现,本题的二分图似乎不像寻常套路一样是一个点和它相邻的点分别属于两部,而是一个点的横点和竖点分属于两部。
在上图中,我们用红色表示横向的连边,蓝色表示竖点的连边。在同一个Z轴左部上的两个点是对应某一个格子的横点和竖点。我们发现如果采用二分图匹配的模型,那么红边和蓝边都无法被连续选中,于是就保证了不会出现连续直线的管道。进一步地,当某一个点的横点已经被匹配的时候,它的竖点仍然是不受影响的,于是它可以和相邻的两个竖点中的任意一个进行匹配,这样实际上就保证了管道的连接是衔接上的。
但我们还需要保证管道最终全部形成环,这一点怎么处理呢?我们不妨来先考虑怎么表示一个格子没有放管道的情况,很容易想到我们只需要在横点和竖点连一条费用为0的边,如果这条边被选中就意味着该格子上没有管道,同样地也解决了如何强制一个格子上必须有管道的问题:这个格子的横点和竖点之间没有边相连。
于是我们发现,无论格子上是否存在管道,它都会贡献一个匹配数。并且不难发现一种合法的管道放置情况一定对应完美匹配。因为如果管道没有形成环,那么剩出来的格子的横点或者竖点一定是已经被匹配了一个的,多出来的一个点就没法和自己的横点(竖点)再匹配了。
于是到此我们的模型就建立完了。接下来跑一下费用流,看一看最大流量是否等于n*m就可以了,然后输出最大费用即可。
AC代码(仅保留关键部分):
注:本题中我的费用流模板如果使用堆优化dijkstra的方法会在codeforces上tle,但是使用slf的spfa费用流则速度非常快,只有时限的1/3。
int n, m;
int T;
auto pos = [&](int i, int j) { return (i * m + j) * 2; };
scanf("%d", &T);
vector<vector<int>> G;
vector<int> dis, h;
vector<vector<int>> row, column;
for (int t = 1; t <= T; t++) {
scanf("%d%d", &n, &m);
mcmf<int, int> graph(n * m * 2 + 2, G, dis, h);
const int S = n * m * 2, T = n * m * 2 + 1;
column.assign(n, vector<int>(m - 1));
row.assign(n - 1, vector<int>(m));
for (auto& i : column)
for (auto& j : i) scanf("%d", &j);
for (auto& i : row)
for (auto& j : i) scanf("%d", &j);
int E;
scanf("%d", &E);
int x, y;
vector<vector<bool>> judge(n, vector<bool>(m));
for (int i = 0; i < E; i++) {
scanf("%d%d", &x, &y);
judge[x - 1][y - 1] = true;
}
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++) {
graph.add_edge(S, pos(i, j), 1, 0);
graph.add_edge(pos(i, j) + 1, T, 1, 0);
if (!judge[i][j]) graph.add_edge(pos(i, j), pos(i, j) + 1, 1, 0);
if ((i + j) & 1) {
if (j != 0)
graph.add_edge(pos(i, j), pos(i, j - 1) + 1, 1, -column[i][j - 1]);
if (j != m - 1)
graph.add_edge(pos(i, j), pos(i, j + 1) + 1, 1, -column[i][j]);
} else {
if (i != 0)
graph.add_edge(pos(i, j), pos(i - 1, j) + 1, 1, -row[i - 1][j]);
if (i != n - 1)
graph.add_edge(pos(i, j), pos(i + 1, j) + 1, 1, -row[i][j]);
}
}
auto ans = graph.get_mcmf(S, T);
printf("Case #%d: ", t);
if (ans.second != n * m)
printf("Impossiblen");
else
printf("%dn", -ans.first);
}
ICPC Shanghai 2019 M
评论区里的本题出题人要求更新本题,于是今天下午就来更新了。
好像现在还没有JOJ收录本题?那么就只能只说思路暂不提供代码了。btw光找题面都找了我好久。。。(因此下面附上题面截图)
题意大致为:给你一个长为n的数组
显然本题可以把原数组中的一个合法插入位置视作一个顶点,这样会出现n+1个点。从形象的角度来说,我们可以把其中n-1个插入位置视作“区间”(头部的插入位置和尾部的插入位置除外)。m个数与这n+1个顶点进行匹配,于是原问题转换为了二分图的最大带权匹配问题。
会有那么简单吗?虽然我没去ICPC上海2019且是今天才看了这题的题面,但是ICPC上海现场赛之后发生了什么我还是知道的。
我们先来简单分析一下这种朴素的建图方法的费用流时间复杂度。我们的二分图是一个完全二分图,即对于图两部的任意两点都存在一条边相连。于是这需要
据说这样写的都TLE了。
刚刚思考这道题的时候总觉得有贪心的高论,但是呢在这里我们只考虑网络流的解法。于是现在摆在我们面前的只有一条路:通过合适的数据结构来优化建图。
(P.S:决定了下一题就更一道线段树优化建图的二分图匹配。。。)
我们刚刚说到,可以把除了头尾之外的其它合法插入位置视为一个区间,如下图所示:
其中
-
。此时的值在之间,插入以后其实不影响答案,这一小段的贡献仍然是。
-
。此时如上图中的,插入后这一小段对答案的贡献是。
-
。此时如上图中的,插入后这一小段对答案的贡献是。
我们发现,无论哪一种情况,对答案的贡献中都会包括
于是情况1我们可以直接忽视,只考虑情况2和情况3。又因为情况3其实本质和情况2一样,因此我们接下来只讨论情况2,最后再将结论运用于情况3即可。
不难发现,对于情况2,对于所有的
到这里我们发现了突破口,借助于前缀和性质,我们可以反复利用之前的一些边,这些边的费用表示了前面的
上述的这段话很抽象吗?具体来说我们需要新引入n-1个点作为“前缀和”。首先将所有的区间按照
那么现在处理完了情况2,对于情况3该怎么办呢?很显然我们再新建n-1个点作为“后缀和”,然后再做类似的事情就可以了。
最后对于头尾两个可以插入的位置,我们用朴素的方法处理即可。
于是我们一共使用了
不知道这是不是正解? @snowy smile
一晃一个多月没更新了,明明ICPC上海2019已经重现了却还没把M给写一下。接下来即将去实习就更没有多少时间能投入到算法竞赛上了。今天有人私信问了我一道题,那么就顺便更新一下吧。(说好的下一题是线段树优化建图呢?)
ICPC Asia Hatyai 2012 A
https://codeforces.com/gym/101549
这道题在现场赛的时限是5s,不知道为什么GYM上给了10s,而SPOJ上则仅仅有可怜的1S。。。
大致题意为:有一个
我们可以枚举行或列上值为1的元素的个数,找出对应的最小取反次数,最后输出所有方案中最小的那一个。
因为存在一个守恒关系
然而这道题单组样例的数据组数足足有1000组。如果对于每组样例的每一种情况都独立建图跑费用流,那么显然是会超时的(GYM上给了10s好像能卡过去?)。实际上对于不同的情况,我们只是改变了一些边的容量,所以我们是可以重复利用之前费用流的结果的。此外注意到费用流建的图是完全二分图,那么我们用朴素的dijkstra算法来求增广路是要比别的方法快的。
AC代码(仅保留关键部分):
int T;
cin >> T;
string mat[45];
vector<vector<int>> G;
vector<int> dis, h;
for (int t = 1; t <= T; t++) {
int n, m;
cin >> n >> m;
for (int i = 0; i < n; i++) {
cin >> mat[i];
}
mcmf<int, int> cost_flow(n + m + 2, G, dis, h);
vector<int> Redge, Cedge;
Redge.reserve(n);
Cedge.reserve(m);
int S = n + m, T = n + m + 1;
for (int i = 0; i < n; i++) Redge.push_back(cost_flow.add_edge(S, i, 0, 0));
for (int i = 0; i < m; i++)
Cedge.push_back(cost_flow.add_edge(i + n, T, 0, 0));
int num = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++) {
num += mat[i][j] == '1';
cost_flow.add_edge(i, j + n, 1, mat[i][j] != '1');
}
int prer = 0, prec = 0, cost = 0, flow = 0, mini = 1e9;
for (int r = 1; r <= m; r++) {
int c = r * n / m;
if (c * m != r * n) continue;
for (int i : Redge) cost_flow.E[i].cap = r - prer;
for (int i : Cedge) cost_flow.E[i].cap = c - prec;
prer = r, prec = c;
auto res = cost_flow.get_mcmf(S, T);
cost += res.first, flow += res.second;
int tmp = cost * 2 + num - flow;
mini = min(mini, tmp);
}
cout << "Case " << t << ": " << min(num, mini) << endl;
}