Skip to content

二分图 ​

染色法判断二分图 ​

  • 不含奇数环等价于染色法过程不存在矛盾
cpp
bool dfs(int u,int c){
    col[u]=c;
    for(auto v:adj[u]){
        if(col[v]){
            if(col[v]==c)return false;
        }
        else if(!dfs(v,3-c))return false;
    }
    return true;
}

匈牙利算法 ​

cpp
int st[N],timestamp;//表示当轮是否被匹配过,bool改用时间戳
int match[N];//表示匹配的对象
bool find(int u){
    for(auto v:adj[u]){
        if(st[v]!=timestamp){
            st[v]=timestamp;
            if(match[v]==0||find(match[v])){
                //没被匹配或者找到对应的增广路
                match[v]=u;
                return true;
            }
        }
    }
    return false;
}
int main(){
    int res=0;
    for(int i=1;i<=n;i++){
        timestamp++;
        if(find(i))res++;
    }
    cout<<res<<"\n";
    return 0;
}

UVA1194 Machine Schedule ​

题意:
n个任务,A机器有n种模式,B机器有m种模式,每个任务可以被A或B的一种模式完成
切换任务需要重启机器,求最少重启次数
思路:

  • 排除可以用0模式的,因为可以放到一开始全部做完
  • A的模式向B的模式连边
  • 题意即求最小点覆盖,即每条边至少有一个点被选
cpp
int g[N][N],match[N],n,m,k;
bool st[N];
bool find(int u){
    for(int v=1;v<m;v++){
        if(!st[v]&&g[u][v]){
            st[v]=1;
            int t=match[v];
            if(t==0||find(t)){
                match[v]=u;
                return true;
            }
        }
    }
    return false;
}
void solve(){
    while(cin>>n,n){
        memset(match,0,sizeof match);
        memset(g,0,sizeof g);
        cin>>m>>k;
        int t;
        while(k--){
            int t,a,b;
            cin>>t>>a>>b;
            if(!a||!b)continue;
            g[a][b]=1;
        }
        int res=0;
        for(int i=1;i<n;i++){
            memset(st,0,sizeof st);
            if(find(i))res++;
        }
        cout<<res<<"\n";
    }
}

P6062 [USACO05JAN] Muddy Fields G ​

题意:
给定n*m的矩形,每个方格代表草地和泥地
可以使用宽度为1的任意长度木板覆盖泥地,木板可以重复覆盖,求最少木板
思路:
横着放的木板为左端点,竖着放的为右端点,对每个泥地,横竖连边
即求最小权点覆盖

cpp
int idx,n,m;
vector<int> adj[N];
bool st[N];
int match[N];
char a[60][60];
int r[60][60];
bool find(int u){
    for(auto v:adj[u]){
        if(!st[v]){
            st[v]=1;
            int t=match[v];
            if(t==0||find(t)){
                match[v]=u;
                return true;
            }
        }
    }
    return false;
}
void solve(){
    map<int,int> mp;
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++)cin>>a[i][j];
        for(int j=1;j<=m;j++){
            if(a[i][j]=='*'){
                if(j==1||a[i][j-1]=='.')r[i][j]=++idx;
                else r[i][j]=idx;
            }
        }
    }
    for(int j=1;j<=m;j++){
        for(int i=1;i<=n;i++){
            if(a[i][j]=='*'){
                if(i==1||a[i-1][j]=='.')++idx;
                adj[r[i][j]].push_back(idx);
            }
        }
    }
    int res=0;
    for(int i=1;i<=idx;i++){
        memset(st,0,sizeof st);
        if(find(i))res++;
    }
    cout<<res<<"\n";
}

P1129 [ZJOI2007] 矩阵游戏 ​

题意:
给定01矩阵,可以进行两种操作
每次可以交换行和列
求是否可以使得主对角线上全是1
思路:

  • 行向列连边,如果二分图最大匹配res==n那么就可以
  • 每一行列都至少有一个匹配,交换行列不会影响匹配性
cpp
int match[N];
bool st[N];
vector<int> adj[N];
int g[N][N];
bool find(int u){
    for(auto v:adj[u]){
        if(!st[v]){
            st[v]=1;
            int t=match[v];
            if(t==0||find(t)){
                match[v]=u;
                return true;
            }
        }
    }
    return false;
}
void solve(){
    int n;
    cin>>n;
    memset(match,0,sizeof match);
    for(int i=1;i<=2*n;i++)adj[i].clear();
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            cin>>g[i][j];
            if(g[i][j]==1){
                adj[i].push_back(n+j);
            }
        }
    }
    int res=0;
    for(int i=1;i<=n;i++){
        for(int j=n+1;j<=2*n;j++)st[j]=0;
        res+=find(i);
    }
    cout<<(res==n?"Yes\n":"No\n");
}

P1640 [SCOI2010] 连续攻击游戏 ​

题意:
一个装备有2种属性,且只能使用其中一种属性
i从1开始,每次可以使用属性为i的一个装备攻击
求最多能连续攻击多少下
思路:

  • 一个装备匹配一个属性,使用二分图
  • 遇到增广失败的属性,直接break
  • 小技巧:使用时间戳代替memset st
cpp
int match[N],now;
int st[N];
vector<int> adj[N];
bool find(int u){
    for(auto v:adj[u]){
        if(st[v]!=now){
            st[v]=now;
            int t=match[v];
            if(t==0||find(t)){
                match[v]=u;
                return true;
            }
        }
    }
    return false;
}
void solve(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        int x,y;
        cin>>x>>y;
        adj[x].push_back(i+10000);
        adj[y].push_back(i+10000);
    }
    int res=0;
    for(int i=1;i<=10000;i++){
        now=i;
        if(find(i))res++;
        else break;
    }
    cout<<res<<"\n";
}

相关应用 ​

  • 最大匹配数=最小点覆盖=总点数-最大独立集=总点数-最小路径点覆盖(最小路径重复点覆盖)

最小点覆盖:每条边都有一个点被选 ​

  • 最小权点覆盖>=最大匹配数
  • 证明等号成立。
  • 构造方案:
  • 从左边所有非匹配点出发,跑一遍增广,标记路径上的点
  • 左边所有未被标记的点和右边所有标记的点组成一种方案
  • 左边非匹配点一定被标记,右边非匹配点一定不被标记
  • 对于匹配边,要么同时被标记,要么同时不被标记
  • 左边非匹配点->右边匹配点,右边匹配点一定被标记,右边会被选择
  • 左边匹配点->右边非匹配点,左边匹配点一定不被标记,左边点会被选择

最大独立集:选出最多的点集,使得内部没有边 ​

  • 最大独立集=选最少的点将图破坏掉=最小点覆盖

最小路径点覆盖:用最少的互不相交的路径将点覆盖 ​

针对DAG(有向无环图)

  • 拆点,如果一条路径i->j,那么i的出点连向j的入点
  • 对于路径终点,其没有出边,因此为左部的非匹配点
  • 即让左部非匹配点尽可能少,左部匹配点尽可能多,记为m,答案即n-m

最小路径重复点覆盖:路径可以相交 ​

  • 求传递闭包,获得图G'
  • 原图的最小路径重复点覆盖=新图最小路径点覆盖

acwing379 捉迷藏 ​

题意:
给定一张有向无环图,如果a能沿一条路径走到b那么a和b是能相互看见的
玩家1要选择k个藏身点,玩家2要找这k个藏身点
需要保证藏身点之间互不可达,问最多能选多少个藏身点
思路:
答案k=最小路径重复点覆盖cnt

  • 首先对于每条路径最多只能选一个点,即cnt>=k
  • 然后证明:cnt=k
    • 构造一种方法,选择每条路径的终点e
    • 设终点集合为E,终点能到的点为next(E)
    • 如果两个集合交集为空,说明终点不能被任意点到达,答案即为k=cnt
    • 两个集合不为空,对于能被其他点到达的终点ei,一直往回退,直至交集为空
    • 证明一定可以找到这样的点ei:
    • 如果找不到,那么整条路径都可以被其他点到达,那么其他店连上起点就会将两条路合并成一条,与最小路径覆盖矛盾
cpp
int g[N][N],match[N],n,m;
bool st[N];
bool find(int u){
    for(int i=1;i<=n;i++){
        if(g[u][i]&&!st[i]){
            st[i]=1;
            int t=match[i];
            if(t==0||find(t)){
                match[i]=u;
                return true;
            }
        }
    }
    return false;
}
void solve(){
    cin>>n>>m;
    while(m--){
        int a,b;
        cin>>a>>b;
        g[a][b]=1;
    }
    for(int k=1;k<=n;k++){
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++)g[i][j]|=g[i][k]&g[k][j];
        }
    }
    int res=0;
    for(int i=1;i<=n;i++){
        memset(st,0,sizeof st);
        if(find(i))res++;
    }
    cout<<n-res<<"\n";
}