图论补档——KM算法+稳定婚姻问题

突然发现考前复习图论的时候直接把 KM 和 稳定婚姻 给跳了……emmm

结果现在刷训练指南就疯狂补档。QAQ。

KM算法——二分图最大带权匹配

提出问题

(不严谨定义,理解即可)

二分图 定义:将点集 \(V\) 划分成两个不相交的集合 \(V_1,V_2\) (通常称为左右部点)使得不存在 \(u\in V_1,v\in V_2\)\((u,v)\in E\) .

最大匹配 :给定一张二分图,求一个子图 \(G'\) ,称 \(G'\) 中的边为匹配边,原图 \(G\) 中的其他边为非匹配边,限制 \(G'\) 中每个点度数不超过 1,求匹配边的最大数量。

前置知识:匈牙利算法解决二分图最大匹配问题

定义 交替路 为:从一个未匹配点出发,依次经过非匹配边、匹配边交替形成的一条路径

定义 增广路 为:从一个未匹配点出发,以一个未匹配点结束的交替路。

很显然,当我们在原图中找出一条增广路时,非匹配边恰好比匹配边多一条,那么只要对这条路上匹配和非匹配情况取反,就可以使得匹配边数目增加1.重复这个过程,直到找不到增广路,得到的匹配就是最大匹配。

剩余流程和代码见 这里 (该链接已经指向匈牙利算法部分)

完备匹配 :左右部点个数均为 \(n\) 且最大匹配包含 \(n\) 条边。

带权匹配 :二分图中每条边有权值,在最大匹配的基础 上最大化这个边集的 权值和

分析问题

解决带权匹配一般有两种思路:

  • 费用流
  • KM算法(只适用于存在完备匹配的情况,在稠密图上效率较高。)

这里只讨论 KM 算法。

定义

顶标 :顾名思义,顶点的标记,存在于左右部点。为了方便叙述,左部点的顶标称为 \(a_i\) ,右部点的顶标称为 \(b_i\) ,顶标不唯一,可以调整。令左部点 \(i\) 和右部点 \(j\) 之间的边权为 \(w_{i,j}\) ,那么两个顶标满足 \(a_i+b_j=w_{i,j}\) .

相等边 : 满足 \(a_i+b_j=w_{i,j}\) 的边 \((i,j)\)

相等子图 :相等边构成的子图

交错树 :增广路径形成的树。考虑什么时候会匹配失败。发现我们当前求相等子图的完备匹配失败了,是因为对于某个左部点,我们找不到一条从它出发的增广路。那么这时候所有增广路形成了交错树,而且由于无法增广,叶子节点一定都是左部点。

结论

一个显然的结论:当每个相等子图完备匹配时,二分图得到最大匹配

流程

根据上面的定义,现在我们需要找到一个合适的顶标分配使得 “每个相等子图完备匹配”

具体步骤:

  1. 分配可行顶标。为了让 \(a_i+b_j\ge w_{i,j}\) 恒成立,令 \(a_i\) 为左部点 \(x_i\) 所有连边中的最大边权,\(b_j=0\) .

  2. 匈牙利算法找增广路。这部分就不在赘述。

  3. 找不到增广路就调整顶标。为了让更多的点进入相等子图,我们把交错树中左部点的顶标都减小某个值 \(del\) ,右部点的顶标都增加 \(del\) ,那么会发现:

    • 对于原本就在交错树中的边 \((i,j)\)\(a_i+b_j\) 不变,仍然在相等子图中。
    • 对于两端都不在交错树上的边 \((i,j)\)\(a_i,b_j\) 不变,仍然不在相等子图中。
    • 左端在交错树中,右端不在的边 \((i,j)\)\(a_i+b_j\) 减小,仍然不在相等子图中。
    • 左端不在交错树中,右端在的边 \((i,j)\)\(a_i+b_j\) 增大,有可能进入相等子图

    这样相等子图就得到了扩大。

  4. 重复 2、3 直到找到增广路

现在的问题就是如何求这个 \(del\) 了。为了让 \(a_i+b_j\ge w_{i,j}\) 始终成立,且至少有一条边进入相等子图,那么 \(del=\min\{a_i+b_j-s_{i,j}\}\) .

解决问题

优良模板题 花姐姐的数据用心了awa

DFS

一次只能找一条增广路,复杂度可以卡成 \(\mathcal{O}(n^4)\) .

据说这个东西能过 50 分……但是不知道是我常数大还是写假了,反正我只有 10pts,其余全 TLE。

这不重要,反正 BFS 的复杂度才是对的

//Author: RingweEH
const int N=510;
const ll inf=0x7f7f7f7f;
int n,m,match[N],visa[N],visb[N];
ll edge[N][N],del[N],vala[N],valb[N];

bool dfs( int u )
{
    visa[u]=1;
    for ( int v=1; v<=n; v++ )
        if ( !visb[v] )
        {
            if ( vala[u]+valb[v]-edge[u][v]==0 )
            {
                visb[v]=1;
                if ( !match[v] || dfs( match[v]) ) return match[v]=u,1;
                else del[v]=min( del[v],vala[u]+valb[v]-edge[u][v] );
            }
        }
    return 0;
}

int KM()
{
    memset( vala,-inf,sizeof(vala) );
    for ( int u=1; u<=n; u++ )
        for ( int v=1; v<=n; v++ )
            vala[u]=max( vala[u],edge[u][v] );
    for ( int u=1; u<=n; u++ )
    {
        while ( 1 )
        {
            memset( visa,0,sizeof(visa) ); memset( visb,0,sizeof(visb) );
            memset( del,inf,sizeof(del) );
            if ( dfs(u) ) break; 
            ll delta=inf;
            for ( int v=1; v<=n; v++ ) 
                if ( !visb[v] ) delta=min( delta,del[v] );
            for ( int v=1; v<=n; v++ )
                if ( visa[v] ) vala[v]-=delta;
            for ( int v=1; v<=n; v++ )
                if ( visb[v] ) valb[v]+=delta;
        }
    }
    ll res=0;
    for ( int v=1; v<=n; v++ )
        res+=edge[match[v]][v];
    return res;
}

BFS

换成 BFS 之后复杂度是 \(\mathcal{O}(n^3)\) .(因为 DFS 每次都是从头去找增广路……肯定慢啊)

//Author: RingweEH
const int N=510;
const ll inf=0x7f7f7f7f;
int n,m,match[N],visa[N],visb[N],pas[N];
ll edge[N][N],del[N],vala[N],valb[N],c[N];

void bfs( int u )
{
    int a,v=0,vl=0,delta;
    for ( int i=1; i<=n; i++ )
        pas[i]=0,c[i]=inf;
    match[v]=u;
    do
    {
        a=match[v]; delta=inf; visb[v]=1;
        for ( int b=1; b<=n; b++ )
            if ( !visb[b] )
            {
                if ( c[b]>vala[a]+valb[b]-edge[a][b] )
                    c[b]=vala[a]+valb[b]-edge[a][b],pas[b]=v;
                if ( c[b]<delta ) delta=c[b],vl=b;
            }
        for ( int b=0; b<=n; b++ )
            if ( visb[b] ) vala[match[b]]-=delta,valb[b]+=delta;
            else c[b]-=delta;
        v=vl;
    }while ( match[v] );
    while ( v ) match[v]=match[pas[v]],v=pas[v];
}

ll KM()
{
    for ( int i=1; i<=n; i++ )
        match[i]=vala[i]=valb[i]=0;
    for ( int u=1; u<=n; u++ )
    {
        for ( int v=1; v<=n; v++ )
            visb[v]=0;
        bfs( u );
    }
    ll res=0;
    for ( int u=1; u<=n; u++ )
        res+=edge[match[u]][u];
    return res;
}

What's More?

  • 题目可能不存在完备匹配。
  • 这种情况可以在 KM 的基础上在右部点里面加虚点使得可以形成完备匹配,然后再加虚边。显然,如果被迫选了虚边,那在原图的情况上就是无解,所以把虚边的权值赋成 \(-inf\) 即可(这是针对无解的情况,如果是非完备匹配那就设成 0 )。

参考

George1123的题解 话说 George 的找遍全网真的不是看的百度百科或者wiki吗

稳定婚姻问题

提出问题

给一张完全二分图,每一对左边的点与右边的点之间都有一个评分 \(w_{i,j}\) ,要求把点两两匹配,满足:

设点 \(a,c\) 在同一边,点 \(b,d\) 在另一边,当前 \((a,b),(c,d)\) 匹配。那么对于所有这种 \((a,b,c,d)\) 不能存在 \(w_{b,a}<w_{b,c}\)\(b\) 认为 \(c\)\(a\) 优)且 \(w_{c,b}>w_{c,d}\)\(c\) 认为 \(b\)\(d\) 优)的情况。

如果存在,那么显然 \(b,c\) 会组成一对,因为这对于 \(b,c\) 来说都更优,那么他们会各自拒绝自己原来的匹配,就不稳定了。

分析问题

流程

定义 \(match[i]\) 表示点 \(i\) 所匹配的点。

每次取出一个没有匹配的左部点 \(u\)

  • 如果 \(u\) 的最优匹配 \(v\) 没有匹配,那么 \(u,v\) 匹配
  • 如果 \(v\) 有匹配,且对于点 \(v\)\(u\) 比它当前的匹配点更优,那么 \(u,v\) 匹配,\(v\) 原先的匹配点失配

直到每个点都有匹配位置。

正确性证明

  • 可行性:假设结束后,有一个左部点和一个右部点没有匹配。

    • 如果一个左部点一直没有匹配,显然会尝试和所有右部点匹配;
    • 如果一个右部点被左部点尝试过了,那么一定会有匹配;

    所以右部点一定有匹配,不会出现失配的情况。

  • 复杂度:每个左部点最多尝试 \(n\) 次与右部点匹配,上界为 \(\mathcal{O}(n^2)\) .

性质

  • 主动方可以选择到他可以匹配到的最优的匹配。所以是对男生有优势是吗(

解决问题

模板题链接 (洛谷那个长得像模板题的东西是假的,那个是 Tarjan 求 SCC)

//Author: RingweEH
//稳定婚姻问题模板,POJ3487
const int N=40;
int lef[N],list_lr[N][N],list_rl[N][N],nxt_l[N];
int match_l[N],match_r[N],n;
queue<int> q;

void get_match( int le,int ri )
{
    int now=match_r[ri];
    if ( now )
    {
        match_l[now]=0; q.push( now );
    }
    match_r[ri]=le; match_l[le]=ri;
}

int main()
{
    ios::sync_with_stdio(0);
    int T=read();
    while ( T-- )
    {
        memset( lef,0,sizeof(lef) );
        cin>>n; char ch;
        for ( int i=0; i<n; i++ )
        {
            cin>>ch; lef[ch-'a'+1]=1;
        }
        for ( int i=0; i<n; i++ )
            cin>>ch;
        char s[N];
        for ( int i=0; i<n; i++ )
        {
            cin>>s; int c=s[0]-'a'+1;
            for ( int j=2; s[j]; j++ )
                list_lr[c][j-1]=s[j]-'A'+1;
            nxt_l[c]=1; match_l[c]=0;
            q.push( c ); 
        }
        for ( int i=0; i<n; i++ )
        {
            cin>>s; int c=s[0]-'A'+1;
            for ( int j=2; s[j]; j++ )
                list_rl[c][s[j]-'a'+1]=j-1;
            match_r[c]=0;
        }
        //-------------Input Finished.--------------
        while ( !q.empty() )
        {
            int now=q.front(); q.pop();
            int rnow=list_lr[now][nxt_l[now]++];
            if ( !match_r[rnow] ) get_match( now,rnow );
            else if ( list_rl[rnow][now]<list_rl[rnow][match_r[rnow]] ) get_match( now,rnow );
            else q.push( now );
        }
        //-------------Matched---------------
        for ( int i=1; i<35; i++ )
            if ( lef[i] ) cout<<(char)(i-1+'a')<<' '<<(char)(match_l[i]+'A'-1)<<endl;
        if ( T ) cout<<endl;
    }

    return 0;
}
posted @ 2020-12-07 19:41  MontesquieuE  阅读(523)  评论(0编辑  收藏  举报