分金币(Spreading the Wealth, UVa 11300)

分金币(Spreading the Wealth, UVa 11300)

  圆桌旁坐着n个人,每人有一定数量的金币,金币总数能被n整除。每个人可以给他左右相邻的人一些金币,最终使得每个人的金币数目相等。你的任务是求出被转手的金币数量的最小值。比如,n=4,且4个人的金币数量分别为1,2,5,4时,只需转移4枚金币(第3个人给第2个人两枚金币,第2个人和第4个人分别给第1个人1枚金币)即可实现每人手中的金币数目相等。
【输入格式】
  输入包含多组数据。每组数据第一行为整数n(n≤1 000 000),以下n行每行为一个整数,按逆时针顺序给出每个人拥有的金币数。输入结束标志为文件结束符(EOF)。
【输出格式】
  对于每组数据,输出被转手金币数量的最小值。输入保证这个值在64位无符号整数范围内。
【样例输入】

3
100
100
100
4
1
2
5
4

【样例输出】

0
4

分析1:

  这道题目看起来很复杂,让我们慢慢分析。首先,最终每个人的金币数量可以计算出来,它等于金币总数除以人数n。接下来我们用M来表示每人最终拥有的金币数。
  假设有4个人,按顺序编号为1, 2, 3, 4。假设1号给2号3枚金币,然后2号又给1号5枚金币,这实际上等价于2号给1号2枚金币,而1号什么也没给2号。这样,可以设x2表示2号给了1号多少个金币。如果x2<0,说明实际上是1号给了2号-x2枚金币。x1,x3和x4的含义类似。注意,由于是环形,x1指的是1号给4号多少金币。
现在假设编号为i的人初始有Ai枚金币。对于1号来说,他给了4号x1枚金币,还剩Ai-x1枚;但因为2号给了他x2枚金币,所以最后还剩A1-x1+x2枚金币。根据题设,该金币数等于M。换句话说,我们得到了一个方程:A1-x1+x2=M。
同理,对于第2个人,有A2-x2+x3=M。最终,我们可以得到n个方程,一共有n个变量,是不是可以直接解方程组了呢?很可惜,还不行。因为从前n-1个方程可以推导出最后一个方程(想一想,为什么)。所以,实际上只有n-1个方程是有用的。
尽管无法直接解出答案,我们还是可以尝试着用x1表示出其他的xi,则本题就变成了单变量的极值问题。
  对于第1个人,A1-x1+x2=M  x2=M-A1+x1=x1-C1(规定C1=A1-M,下面类似)
  对于第2个人,A2-x2+x3=M  x3=M-A2+x2=2M-A1-A2+x1=x1-C2
  对于第3个人,A3-x3+x4=M  x4=M-A3+x3=3M-A1-A2-A3+x1=x1-C3

  对于第n个人,An-xn+x1=M。这是一个多余的等式,并不能给我们更多的信息(想一想,为什么)。
  我们希望所有xi的绝对值之和尽量小,即|x1|+|x1-C1|+|x1-C2|+…+|x1-Cn-1|要最小。注意到|x1-Ci|的几何意义是数轴上点x1到Ci的距离,所以问题变成了:给定数轴上的n个点,找出一个到它们的距离之和尽量小的点。
下一步可能有些跳跃。不难猜到,这个最优的x1就是这些数的“中位数”(即排序以后位于中间的数),因此只需要排个序就可以了。性急的读者可能又想跳过证明了,但是笔者希望您这次能好好读一读,因为它实在是太优美、太巧妙了,而且不少其他题目也能用上(我们很快就会再见到一例)。
  注意,我们要证明的是:给定数轴上的n个点,在数轴上的所有点中,中位数离所有顶点的距离之和最小。凡是能转化为这个模型的题目都可以用中位数求解,并不只适用于本题。
  让我们把数轴和上面的点画出来。
图片描述
  任意找一个点,比如图中的灰点。它左边有4个输入点,右边有2个输入点。把它往左移动一点,不要移得太多,以免碰到输入点。假设移动了d单位距离,则灰点左边4个点到它的距离各减少了d,右边的两个点到它的距离各增加了d,但总的来说,距离之和减少了2d。
如果灰点的左边有2个点,右边有4个点,道理类似,不过应该向右移动。换句话说,只要灰点左右的输入点不一样多,就不是最优解。什么情况下左右的输入点一样多呢?如果输入点一共有奇数个,则灰点必须和中间的那个点重合(中位数);如果有偶数个,则灰点可以位于最中间的两个点之间的任意位置(还是中位数)。

代码1:

#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn = 1000000 + 10;
long long A[maxn], C[maxn], tot, M;
int main() {
  int n;
  while(scanf("%d", &n) == 1) { //输入数据大,scanf比cin快 
    tot = 0;
    for(int i = 1; i <= n; i++) { scanf("%lld", &A[i]); tot += A[i]; }            //用%lld输入long long
    M = tot / n;
    C[0] = 0; 
    for(int i = 1; i < n; i++) C[i] = C[i-1] + A[i] - M; //递推C数组
    sort(C, C+n);
    long long x1 = C[n/2], ans = 0; //计算x1
    for(int i = 0; i < n; i++) ans += abs(x1 - C[i]); 
    //把x1代入,计算转手的总金币数
    printf("%lld\n", ans);
  }
  return 0;
}

  程序本身并没有太多技巧可言,但需要注意的是long long的输入输出。在《入门经典》中我们已经解释过了,%lld这个占位符并不是跨平台的,比如,Windows下的mingw需要用%I64d而不是%lld。虽然cin/cout没有这个问题,但是本题输入量比较大,cin/cout会很慢。有两个解决方案。一是自己编写输入输出函数(前面已经给过范例),二是使用ios::sync_ with_stdio(false),通过关闭ios和stdio之间的同步来加速,有兴趣的读者可以自行搜索详细信息。
  中位数可以在线性时间内求出,但不是本例题的重点(代数分析才是重点),有兴趣的读者可以自行搜索“快速选择”算法的资料。另外,这个程序里的A数组实际上是不必保存的。

笔者分析2:

代码2:

#include <cstdio>
#include <string.h>
#include <algorithm>  //sort的使用
#include <math.h>
#include <stdlib.h>
using namespace std;

int A[10000];
int C[10000];

int MedianOf(int n)
{
    sort(C, C+n);
    return C[n/2];
}

int main()
{
    int n;
    int i;
    int M;  //每人金币平均值
    int median, sum;  //C[]的中位数,最终需要移动的金币总数
    while (scanf("%d", &n) == 1)
    {
        M = 0;
        sum = 0;
        for(i = 0; i < n; i++)
            scanf("%d", &A[i]);

        for(i = 0; i < n; i++)
            M = M + A[i];
        M = M / n;  //求得平均值

        C[0] = 0;
        for(i = 1; i < n; i++)
            C[i] = C[i-1] + A[i] - M;  //求C[]

        median = MedianOf(n);  //求C[]的中位数

        for(i = 0; i < n; i++)
            sum = sum + abs(C[i] - median);
        printf("%d\n", sum);
    }
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值