关于纸牌均分的解法及扩展
一、纸牌均分
给定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]); }
注:代码仅供参考