[题解] 丘比特的烦恼(费用流求二分图带权匹配)

原博客地址

前言

文化课选手,最近没多少时间写题解,这题做了快两周了。若题解有误,欢迎指出。

简述费用流。

题目链接

题目大意

在平面直角坐标系内,有 n n n 个男性, n n n 个女性。将这些男女配对,每对男女若配对成功,将做出贡献,这些贡献会在输入中描述,若没有描述,则贡献为 1 1 1 。配对是有条件的,若在平面直角坐标系内的,将这对男女用线段连起来,若中间没有别的人(不分男女),且两点之间的距离小于一个定值,则可以配对,反之不能。求做的最大贡献。(注意,没有基友,不能百合,不能开后宫!!!)

易错点

其实是输入问题。

  • 不区分大小写,先将名字全部转换为大写或小写。
  • 没有描述的人之间贡献为 1 1 1

注意点就好了。

思路

男男女女的配对问题,很容易就想到是二分图带权匹配。其中,男性为左部点,女性为右部点,花费就为做的贡献。可以使用 K M KM KM 算法,但这里介绍使用费用流求解的二分图带权匹配。

费用流如何来求解二分图的带权匹配很简单:将源点与左部点连接起来,将汇点用右部点连接起来,左部点与右部点该咋连就咋连。其中,每条边的容量为 1 1 1 ,花费就为这条边的贡献。

结合图像理解:
在这里插入图片描述

在本张路中,绿色的点是源点,粉色的点是汇点,红色的点是左部点,蓝色的点是右部点。

先明确匹配的一条重要性质:每个点只有能有一条匹配边。也就意味着每个点只能被利用一次。而源点与汇点就很好地限制了每个点的利用,源点到达汇点只需要经过 3 3 3 条边,且严格遵守源点->左部点->右部点->汇点这条路径走。对于 1 1 1 条匹配边来说,它会消耗源点到达它的左部点,它的右部点到达汇点这条边,这就意味着它的左部点和右部点不能再次利用,从而满足匹配的这条性质。

故而,按照上述方法建一张网络,对与这张网络跑一边最大费用最大流即可。本文使用著名又基础的 E d m o n d − K a r p Edmond-Karp EdmondKarp 算法实现。

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;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值