了解这个算法之前 首先了解一个概念 :增广路
增广路 :简单的说 ,是二分图匹配中的一条边,他总是从 左边集合的一个点出发通过一条没有被匹配的边连接到右边集合,再从该点通过一条 匹配过的边连接到右边集合。
图1为 正常的二分图
图2为二分图的最大匹配
图3 为二分图最大匹配中的增广路 两条 紫色一条 黄色一条。
了解这个算法之前,还需要对一个算法进行比较: 匈牙利算法
匈牙利算法 如果用增广路来说明的话,就是从一个点出发,如果找到一个匹配 就连接这个匹配。
如果当前这个点 被匹配过的话,就去寻找一个增广路,找到左边集合中的点,看该点是否还能匹配到 右边集合的点 匹配到最后 增广路的长度为 2*n,最终无法匹配。
简单的说就是,每次判断能匹配就匹配,匹配不到就去寻找一个增广路使他继续匹配。
这样的复杂度 O(N*M)
如果数据太大,会爆TLE,所以HK算法将其复杂度优化 -> O(sqrt(n)*m)
再者:了解HK算法的本质:是对匈牙利的算法的优化,匈牙利算法一次只更新一条增广路,而HK算法,同时更新多条互不相交的最短增广路。
经证明 :当更新到sqrt(n)的时候,最大匹配已被全部OK。复杂度证明 自行搜索。
然后开始正经的 算法讲解(一定要了解本质是对匈牙利算法的优化):
所以根据 优化这条性质出手
第一步:他需要找到当前最短的匹配 找不到就去找最短的增广路
左边第一个点 链接到 右边第一个点 发现右边第一个点未匹配 ,那么就连起来 更新当前最短的路程
如果发现右边的点匹配过,那么就去寻找 一条增广路,用BFS实现。
比如上图中:第一次 左1连接右3,左2连接右2,左4连接右4 这是长度为1的匹配.
第二次更新:左边只有第三个点还未进行匹配,所以用第三个点更新,左3连右2,右2已经被标记过了,那么找到右2的归属者:左2,对左2进行 搜索发现可以去连接到右3,所以当前的匹配长度为3,所以继续寻找匹配,用匈牙利算法递归进行匹配即可。
这一部分的代码总结为:
/****
前期数组定义(一遍):
nR :标记右边集合的点是否被标记
nL :标记左边集合的点是否被标记
dR : 更新当前增广路的距离,也便于更新
dL :同上
****/
bool judge()// 更新当前最短的增广路
{
memset(dR,-1,sizeof(dR));//初始化的意思是 :因为多条增广路互不相交,所以一旦发生变化,另一个就不能再去标记了。
memset(dL,-1,sizeof(dL));
dis=INF;
queue<int>q;
for(int i=1;i<=n;i++) {
if(nL[i]==-1)
{
q.push(i);
dL[i]=0;
}
}
while(!q.empty())
{
int u=q.front();q.pop();
if(dL[u]>dis) break;//保证最短长度的赠广路
for(int i=1;i<=m;i++)
{
if(mp[u][i]<=t&&dR[i]==-1)//dR[i]!=-1 保证所有增广路都不相交
{
dR[i]=dL[u]+1;
if(nR[i]==-1) dis=dR[i]; //如果当前点没有被标记 ,那么可以匹配到
else//否则找到他的归属者,用归属者进入队列 ,寻找增广路
{
dL[nR[i]]=dR[i]+1;
q.push(nR[i]);
}
}
}
}
return dis!=INF;//只要能找到增广路 dis就不为INF
}
第二步:我们匹配完多条增广路之后,我们就用匈牙利的思想就去更新,这多条增广路,也是遍历左边集合中没有匹配到的点,看他们是否可以通过一条增广路匹配到,但会出现一个问题: 会增加时间,例如:
如果一个点 已经被匹配到 ,并且他是最后一个匹配的节点,那么这个节点连过去之后一定不可能成为增广路。所以不需要判断否则会耽误很多时间:
bool Find(ll x)
{
for(int i=1;i<=m;i++)
{
if(!vis[i]&&mp[x][i]<=t&&dL[x]+1==dR[i])
{
vis[i]=1;
if(nR[i]!=-1&&dR[i]==dis) continue; //特判一下
if(nR[i]==-1||Find(nR[i]))//找到就匹配,找不到就匹配他的父辈节点
{
nR[i]=x;
nL[x]=i;
return true;
}
}
}
return false;
}
第三步:进行最大匹配就可以了。结束条件是 找不到增广路:
ll MaxMatch()
{
ll cnt=0;
while(judge())
{
for(int i=1;i<=n;i++)
{
for(int k=1;k<=m;k++) vis[k]=false;
if(nL[i]==-1)
if(Find(i)) cnt++;
}
}
return cnt;
}
最后附加一个例题:
Rain on your Parade POJ的一个题:可以去搜搜
AC:
#include<bits/stdc++.h>
#define ll long long
#include <stdio.h>
#include <algorithm>
#pragma GCC optimize(2)
using namespace std;
const int maxn=1e6+1000;
const ll INF=10000000000;
inline ll read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
ll n,m;
ll dis;
double t;
bool vis[5000];
int nR[5000],nL[5000];
int dR[5000],dL[5000];
struct node{
double x,y;
double v;
}gest[5000],umb[5000];
double cal(node a,node b)
{
return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
bool judge()// 更新当前最短的增广路
{
memset(dR,-1,sizeof(dR));
memset(dL,-1,sizeof(dL));
dis=INF;
queue<int>q;
for(int i=1;i<=n;i++) {
if(nL[i]==-1)
{
q.push(i);
dL[i]=0;
}
}
while(!q.empty())
{
int u=q.front();q.pop();
if(dL[u]>dis) break;//保证最短长度的赠广路
for(int i=1;i<=m;i++)
{
if((cal(umb[i],gest[u])/gest[u].v)<=t&&dR[i]==-1)//dR[i]!=-1 保证所有增广路都不相交
{
dR[i]=dL[u]+1;
if(nR[i]==-1) dis=dR[i];
else
{
dL[nR[i]]=dR[i]+1;
q.push(nR[i]);
}
}
}
}
return dis!=INF;
}
bool Find(ll x)
{
for(int i=1;i<=m;i++)
{
if(!vis[i]&&(cal(umb[i],gest[x])/gest[x].v)<=t&&dL[x]+1==dR[i])
{
vis[i]=1;
if(nR[i]!=-1&&dR[i]==dis) continue;
if(nR[i]==-1||Find(nR[i]))
{
nR[i]=x;
nL[x]=i;
return true;
}
}
}
return false;
}
ll MaxMatch()
{
ll cnt=0;
while(judge())
{
for(int i=1;i<=n;i++)
{
for(int k=1;k<=m;k++) vis[k]=false;
if(nL[i]==-1)
if(Find(i)) cnt++;
}
}
return cnt;
}
void restart()
{
memset(nR,-1,sizeof(nR));
memset(nL,-1,sizeof(nL));
}
int main()
{
int T;scanf("%d",&T);
int cas=0;
bool f=false;
while(T--)
{
restart();
scanf("%lf",&t);
scanf("%lld",&n);
for(int i=1;i<=n;i++) scanf("%lf%lf%lf",&gest[i].x,&gest[i].y,&gest[i].v);
scanf("%lld",&m);
for(int i=1;i<=m;i++) scanf("%lf%lf",&umb[i].x,&umb[i].y);
printf("Scenario #%d:\n%lld\n\n",++cas,MaxMatch());
}
return 0;
}
PS:还是注意 优化那个 如果当前节点被标记过并且是最后一个 匹配点,该条路就一定不能成为增广路!