猫史档案馆


【每日不刷通天塔 蒟蒻也能懂算法】二分图的提高教程

用户:爵士OIer爵士OIer查看:0 回复:2 评论:0 创建时间:2020-05-04T21:16:28


这是提高教程。

 

完备匹配、多重匹配

 

给定一张二分图,其左部,右部节点数相同,均为N个节点。如果该二分图的最大匹配包含N条匹配边,则称该二分图具有完备匹配。

给定一张包含N个左部节点、M个右部节点的二分图。从中选出尽量多的边。使第i(1≤i≤N)个左部节点至多与kl_i条选出的边相连,第j(1≤j≤M)个右部节点至多与kr_ j条选出的边相连。该问题被称为二分图的多重匹配。

当kl_i=kr_j=1时,二分图的多重匹配即简化为最大匹配。

 

 

多重匹配的解决方案

 

1.拆点:把第i个左部节点拆成kl_i个不同的左部节点,第j个右部节点拆成kr_j个右部节点。对于原图中的每条边(i,j),在i拆成的所有节点与j拆成的所有节点之间连边。然后求解二分图最大匹配。

2.网络流(想展开讲是不可能的)

 

 

例题:导弹防御塔

 

问题描述

Freda Shi 的城堡遭受了M个入侵者的攻击!     

Freda 控制着N座导弹防御塔,每座塔都有足够数量的导弹,但是每次只能发射一枚。每座塔每次发射前需要预热T1秒,发射后需要冷却T2秒。

给定塔和入侵者的坐标。导弹从塔发出直到击中目标所需的飞行时间=塔和入侵者之间的距离。

由于小伙伴 Rainbow Li 就要来拜访她的城堡,Freda 想用最少的时间击退所有的入侵者,请你告诉她一种攻击方案。

 

输入格式

第一行五个正整数N,M,T1,T2,V。

接下来 M 行每行两个整数,代表入侵者的坐标。

接下来 N 行每行两个整数,代表防御塔的坐标。

 

输出格式

输出一个实数,表示最少需要多少分钟才能击中所有的入侵者,四舍五入保留六位小数。

数据范围

1N,M50,坐标绝对值不超过10000,T1,T2,V不超过2000。

 

输入样例:

3 3 30 20 1
0 0
0 50
50 0
50 50
0 1000
1000 0


输出样例:
91.500000


 

 

Code:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<vector>
#include<cmath>
using namespace std;
const int maxn=50+10;
struct Node
{
    int x,y;
    Node() {}
    Node(int a,int b):x(a),y(b) {}
} def[maxn],ta[maxn];
int vis[maxn*maxn],pre[maxn*maxn];
double times[maxn][maxn];
double t1,t2,V;
vector<int >G[maxn];
int n,m;
bool DFS(int u)
{
    for(int i=0; i<G[u].size(); i++)
    {
        int v=G[u][i];
        if(vis[v])
            continue;
        vis[v]=1;
        if(pre[v]==0||DFS(pre[v]))
        {
            pre[v]=u;
            return true;
        }
    }
    return false;
}
bool judge(double mid)
{
    for(int i=1; i<=m; i++)
        G[i].clear();
    for(int i=1; i<=m; i++)
        for(int j=1; j<=n; j++)
        {
            double now=0;
            for(int k=0; k<=m; k++)
            {
                now=k*(t1+t2);
                if(mid-(now+t1+times[i][j])>1e-7)
                {
                    G[i].push_back(j*(m+1)+k-m-1);
                }
                else
                    break;
            }
        }
    memset(pre,0,sizeof(pre));
    for(int i=1; i<=m; i++)
    {
        memset(vis,0,sizeof(vis));
        if(!DFS(i))
            return false;
    }
    return true;
}
int main()
{
    cin>>n>>m>>t1>>t2>>V;
    t1/=60.0;
    for(int i=1; i<=m; i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        ta[i]=Node(x,y);
    }
    for(int i=1; i<=n; i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        def[i]=Node(x,y);
    }
    for(int i=1; i<=m; i++)
        for(int j=1; j<=n; j++)
            times[i][j]=sqrt((ta[i].x-def[j].x)*(ta[i].x-def[j].x)+(ta[i].y-def[j].y)*(ta[i].y-def[j].y))/V;
    double l=0,r=1000000,ans=r;
    while(r-l>1e-9)
    {
        double mid=(l+r)/2.0;
        if(judge(mid))
            ans=min(ans,mid),r=mid;
        else
            l=mid;
    }
    printf("%.6lf\n",ans);
}

 

 

 

二分图最小点覆盖(Minimum Vertex Cover)

 

center_image

 

 

König定理

二分图最小点覆盖包含的点数等于二分图最大匹配包含的边数

 

证明:

首先因为最大匹配是原二分图边集的一个子集,并且所有边都不相交,所以至少需要从每条匹配中选出一个端点。因此最小点覆盖包含的点数不可能小于最大匹配包含的边数。如果能对任意二分图构造出一组点覆盖,其包含的点数等于最大匹配包含的边数,定理即得证。

构造方法

       1.求出最大匹配

       2.从右部每个未匹配点出发寻找增广路(一定失败),标记访问过的节点。

       3.取左部标记点,右部未标记点,构成一组最小覆盖。

 

经过上述构造方法后,

       右部未匹配点一定是标记点——因为它们是出发点

       左部未匹配点一定是未标记点——如果被标记(访问)则找到了增广路,矛盾

       一对匹配点都被标记或者都未标记——因为左部匹配点只能通过右部到达

取左部标记点、右部未标记点,恰好使得每对匹配点被取走一个

数值上的相等性得证

 

       匹配边一定被覆盖——每对匹配点取走一个

       不存在连接两个未匹配点的边——否则出现仅包含1条边的增广路,矛盾

       连接左部匹配点和右部未匹配点的边——后者是出发点,前者一定被标记

       连接左部未匹配点和右部匹配点的边——后者未被标记,否则存在增广路

所有边都被覆盖,合法性得证

 

 

例题:Muddy Fields

 

问题描述

在一块N*M的矩形地面上,有一些格子是泥泞的。

用一些宽为1的木板把泥地盖住,并且不能盖住好地,木板可以重叠。

问最少需要多少木板? N,M<=50。

 

解题思路

不能盖住好地,那么宽为1的木板只能放在行、列泥泞块里。

每个泥格子都要被盖住,选择一个块可以盖住一些泥格子。

行、列泥泞块对应左、右部中的点,泥格子对应边。

答案=二分图最小点覆盖。

 

Code:

#include<stdio.h>
#include<math.h>
#include<string.h>
#include<stdlib.h>
#define N 1100

int G[N][N], vis[N], used[N];
char maps[N][N];
int m, n, x, y;

bool Find(int u)
{
    int i;
    for(i = 1 ; i <= y ; i++)
    {
        if(!vis[i] && G[u][i])
        {
            vis[i] = 1;
            if(!used[i] || Find(used[i]))
            {
                used[i] = u;
                return true;
            }
        }
    }
    return false;
}

void Build()//构图
{
    int i, j, a[N][N] , b[N][N];
    x = y = 0;
    memset(a, 0, sizeof(a));
    memset(b, 0, sizeof(b));
    for(i = 1 ; i <= m ; i++)
    {
        for(j = 1 ; j <= n ; j++)
        {
            if(maps[i][j] == '*')
            {
                if(maps[i][j - 1] == '*')
                    a[i][j] = a[i][j - 1];
                else
                    a[i][j] = ++x;
            }
        }
    }//木板横着放
    for(i = 1 ; i <= m ; i++)
    {
        for(j = 1 ; j <= n ; j++)
        {
            if(maps[i][j] == '*')
            {
                if(maps[i - 1][j] == '*')
                    b[i][j] = b[i - 1][j];
                else
                    b[i][j] = ++y;
                G[a[i][j]][b[i][j]] = 1;
            }
        }
    }//木板竖着放
}

int main()
{
    int i, j, ans;
    while(~scanf("%d%d", &m, &n))
    {
        ans = 0;
        memset(G, 0, sizeof(G));
        for(i = 1 ; i <= m ; i++)
           {
               getchar();
               for(j = 1 ; j <= n ; j++)
               {
                   scanf("%c", &maps[i][j]);
               }
           }
        Build();
        memset(used, 0, sizeof(used));
        for(i = 1 ; i <= x ; i++)//X集合中的点与Y集合中的点找最大匹配
        {
            memset(vis, 0, sizeof(vis));
            if(Find(i))
                ans++;
        }
        printf("%d\n", ans);
    }
    return 0;
}

 

 

二分图最大独立集(Maximum Independent Set)

 

任意两点在图中都没有边相连的点集称为图的独立集。

定理:二分图最大独立集 = 图的点数 二分图最大匹配

证明:选出最多的点构成独立集

       在图中去掉最少的点,使剩下的点之间没有边。

       用最少的点覆盖所有的边,去掉的是最小覆盖。

 

 

例题:骑士放置

 

问题描述

给定一个N*M的棋盘,有一些格子不能放棋子。问棋盘上最多能放多少个不能互相攻击的骑士(马)。

 

解题思路:

对棋盘黑白染色,黑、白色的点分别属于左、右部。

可以攻击到的两个格子之间连边。

马沿日字形攻击,所以肯定是在不同颜色的点之间连边。

不能互相攻击,就是寻找一个最大独立集。

 

Code:

#include<bits/stdc++.h>
#define rg register
#define il inline
#define co const
template<class T>il T read(){
    rg T data=0,w=1;rg char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-') w=-w;
    for(;isdigit(ch);ch=getchar()) data=data*10+ch-'0';
    return data*w;
}
template<class T>il T read(rg T&x) {return x=read<T>();}
typedef long long ll;
using namespace std;

co int N=101;
int n,m,t,ans,fx[N][N],fy[N][N];
bool a[N][N],v[N][N];
co int dx[8]={-2,-2,-1,-1,1,1,2,2};
co int dy[8]={-1,1,-2,2,-2,2,-1,1};
bool dfs(int x,int y){
	for(int i=0;i<8;++i){
		int nx=x+dx[i],ny=y+dy[i];
		if(nx<1||nx>n||ny<1||ny>m||a[nx][ny]||v[nx][ny]) continue;
		v[nx][ny]=1;
		if(!fx[nx][ny]||dfs(fx[nx][ny],fy[nx][ny])){
			fx[nx][ny]=x,fy[nx][ny]=y;
			return 1;
		}
	}
	return 0;
}
int main(){
	read(n),read(m),read(t);
	for(int i=1;i<=t;++i) a[read<int>()][read<int>()]=1;
	for(int i=1;i<=n;++i)for(int j=1;j<=m;++j){
		if(i+j&1||a[i][j]) continue;
		memset(v,0,sizeof v);
		ans+=dfs(i,j);
	}
	printf("%d\n",n*m-t-ans);
	return 0;
}

 

 

 

有向无环图的最小路径点覆盖

 

 

最小路径覆盖:用尽量少的不相交简单路径覆盖有向无环图的所有顶点。(即每个顶点恰好被覆盖一次)

 

把原图中的每个点拆成二分图中左、右两个点;

对于每条有向边(u,v),从u的左部点(成为出点)向v的右部点(称为入点)连一条有向边。

 

最小路径覆盖数 = 原有向图节点数 – 新二分图最大匹配数。

 

 

 · 不相交的最小路径覆盖->每个点的入度、出度均不超过1

 · 拆点转化->二分图每个点连接的边不超过1条->一个匹配

 

 · 有向图路径上的每条边 对应 二分图匹配中的一条匹配边

 · 有向图路径上每条边的出点 对应 二分图匹配边的左部点

 · 每条路径的最后一个点没有对应

 

 · 最小路径覆盖的路径数 = 最少未匹配的点数 = 原图节点数 – 二分图最大匹配数。

 

 

例题:Air Raid

 

问题描述

N个城市M条道路形成有向无环图。

现在要求派一些伞兵空降在某些城市,然后这些伞兵可以沿着道路访问到其他城市,但是不能有两个或两个以上伞兵访问同一个城市。

问最少需要多少个伞兵。N<=120。

 

代码不上了,自己想。

 

 

 

 

 

 

 

 


回复

上一页1 页 / 共 1下一页
爵士OIer爵士OIer

毒瘤,一晚上打的一个教程,还没把二分图讲完。。。

点赞0


评论


爵士OIer爵士OIer

点赞0


评论