1️⃣增广路径
1.概念:
若路径P是图G中一条连通两个顶点,这两个顶点与M中无匹配关系(即这两个点都是非匹配点)的路径,且属于M的边和不属于M的边在P上交替出现,则称P为相对于M的一条增广路径。
2.性质
图中的增广路径P为12345
- 根据概念可知,处于P两端的边一定不属于M,且P长度肯定是奇数的
属于M的边有2和4,匹配数量为2,其补图有边1,3,5,匹配数量为3
- 增广路径取反后必然能得到一个更大的匹配
- M为最大匹配当且仅当增广路径P不存在
这就提供了一个求二分图最大匹配的一个思路:
如果增广路径的长度不断变大,就相当于M的匹配也在变大!
2️⃣匈牙利算法解决二分图最大匹配
(注意:点不能重复使用,故需要用一个数组进行标记)
一开始,M中没有匹配关系,随意取一条
x
1
y
1
x_1y_1
x1y1
然后对于
x
2
x_2
x2,也可以取
x
2
y
2
x_2y_2
x2y2,M变成下图的匹配关系
但是当
x
3
x_3
x3想要和
y
1
y_1
y1匹配的时候发现
y
1
y_1
y1已经被占用了
就把
y
1
y_1
y1的原主
x
1
x_1
x1暴揍一顿赶走了
x
1
x_1
x1很不爽就去找"第二归宿"
y
2
y_2
y2,结果还是
x
2
x_2
x2占用着
就把
x
2
x_2
x2揍了一顿,于是
x
2
x_2
x2只能去找
y
5
y_5
y5
这是一个递归揍人的过程,揍完之后的匹配图如下:
路径:
x
3
−
y
1
−
x
1
−
y
2
−
x
2
−
y
5
x_3-y_1-x_1-y_2-x_2-y_5
x3−y1−x1−y2−x2−y5
在图二中对照发现正好是一条增广路径
取反之后得到图三,此时的匹配状态由于构造出了增广路径,故匹配数量变大,由2变成了3
匈牙利算法的算法核心就是通过构造增广路径来扩大匹配。
至于这个构造的过程就是递归揍人的过程,反正我不懂是如何实现的,不深究
3️⃣例题
①51Nod 2006
直接对两部分进行匹配
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<string>
#include<vector>
#include<stack>
#include<bitset>
#include<cstdlib>
#include<cmath>
#include<set>
#include<list>
#include<deque>
#include<queue>
#include<map>
#define ll long long
#define pb push_back
#define rep(x,a,b) for (int x=a;x<=b;x++)
#define repp(x,a,b) for (int x=a;x<b;x++)
#define W(x) printf("%d\n",x)
#define mem(a,x) memset(a,x,sizeof a)
using namespace std;
const int maxn=1e3+7;
int mapp[maxn][maxn];
int linker[maxn];
bool vis[maxn];
int ans,n,m,a,b;
bool dfs(int u)
{
for (int i=m+1;i<=n;i++)
{
if (mapp[u][i]&&!vis[i])
{
vis[i]=true;
if (linker[i]==-1||dfs(linker[i]))
{
linker[i]=u;
return true;
}
}
}
return false;
}
void init()
{
mem(linker,-1);
ans=0;
}
int count_()
{
for (int i=1;i<=m;i++)
{
mem(vis,false);
if (dfs(i))ans++;
}
return ans;
}
int main()
{
cin>>m>>n;
init();
while(scanf("%d%d",&a,&b)&&!(a==-1&&b==-1))
{
mapp[a][b]=1;
}
int x=count_();
if (x==0)cout<<"No Solution!"<<endl;
else cout<<x<<endl;
return 0;
}
②洛谷P3386(模板题)
代码略
https://www.luogu.com.cn/problemnew/show/P3386
③PKUOJ1469(模板题)
代码略
http://poj.org/problem?id=1469
④洛谷P1640
https://www.luogu.com.cn/problem/P1640
属性值当作一个点,武器当作一个点,一个武器对应连两个属性值的边
两部分进行二分匹配
不太一样的是:选择第i+1属性值的前提是选择第i属性值,那么在匈牙利算法时,如果dfs(i)失败时,即无法将第i个属性值纳入增广路径,就break退出即可
本题由于数据范围,不能使用邻接矩阵存储,改写邻接矩阵
const int maxn=2e6+7;
int linker[maxn],n,a,b;
bool vis[maxn];
struct Edge
{
int to,next;
}edge[maxn];
int head[maxn],tot;
void init()
{
tot=0;
mem(head,-1);
mem(linker,-1);
}
void add(int u,int v)
{
edge[tot].to=v;
edge[tot].next=head[u];
head[u]=tot++;
}
bool dfs(int u)
{
for (int i=head[u];i!=-1;i=edge[i].next)
{
int v=edge[i].to;
if (!vis[v])
{
vis[v]=true;
if (linker[v]==-1||dfs(linker[v]))
{
linker[v]=u;
return true;
}
}
}
return false;
}
int count_()
{
int ans=0;
for (int i=1;i<=10000;i++)
{
mem(vis,false);
if (dfs(i))ans++;
else break;
}
return ans;
}
int main()
{
cin>>n;
init();
rep(i,1,n)
{
scanf("%d%d",&a,&b);
add(a,i);
add(b,i);
}
W(count_());
return 0;
}
⑤洛谷P1129
https://www.luogu.com.cn/problem/P1129
将矩阵的行[1-n]和矩阵的列[1-n]作为左右二部分
可以证明的是当最大匹配达到n的时候
通过几次行变换就可以实现对角线全是1
const int maxn=2e2+7;
bool vis[maxn];
int mapp[maxn][maxn];
int linker[maxn],n,x,T;
bool dfs(int u)
{
for (int i=1;i<=n;i++)
{
if (mapp[u][i]&&!vis[i])
{
vis[i]=true;
if (linker[i]==-1||dfs(linker[i]))
{
linker[i]=u;
return true;
}
}
}
return false;
}
int count_()
{
int ans=0;
for (int i=1;i<=n;i++)
{
mem(vis,false);
if (dfs(i))ans++;
}
return ans;
}
int main()
{
scanf("%d",&T);
while(T--)
{
mem(linker,-1);
mem(mapp,0);
scanf("%d",&n);
rep(i,1,n)
{
rep(j,1,n)
{
scanf("%d",&x);
if (x==1)mapp[i][j]=1;
}
}
if (count_()==n)cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
return 0;
}
⑥洛谷1963
https://www.luogu.com.cn/problem/P1963
左边为[0~n-1]的排列,右边为每一个数字衍生出的两个可能性,用最大匹配解即可
如果是从正向建立增广路径,那么会后面的会不断的覆盖前面的最优解,那么正确的做法就是从后到前,这时候记录的也就不是linker,而是作为反函数的inv
还有一个坑点就是这两个衍生数字的大小问题,应该是优先选择小的,所以小的在遍历过程中应该处于前面点的位置。
const int maxn=2e6+7;
int tot,linker[maxn],n,a[maxn],inv[maxn];
bool vis[maxn];
int mapp[maxn][2];
bool dfs(int u)
{
for (int i=0;i<2;i++)
{
int v=mapp[u][i];
if (!vis[v])
{
vis[v]=true;
if (linker[v]==-1||dfs(linker[v]))
{
linker[v]=u;
inv[u]=v;
return true;
}
}
}
return false;
}
int count_()
{
int ans=0;
for (int i=n-1;i>=0;i--)
{
mem(vis,false);
if (dfs(i))ans++;
}
return ans;
}
int main()
{
scanf("%d",&n);
mem(linker,-1);
rep(i,0,n-1)
{
scanf("%d",&a[i]);
int p=(i+a[i])%n;
int q=(i-a[i]+n)%n;
if (p>q)swap(p,q);
mapp[i][0]=p;mapp[i][1]=q;
}
int x=count_();
if (x<n)cout<<"No Answer"<<endl;
else
{
rep(i,0,n-1){cout<<inv[i];if (i!=n-1)cout<<" ";}cout<<endl;
}
return 0;
}
⑦洛谷P3231
https://blog.csdn.net/w_udixixi/article/details/103982191
⑧HDU1281
棋盘问题直接最大匹配处理,但是“重要的点”不能一眼看出,就先把它去掉,再算一次最大匹配,如果小于原值,就说明是重要的点
算法过于暴力,时间够
const int maxn=2e2+7;
const int INF=1e9;
const ll INFF=1e18;
int mapp[maxn][maxn],x[maxn],y[maxn];
bool vis[maxn];
int linker[maxn];
int n,m,k,cnt=1;
bool dfs(int u)
{
for (int i=1;i<=m;i++)
{
if (mapp[u][i]&&!vis[i])
{
vis[i]=true;
if (linker[i]==-1||dfs(linker[i]))
{
linker[i]=u;
return true;
}
}
}
return false;
}
int count_()
{
int ans=0;
for (int i=1;i<=n;i++)
{
mem(vis,false);
if (dfs(i))ans++;
}
return ans;
}
int main()
{
while(~scanf("%d%d%d",&n,&m,&k))
{
mem(linker,-1);
mem(mapp,0);
rep(i,1,k)
{
scanf("%d%d",&x[i],&y[i]);
mapp[x[i]][y[i]]=1;
}
int X=count_(),num=0;
for (int i=1;i<=k;i++)
{
mem(linker,-1);
for (int j=1;j<=k;j++)
{
if (j==i)mapp[x[i]][y[i]]=0;
}
int Y=count_();
if (Y<X)num++;
mapp[x[i]][y[i]]=1;
}
printf("Board %d have %d important blanks for %d chessmen.\n",cnt++,num,X);
}
}
4️⃣Hopcroft-Karp算法
复杂度: O ( E ∗ V ) O(E*\sqrt{V}) O(E∗V)
最大匹配建图是核心,那么这个算法就不研究了。
一般一提到最大匹配就会想到匈牙利算法,但是这种的算法复杂度确实是要小一点的,没遇到过具体的题目所以不知所用
下面放一道模板题:除了vector存图其余全是板子
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<string>
#include<vector>
#include<stack>
#include<bitset>
#include<cstdlib>
#include<cmath>
#include<set>
#include<list>
#include<deque>
#include<queue>
#include<map>
#define ll long long
#define pb push_back
#define rep(x,a,b) for (int x=a;x<=b;x++)
#define repp(x,a,b) for (int x=a;x<b;x++)
#define W(x) printf("%d\n",x)
#define mem(a,x) memset(a,x,sizeof a)
using namespace std;
const int maxn=2e3+7;
const int INF=1e9;
vector<int> G[maxn];
int Mx[maxn],My[maxn];
int dx[maxn],dy[maxn];
int dis,p,n;
bool vis[maxn];
bool bfs()
{
queue<int> Q;
dis=INF;
mem(dx,-1);mem(dy,-1);
for (int i=1;i<=p;i++)
{
if (Mx[i]==-1)
{
Q.push(i);
dx[i]=0;
}
}
while(!Q.empty())
{
int u=Q.front();
Q.pop();
if (dx[u]>dis)break;
repp(i,0,G[u].size())
{
int v=G[u][i];
if (dy[v]==-1)
{
dy[v]=dx[u]+1;
if (My[v]==-1)dis=dy[v];
else
{
dx[My[v]]=dy[v]+1;
Q.push(My[v]);
}
}
}
}
return dis!=INF;
}
bool dfs(int u)
{
repp(i,0,G[u].size())
{
int v=G[u][i];
if (!vis[v]&&dy[v]==dx[u]+1)
{
vis[v]=true;
if (My[v]!=-1&&dy[v]==dis)continue;
if (My[v]==-1||dfs(My[v]))
{
My[v]=u;
Mx[u]=v;
return true;
}
}
}
return false;
}
int count_()
{
int res=0;
mem(Mx,-1);
mem(My,-1);
while(bfs())
{
mem(vis,false);
for (int i=1;i<=p;i++)if (Mx[i]==-1&&dfs(i))res++;
}
return res;
}
int main()
{
int t,a,b;
scanf("%d",&t);
while(t--)
{
scanf("%d%d",&p,&n);
rep(i,1,p)G[i].clear();
rep(i,1,p)
{
scanf("%d",&a);
repp(j,0,a)
{
scanf("%d",&b);
G[i].pb(b);//vector存图-
//这是一道模板题,最基本的建图
}
}
int x=count_();
if (x==p)cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
return 0;
}