无聊,做了几道baidu之星的题目

本文探讨了一种比赛分组算法,旨在解决特定人数下是否能通过合理分组达到预定比赛场次的问题。作者通过两种不同思路实现了算法,包括穷举法和基于数学公式的简化方法,并对比了它们的性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

baidu之星以前参加过,那个时候只是懂c,对c++的stl不是很熟悉,记得当时做的时候还花了很多的时间在调试链表操作,很是狼狈。今天恰好没有什么事,也就从网上看了几道题目,尝试做了一下。

下面是第一道题目:

有 N 个人,分成 M 组(2<=M<=N)打比赛,每组内部不竞争,每个人和不在同一组的其他人每人打一场。

比如有ABCD四个人,如果分2组,{ABC}{D}要打3场,{AB}{CD}打4场,
分3组{A}{B}{CD}打5场,分4组{A}{B}{C}{D}打6场。显见没有哪种分组能只打1场。

问题:有 N 个人,有没有哪种分组恰好打 K 场比赛?

bool fight(int N, int K);

2<=N<=500, 1 <= K <= 10000.

我首先想到的方法有点菜。基本上是穷举法,遵循了一般的思维吧。假设S(n)为n个人分组所有可能的比赛次数。那么S(n)就是把n个人分成1和n-1,2和n-2,…,n/2和n/2的所有可能次数的集合。当然这里有很多的重复,公式为S(n)={1*(n-1)+S(1)+S(n-1), 2*(n-2)+S(2)+S(n-2), … , n/2*n/2+S(n/2)+S(n/2)},这里涉及到很多的集合求和以及合并,非常耗计算量。

如下是以上思路的一个实现,可能不够简洁吧,一些像查找的功能实际上可以用list的函数做。加入了一些简单的profiling代码,并且把迭代过程中得到的中间结果保存下来以备后用。其中函数addMetas实现的是集合的求和,mergeMeta则是集合的合并,getCompetetions实际上就是上面公式的体现。整个过程有内存泄露,因为测试的性能很差,代码没有加进去,不是很好的习惯。

#include
#include

#define MAX_K 10000
#define MAX_N 500

/*
* this is the profiling defines
* */
static long int g_profilingCounters[10];
#define INCREASE_COUNTER(i) {g_profilingCounters[i]++;}
inline void printCounter()
{
    int i=0;
    for(i=0;i<10;i++)
    {
        if(g_profilingCounters[i]>0)
        {
            printf("Counter %2d: %d/n", i, g_profilingCounters[i]);
        }
    }
    return;
}

typedef struct {
    int N;
    list nums;/* all of the possible number with different groups */
}META;

META *g_knownMeta[MAX_N];

META * addMetas(META *pM1, META *pM2, int inc)
{
    META *pMeta = new META;
    list::iterator it1=pM1->nums.begin();
    list::iterator it2=pM2->nums.begin();
    while(it1!=pM1->nums.end())
    {
        it2 = pM2->nums.begin();
        while(it2!=pM2->nums.end())
        {
            int value = *it1+*it2+inc;
            int exist=0;
            list::iterator it = pMeta->nums.begin();
            while(it!=pMeta->nums.end() && *it<=value)
            {
                if(*it==value)
                {
                    exist = 1;
                    break;
                }
                it++;
            }
            if(exist == 0)
            {
                pMeta->nums.insert(it, value);
            }
            it2++;
        }
        it1++;
    }
    return pMeta;
}

void mergeMeta(META *pDest, META *pSrc)
{
    list::iterator it1=pDest->nums.begin();
    list::iterator it2=pSrc->nums.begin();
    while(it1!=pDest->nums.end() && it2!=pSrc->nums.end())
    {
        if(*it2<*it1)
        {
            it1 = pDest->nums.insert(it1, *it2);
            it2++;
            it1++;
        }
        else if(*it2>*it1)
        {
            it1++;
        }
        else{//equal
            it2++;
            it1++;
        }
    }
}

void printVector(list nums)
{
    list::iterator it = nums.begin();
    while(it!=nums.end())
    {
        printf("%4d", *it++);
    }
}

/* N is the size of people to be grouped
* N must > 1
*/
META* getCompetetions(int N)
{
    int i;
    int num;
    int maxCombines = ((N%2)==0?N/2:(N/2+1));
    META *pNMeta = NULL;
    META *pCurMeta;
    META *pLMeta;
    META *pBMeta;
    // for profiling
    INCREASE_COUNTER(0)
    if(g_knownMeta[N]!=NULL)
    {
        return g_knownMeta[N];
    }

    INCREASE_COUNTER(1)
    for(i=1; i<=maxCombines; i++)
    {
        int j;
        num = i*(N-i);
        if(g_knownMeta[i]==NULL)
        {
            // add this to known
            g_knownMeta[i] = getCompetetions(i);
        }
        pLMeta = g_knownMeta[i];

        if(g_knownMeta[N-i]==NULL)
        {
            // add this to known
            g_knownMeta[N-i] = getCompetetions(N-i);
        }
        pBMeta = g_knownMeta[N-i];
        // need merge these two sets' number
        pCurMeta = addMetas(pLMeta, pBMeta, num);
        if(pNMeta)
        {
            mergeMeta(pNMeta, pCurMeta);
            delete pCurMeta;
        }
        else{
            pNMeta = pCurMeta; // set the initial set
        }
    }
    pNMeta->N = N;
    return pNMeta;
}

/* N is the size of people to be grouped
* and k is the desired number of competetion
*/
int canGetKCompetetion(int N, int k)
{
    int i;
    int num=0;
    META *pMeta = getCompetetions(N);
    if(pMeta)
    {
        list::iterator it = pMeta->nums.begin();
        while(it!=pMeta->nums.end())
        {
            if(*it > k)
            {
                return 0;
            }
            else if(*it == k){
                return 1;
            }
            else{
                it++;
            }
        }
    }
    return 0;   
}

int main(int argc, char **argv)
{
    int people, competetion;
    /*init known array*/
    memset(g_knownMeta, 0, MAX_N*sizeof(META *));
    g_knownMeta[0] = NULL;
    g_knownMeta[1] = new META;
    g_knownMeta[2] = new META;
    g_knownMeta[3] = new META;
    g_knownMeta[1]->N = 1;
    g_knownMeta[1]->nums.push_back(0);
    g_knownMeta[2]->N = 2;
    g_knownMeta[2]->nums.push_back( 0);
    g_knownMeta[2]->nums.push_back(1);
    g_knownMeta[3]->N = 3;
    g_knownMeta[3]->nums.push_back(0);
    g_knownMeta[3]->nums.push_back(2);
    g_knownMeta[3]->nums.push_back(3);
    printf("Please input the people size and the desired competetion/'s number: /n");
    scanf("%d %d", &people, &competetion);
    if(canGetKCompetetion(people, competetion)==1)
    {
        printf("Yes/n");
    }
    else{
        printf("No/n");
    }
    // profiling
    printCounter();
    getchar();
    return 0;
}

在我的机器上N<100的话,性能还可以,但是500的话就非常非常的慢了。从过程中可以看出,函数嵌套的次数非常的多,并且没有剪枝,求100的话,基本上把100以下的所有的K值全部求出来了。因为原题的测试是很多的数据的,一开始我以为这样的话只要一次长时间的计算,后面的都可以免了,没想到性能极低。主要耗费在迭代和集合的计算上面。关于集合的计算我没有更好的方法,另一方面,倒是可以从小到大以避免函数的嵌套,也许能够提高性能。不过没有试。

想了一会儿,觉得我的实现没有多大的提高余地,要达到数据集的测试时间小于10s是不大可能的。所以我开始找其他的方法。首先尝试的是能否证明S(n)是连续的(除掉0)。因为3,4,5基本上都是,推而广之有没有严格的证明?想到的一种方法还是利用上面的公式,不过也就尝试了一下,没有办到。换一种思路,上面都是从整体到部分,如果从部分到整体呢?很显然,对于N个人的分组,没人一组所需要的比赛次数是最多的,而一个人一组,另外N-1个人一组的情况需要的次数是最小的。那么可不可以遵循某种方法从最大的情况一次一个地减少到达最小的情况呢?通过在纸上画了n多的小圆圈,似乎有戏。原则之一是每次选择两个相同大小的组S1,从一组中拿出一个到另外一组,则比赛次数就比原来情形小1。因为被选中的人原来要和size(S1)个人比,现在只要和原来自己组里面的size(S1)-1个人比了,其他组的情况都不变。但是用这个原则试了几次,似乎还是不够达到证明的目的。

证明不可行,只有回原来的路了。上面的思路有一个弱点,也是我已开始想当然以为会是优点的地方就是,每次求K的情况都需要得到N的所有可能的情形。这是不必要的。所以反过来,可不可以直接求K是不是一种可能的情况呢?假设K是可能的,N被分成了n1, n2, n3, …, nn组,也用n1来表示n1成员的大小。那么时间上就是求在满足sum(ni)=N, 1<=i<=n的条件下,sum(ni*nj)=K,1<= i!=j <=n是不是可能的。因为ni,nj还是不同的集合,直接球的话又会走到第一个的老路,这里可以变换一下,sum(ni) = n1+n2+n3+ … +nn, 那么(n1+n2+n3+ … +nn)*(n1+n2+n3+ … +nn) = sum(ni*ni) + 2*sum(ni*nj) = sum(ni*ni) +2*K = N*N。所以问题变换为,可不可以找到一组值,n1, n2, n3, … , nn,使得sum(ni)=N, 1<=i<=n,并且sum(ni*ni) =N*N-2*K。似乎变成了一个线性规划的问题。

上述思路的实现在下面:

#include
using namespace std;

int canGetKFromGroupN(int sum, int squSum)
{
    int i=0;
    // check the basic ones
    if(squSum == sum*sum)
    {
        return 1;
    }
    if((squSum>sum*sum) || (squSum
    {
        return 0;
    }
    for(i=1;i<=sum/2;i++)
    {
        if(canGetKFromGroupN(sum-i, squSum-i*i))
        {
            return 1;
        }
    }
    return 0;
}

int main(void)
{
    int N, K;
    cout<<"Please input the N and K:"<
    cin>>N>>K;
    if(canGetKFromGroupN(N,N*N-2*K))
    {
        cout<<"Yes"<
    }
    else{
        cout<<"No"<
    }
    return 0;
}

简单得多,但是测试下来的性能比起上面来好得不是一两倍,数学真好啊!

第一次实现可能没有什么大问题,性能的罪魁祸首还是思路不对。动手之前没有好好分析,花了很多时间做的东西确实没有什么用处,怪就怪没有对性能做一个很好的判断。不过也是自己不够聪明,没有一开始就想到简单的方法。嗯,笨一点没有关系,咋可以慢慢学这来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值