zoukankan      html  css  js  c++  java
  • 图论补档——KM算法+稳定婚姻问题

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

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

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

    提出问题

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

    二分图 定义:将点集 (V) 划分成两个不相交的集合 (V_1,V_2) (通常称为左右部点)使得不存在 (uin V_1,vin 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_jge 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_jge 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;
    }
    
  • 相关阅读:
    软件工程结对作业02
    软件工程个人作业04
    第五周学习进度条
    软件工程中的形式化方法
    需求工程
    软件过程
    软件项目管理
    软件概论概述
    人月神话读后略有感想
    软件工程—理论、方法和实践 第一章:概述
  • 原文地址:https://www.cnblogs.com/UntitledCpp/p/14099182.html
Copyright © 2011-2022 走看看