关于纸牌均分的解法及扩展

一、纸牌均分

  给定n堆纸牌,每堆纸牌有若干张,现要使着n堆纸牌平均分配,即每堆张数相等。每次移动可以使一堆牌向其左边的一堆或右边的一堆移动若干张牌。求最少移动次数。

  这是我们最熟悉的纸牌均分问题。正确的解法是贪心。我们可以知道均分后每堆纸牌的张数,将现在的牌数减去,就可以得到差值b[i],表示第i堆牌比目标牌数多几张或少几张(b[i]有正有负)。然后我们从第一堆开始往后做,我们把第i堆多出来的b[i]张牌直接移到i + 1上,b[i + 1] += b[i],然后移动数bns加一,如果b[i]为0则不用。注意,此处的关键在于,无论b[i]为正为负都会往右移,即使是负数也没有关系,因为我们这里指的不是真正的移牌,而是指第i堆与目标牌数的差异b[i]由i + 1堆来承担,多了移给i + 1,少了从i + 1移过来,i + 1不够了再从i + 2移过来,以此类推。

  这个贪心的正确性还是比较明显的。

  题目见noip2002提高组。

  

{
    for (int i = 1;i <= n;i ++)
        b[i] = a[i] - t / n;
    for (int i = 1;i <= n;i ++)
    {
         if (b[i] != 0) ans ++;
         b[i + 1] += b[i];
    }      
}

二、每次只能移动一张牌的纸牌均分

  和上道题差不多,只不过这回一次只能移动1张牌。

  这题虽然每次只能移动一张,但是其实贪心的方法是一样的,只不过是移动次数的统计不一样而已。我们将刚才求得的b数组求前缀和sum[i],sum[i] = ∑b[j] (j <= i)。sum[i]其实表示的就是i这堆总共要向左边移动的牌数(如果小于0则表示要向右移动的牌),那么移动总次数就是∑sum[i]

{
    for (int i = 1;i <= n;i ++)
        b[i] = a[i] - t / n;
    for (int i = 1;i <= n;i ++)
    {
         sum[i] = sum[i - 1] + b[i];
         ans += abs(sum[i]);    
    }      
}

三、每次只能移动一张牌的环状纸牌均分

  这个扩展比上面一个就是多了个环状。环状,即首尾相连,第一堆可以向最后一堆移牌,最后一堆可以向第一堆移牌。看似区别不大,但算法还是需要进一步改进。

  正常的想法,直接枚举断点,将环断成一条链,然后就是和二一样的做法了。但是这样的话复杂度就升高到了O(N^2),要过大数据有点难,我们还是期望时间复杂度能够尽量小一点。

  如果我们在1断开,那么就是一条链,和二一样求前缀和sum求和就可以了。假设我们在i - 1,i之间断开,那么就是以i为起点重新排成一条链,然后重新计算前缀和sum。但是其实并不需要重新计算前缀和,对于一个点j,它新的前缀和即是i到j的所有点的b值加起来,以我们之前算的从1开始的前缀和来算的话,

  sum'[j] = sum[j] - sum[i - 1]。

  又ans = ∑abs(sum'[j]),

  即ans = ∑abs(sum[j] - sum[i - 1]),(其中i为断点)。

  要使ans最小,很容易想到的就是sum[i - 1]取所有数的中位数,这样便能保持ans的最小。这样的话我们的做法就很简单了,把sum数组排序一遍,然后找到中位数,按照上式就能模拟出答案了。  

  tyvj1924(Poetize 杯NOIP模拟赛 II 七夕祭 )也就是这个模型。

  

{
    for (int i = 1;i <= n;i ++)
        b[i] = a[i] - t / n;
    for (int i = 1;i <= n;i ++)
        sum[i] = sum[i - 1] + b[i];
    sort(sum + 1,sum + n + 1);
    int ans = 0;
    for (int i = 1;i <= n;i ++)
        ans += abs(sum[i] - sum[(1 + n) / 2]);
}

注:代码仅供参考

posted @ 2013-10-22 15:08  N_C_Derek  阅读(867)  评论(0编辑  收藏  举报