均分纸牌问题
1. 概述
-
有
n
个小朋友,每个小朋友手中有一些糖果,每个小朋友都可以把他手中的糖果给相邻的小朋友,问使得所有小朋友手中糖果数量相同需要多少至少给多少次?或者至少传递多少糖果? -
存在两种类型:
-
均分纸牌问题,即小朋友没有形成一个环,第一个小朋友只能给右侧小朋友,最右侧小朋友只能给左侧小朋友,对应例题:AcWing 1536. 均分纸牌;
-
环形均分纸牌问题,小朋友形成一个环,每个小朋友都可以向左右小朋友给糖果,对应例题:AcWing 122. 糖果传递、AcWing 105. 七夕祭。
-
2. 例题
AcWing 1536. 均分纸牌
问题描述
-
问题链接:AcWing 1536. 均分纸牌
分析
- 如下图,按下图方向所示进行操作,如果
x
是负数,表示反向给糖果。
-
因为最终每个小朋友的糖果数目相同,因此每个小朋友的糖果数目应该为平均值,即:
a = a 1 + a 2 + . . . + a n n a = \frac{a_1 + a_2 + ... + a _n}{n} a=na1+a2+...+an -
因此:
{ a 1 + x 2 = a a 2 − x 2 + x 3 = a . . . . . . a n − 1 − x n − 1 + x n = a a n − x n = a \begin{cases} a_1 + x_2 = a \\ a_2 - x_2 + x_3 = a \\ ...... \\ a_{n-1} - x_{n-1} + x_n = a \\ a_n - x_n = a \end{cases} ⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧a1+x2=aa2−x2+x3=a......an−1−xn−1+xn=aan−xn=a
- 可以发现,我们用前
n-1
个等式就可以求出 x 2 、 x 3 、 . . . 、 x n x_2、x_3、...、x_n x2、x3、...、xn ,如下:
{ x 2 = a − a 1 x 3 = a − a 2 + x 2 . . . . . . x n = a − a n − 1 + x n − 1 \begin{cases} x_2 = a - a_1 \\ x_3 = a - a_2 + x_2 \\ ...... \\ x_n = a - a_{n-1} + x_{n-1} \\ \end{cases} ⎩⎪⎪⎪⎨⎪⎪⎪⎧x2=a−a1x3=a−a2+x2......xn=a−an−1+xn−1
-
可以发现使用递推就可以求出这些
x
值。 -
非零的
x
值表示需要操作一次,因此统计有多少非零值,答案就是多少。 -
还需要考虑一个问题,就是能否取到这个结果,就是说能否构造出这样的方案。
-
这里类似数学归纳法,当只有两堆的话结论成立,当右侧
n-1
堆存在一种转移方案,则考虑加上左边一堆 a 1 a_1 a1 是否仍然能够实现。 -
如果此时 x 2 = = 0 x_2 == 0 x2==0 ,说明不用转移,结论成立;如果 x 2 < 0 x_2 < 0 x2<0,说明 a 1 = a + ∣ x 1 ∣ a_1 = a+ |x_1| a1=a+∣x1∣, a 1 a_1 a1 向右侧转移 x 2 x_2 x2 数量的牌,问题就变为了
n-1
的子问题,由归纳假设,结论成立;如果 x 2 > 0 x_2 > 0 x2>0,可以先操作右侧n-1
堆,会使得 a 3 a_3 a3 ~ a n a_n an 都变为a
, a 2 a_2 a2 变为 a + x 2 a + x_2 a+x2,此时 a 2 a_2 a2 向 a 1 a_1 a1 转移 x 2 x_2 x2 数量的牌即可,成立。
代码
- C++
#include <iostream>
using namespace std;
const int N = 110;
int n;
int a[N];
int main() {
cin >> n;
int sum = 0;
for (int i = 1; i <= n; i++) cin >> a[i], sum += a[i];
int res = 0; // 需要操作的次数
int avg = sum / n;
for (int i = 1, x = 0; i <= n; i++) {
x = avg - a[i] + x;
if (x) res++;
}
cout << res << endl;
return 0;
}
AcWing 122. 糖果传递
问题描述
-
问题链接:AcWing 122. 糖果传递
分析
- 如下图,按下图方向所示进行操作,如果
x
是负数,表示反向给糖果。我们的目标是求当a1=a2=...=a2
时|x1|+|x2|+...+|xn|
的最小值。
-
因为最终每个小朋友的糖果数目相同,因此每个小朋友的糖果数目应该为平均值,即:
a = a 1 + a 2 + . . . + a n n a = \frac{a_1 + a_2 + ... + a _n}{n} a=na1+a2+...+an
-
因此:
{ a 1 − x 1 + x 2 = a a 2 − x 2 + x 3 = a . . . . . . a n − 1 − x n − 1 + x n = a a n − x n + x 1 = a \begin{cases} a_1 - x_1 + x_2 = a \\ a_2 - x_2 + x_3 = a \\ ...... \\ a_{n-1} - x_{n-1} + x_n = a \\ a_n - x_n + x_1 = a \end{cases} ⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧a1−x1+x2=aa2−x2+x3=a......an−1−xn−1+xn=aan−xn+x1=a
- 整理得到:
{ x 1 − x 2 = a 1 − a x 2 − x 3 = a 2 − a . . . . . . x n − 1 − x n = a n − 1 − a x n − x 1 = a n − a \begin{cases} x_1 - x_2 = a_1 - a \\ x_2 - x_3 = a_2 - a \\ ...... \\ x_{n-1} - x_n = a_{n-1} - a \\ x_n - x_1 = a_n - a \end{cases} ⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧x1−x2=a1−ax2−x3=a2−a......xn−1−xn=an−1−axn−x1=an−a
- 再次整理得到:
{ x 1 = x 1 − 0 x 2 = x 1 − ( a 1 − a ) x 3 = x 2 − ( a 2 − a ) = x 1 − ( a 1 + a 2 − 2 a ) . . . . . . x n = x 1 − ( a 1 + a 2 + . . . + a n − 1 − ( n − 1 ) a ) \begin{cases} x_1 = x_1 - 0 \\ x_2 = x_1 - (a_1 - a) \\ x_3 = x_2 - (a_2 - a) = x_1 - (a_1 + a_2 - 2a) \\ ...... \\ x_n = x_1 - (a_1 + a_2 + ... + a_{n-1} - (n-1)a) \end{cases} ⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧x1=x1−0x2=x1−(a1−a)x3=x2−(a2−a)=x1−(a1+a2−2a)......xn=x1−(a1+a2+...+an−1−(n−1)a)
- 将上述等号右侧常数部分记为
c
,则上式变为:
{ x 1 = x 1 − c 1 x 2 = x 1 − c 2 x 3 = x 1 − c 3 . . . . . . x n = x 1 − c n \begin{cases} x_1 = x_1 - c_1 \\ x_2 = x_1 - c_2 \\ x_3 = x_1 - c_3 \\ ...... \\ x_n = x_1 - c_n \end{cases} ⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧x1=x1−c1x2=x1−c2x3=x1−c3......xn=x1−cn
-
因此原问题转变为求
|x1-c1|+|x2-c2|+...+|x_n-cn|
的最小值,此时问题变为了AcWing 104. 货仓选址。 -
将数组
c
排序后,x
取中位数即可取到最小值。 -
还需要考虑是否存在可以取到最小值,即存在合法方案。
-
这里所有的
x
不可能全是正数或者全是负数,例如全是正数,说明存在冗余操作,整体可以减去数据中的最小值。因此一定存在一个位置,对应的两侧x
值:一个是正数、一个是非正数,因此可以首先将这个位置处理完毕(因为这个位置只向外给),然后去掉这个位置,此时就变成了链,即变成了AcWing 1536. 均分纸牌,按照这题的构造方式构造即可。
代码
- C++
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1000010;
int n;
LL s[N], c[N];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%lld", &s[i]);
for (int i = 1; i <= n; i++) s[i] += s[i - 1];
LL avg = s[n] / n;
for (int i = 2; i <= n; i++) c[i] = s[i - 1] - (i - 1) * avg;
sort(c + 1, c + n + 1);
LL res = 0;
for (int i = 1; i <= n; i++) res += abs(c[i] - c[(n + 1) / 2]);
printf("%lld\n", res);
return 0;
}
AcWing 105. 七夕祭
题目描述:AcWing 105. 七夕祭
分析
-
本题的考点:排序。
Vani
喜欢的摊点下面记为糖果。 -
本题中对于行列的处理是独立的,当我们让糖果在行之间交换(上下交换)时,对列没有任何影响;反过来也是成立的。因此可以行列分别单独处理。可以先操作行,然后操作列。要让操作次数最少,则让两次的操作最少即可。
-
此时问题就变为了环形均分纸牌问题。可以参考:AcWing 122. 糖果传递。
代码
- C++
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int n, m, cnt;
int row[N], col[N], s[N], c[N];
LL work(int n, int a[]) {
// 计算前缀和
for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];
if (s[n] % n) return -1; // 说明不能均分
int avg = s[n] / n;
c[1] = 0;
for (int i = 2; i <= n; i++) c[i] = s[i - 1] - (i - 1) * avg;
sort(c + 1, c + n + 1);
LL res = 0;
for (int i = 1; i <= n; i++) res += abs(c[i] - c[(n + 1) / 2]);
return res;
}
int main() {
scanf("%d%d%d",&n, &m, &cnt);
while (cnt--) {
int x, y;
scanf("%d%d", &x, &y);
row[x]++, col[y]++;
}
LL r = work(n, row);
LL c = work(m, col);
if (r != -1 && c != -1) printf("both %lld\n", r + c);
else if (r != -1) printf("row %lld\n", r);
else if (c != -1) printf("column %lld\n", c);
else puts("impossible");
return 0;
}