匈牙利算法(二分图最大匹配;例题:HDU1083)

匈牙利算法

前导知识

  • 什么是二分图?

设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图

简单的说就是有两个集合的点,每个集合中的点只能和另外一个集合中的点相连。

  • 链式前向星

一种存图的数据结构,相对不好写但是速度较快。

参考代码:

// 存边,其中to表示当前边指向的点,next表示下一条边
struct Edge {
    int to,next;
}edge[MAXM];

// head用来存起始边的编号(地址)
int head[MAXN],tot;

// 初始化
void init()
{
    tot=0;
    memset(head,-1,sizeof(head));
}

// 新增一条边
void addedge(int u,int v)
{
	// 当前边指向的点为v
    edge[tot].to=v;
    // 模拟链表,当前边的下一个边的编号(类似地址)为原来的head
    edge[tot].next=head[u];
    // 模拟链表,每次都把新的编号(地址)当做head
    head[u]=tot++;
}

// 遍历
for(int i=head[u];i!=-1;i=edge[i].next)
	操作...
  • 最大匹配

在图论中,一个「匹配」(matching)是一个边的集合,其中任意两条边都没有公共顶点。一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。

简单的说最大匹配就是最多边数的匹配,而匹配是很多条没有公共点的边。
在二分图中就是,选取一个边集,使其没有公共点,同时边数最多。

  • 交替路

从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。

也就是说这条路是这样的:
未匹配边——>匹配边——>未匹配边——>匹配边——>未匹配边
其中匹配边可以认为是最后的最大匹配中的其中一个匹配的边。

  • 增广路

若P是图G中一条连通两个未匹配顶点的路径,并且属于M的边和不属于M的边(即已匹配和待匹配的边)在P上交替出现,则称P为相对于M的一条增广路径(举例来说,有A、B集合,增广路由A中一个点通向B中一个点,再由B中这个点通向A中一个点……交替进行)。

简单的说就是从一个未匹配点(不是已有匹配边的端点)走到另一个未匹配点的一条交替路
几个性质:

  1. 未匹配边条数-1 = 匹配边条数(交题路的性质)
  2. 路径长度为奇数(显然)
  3. 进行取反操作后会产生一个更大的匹配(原来的匹配边和未匹配边互换,也就是说匹配边条数-1 = 未匹配边条数,所以会产生一个更大的匹配)

算法:匈牙利算法

这个算法的核心就是:

一个匹配是最大匹配的充要条件是不存在增广路——from 蓝书(刘汝佳)
所以最大匹配可以通过反复找增广路来求解。找增广路的过程可以用dfs来实现。

对于匈牙利算法的过程,这个博客讲解的比较浅显易懂:趣写匈牙利算法

模板

以下是kuangbin的模板(适用于链式前向星存图)
找增广路:

// 利用dfs找增广路
bool dfs(int u)
{
	// 遍历与当前点相连的点
    for(int i=head[u];i!=-1;i=edge[i].next)
    {
        int v=edge[i].to;
        // 当前的点是否访问过
        if(!used[v])
        {	
        	// 标记为已访问
            used[v]=true;
            // 当前点未匹配 或者 能否将与v匹配的点腾出来(也就是说看妹子能不能换个男朋友)
            if(linker[v]==-1 || dfs(linker[v]))
            {
            	// 更新当前点的匹配点
                linker[v]=u;
                return true;
            }
        }
    }
    return false;
}

匈牙利算法主函数:

int hungary()
{
    int res=0;
    // 初始化为未匹配
    memset(linker,-1,sizeof(linker));
    for(int u=0;u<uN;u++)
    {
    	// 初始化为未访问
        memset(used,false,sizeof(used));
        // 如果能给u找到匹配则匹配边++
        if(dfs(u)) res++;
    }
    return res;
}

例题 HDU083

Courses

Time Limit: 20000/10000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)
Total Submission(s): 12252 Accepted Submission(s): 5721

Problem Description

Consider a group of N students and P courses. Each student visits zero, one or more than one courses. Your task is to determine whether it is possible to form a committee of exactly P students that satisfies simultaneously the conditions:

. every student in the committee represents a different course (a student can represent a course if he/she visits that course)

. each course has a representative in the committee

Your program should read sets of data from a text file. The first line of the input file contains the number of the data sets. Each data set is presented in the following format:

P N
Count1 Student1 1 Student1 2 … Student1 Count1
Count2 Student2 1 Student2 2 … Student2 Count2

CountP StudentP 1 StudentP 2 … StudentP CountP

Input

The first line in each data set contains two positive integers separated by one blank: P (1 <= P <= 100) - the number of courses and N (1 <= N <= 300) - the number of students. The next P lines describe in sequence of the courses . from course 1 to course P, each line describing a course. The description of course i is a line that starts with an integer Count i (0 <= Count i <= N) representing the number of students visiting course i. Next, after a blank, you’ll find the Count i students, visiting the course, each two consecutive separated by one blank. Students are numbered with the positive integers from 1 to N.

There are no blank lines between consecutive sets of data. Input data are correct.

Output

The result of the program is on the standard output. For each input data set the program prints on a single line “YES” if it is possible to form a committee and “NO” otherwise. There should not be any leading blanks at the start of the line.

An example of program input and output:

Sample Input

2
3 3
3 1 2 3
2 1 2
1 1
3 3
2 1 3
2 1 3
1 1

Sample Output

YES
NO 

Source

HDU1083

分析

有P个课,N个学生,要给每个课找到至少一个学生,就相当于课是男生,学生是妹子,直接用二分图最大匹配就行了,判断最大匹配是否等于课程数,等于就是能够找到。

参考代码

#include 
#include 
#include 
const int MAXN=505;
const int MAXM=30005;
using namespace std;

// 链式前向星部分,当然也可以用其他存图方式,我这里懒得改就用的链式前向星
struct Edge {
    int to,next;
}edge[MAXM];

int P,N,x,y;
int head[MAXN],tot;

void init()
{
    tot=0;
    memset(head,-1,sizeof(head));
}

void addedge(int u,int v)
{
    edge[tot].to=v;
    edge[tot].next=head[u];
    head[u]=tot++;
}

// 匈牙利模板部分
int linker[MAXN];
bool used[MAXN];
int uN;

bool dfs(int u)
{
    for(int i=head[u];i!=-1;i=edge[i].next)
    {
        int v=edge[i].to;
        if(!used[v])
        {
            used[v]=true;
            if(linker[v]==-1 || dfs(linker[v]))
            {
                linker[v]=u;
                return true;
            }
        }
    }
    return false;
}

int hungary()
{
    int res=0;
    memset(linker,-1,sizeof(linker));
    for(int u=0;u<uN;u++)
    {
        memset(used,false,sizeof(used));
        if(dfs(u)) res++;
    }
    return res;
}

// 主函数
int main()
{
    int T;
    cin >> T;
    while(T--)
    {
        init();
        scanf("%d%d",&P,&N);
        for (int i=0;i<P;i++)
        {
            scanf("%d",&x);
            for (int j=0;j<x;j++)
            {
                scanf("%d",&y);
                // 链式前向星加边
                addedge(i,y);
            }
        }
        uN=P;
        // 最大匹配等于课程数量的话就说明可以
        if (hungary()==P)
            cout << "YES" << endl;
        else
            cout << "NO" << endl;
    }
    return 0;
}

你可能感兴趣的:(算法,图论)