题意:地球和火星发生了战争。现火星人要在一个N*M的网格降落他们的伞兵,而我们要消灭掉这些伞兵。有这样一种武器,它能够瞬间摧毁网格上某一行或者是某一列的所有目标。然而在哪一行或者哪一列部署这样的武器的花费是不同的。因此要求给出一种最省的方式使得所有的伞兵都能够被消灭。花费的定义是某种部署方案能够消灭所有伞兵,然后根据给定的行列费用把它们相乘即为。希望这一天永远不要到来吧。
解法:首先这题应该让无数少男少女想到了二分匹配中做到的关于行列匹配的题吧。只不过这题中并不是所有的行和列等价。而是行列的权值各不相同。类比于原来问题中的最小顶点覆盖,这里就是一个最小点权覆盖,也即使用最小点权和的点集使得所有的边至少有一个端点属于该点集。
将这个二分图稍加修改就变成了一个流网络。添加从源点到每行以及每列到汇点的边,容量待会儿再说。
假设有一个3*3的网格,其中(1,2)和(2,3)有伞兵:
我们再来看看割和最小割的定义:
割:在一个图G(V,E)中V是点集,E是边集。在E中去掉一个边集C使得G(V,E-C)不连通,C就是图G(V,E)的一个割,割将G的顶点集V划分成两个子集S和T=V-S。
最小割:在G(V,E)的所有割中,边权总和最小的割就是最小割。
对于流网络我们只对s-t割进行讨论,即划分出来的两个顶点子集一个包含源点s,一个包含汇点t。
假设题目所求的是权值之和而非之积最小。接着对上面图我们再来加入容量:
其中R[i]表示选择第i行的花费,C[i]表示选择第i列的花费。中间连接无穷大的边,这样我们考虑如下的一个割:
由于割的容量是所有前向割边的容量,又因为整个图的流量是有限的且小于容量最小的割,所以割不可能取到中间那些无穷大的边。又因为每一条INF边连接了S和T,那么每一条INF边的左端点或者右端点就必须是割边的一个端点,上图中的行1和列3即是的,否则的话将存在一条从S到T的通路。这样也就保证了每条割边的非源非汇端点覆盖了所有的INF边,也就满足了点覆盖。再根据割边的另一个端点是S还是T就能够判定是匹配行还是列了。那么对于一个最小割显然就是最佳匹配方法了。注意带了权之后的二分图,可能会部署大于最大匹配数的武器,原因很简单,有些行虽然能够消除多个目标,但是开销太大,选择一列一列消除可能花费更少。
由于我们假定是做的点权之和,题目是要求点权之积。只要对每个点权取一个对数,乘法就变成了加法,然后再取个幂就又回来了。
代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <cstdlib> #include <cstdio> #include <cstring> #include <cmath> #include <iostream> #include <algorithm> using namespace std; int N, M, L; // 原题定义的M*N的矩阵,自己改成N*M const int SS = 105, TT = 106; const double INF = 1e9; const double eps = 1e-8; double rf[55], cf[55]; struct Edge { double c; // 一个实数化的流量 int v, next; }; Edge e[10000]; int idx, head[110], lv[110]; int front, tail, que[110]; void insert(int a, int b, double c) { // printf("a = %d, b = %d, c = %f\n", a, b, c); e[idx].v = b, e[idx].c = c; e[idx].next = head[a]; head[a] = idx++; } bool sign(double x) { if (x > eps) return 1; else if (x < -eps) return -1; else return 0; } bool bfs() { front = tail = 0; memset(lv, 0xff, sizeof (lv)); lv[SS] = 0; que[tail++] = SS; while (front < tail) { int u = que[front++]; for (int i = head[u]; i != -1; i = e[i].next) { int v = e[i].v; if (!(~lv[v]) && sign(e[i].c)>0) { lv[v] = lv[u] + 1; if (v == TT) return true; que[tail++] = v; } } } return false; } double dfs(int u, double sup) { if (u == TT) return sup; double tf = 0, f; for (int i = head[u]; i != -1; i = e[i].next) { int v = e[i].v; if (lv[u]+1==lv[v] && sign(e[i].c)>0 && sign(f=dfs(v, min(e[i].c, sup-tf)))>0) { tf += f; e[i].c -= f, e[i^1].c += f; if (sign(sup-tf) == 0) return sup; } } if (sign(tf) == 0) lv[u] = -1; return tf; } double dinic() { double ret = 0; while (bfs()) { ret += dfs(SS, INF); } //printf("ret = %f\n", ret); return ret; } int main() { int T; scanf("%d", &T); while (T--) { idx = 0; memset(head, 0xff, sizeof (head)); scanf("%d %d %d", &N, &M, &L); for (int i = 1; i <= N; ++i) { scanf("%lf", &rf[i]); // 编译器竟然是读入double必须用lf,输出又必须用f...... rf[i] = log(rf[i]); insert(SS, i, rf[i]); insert(i, SS, 0); // 由于要花费是以乘法定义的,因此转化为对数后能够已加法来搞 } for (int i = 1; i <= M; ++i) { scanf("%lf", &cf[i]); cf[i] = log(cf[i]); insert(i+50, TT, cf[i]); insert(TT, i+50, 0); } int a, b; for (int i = 0; i < L; ++i) { scanf("%d %d", &a, &b); insert(a, 50+b, INF); insert(50+b, a, 0); } printf("%.4f\n", exp(dinic())); } return 0; }