二分图 题目小结

脑子都要废了最近,终于有空整理题目了,学了两天二分图,然后机房大佬讲,你二分图写的我网络流都能写,心神俱疲;

我不想在这里过多介绍二分图,必将网上大佬博客都很多;

二分图

如果一张无向图的 n(n2) 个节点可以分成 A,BA,B 两个非空集合,

其中 AB 为空,并且在同一集合内的点之间都没有边相连,那么称这张无向图为一张二分图。 A,B 分别称为二分图的左部和右部。

二分图判定定理

无向图是二分图⇔图中无奇环(长度为奇数的环)。

常用方法dfs染色法,判断一张图是否为二分图;

有1表示黑色,2表示白色;

inline bool dfs(int x,int color) {
    v[x]=color;
    for(int i=0;i<edge[x].size();i++) {
        int y=edge[x][i].first;
        if(v[y]==color) return 0;
        if(!v[y]&&!dfs(y,3-color)) return 0; 
    }
    return 1;
}

 

这里想总结一下算法进阶的例题,写一下自己的理解;

1.关押罪犯  洛谷

最开始使用并差集写的,以前写过一篇并查集的博客,这里我们考虑用二分图的做法写这道题目;

最大的怒气值最小,考虑一个判定问题,市长能否在一种方案分配下,看到怒气值最大为mid;显然当对于较小的mid成立时,较大的mid也成立,所以答案具有可二分性;

所以我们转化为一个判定问题;

二分答案,我们假设当前怒气值为mid,那么我们将大于mid的怒气值的罪犯(当作节点)连边,如果建出来一张二分图,答案显然合理,令r=mid即可;

二分图code;

#include<bits/stdc++.h>
using namespace std;
#define N 500100 
#define pii pair<int,int>
int n,m,maxn,v[N];
template<typename T>inline void read(T &x)
{
    x=0;T f=1,ch=getchar();
    while(!isdigit(ch))  {if(ch=='-')  f=-1;  ch=getchar();}
    while(isdigit(ch))  {x=(x<<1)+(x<<3)+(ch^48);  ch=getchar();}
    x*=f;
}

struct gg {
    int x,y,v;
}a[N<<1];

vector<pii> edge[N];

inline bool mycmp(gg x,gg y) {
    return x.v>y.v;
}

inline bool dfs(int x,int color) {
    v[x]=color;
    for(int i=0;i<edge[x].size();i++) {
        int y=edge[x][i].first;
        if(v[y]==color) return 0;
        if(!v[y]&&!dfs(y,3-color)) return 0; 
    }
    return 1;
}

inline bool check(int mid) {
    for(int i=1;i<=n;i++) edge[i].clear();
    for(int i=1;i<=m;i++) {
        if(a[i].v<=mid) break;
        edge[a[i].x].push_back(make_pair(a[i].y,a[i].v));
        edge[a[i].y].push_back(make_pair(a[i].x,a[i].v));
    }
    memset(v,0,sizeof(v));
    for(int i=1;i<=n;i++) {
        if(!v[i]&&!dfs(i,1)) return false;
    }
    return true;
}

int main() {
    
    read(n); read(m);
    for(int i=1;i<=m;i++) {
        read(a[i].x); read(a[i].y); read(a[i].v);
        maxn+=a[i].v;
    }
    sort(a+1,a+m+1,mycmp);
    int l=0,r=maxn;
    while(l<r) {
        int mid=(l+r)>>1;
        if(check(mid)) r=mid;
        else l=mid+1;
    } 
    cout<<l<<endl;
    return 0;
}
View Code

并差集code1;

#include<bits/stdc++.h>
using namespace std;
#define N 500001
template<typename T>inline void read(T &x)
{
    x=0;T f=1,ch=getchar();
    while(!isdigit(ch))  {if(ch=='-')  f=-1;  ch=getchar();}
    while(isdigit(ch))  {x=x*10+ch-'0';  ch=getchar();}
    x*=f;
}
int n,m,d[N],f[N];
struct pink
{
    int x,y,v;
}a[N<<1];
bool mycmp(pink x,pink y)
{
    return x.v>y.v;
}
int find(int x)
{
    return f[x]==x?x:f[x]=find(f[x]);
}
int main()
{
    int flag=0;
    read(n);read(m);
    for(int i=1;i<=m;i++)
    {
        read(a[i].x);read(a[i].y);read(a[i].v);
    }
    for(int i=1;i<=2*n;i++)    f[i]=i;
    sort(a+1,a+m+1,mycmp);
    for(int i=1;i<=m;i++)
    {
        int x,y;
        x=a[i].x,y=a[i].y;
        int xx=find(a[i].x);
        int yy=find(a[i].y);
        if(xx==yy)
        {
            cout<<a[i].v<<endl;
            return 0;
        }
        f[yy]=find(a[i].x+n);
        f[xx]=find(a[i].y+n);
    }
    puts("0");
    return 0;
}
View Code

并差集code2;

#include<bits/stdc++.h>
using namespace std;
#define N 500001
template<typename T>inline void read(T &x)
{
    x=0;T f=1,ch=getchar();
    while(!isdigit(ch))  {if(ch=='-')  f=-1;  ch=getchar();}
    while(isdigit(ch))  {x=x*10+ch-'0';  ch=getchar();}
    x*=f;
}
int n,m,d[N],f[N];
struct pink
{
    int x,y,v;
}a[N<<1];
bool mycmp(pink x,pink y)
{
    return x.v>y.v;
}
int find(int x)
{
    return f[x]==x?x:f[x]=find(f[x]);
}
int main()
{
    int flag=0;
    read(n);read(m);
    for(int i=1;i<=m;i++)
    {
        read(a[i].x);read(a[i].y);read(a[i].v);
    }
    for(int i=1;i<=n;i++)
        f[i]=i;
    sort(a+1,a+m+1,mycmp);
    for(int i=1;i<=m;i++)
    {
        int x,y;
        x=a[i].x,y=a[i].y;
         if(find(x)==find(y))
        {
            printf("%d\n",a[i].v);
            flag=1;
            break;
        }
        else
        {
            if(!d[x]) d[x]=y;
            else
            {
                int p=find(d[x]);
                f[p]=find(y);
            }
            if(!d[y]) d[y]=x;
            else
            {
                int q=find(d[y]);
                f[q]=find(x);
            }
        }
    }
    if(!flag) printf("%d\n",0);
    return 0;
}
View Code

二分图最大匹配

图的匹配

任意两条边都没有公共端点的边的集合被称为图的一组匹配。

二分图最大匹配

在二分图中,包含边数最多的一组匹配被称为二分图的最大匹配。

其他相关定义

对于任意一组匹配 S(边集),属于 S 的边被称为匹配边,不属于 S 的边被称为非匹配边。

匹配边的端点被称为匹配点,其他节点被称为非匹配点。

如果二分图中存在一条连接两个非匹配点的路径 path ,使得非匹配边与匹配边在 path 上交替出现,那么称 path是匹配 S 的增广路(也称交错路)。

增广路的性质

  1. 长度为奇数
  2. 奇数边是非匹配边,偶数边是匹配边。
  3. 如果把路径上所有边的状态(是否为匹配边)取反,那么得到的新的边集 S' 仍然是一组匹配,并且匹配的边数增加了 1 。

结论

二分图的一组匹配 S 是最大匹配⇔图中不存在 S 的增广路。

匈牙利算法(增广路算法)

主要过程

  1. 设 SS 为空集,即所有边都是非匹配边。
  2. 寻找增广路 path ,把 path 上所有边的匹配状态取反,得到一个更大的匹配 S' 。
  3. 重复第 22 步,直至图中不存在增广路。

寻找增广路

依次尝试给每一个左部节点 x 寻找一个匹配的右部节点 y 。

y 与 x 匹配需满足下面两个条件之一:

  1. y 是非匹配点。
  2. y 已与 x' 匹配,但从 x' 出发能找到另一个 y' 与之匹配。

时间复杂度

O(nm);

1.棋盘覆盖

对于1*2的骨牌放置在N*M的矩阵中的方案数,我们可以用状压DP来写,

但对于这道题,有些各自有限制,并且数据范围限制,无法用状压写;

所以我们为什么考虑到二分图匹配的呢;

对于二分图匹配,他一定具有一个两个性质,我们成为0元素,1元素;

0元素:节点分为两个集合,集合内部0条边;

1要素:每个节点只能和一条匹配边相连;

映射到这个题目;

1元素对应,一个各自只能被一个骨牌覆盖,覆盖两个相邻的格子,那么假设这些格子没有被限制,我们将其连边,我们将棋盘进行黑白染色,我们显然发现相同颜色无法连边,对应0元素;

所以黑色格子为一个集合,白色格子为一个集合;

让骨牌不重叠放置最多,是一个二分图最大匹配问题;

#include<bits/stdc++.h>
using namespace std;
#define N 20010
struct gg {
    int x,y,next;
}a[N<<1];

template<typename T>inline void read(T &x)
{
    x=0;T f=1,ch=getchar();
    while(!isdigit(ch)) {if(ch=='-')  f=-1;  ch=getchar();}
    while(isdigit(ch))  {x=(x<<1)+(x<<3)+(ch^48);  ch=getchar();}
    x*=f;
}


int n,m,tot,x,y,b[210][210],v[N],match[N<<1],lin[N];

inline void add(int x,int y) {
    a[++tot].y=y;
    a[tot].next=lin[x];
    lin[x]=tot;
}

inline bool dfs(int x) {
    for(int i=lin[x];i;i=a[i].next) {
        int y=a[i].y;
        if(!v[y]) {
            v[y]=1;
            if(!match[y]||dfs(match[y])) {
                match[y]=x; match[x]=y; return 1;
            }
        }
    }
    return 0;
}

int main() {
    read(n); read(m);
    for(int i=1;i<=m;i++) {
        read(x); read(y);
        b[x][y]=1;
    }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++) {
            if(b[i][j]) continue;
            int id=(i-1)*n+j;//记录是第几个格子; 
            if(!b[i][j-1]&&j>1) {
                add(id,id-1);//放置一个横着的骨牌; 
            }
            if(!b[i][j+1]&&j<n) {
                add(id,id+1);
            }
            if(!b[i-1][j]&&i>1) {
                add(id,id-n);
            }
            if(!b[i+1][j]&&i<n) {
                add(id,id+n);
            }
        }
    int ans=0;
    for(int i=1;i<=n*n;i++){
        if(!match[i]) {
            if(dfs(i)==1) ans++;
            memset(v,0,sizeof(v));
        }
    }
    cout<<ans<<endl;
    return 0;
}
View Code

2.车的放置

类比刚才那道题,以及是有限制,我们尝试寻找一个01要素;

1要素:每行每列只能放1辆车,某个格子(i,j)连边,说明第i行第j列占据一个名额;

0要素:每个车不能即在第i行又在第j行,所以两个行对应没有连边;

求二分图最大匹配即可;

#include<bits/stdc++.h>
using namespace std;
int n,m,T,tot,x,y,a[201][201],match[400010],lin[100010],v[100010];

template<typename T>inline void read(T &x)
{
    x=0;T f=1,ch=getchar();
    while(!isdigit(ch)) {if(ch=='-')  f=-1;  ch=getchar();}
    while(isdigit(ch))  {x=(x<<1)+(x<<3)+(ch^48);  ch=getchar();}
    x*=f;
}

struct gg {
    int y,next;
}e[400010];

inline void add(int x,int y) {
    e[++tot].y=y;
    e[tot].next=lin[x];
    lin[x]=tot;
}

inline bool dfs(int x) {
    for(int i=lin[x];i;i=e[i].next) {
        int y=e[i].y;
        if(!v[y]) {
            v[y]=1;
            if(!match[y]||dfs(match[y])) {
                match[y]=x; match[x]=y; return 1;
            }
        }
    }
    return 0;
}

int main() {
    read(n); read(m); read(T);
    for(int i=1;i<=T;i++) {
        read(x); read(y);
        a[x][y]=1;
    }
    for(int i=1;i<=n;i++) {
        for(int j=1;j<=m;j++) {
            if(a[i][j]) continue;
            add(i,j+n);
        }
    }
    int ans=0;
    for(int i=1;i<=n;i++) {
        if(!match[i]) {
            if(dfs(i)) ans++;
            memset(v,0,sizeof(v));
        }
    }
    cout<<ans<<endl;
    return 0;
}
View Code

二分图最小点覆盖

给定一张二分图,求出一个最小的点集 S,使得图中任意一条边都已至少一个端点属于 S 。这个问题被称为二分图的最小点覆盖,简称最小覆盖。

定理

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

Machine Schedule

在二分图最大匹配中,我们寻找的时01要素,而在二分图最小点覆盖时,我们寻找的时2要素;

即每条边有两个端点,两者至少选择一个;

而在这道题中,每个任务要么在A中完成,要么在B中完成,二者必选其一,因为我们将A的M中模式作为左部点,将B的M种模式作为右部点,

将每个任务作为边连接a[i]和b[i],那么显然最少启动次数就是二分图最小匹配;

时间复杂度O(NM);

#include<bits/stdc++.h>
using namespace std;
int n,m,k;
const int maxx=1000001;
struct node{
    int y;
    int next;
}e[maxx];
int kk,lin[maxx],v[20002];
int match[maxx]; 
void add(int u,int v) {
    e[++kk].y=v;
    e[kk].next=lin[u];
    lin[u]=kk;
}
int dfs(int u) {
    for(int i=lin[u];i;i=e[i].next) {
        int y=e[i].y;
        if(!v[y]) {
            v[y]=1;
            if(!match[y]||dfs(match[y])) {
                match[y]=u; match[u]=y;
                return 1;
            }
        }
    }
    return 0;
}
int main() { 
    while(1) {
        memset(match,0,sizeof(match));
        memset(lin,0,sizeof(lin));
        int sum=0;
        scanf("%d",&n);
        if(n==0) break;
        scanf("%d%d",&m,&k);
        for(int i=1;i<=k;i++) {
            int x,y,z;
            scanf("%d%d%d",&x,&y,&z);
            if(y==0||z==0) continue;
            else  add(y,z+n);
        }
        for(int i=1;i<=n;i++) {
            memset(v,0,sizeof(v));
            if(dfs(i)) sum++;
        }
        printf("%d\n",sum);
    }
}
View Code

POJ2226 Muddy Fields

泥泞的区域

每块泥地要么被一块横着的木板覆盖,要么被一块竖着的木板覆盖,二者至少选择一个;

因为木板不能覆盖干净的地面,所以我们处理出来行泥泞块和列泥泞块,作为二分图的左右部点;

求出二分图最小点覆盖;

#include<bits/stdc++.h>
using namespace std;
#define N 1100 
int v[N],match[N],lin[N];
int n,m,k,cnt,tot1,tot2;
char Map[60][60];
int a[1100][1100],b[1100][1100];
template<typename T>inline void read(T &x)
{
    x=0;T f=1,ch=getchar();
    while(!isdigit(ch)) {if(ch=='-')  f=-1;  ch=getchar();}
    while(isdigit(ch))  {x=(x<<1)+(x<<3)+(ch^48);  ch=getchar();}
    x*=f;
}

struct gg {
    int y,next;
}e[400010];

inline void add(int x,int y) {
    e[++cnt].y=y;
    e[cnt].next=lin[x];
    lin[x]=cnt;
}

inline bool dfs(int x) {
    for(int i=lin[x];i;i=e[i].next) {
        int y=e[i].y;
        if(!v[y]) {
            v[y]=1;
            if(!match[y]||dfs(match[y])) {
                match[y]=x;
                return 1;
            }
        }
    }
    return 0;
}

int main() {
    read(n); read(m);
    for(int i=1;i<=n;i++) 
        for(int j=1;j<=m;j++) {
            cin>>Map[i][j];
        }
    for(int i=1;i<=n;i++) {
        for(int j=1;j<=m;j++) {
            if(Map[i][j]=='*') {
                if(Map[i][j-1]=='*') {
                    a[i][j]=a[i][j-1];
                }
                else  a[i][j]=++tot1;
            }
        }
    }//横着放;
    for(int i=1;i<=n;i++) {
        for(int j=1;j<=m;j++) {
            if(Map[i][j]=='*') {
                if(Map[i-1][j]=='*') {
                    b[i][j]=b[i-1][j]; 
                }
                else {
                    b[i][j]=++tot2;
                }
            }
        }
    } 
    for(int i=1;i<=n;i++) {
        for(int j=1;j<=m;j++) {
            if(Map[i][j]=='*') {
                add(a[i][j],b[i][j]);
            }
        }
    }
    int ans=0;
    for(int i=1;i<=tot1;i++) {
        memset(v,0,sizeof(v));
        if(dfs(i)) ans++;
    }
    cout<<ans<<endl;
    return 0;
}
View Code

二分图最大独立集

图的独立集

在一张无向图中,满足任意两点之间都没有边相连的点集被称为图的独立集。包含点数最多的一个被称为图的最大独立集。

图的团

在一张无向图中,满足任意两点之间都有边相连的子图被称为图的团。包含点数最多的一个被称为图的最大团。

定理

  1. 无向图 G 的最大团 == 补图 G' 的最大独立集。
  2. 对于一般无向图,最大团、最大独立集是 NPC 问题。
  3. 设 G 是有 n个节点的二分图, G 的最大独立集大小 =n- 最小点覆盖数 =n-最大匹配数。

P3355 骑士共存问题

ACWing(两道题目输入不一样);

与棋盘覆盖类似;

黑白相间染色棋盘,把黑、白色格子分别作为左、右部节点。若两个格子是“日”字对角,则在对应的节点之间连边。“日”字对角的两个节点显然颜色一定不同。

求上述二分图的最大独立集即可。

#include<bits/stdc++.h>
using namespace std;
#define N 10010

template<typename T>inline void read(T &x)
{
    x=0;T f=1,ch=getchar();
    while(!isdigit(ch)) {if(ch=='-')  f=-1;  ch=getchar();}
    while(isdigit(ch))  {x=(x<<1)+(x<<3)+(ch^48);  ch=getchar();}
    x*=f;
}

const int dx[8]={-1,-2,-2,-1,1,2,2,1};
const int dy[8]={-2,-1,1,2,2,1,-1,-2};

int n,m,t,ans,tot,x,y,match[N],lin[N],v[N];
int Map[110][110];
vector<int> e;

struct gg {
    int x,y,next;
}a[100*100<<2];

inline void add(int x,int y) {
    a[++tot].y=y;
    a[tot].next=lin[x];
    lin[x]=tot;
}

inline bool dfs(int x) {
    for(int i=lin[x];i;i=a[i].next) {
        int y=a[i].y;
        if(!v[y]) {
            v[y]=1;
            if(!match[y]||dfs(match[y]))  {
                match[y]=x; return 1;
            }
        }
    }
    return 0;
}

int main() {
    read(n); read(m); read(t);
    for(int i=1;i<=t;i++) {
        read(x); read(y);
        Map[x][y]=1;
    }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            if(!Map[i][j]&&(i+j)%2)
            {
                e.push_back(m*(i-1)+j);
                for(int k=0;k<8;k++)
                {
                    int x=i+dx[k],y=j+dy[k];
                    if(x<1||x>n||y<1||y>m) continue;
                    if(!Map[x][y])
                        add(m*(i-1)+j,m*(x-1)+y);
                }   
            }
    for(int i=0;i<e.size();i++) {
        memset(v,0,sizeof(v));
        if(dfs(e[i])) ans++;
    } 
    cout<<n*m-t-ans<<endl;
    return 0;
}
View Code

 

posted @ 2019-06-29 06:53  Tyouchie  阅读(418)  评论(0编辑  收藏  举报