二分圖最大匹配&增廣路徑&匈牙利算法&HK算法

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️⃣匈牙利算法解決二分圖最大匹配

(注意:點不能重複使用,故需要用一個數組進行標記)
圖1
一開始,M中沒有匹配關係,隨意取一條x1y1x_1y_1
然後對於x2x_2,也可以取x2y2x_2y_2,M變成下圖的匹配關係
在這裏插入圖片描述
但是當x3x_3想要和y1y_1匹配的時候發現y1y_1已經被佔用了
就把y1y_1的原主x1x_1暴揍一頓趕走了
x1x_1很不爽就去找"第二歸宿"y2y_2,結果還是x2x_2佔用着
就把x2x_2揍了一頓,於是x2x_2只能去找y5y_5
這是一個遞歸揍人的過程,揍完之後的匹配圖如下:
在這裏插入圖片描述
路徑:x3y1x1y2x2y5x_3-y_1-x_1-y_2-x_2-y_5
在圖二中對照發現正好是一條增廣路徑

取反之後得到圖三,此時的匹配狀態由於構造出了增廣路徑,故匹配數量變大,由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(EV)O(E*\sqrt{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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章