《训练指南》——6.15
Uva 11300:
圆桌旁坐着n个人,没人有一定数量的金币,基尼总数能被n整除。每个人可以给他左右相邻的人一些金币,最重视的每个人的金币数目相等。你的任务是求出被转手的金币数量的最小值。
分析:似乎有约瑟夫环的即视感,但是模拟出来的情形显然要复杂的多而且无法控制“最少”这个指标,而达到“最小”的有力工具是贪心、dp或者数学,而这里貌似与dp和贪心没有多大关联,因此我们要考虑进行数学建模。
对于逆时针给出的n个人手中的初始金币我们记作A[i],并且对每个人从1开始进行标号。每人手里最终得到的金币是M个,我们定义i号给i+1号的金币x[i]个(注意我们赋予x[i]矢量性,即如果x[i]是负数依然有意义,表示i+1号给了i号|xi|个金币),容易得到下面的方程:
A[i] + x[i-1] – x[i] = M.
基于这个等式我们会列出如下的方程组:
A[1] + x[n] - x[1] = M
A[2] + x[1] –x[2] = M
A[3] + x[2] –x[3] = M
A[4] + x[3] – x[4] =M
……
我们不难进行归纳,x[i] = x[i-1] - M +A[i].
考虑到上文中我们给出的关于x[i]的矢量性定义,即我们要求min{∑|x[i]|}。我们再基于递推式,将其迭代进入所求表达式,会将其表述成如下形式:
f(x1) = |x1-c1| + |x1 – c2| + |x1 – c3| + |x1 – c4|… =∑|x - ci|(ci是基于递推式得到的常数,ci = -A[2]-A[3]-…-A[i] + (i-1)M。求f(x)的最小值。
观察到绝对值,我们再考虑其几何意义,即我们能将问题转化成如下的形式:
给出数轴上n个点(c1,c2,c3,c4….),求解某个点,使得x到这n个点的距离之和最小,求得x之后带入f(x1)即可。
那么现在我们面临的最后问题,就是x怎么求?
对于n个点中间形成的n-1个区间段,我们设置一个动点x’任移动,能够看到如果x’所在的区间,左边的点和右边的点个数不同,则必然能够移动x’使得距离和减小,那么我们按照这个使距离和减小的方向(其实就是点较多的那个方向),继续移动,会发现当左右点的数量相同的时候(对于奇数个点就是在最中间的那个点上),距离和不再减小,如果再往这个方向移动,很容易发现距离和又会增大,由此我们不难得出结论,x取得n个数的中位数的时候,该点到其余n个点的距离和最小,即f(x)取得最小。
基于这层数学化的分析,我们容易进行如下的编程实现。
#include<cstdio> #include<cmath> #include<algorithm> using namespace std; const int maxn = 1000005; int main() { int n; long long A[maxn]; long long C[maxn]; while(scanf("%d",&n) != EOF) { long long sum1 = 0 , M; for(int i = 1;i<= n;i++) { scanf("%lld",&A[i]); sum1 += A[i]; } M = sum1 / n; C[1] = 0; long long temp = 0; for(int i = 2;i <= n;i++) { temp -= A[i]; C[i] = temp + (i-1)*M; } sort(C+1,C+1+n); long long x = 0; if(n%2 == 1) x = C[n/2+1]; else x = C[n/2]; //printf("%lld\n",x); long long sum2 = 0; for(int i = 1;i <= n;i++) sum2 += abs(x - C[i]); printf("%lld\n",sum2); } }