二分图匹配模型匈牙利算法的复杂度为 O ( n m ) O(nm) O(nm) 最大流(Dinic) 复杂度为 O ( m n ) O(m\sqrt{n}) O(mn)。
二分图匹配问题见图方式较为固定,设两个集合男孩集合A 和 女孩集合B 进行配对,首先从源点向女生集合(男生具体哪个集合连源点根据题目所给的边决定)中的所有点连一条边,从另外一个集合中所有点向汇点连一条边,边权均为1跑最大流即为二分图匹配数。
飞行员配对方案
根据题意可得,外籍飞行员和英国本土飞行员两两成为一组,典型的二分图匹配问题,输出的答案为二分图匹配数,直接上最大流集合。
建图大致如下:
证明:
(1)、流量守恒:对于每个点都有两种可能,一是成功找到相匹配的飞行员,在这对匹配中的两个点流入流出的流量均为1,满足流量守恒。二是没有找到相匹配的飞行员,流入流出的流量均为0,仍然满足流量守恒。
(2)、容量限制:可以看出流量只有可能是0或者1,虽然在dinic算法中的流量不一定是整型,但是流量的这两种取值均满足熔炼限制,且整型流的最大流也一定是整个流网络的最大流。
参考代码:
#include
#include
#include
#include
#define int long long
using namespace std;
const int N = 1e3 + 10, M = 1e4 + 10, inf = 1e18;
int n, m, S, T;
int h[N], e[M], ne[M], f[M], idx;
int q[N], d[N], cur[N];
void add(int a, int b, int c){
e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++;
e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++;
}
bool bfs(){
int hh = 0, tt = 0;
memset(d, -1, sizeof(d));
q[0] = S;
d[S] = 0;
cur[S] = h[S];
while(hh <= tt){
int t = q[hh ++];
for(int i = h[t] ; i != -1; i = ne[i]){
int ver = e[i];
if(d[ver] == -1 && f[i]){
cur[ver] = h[ver];
d[ver] = d[t] + 1;
if(ver == T){
return true;
}
q[++ tt] = ver;
}
}
}
return false;
}
int find(int u, int limit){
if(u == T){
return limit;
}
int flow = 0;
for(int i = cur[u]; i != -1 && flow < limit; i = ne[i]){
cur[u] = i;
int ver = e[i];
if(d[ver] == d[u] + 1 && f[i]){
int t = find(ver, min(f[i], limit - flow));
if(!t){
d[ver] = -1;
}
f[i] -= t;
f[i ^ 1] += t;
flow += t;
}
}
return flow;
}
int dinic(){
int r = 0, flow;
while(bfs()){
while(flow = find(S, inf)){
r += flow;
}
}
return r;
}
signed main(){
cin >> m >> n;
memset(h, -1, sizeof(h));
S = 0;
T = n + 1;
// 总共n个飞行员 1 ~ m 号为外籍, m + 1 ~ n 号为英国飞行员。
for(int i = 1; i <= m ; i ++){
add(S, i, 1);
}
for(int i = m + 1; i <= n ; i ++){
add(i, T, 1);
}
int a, b;
while(cin >> a >> b){
if(a == -1 && b == -1){
break;
}
add(a, b, 1);
}
// 输出结果, 即统计有哪些边的流量满流并且出点是本土飞行员。
printf("%lld\n", dinic());
for(int i = 0; i < idx; i += 2){
if(e[i] > m && e[i] <= n && !f[i]){
printf("%lld %lld\n", e[i ^ 1], e[i]);
}
}
}
假期宿舍
根据题意可得,有一些在校生放假要回家,则他的床位可以给他认识的同学使用。也有放假不想回家的学生他可能会邀请一些非本校的学生来学校玩,问题是床位够不够这些来学校玩的外校生,和放假不回家的本校生使用,注意:一个床只能够一个人使用。也是经典的二分图匹配问题,总共n个学生,设其中需要使用床位的有x个,本校生有m个,学生和床两两配对,问存不存在x个配对使得所有需要勇床位的学生都能够分到床。
直接最大流求二分图匹配,看最后的匹配数是否等于x。
参考代码:
#include
#include
#include
#include
#define int long long
using namespace std;
const int N = 110, M = 2e4 + 10, inf = 1e18;
int h[N], ne[M], e[M], f[M], idx;
int q[N], d[N], cur[N];
int n, m, S, T;
int w[N];
void add(int a, int b, int c){
e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++;
e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++;
}
bool bfs(){
int hh = 0, tt = 0;
memset(d, -1, sizeof(d));
q[0] = S;
d[S] = 0;
cur[S] = h[S];
while(hh <= tt){
int t = q[hh ++];
for(int i = h[t]; i != -1; i = ne[i]){
int ver = e[i];
// 类似于最短路 将图分层
if(d[ver] == -1 && f[i]){
d[ver] = d[t] + 1;
cur[ver] = h[ver];
// 已经找到汇点了
if(ver == T){
return true;
}
q[++ tt] = ver;
}
}
}
return false;
}
int find(int u, int limit){
// S 到 u 的流量为limit
if(u == T){
return limit;
}
// 记录 u 向后流到T的流量
int flow = 0;
// 当 flow >= limit 就不用再搜索了, 因为瓶颈在前面一部分
for(int i = cur[u]; i != -1 && flow < limit; i = ne[i]){
// 当前弧优化
cur[u] = i;
int ver = e[i];
// 将图分层之后找增广路径时只能从前一层到后面一层的点。
if(d[ver] == d[u] + 1 && f[i]){
int t = find(ver, min(limit - flow, f[i]));
// 不存在增广路径时直接将该点删去
if(!t){
d[ver] = -1;
}
// 更新残留网络
f[i] -= t;
f[i ^ 1] += t;
flow += t;
}
}
return flow;
}
int dinic(){
int r = 0, flow;
while(bfs()){
while(flow = find(S, inf)){
r += flow;
}
}
return r;
}
signed main(){
int my_text;
scanf("%lld", &my_text);
while(my_text --){
scanf("%lld", &n);
memset(h, -1, sizeof(h));
memset(w, 0, sizeof(w));
idx = 0;
S = 0;
T = 2 * n + 1;
int sum = 0;
for(int i = 1; i <= n ;i ++){
scanf("%lld", &w[i]);
if(w[i] == 1){
add(i + n, T, 1);
}
}
for(int i = 1; i <= n ; i ++){
int x;
scanf("%lld", &x);
if((w[i] == 1 && x == 0) || (w[i] == 0)){
add(S, i, 1);
sum ++;
}
}
for(int i = 1; i <= n ; i ++){
for(int j = 1; j <= n ; j ++){
int x;
scanf("%lld", &x);
if(x == 1 || i == j){
add(i, j + n, 1);
}
}
}
if(dinic() >= sum){
printf("^_^\n");
}
else{
printf("T_T\n");
}
}
}
圆桌问题
由题意得,在一个可行流中第 i i i个单位派出 r i r_i ri个代表,则有 r i r_i ri 个代表会从该点流出到 n n n张桌子之一的桌子,可以从源点连一条边权为1的边到该点,第 i i i 个桌子可以容纳 c i c_i ci 个代表,可以从该点向汇点连一条容量为 c i c_i ci 的边。
建图大概是这样滴:
参考代码:
#include
#include
#include
#include
#define int long long
using namespace std;
const int N = 1e3 + 10, M = 2e5 + 10, inf = 1e18;
int h[N], e[M], ne[M], f[M], idx;
int d[N], q[N], cur[N];
int n, m, S, T;
void add(int a, int b, int c){
e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++;
e[idx] = a, f[idx] = 0, ne[idx] = h[b], h[b] = idx ++;
}
bool bfs(){
int hh = 0, tt = 0;
memset(d, -1, sizeof(d));
q[0] = S;
d[S] = 0;
cur[S] = h[S];
while(hh <= tt){
int t = q[hh ++];
for(int i = h[t]; i != -1; i = ne[i]){
int ver = e[i];
if(d[ver] == -1 && f[i]){
d[ver] = d[t] + 1;
cur[ver] = h[ver];
if(ver == T){
return true;
}
q[++ tt] = ver;
}
}
}
return false;
}
int find(int u, int limit){
if(u == T){
return limit;
}
int flow = 0;
for(int i = cur[u]; i != -1 ;i = ne[i]){
cur[u] = i;
int ver = e[i];
if(d[ver] == d[u] + 1 && f[i]){
int t = find(ver, min(f[i], limit - flow));
if(!t){
d[ver] = -1;
}
f[i] -= t;
f[i ^ 1] += t;
flow += t;
}
}
return flow;
}
int dinic(){
int r = 0, flow;
while(bfs()){
while(flow = find(S, inf)){
r += flow;
}
}
return r;
}
signed main(){
scanf("%lld%lld",&m, &n);
memset(h, -1, sizeof(h));
S = 0;
T = n + m + 1;
int sum = 0;
for(int i = 1; i <= m; i ++){
int x;
scanf("%lld",&x);
add(S, i, x);
sum += x;
}
for(int i = 1; i <= n ; i ++){
int x;
scanf("%lld", &x);
add(i + m, T, x);
}
for(int i = 1; i <= m ; i ++){
for(int j = 1; j <= n ; j ++){
add(i, j + m, 1);
}
}
if(dinic() == sum){
printf("1\n");
for(int i = 1; i <= m ; i ++){
for(int j = h[i]; j != -1; j = ne[j]){
if(e[j] > m && e[j] <= n + m && !f[j]){
printf("%lld ", e[j] - m);
}
}
printf("\n");
}
}
else{
printf("0\n");
}
}