1. 程式人生 > 實用技巧 >[題解] 丘位元的煩惱(費用流求二分圖帶權匹配)

[題解] 丘位元的煩惱(費用流求二分圖帶權匹配)

前言

文化課選手,最近沒多少時間寫題解,這題做了快兩週了。若題解有誤,歡迎指出。

簡述費用流。

題目連結

題目大意

在平面直角座標系內,有 \(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;
}