UVa1627 - Team them (二分图染色+dp)

112 篇文章 0 订阅

题目链接

简介:
有n个人,ta们并不是都认识对方
要把ta们恰好分成两组,使得每组之中的人都互相认识

分析:
人与人之间的关系就像一张图
显然,互相不认识的人(包括X不认识Y且Y不认识X,X认识Y但是Y不认识X,X不认识Y但是Y认识X这三种情况)一定不会在同一个组内,
因此我们可以根据认识关系建出一个图:不认识的人之间连一条边(X认识Y,Y不认识X的情况也要连接XY)
之后我们就可以二分图染色,把一个连通块染成两种颜色(B&W)
这样我们就形成了两个互相对立的集合

注意:如果我们二分图染色不成功,说明无解

由于会有多个连通块,对于每个连通块都(至多)有两个集合:B&W,我们有两种方案:

  • B—>1组,W—>2组
  • B—>2组,W—>1组

我们通过分配这些集合,使得两个组的人数相差最小
我们要怎么确定集合到底属于哪个组呢?

动态规划

有的资料上把这个问题转化成了01背包
但是在0-1背包中,目标很明确:装的物品总价值最大,因此每次决策都要从两种决策中选出使得物品总价值最大的
相比之下,这道题的动态规划,不应该从两种决策中选出“两队人数差最小的”,如果用这种贪心方法,会丢失全局最优解
事实上,只有最后一个连通块的决策才有着明确的目标,动态规划的其他中间过程在做决策时,是无法判断优劣的,只能枚举所有可能性
我们利用dp只是维护差值的可行性
说是0-1背包的变式还是有点牵强的

dfs复杂度O(n)(每个人恰好访问一次),动态规划复杂度O(n^2)(枚举决策复杂度将是O(2^n),这里用动态规划的思想消除了重叠自问题,很好的降低了复杂度),总复杂度O(n^2)


这道题的分析到此结束了,思路很清晰,但是实现有一定的难度,所以下面就针对代码详细解释一下:

主程序
int main()
{
    int T,cas=0;
    scanf("%d",&T);
    while (T--)
    {
        if (cas++) printf("\n");

        memset(G,1,sizeof(G));           //我们用G这个邻接矩阵存储图,比较方便
        C.clear();
        scanf("%d",&n);
        for (int i=0;i<n;i++)
        {
            int x;
            while (scanf("%d",&x)!=EOF&&x)
                G[i][x-1]=0;
        }

        ff=1;                           //判断二分图染色是否成功的变量
        doit();
        if (ff) dp();
        else printf("No solution\n");
    }
    return 0;
} 

我们在主程序中看到了一个变量C,先不具体进行解释(随着程序的进行,我们会看到ta的用处的)

二分图染色
struct node{                 //看一下,这就是C和cc的定义
    vector<int> g[2];        //cc表示一个连通块,把一个连通块的两个集合打包成一个
    void clear()             //C记录的就是所有连通块(cc的一个集合)
    {                        
        g[0].clear();        
        g[1].clear();
    }
};
node cc; 
vector<node> C;

int dfs(int now,int flag)
{
    a[now]=flag;

    for (int i=0;i<n;i++){
        if (i!=now&&(G[now][i]||G[i][now])){       //如果两个点之间有连边(注意判断方式)
            if (a[i]==-1&&!dfs(i,flag^1)) return 0;
            if (a[i]!=-1&&a[i]==a[now]) return 0;
        }
    }

    if (flag==0) cc.g[0].push_back(now);           //cc是一个vector,记录的就是当前连通块的染色状况
    else cc.g[1].push_back(now);
    return 1;
}

void doit()
{
    memset(a,-1,sizeof(a));   //每个节点的颜色

    for (int i=0;i<n;i++)
        if (a[i]==-1)         //还未染色
        {
            cc.clear();       //一个连通块,分成两部分
            if (!dfs(i,0)) {ff=0; return;}   //染色失败
            else C.push_back(cc);
        }    
}

程序中大量使用vector,这样代码复杂度才不会太高

动态规划

这里有一点小细节:
首先我们把两种不同的方法抽象成两个数(B—>1组,W—>2组;B—>2组,W—>1组)
每个数代表的是:1组大小-2组大小(1组较大,数就为正;否则为负数)

dp的状态是:f[i][j]表示考虑前i个集合,状态j是否可以达到(我们只维护可行性)

这个状态j可正可负,实际上就是前面i个集合的【1组大小-2组大小(1组较大,数就为正;否则为负数)】之和
这就引出了一个问题:C++的数组下标不能为负
所以我们需要j下标都加上一个数,使ta变成非负数(加上n即可)

注意我们在记录状态的时候,两种不同的决策,f[i][j]的值是一正一负,这是为了方便解的打印

void dp()
{
    memset(f,0,sizeof(f));
    cnt=C.size();                                     //连通块的个数
    for (int i=0;i<cnt;i++)
    {
        int d=C[i].g[0].size()-C[i].g[1].size();      //两个集合的size差 
        for (int j=0;j<=2*n;j++)                      //这里为什么是0~2n呢,实际上是-n~n,我们都加上了n,保证数组下标非负 
        {
            if (i==0)
            {
                f[i][n+d]=1;    //n--->0  两种方法:black-->group A  white-->group B    
                f[i][n-d]=-1;   //                 black-->group B  white-->group A    
            }
            else 
            {
                if (f[i-1][j]){   //只维护可行性
                    f[i][j+d]=1;  
                    f[i][j-d]=-1;
                }
            }
        }
    }

    for (int i=0;i<=n;i++)      //解的打印
    {
        if (f[cnt-1][n+i])
        {
            print(n+i);
            return;
        }
        if (f[cnt-1][n+i])
        {
            print(n-i);
            return;
        }
    }
}
解的打印
void insert(vector<int>& a,vector<int>& b)  //解的复制
{
    for (int i=0;i<a.size();i++) b.push_back(a[i]);
}

void print(int j)
{
    vector<int> ans[2];
    for (int i=cnt-1;i>=0;i--)
    {
        int d=C[i].g[0].size()-C[i].g[1].size();
        if (f[i][j]==1)        //值为1:方案1,颜色为0的在1组,颜色为1的在2组
        {
            insert(C[i].g[0],ans[0]);
            insert(C[i].g[1],ans[1]);
            j-=d;
        }
        else if (f[i][j]==-1)  //值为-1:方案1,颜色为0的在2组,颜色为1的在1组
        {
            insert(C[i].g[0],ans[1]);
            insert(C[i].g[1],ans[0]);
            j+=d;
        }
    }
    printf("%d",ans[0].size());
    for (int i=0;i<ans[0].size();i++)
       printf(" %d",ans[0][i]+1);   //不要忘了+1,我们一开始为了节约空间,是从0开始编号的
    printf("\n");

    printf("%d",ans[1].size());
    for (int i=0;i<ans[1].size();i++)
       printf(" %d",ans[1][i]+1);
    printf("\n");
}

tip

这道题很好的体现了stl的运用,熟练掌握一些必要的stl,对于代码实现是很有好处的

//这里写代码片
#include<cstdio>
#include<cstring>
#include<iostream>
#include<vector>

using namespace std;

const int INF=0x33333333;
bool G[103][103],ff;
int n,a[103],f[103][2*103],cnt;
struct node{
    vector<int> g[2];
    void clear()
    {
        g[0].clear();
        g[1].clear();
    }
};
node cc; 
vector<node> C;

int dfs(int now,int flag)
{
    a[now]=flag;

    for (int i=0;i<n;i++){
        if (i!=now&&(G[now][i]||G[i][now])){      //如果两个点之间有连边(注意判断方式)
            if (a[i]==-1&&!dfs(i,flag^1)) return 0;
            if (a[i]!=-1&&a[i]==a[now]) return 0;
        }
    }

    if (flag==0) cc.g[0].push_back(now);         //cc是一个vector,记录的就是当前连通块的染色状况
    else cc.g[1].push_back(now);
    return 1;
}

void doit()
{
    memset(a,-1,sizeof(a));   //每个节点的颜色

    for (int i=0;i<n;i++)
        if (a[i]==-1)         //还未染色
        {
            cc.clear();       //一个连通块,分成两部分
            if (!dfs(i,0)) {ff=0; return;}   //染色失败
            else C.push_back(cc);
        }    
}

void insert(vector<int>& a,vector<int>& b)
{
    for (int i=0;i<a.size();i++) b.push_back(a[i]);
}

void print(int j)
{
    vector<int> ans[2];
    for (int i=cnt-1;i>=0;i--)
    {
        int d=C[i].g[0].size()-C[i].g[1].size();
        if (f[i][j]==1)
        {
            insert(C[i].g[0],ans[0]);
            insert(C[i].g[1],ans[1]);
            j-=d;
        }
        else if (f[i][j]==-1)
        {
            insert(C[i].g[0],ans[1]);
            insert(C[i].g[1],ans[0]);
            j+=d;
        }
    }
    printf("%d",ans[0].size());
    for (int i=0;i<ans[0].size();i++)
       printf(" %d",ans[0][i]+1);
    printf("\n");

    printf("%d",ans[1].size());
    for (int i=0;i<ans[1].size();i++)
       printf(" %d",ans[1][i]+1);
    printf("\n");
}

void dp()
{
    memset(f,0,sizeof(f));
    cnt=C.size();
    for (int i=0;i<cnt;i++)
    {
        int d=C[i].g[0].size()-C[i].g[1].size();        //两个集合的size差 
        for (int j=0;j<=2*n;j++)                        //这里为什么是0~2n呢,实际上是-n~n 
        {
            if (i==0)
            {
                f[i][n+d]=1;    //n--->0  两种方法:black-->group A  white-->group B    
                f[i][n-d]=-1;   //                 black-->group B  white-->group A    
            }
            else 
            {
                if (f[i-1][j]){
                    f[i][j+d]=1;
                    f[i][j-d]=-1;
                }
            }
        }
    }

    for (int i=0;i<=n;i++)
    {
        if (f[cnt-1][n+i])
        {
            print(n+i);
            return;
        }
        if (f[cnt-1][n+i])
        {
            print(n-i);
            return;
        }
    }
}

int main()
{
    int T,cas=0;
    scanf("%d",&T);
    while (T--)
    {
        if (cas++) printf("\n");

        memset(G,1,sizeof(G));
        C.clear();
        scanf("%d",&n);
        for (int i=0;i<n;i++)
        {
            int x;
            while (scanf("%d",&x)!=EOF&&x)
                G[i][x-1]=0;
        }

        ff=1;
        doit();
        if (ff) dp();
        else printf("No solution\n");
    }
    return 0;
} 
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值