国际大学生程序设计竞赛例题解(八)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.3 纳克萨玛斯(难度:★★★★☆)

1.3.1 试题

题目描述

“英雄们,天灾亡灵军团已经入侵到暴风城周围的村庄了,为了艾泽拉世界的未来,让我们携手对抗敌人吧!”先知eternized召集了联盟和部落的领袖们,准备组建一支讨伐军征战天灾亡灵的大本营“纳克萨玛斯”。

三天后,联盟首领bug和部落首领spacesheep分别带来了各自阵营中的精英部队。这批勇士一共有N人,属于不同的种族和各种职业。他们中不但有能沟通大自然的牛头人德鲁依,也有圣光保佑的圣骑士矮人,甚至还有能连通阴阳界的精灵术士。

“好吧,现在我们来挑选勇士吧。”先知首先挑选出一批勇士,然后将他们分为一个个单独的小队,其中每个小队包括M种角色,例如队长(leader)、攻击(Attack)、治疗(Cure)、防御(Tank)等,而且队中每种角色都有且仅有一个。由于每个勇士的能力不同,他们可能同时充当若干种角色,比如德鲁依,受到自然的恩惠,使他能成为一个合格的医疗师和防御者,而暗影牧师则能提供足够的攻击力和治疗力。另外,先知为了平衡各方阵营利益,联盟和部落被选上的人数相差不能超过D人。

先知当然希望能组织起来的小队越多越好。聪明的你能给出一个最优方案吗?

输入格式

第1行包含三个整数NN≤10000), M(0≤M≤10), D(1≤DN)。

第2行到第N+1行,每行包含一个长度为M+1的“01”字串。串中第i位表示第i种角色,1表示可以充当该角色,0表示不能。最后一位表示阵营情况,1表示联盟,0表示部落。

输出格式

只有一个整数,表示能组建的小队的最大数目。

输入样例

6 3 2

1000

1000

1101

0111

0010

0010

输出样例

2

1.3.2 题目分析和算法实现

本题是一道图论题目,主要考查网络流和构造图论模型的能力。

初看题目,好像是一道搜索题目,但是一看到数据范围,发现搜索算法是无能为力的,是否可以利用贪心或者动态规划解决呢?又似乎并不能分析出问题的状态。不过如果能想到图论,就会隐约觉得有点像匹配了,因为属性只有人和能力值两种状态。但是单纯匹配又和题目要求组队数目没什么关系,因此必须另辟思路。我们可以观察到一个重要的信息,每支队伍的人数是固定的,因为每种能力都各需要一个不同的勇士来担当(比赛的时候许多选手都错误理解了这个条件,直接导致了整道题目不能正确地解决)。于是,假如总的队伍数确定了,那么需要勇士的数目也会确定,问题就转变为能不能组成这么多支队伍的可行性问题了。假如能想到这些,那么后面的思路就顺理成章地出来了。

判断能不能组成K支队伍,容易想到通过构图然后判断是否满流来决定的方法。先看一下怎样构图。忽略联盟和部落人数不超过D人的条件,可以想到的一种简单的办法,就是设立一个源点S和汇点T。对每一个勇士,从源点连一条容量为1的边;对每一种能力,连一条容量为K的边到汇点;而如果某个勇士具有某种能力,那么他们之间也连一条容量无限大的边。我们可以知道,如果能组建K支小队,则这个图必定满流。如果加上刚才忽略的条件,本能地,我们还会加上2个顶点,分别表示联盟和部落,从源点到这两点分别连一条边,然后再由这两点连边到相应的勇士那里,通过源点到联盟和部落的边的容量,就可以“控制”他们的数量关系。那么这两条边的容量是多少呢?用最笨的方法就是枚举容量,使两边容量符合这个条件。不过,这里有一个技巧,我们知道,一旦联盟和部落的总人数确定,他们的差的范围也知道,当然可以求出联盟和部落的最多人数为(KM+D)/2人,只要联盟和部落的人数不超过这个范围,显然不会出现联盟和部落之差超过D人(想想为什么)。至此,我们只需要枚举答案,然后通过求最普通的最大流来判断是否能达到这个答案,就能解决这个问题了。

不过回过头来看,这样做复杂度其实并不低,保守估计有N+M+4个点,超过N条边,用简单的Ford-Fulkerson或Edmonds-Karp算法,复杂度为O(Answer*N3),显然太大了。但是,我们又可以发现,虽然N很大,但是勇士们的种类只有2M种(想想为什么),于是可以压缩这个图,有效地减少节点数为2M,就是说最多也就1000来个点(210)。可以想象一下,复杂度变小了很多。具体的压缩方法其实已经很明显了,给出的勇士的状态不都是一个01串吗?将这个二进制串看做一个整数,那么每个勇士就唯一对应0…2M−1里面的一个数了。我们将这些数当做节点取代原来的勇士。从联盟(部落)节点连到这个数的边的容量为这个类型的勇士的数目。数节点和能力的连接方法和之前完全类似。这样,即使勇士再多,我们也可以轻松应付了。

但是可以看出,复杂度还是有些高,为O(Answer*23M),最大的数据会有比较大的运算量。当然,我们枚举的时候一般也会直觉地使用二分来获得答案,不过其实还能更快一点。这里有一个技巧,就是利用可增广路的性质,为算法提速。因为通常的最大流算法都是不断地增广图的流量来达到最大流的,算法的复杂度的一个决定性因数就是增广的次数(不过比赛的时候有选手采用二分+dicnic全通过数据了,但是dicnic方法还是比较麻烦的)。当答案比较大的时候,往往要将整个图进行完整地增广log(N/M)次。但实际上,我们尽可以最简单地枚举答案,每一次都在前面的流量图上求增广路,而不必每次都重新计算流量。如此下来,算法复杂度就变成O(22M*N/M)。在通常情况下能很好地解决这个问题。

1.3.3 参考程序及程序分析

#include <algorithm>
#include <vector>
using namespace std;
const int maxn=11000;                      //最大人数
const int S=1024;                          //源
const int T=1025;                          //汇
const int AH=1026;                         //AH部落,AH+1联盟
const int types=1028;                      //types+i,表示第i种角色组合
int kind[1024][2];                         //二进制表示的角色组合数量统计,分0部落,1联盟
int n,m,D,ans;
vector<int> G[1040];                     //邻接表存储
int c[1040][1040],f[1040][1040];           //容量和流量
int mrk[1040],d[1040],list[1040],pnt[1040];
//读入数据
void readdata()
{
    int i,j,k;
    char s[20];
    scanf("%d%d%d",&n,&m,&D);
    //统计不同角色的组合数量,分开联盟和部落,存在kind数组中
    memset(kind,0,sizeof(kind));
    for (i=0;i<n;++i)
    {
        scanf("%s",s);
        k=0;                                  //转化01串为十进制数
        for (j=0;j<m;++j)
            if (s[j]=='0') k=k*2;
            else          k=k*2+1;
        if (s[m]=='0') ++kind[k][0];          //统计不同类型的勇士数量
        else          ++kind[k][1];           //仅仅为了方便后面构图∶)
    }
}
//构图
void generateG()
{
    int i,j,k;
    memset(c,0,sizeof(c));
    for (k=0;k<2;++k)
    {
        j=0;
        //枚举每一种能力的组合
        for (i=(1<<m)-1;i>=0;--i)
        if (kind[i][k]>0)
        {
            //假如某一类角色组合存在,则往图中加边
            G[AH+k].push_back(i);             //由联盟或部落节点到i有一条边
            c[AH+k][i]=kind[i][k];            //容量为kind[i][k]
            G[i].push_back(AH+k);
            j+=kind[i][k];
        }
        //添加源和汇的边
        G[S].push_back(AH+k);                 //由源到联盟或部落节点加一条边
        G[AH+k].push_back(S);
    }
    for (i=(1<<m)-1;i>=0;--i)
    if (kind[i][0]+kind[i][1]>0)
    {
        j=i;
        //枚举角色组合的二进制表示i中有哪些有效角色
        for (k=m-1;k>=0;--k)
        {
            if (j%2==1)
            {
                G[i].push_back(types+k);      //从角色组合i连一条边到角色k
                c[i][types+k]=maxn;           //容量为maxn
                G[types+k].push_back(i);
            }
            j/=2;
        }
}
//往汇添加边
    for (i=0;i<m;++i)
    {
        G[types+i].push_back(T);
        G[T].push_back(types+i);
    }
}
//仅仅是简单的最大流增广,广度扩展实现
//通过Edmonds-Karp算法证明了有稳定的复杂度为O(VE∧2),具体原理和实现参考相关书籍
int maxflow()
{
    int cur,tail,i,j,u,v,flow=0;
    vector<int>∶∶iterator ptr;
    memset(pnt,0,sizeof(pnt));
    //循环增广可行流
    do
    {
        memset(mrk,0,sizeof(mrk));
        memset(d,0,sizeof(d));
        list[0]=S;
        mrk[S]=true;
        d[S]=maxn;
        pnt[S]=S;
        //广搜出一条增广路出来
        for (cur=tail=0;cur<=tail&&!mrk[T];++cur)
            for (u=list[cur],ptr=G[u].begin();ptr!=G[u].end();++ptr)
            {
                v=(*ptr);
                if (!mrk[v]&&f[u][v]<c[u][v])
                {
                    mrk[v]=true;
                    list[++tail]=v;
                    pnt[v]=u;
                    if (d[u]<c[u][v]-f[u][v]) d[v]=d[u];
                    else                   d[v]=c[u][v]-f[u][v];
                }
            }
        //假如增广路不存在,则退出循环
        if (!mrk[T]) break;
        //沿着增广路对网络进行增广
        flow+=d[T];
        for (u=T;u!=S;)
        {
            v=u;
            u=pnt[v];
            f[u][v]+=d[T];
            f[v][u]=-f[u][v];
        }
} while (d[T]>0);
//返回最大流,即从源流出的所有流量之和
    return flow;
}
//枚举答案
int check()
{
    int i,j,k;
    for (i=0;i<m;++i)
        c[types+i][T]=ans;
    c[S][AH]=(ans*m+D)/2;
    c[S][AH+1]=(ans*m+D)/2;
maxflow();
//假如从源流出的流量满容,即表示ans组的方案可满足
    return (f[S][AH]+f[S][AH+1]==m*ans);
}
int main()
{
    freopen("naxx.in","r",stdin);
    freopen("naxx.out","w",stdout);
    readdata();                          //读入数据
    generateG();                         //生成图G
    for (ans=1;check();++ans);           //枚举并检测答案
    printf("%d\n",ans-1);
    return 0;
}

1.3.4 部分测试数据和输出结果

测试数据

20 3 3

0000

1011

1001

0010

0000

1001

0111

1011

0101

0001

0000

0001

1001

0110

0010

0000

1010

1001

1000

0000

输出结果

3