1. 程式人生 > >「日常溫習」Hungary演算法解決二分圖相關問題

「日常溫習」Hungary演算法解決二分圖相關問題

前言

二分圖的重點在於建模。以下的題目大家可以清晰的看出來這一點。程式碼相似度很高,但是思路基本上是各不相同。

題目

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?'\n':' ');
        }
    }
    return 0;
}

HDU 1507 Uncle Tom's Inherited Land*