[题解] 丘比特的烦恼(费用流求二分图带权匹配)
前言
文化课选手,最近没多少时间写题解,这题做了快两周了。若题解有误,欢迎指出。
题目大意
在平面直角坐标系内,有 \(n\) 个男性, \(n\) 个女性。将这些男女配对,每对男女若配对成功,将做出贡献,这些贡献会在输入中描述,若没有描述,则贡献为 \(1\) 。配对是有条件的,若在平面直角坐标系内的,将这对男女用线段连起来,若中间没有别的人(不分男女),且两点之间的距离小于一个定值,则可以配对,反之不能。求做的最大贡献。(注意,没有基友,不能百合,不能开后宫!!!)
易错点
其实是输入问题。
- 不区分大小写,先将名字全部转换为大写或小写。
- 没有描述的人之间贡献为 \(1\) 。
注意点就好了。
思路
男男女女的配对问题,很容易就想到是二分图带权匹配。其中,男性为左部点,女性为右部点,花费就为做的贡献。可以使用 \(KM\) 算法,但这里介绍使用费用流求解的二分图带权匹配。
费用流如何来求解二分图的带权匹配很简单:将源点与左部点连接起来,将汇点用右部点连接起来,左部点与右部点该咋连就咋连。其中,每条边的容量为 \(1\) ,花费就为这条边的贡献。
结合图像理解:
在本张路中,绿色的点是源点,粉色的点是汇点,红色的点是左部点,蓝色的点是右部点。
先明确匹配的一条重要性质:每个点只有能有一条匹配边。也就意味着每个点只能被利用一次。而源点与汇点就很好地限制了每个点的利用,源点到达汇点只需要经过 \(3\) 条边,且严格遵守源点->左部点->右部点->汇点这条路径走。对于 \(1\) 条匹配边来说,它会消耗源点到达它的左部点,它的右部点到达汇点这条边,这就意味着它的左部点和右部点不能再次利用,从而满足匹配的这条性质。
故而,按照上述方法建一张网络,对与这张网络跑一边最大费用最大流即可。本文使用著名又基础的 \(Edmond-Karp\) 算法实现。
C++代码
#include <map>
#include <cmath>
#include <queue>
#include <string>
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
using namespace std;
#define eps 1e-100
#define INF 0x3f3f3f3f
#define Min(a, b) ((a) < (b) ? (a) : (b))
#define Swap(a, b) ((a) ^= (b) ^= (a) ^= (b))
void Quick_Read(int &N) {
N = 0;
int op = 1;
char c = getchar();
while(c < '0' || c > '9') {
if(c == '-')
op = -1;
c = getchar();
}
while(c >= '0' && c <= '9') {
N = (N << 1) + (N << 3) + (c ^ 48);
c = getchar();
}
N *= op;
}
struct Node {
int to, val, cost, rev;
Node() {}
Node(int T, int L, int C, int R) {
to = T;
val = L;
cost = C;
rev = R;
}
};
const int MAXN = 1e4 + 5;
vector<Node> v[MAXN];
map<string, int> name;
int ship[MAXN][MAXN];
int x[MAXN], y[MAXN];
int k, n, s, t;
deque<int> q;
int dis[MAXN], maf[MAXN];
pair<int, int> pre[MAXN];
bool inque[MAXN], vis[MAXN];
int ans;
bool SPFA() {//寻找增广路
int iqn = 1, fis = 0;
for(int i = 0; i <= 2 * n + 1; i++)
inque[i] = false, dis[i] = -INF, maf[i] = INF;
dis[s] = 0;
inque[s] = true;
q.push_back(s);
while(!q.empty()) {
int now = q.front(); q.pop_front();
inque[now] = false;
fis -= dis[now];
iqn--;
int SIZ= v[now].size();
for(int i = 0; i < SIZ; i++) {
int next = v[now][i].to;
if(dis[next] < dis[now] + v[now][i].cost && v[now][i].val) {
dis[next] = dis[now] + v[now][i].cost;
maf[next] = Min(maf[now], v[now][i].val);
pre[next].first = now;
pre[next].second = i;
if(!inque[next]) {
inque[next] = true;
if(q.empty() || dis[next] < dis[q.front()] || dis[next] * iqn >= fis)
q.push_back(next);
else
q.push_front(next);
fis += dis[next] + v[now][i].cost;
iqn++;
}
}
}
}
return dis[t] != -INF;
}
int Update() {//沿着增广路增广
int now = t;
while(now != s) {
int next = pre[now].first;
int i = pre[now].second;
v[next][i].val -= maf[t];
v[now][v[next][i].rev].val += maf[t];
ans += v[next][i].cost * maf[t];
now = next;
}
return maf[t];
}
int Edmond_Karp() {//最大费用最大流
int res = 0;
while(SPFA()) {
do {
res += Update();
} while(vis[t]);
}
return res;
}
double Dist_From_To(int A, int B) {//两点间距离公式
double frontx = (x[A] - x[B]) * (x[A] - x[B]) * 1.0;
double fronty = (y[A] - y[B]) * (y[A] - y[B]) * 1.0;
double dist = sqrt(frontx + fronty);
return dist;
}
bool Judge_Dist(int A, int B) {//中间是否有人,是否在射程之内
double dist = Dist_From_To(A, B);
double maxdist = k * 1.0;
if(dist > maxdist)
return false;
for(int i = 1; i <= 2 * n; i++) {
if(i == A || i == B)
continue;
if(Dist_From_To(A, i) + Dist_From_To(i, B) - dist <= eps)
return false;
}
return true;
}
void Convert_Big(string &str) {//名字转换为大写
int SIZ = str.length();
for(int i = 0; i < SIZ; i++)
if(str[i] >= 'a' && str[i] <= 'z')
str[i] += 'A' - 'a';
}
void Build() {//源点到左部点,右部点到汇点连边
t = 2 * n + 1;
for(int i = 1; i <= n; i++) {
int idi = v[i].size();
int ids = v[s].size();
v[s].push_back(Node(i, 1, 0, idi));
v[i].push_back(Node(s, 0, 0, ids));
}
for(int i = n + 1; i <= 2 * n; i++) {
int idi = v[i].size();
int idt = v[t].size();
v[i].push_back(Node(t, 1, 0, idt));
v[t].push_back(Node(i, 0, 0, idi));
}
}
void Read() {
int ValBtoG;
string girl, boy;
Quick_Read(k);
Quick_Read(n);
for(int i = 1; i <= 2 * n; i++) {
Quick_Read(x[i]);
Quick_Read(y[i]);
cin >> boy;
Convert_Big(boy);
name[boy] = i;
}
for(int i = 1; i <= n; i++)
for(int j = n + 1; j <= 2 * n; j++)
ship[i][j] = 1;
cin >> boy;
while(boy != "End") {
Convert_Big(boy);
cin >> girl;
Convert_Big(girl);
int B = name[boy], G = name[girl];
if(G < B)
Swap(B, G);
Quick_Read(ValBtoG);
ship[B][G] = ValBtoG;
cin >> boy;
}
for(int i = 1; i <= n; i++)
for(int j = n + 1; j <= 2 * n; j++)
if(Judge_Dist(i, j)) {
int idi = v[i].size();
int idj = v[j].size();
v[i].push_back(Node(j, 1, ship[i][j], idj));
v[j].push_back(Node(i, 0, -ship[i][j], idi));
}
}
int main() {
Read();
Build();
Edmond_Karp();
printf("%d", ans);
return 0;
}