前言
二分图的重点在于建模。以下的题目大家可以清晰的看出来这一点。代码相似度很高,但是思路基本上是各不相同。
题目
HDU 1179 Ollivanders: Makers of Fine Wands since 382 BC.
题意与分析
有n个人要去买魔杖,有m根魔杖(和哈利波特去买魔杖的时候一样,是由魔杖选人)。接下来是m行,每行第一个数k是第i根魔杖可以选的人数,接着k个数表示这根魔杖选的人的编号。最后问老板最多能卖出多少根魔杖。模板题。
代码
/*
* Filename: hdu1179.cpp
* Date: 2018-11-11
*/
#include <bits/stdc++.h>
#define INF 0x3f3f3f3f
#define PB emplace_back
#define MP make_pair
#define fi first
#define se second
#define rep(i,a,b) for(repType i=(a); i<=(b); ++i)
#define per(i,a,b) for(repType i=(a); i>=(b); --i)
#define ZERO(x) memset(x, 0, sizeof(x))
#define MS(x,y) memset(x, y, sizeof(x))
#define ALL(x) (x).begin(), (x).end()
#define QUICKIO
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
#define DEBUG(...) fprintf(stderr, __VA_ARGS__), fflush(stderr)
using namespace std;
using pi=pair<int,int>;
using repType=int;
using ll=long long;
using ld=long double;
using ull=unsigned long long;
const int MAXN=105;
int n,m;
vector<int> G[MAXN];
int linker[MAXN];
bool used[MAXN];
inline void init(int n)
{
rep(i,1,n) G[i].clear();
}
bool dfs(int u)
{
rep(v, 0, int(G[u].size())-1)
{
if(!used[G[u][v]])
{
used[G[u][v]]=true;
if(linker[G[u][v]]==-1 || dfs(linker[G[u][v]]))
{
linker[G[u][v]]=u;
return true;
}
}
}
return false;
}
inline int hungary(int n)
{
int ret=0;
MS(linker,-1);
rep(u,1,n)
{
ZERO(used);
if(dfs(u)) ret++;
}
return ret;
}
int main()
{
while(cin>>m>>n)
{
init(n);
rep(i,1,n)
{
int k; cin>>k;
rep(j,1,k)
{
int tmp; cin>>tmp;
G[i].PB(tmp);
}
}
cout<<hungary(n)<<endl;
}
return 0;
}
HDU 1281 棋盘游戏
题意与分析
题意是中文的,不解释了。
因为是车,所以每行每列至多只能放一个棋子。我们可以分别把行和列视作点,那么连接两个集合的一条边就是一个棋子。因此,这个显然的二分图的最大匹配就是我能放的最多棋子数目。那么题意就是枚举重要点,一个个删掉,看看会不会改变最大匹配。
这种行/列的二分匹配是基础套路,望周知。类似的还有矩阵中的(i+j)为奇数、偶数的二分集合情况。
代码
/* ACM Code written by Sam X or his teammates.
* Filename: hdu1281.cpp
* Date: 2018-11-14
*/
#include <bits/stdc++.h>
#define INF 0x3f3f3f3f
#define PB emplace_back
#define MP make_pair
#define fi first
#define se second
#define rep(i,a,b) for(repType i=(a); i<=(b); ++i)
#define per(i,a,b) for(repType i=(a); i>=(b); --i)
#define ZERO(x) memset(x, 0, sizeof(x))
#define MS(x,y) memset(x, y, sizeof(x))
#define ALL(x) (x).begin(), (x).end()
#define QUICKIO
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
#define DEBUG(...) fprintf(stderr, __VA_ARGS__), fflush(stderr)
using namespace std;
using pi=pair<int,int>;
using repType=int;
using ll=long long;
using ld=long double;
using ull=unsigned long long;
const int MAXN=105;
struct Edge
{
int u,v;
bool is_ok;
Edge() {}
Edge(int _u, int _v):u(_u), v(_v), is_ok(true) {}
};
vector<Edge> edges;
vector<int> G[MAXN];
void add_edge(int u, int v)
{
edges.PB(u,v);
G[u].PB(int(edges.size())-1);
}
int linker[MAXN];
bool used[MAXN];
bool dfs(int u)
{
rep(i,0,int(G[u].size())-1)
{
int v=edges[G[u][i]].v;
if(!edges[G[u][i]].is_ok) continue;
if(!used[v])
{
used[v]=true;
if(linker[v]==-1 || dfs(linker[v]))
{
linker[v]=u;
return true;
}
}
}
return false;
}
int hungary(int n)
{
int ret=0;
MS(linker, -1);
rep(u,1,n)
{
ZERO(used);
if(dfs(u)) ret++;
}
return ret;
}
int main()
{
int n,m,k,kase=0;
while(cin>>n>>m>>k)
{
rep(i,1,n) G[i].clear();
rep(i,1,k)
{
int x,y; cin>>x>>y;
add_edge(x,y);
}
int max_match=hungary(n),important_cnt=0;
rep(i,0,int(edges.size())-1)
{
edges[i].is_ok=false;
if(hungary(n)!=max_match)
{
important_cnt++;
}
edges[i].is_ok=true;
}
cout<<"Board "<<++kase<<" have "<<important_cnt<<" important blanks for "<<max_match<<" chessmen."<<endl;
}
return 0;
}
HDU 1498 50 years, 50 colors
题意与代码
题意是这样的:给定一个气球矩阵,每次只能消除一行或一列的相同颜色的气球,求有多少种气球在k次内不能消除。
这题看起来和二分图没啥关系,但是这就是二分图题目的魅力所在了:建模是大头。我们先考虑单个颜色,因为要最小次数打掉所有单个颜色,而我们每次只能打掉同一行/列的——问题于是转化成了用最少的行/列来打掉所有气球:这就是二分图的最小顶点覆盖问题。建模方法和上一题差不多:将行和列二分图的两个顶点集合,原图中的每个点相当于二分图中的边然后分别枚举每种颜色并判断就可以了。
对这题没啥头绪的再看看我的这篇博客:https://www.cnblogs.com/samhx/p/HDU-1150.html 。它对最小顶点覆盖有了一个比较好的讲解,并且也讲了下增广路的扩张过程。
代码
虽然思路是这样的,但是代码中仍然有一些实现上的细节。
/* ACM Code written by Sam X or his teammates.
* Filename: hdu1498.cpp
* Date: 2018-11-16
*/
#include <bits/stdc++.h>
#define INF 0x3f3f3f3f
#define PB emplace_back
#define MP make_pair
#define fi first
#define se second
#define rep(i,a,b) for(repType i=(a); i<=(b); ++i)
#define per(i,a,b) for(repType i=(a); i>=(b); --i)
#define ZERO(x) memset(x, 0, sizeof(x))
#define MS(x,y) memset(x, y, sizeof(x))
#define ALL(x) (x).begin(), (x).end()
#define QUICKIO
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
#define DEBUG(...) fprintf(stderr, __VA_ARGS__), fflush(stderr)
using namespace std;
using pi=pair<int,int>;
using repType=int;
using ll=long long;
using ld=long double;
using ull=unsigned long long;
const int MAXN=105;
struct Edge
{
int u,v;
int id;
Edge() {}
Edge(int _u, int _v, int _i):u(_u), v(_v), id(_i) {}
};
vector<Edge> edges;
vector<int> G[MAXN];
void add_edge(int u, int v,int id)
{
edges.PB(u,v,id);
G[u].PB(int(edges.size())-1);
}
int now_id=-1;
int linker[MAXN];
bool used[MAXN];
bool dfs(int u)
{
rep(i,0,int(G[u].size())-1)
{
int v=edges[G[u][i]].v;
if(edges[G[u][i]].id!=now_id) continue;
if(!used[v])
{
used[v]=true;
if(linker[v]==-1 || dfs(linker[v]))
{
linker[v]=u;
return true;
}
}
}
return false;
}
int hungary(int n)
{
int ret=0;
MS(linker, -1);
rep(u,1,n)
{
ZERO(used);
if(dfs(u)) ret++;
}
return ret;
}
unordered_map<int,int> hsm;
vector<int> hsv;
int get_hash(int x)
{
if(hsm.find(x)==hsm.end())
{
hsv.PB(x);
return hsm[x]=hsv.size()-1;
}
else return hsm[x];
}
int main()
{
int n,max_cnt;
while(cin>>n>>max_cnt)
{
hsm.clear();
hsv.clear();
edges.clear();
if(!n && !max_cnt) break;
rep(i,1,n) G[i].clear();
rep(i,1,n)
{
rep(j,1,n)
{
int tmp; cin>>tmp;
add_edge(i,j,get_hash(tmp));
}
}
vector<int> ans;
for(now_id=0;now_id<hsv.size();++now_id)
{
if(hungary(n)>max_cnt)
{
ans.PB(hsv[now_id]);
}
}
if(ans.size()==0) cout<<-1<<endl;
else
{
sort(ALL(ans));
rep(i,0,ans.size()-1)
cout<<ans[i]<<char(i==ans.size()-1?'
':' ');
}
}
return 0;
}