题目地址:
https://www.luogu.com.cn/problem/P1031
题目描述:
有
N
N
N堆纸牌,编号分别为
1
,
2
,
…
,
N
1,2,…,N
1,2,…,N。每堆上有若干张,但纸牌总数必为
N
N
N的倍数。可以在任一堆上取若干张纸牌,然后移动。移牌规则为:在编号为
1
1
1堆上取的纸牌,只能移到编号为
2
2
2的堆上;在编号为
N
N
N的堆上取的纸牌,只能移到编号为
N
−
1
N-1
N−1的堆上;其他堆上取的纸牌,可以移到相邻左边或右边的堆上。现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。例如
N
=
4
N=4
N=4,
4
4
4堆纸牌数分别为:
①
9
②
8
③
17
④
6
①\ 9\ ②\ 8\ ③\ 17\ ④\ 6
① 9 ② 8 ③ 17 ④ 6,移动
3
3
3次可达到目的:从
③
③
③取
4
4
4张牌放到
④
④
④,
(
9
,
8
,
13
,
10
)
→
(9,8,13,10)\to
(9,8,13,10)→从
③
③
③取
3
3
3张牌放到
②
②
②,
(
9
,
11
,
10
,
10
)
→
(9,11,10,10)\to
(9,11,10,10)→从
②
②
②取
1
1
1张牌放到
①
①
①,
(
10
,
10
,
10
,
10
)
(10,10,10,10)
(10,10,10,10)。
输入格式:
两行
第一行为:
N
N
N(
N
N
N堆纸牌,
1
≤
N
≤
100
1≤N≤100
1≤N≤100)
第二行为:
A
1
,
A
2
,
…
,
A
n
A_1,A_2, … ,A_n
A1,A2,…,An(
N
N
N堆纸牌,每堆纸牌初始数,
1
≤
A
i
≤
10000
1≤A_i ≤10000
1≤Ai≤10000)
输出格式:
一行:即所有堆均达到相等时的最少移动次数。
思路如下:一边扫描,一边一步一步直接调整每堆牌数,然后在过程中做计数。也就是贪心算法(正确性需要证明)。具体代码如下:
#include <iostream>
using namespace std;
int pile[105]; // 记录初始状态
int main() {
int N;
cin >> N;
int tot = 0;
for (int i = 0; i < N; i++) {
cin >> pile[i];
tot += pile[i];
}
int avg = tot / N;
int count = 0;
// 开始模拟移动的过程
for (int i = 0; i < N; ++i) {
// 如果当前堆纸牌数大于avg,那就将当前多余的纸牌数移到后一堆里,
// 如果小于,就将后一堆纸牌多余的移到当前堆里,同时做计数
// 事实上这里不排斥pile[i + 1]变为负数的情况,但正确性仍然是可以保证的,具体看后面的证明
if (pile[i] != avg) {
pile[i + 1] += pile[i] - avg;
count++;
}
}
cout << count << endl;
return 0;
}
时间复杂度 O ( N ) O(N) O(N),空间 O ( 1 ) O(1) O(1)。
举个例子说明过程:若 p i l e = ( 9 , 8 , 17 , 6 ) pile=(9,8,17,6) pile=(9,8,17,6),则 a v g = 10 avg=10 avg=10,过程如下: ( 9 , 8 , 17 , 6 ) → ( 10 , 7 , 17 , 6 ) → ( 10 , 10 , 14 , 6 ) → ( 10 , 10 , 10 , 10 ) (9,8,17,6)\rightarrow(10,7,17,6)\rightarrow(10,10,14,6)\rightarrow(10,10,10,10) (9,8,17,6)→(10,7,17,6)→(10,10,14,6)→(10,10,10,10)一共 3 3 3步。当然这样的操作不总是成立的。例如,对于 ( 4 , 0 , 2 , 14 ) (4,0,2,14) (4,0,2,14),第一步 ( 5 , − 1 , 2 , 14 ) (5,-1,2,14) (5,−1,2,14)是不成立的,牌堆不能变为负数。但其实结果仍然是正确的,请看下面的证明。
算法正确性证明如下(以下
x
ˉ
\bar{x}
xˉ和
p
ˉ
\bar{p}
pˉ都代表向量的平均数):
原问题可以转述为,设原牌堆为
(
p
1
,
p
2
,
.
.
.
,
p
n
)
(p_1,p_2,...,p_n)
(p1,p2,...,pn),令
x
i
=
p
i
−
p
ˉ
,
i
=
1
,
2
,
.
.
.
,
n
x_i=p_i-\bar{p},i=1,2,...,n
xi=pi−pˉ,i=1,2,...,n,对于向量
(
x
1
,
x
2
,
.
.
.
,
x
n
)
(x_1,x_2,...,x_n)
(x1,x2,...,xn)(分量可正可负,且和为
0
0
0),每次给它加上形如
(
0
,
.
.
,
0
,
s
,
−
s
,
0
,
.
.
.
,
0
)
(0,..,0,s,-s,0,...,0)
(0,..,0,s,−s,0,...,0)这样的”合法“向量,其中
s
s
s可以为负数(”合法“的意思是,这个向量加到向量
p
⃗
\vec{p}
p上的时候,不会产生负数),至少加多少个这样的向量,就可以变为
(
0
,
0
,
.
.
.
,
0
)
(0,0,...,0)
(0,0,...,0)。设
f
(
x
1
,
x
2
,
.
.
.
,
x
n
)
f(x_1,x_2,...,x_n)
f(x1,x2,...,xn)表示初始状态为
(
x
1
,
x
2
,
.
.
.
,
x
n
)
(x_1,x_2,...,x_n)
(x1,x2,...,xn)时,到达
0
⃗
\vec{0}
0的最少所需步骤数,很显然每个步骤都可以由形如
(
0
,
.
.
,
0
,
s
,
−
s
,
0
,
.
.
.
,
0
)
(0,..,0,s,-s,0,...,0)
(0,..,0,s,−s,0,...,0)这样的向量表示。这个问题为什么和原问题等价呢?理由是此问题的任何一个操作序列,都可以一一对应到原问题的一个操作序列。所以我们现在只要考虑转述后的新问题即可。对于新问题,我们先忽略向量的”合法“性(也就是我们允许对
x
⃗
\vec{x}
x加上一个非法的向量。至于为什么可以忽略,后面有证明)。先证明一个递推式:
f
(
x
1
,
x
2
,
.
.
.
,
x
n
)
=
{
f
(
x
2
,
x
3
,
x
4
,
.
.
.
,
x
n
)
,
x
1
=
0
1
+
f
(
x
2
+
x
1
,
x
3
,
x
4
,
.
.
.
,
x
n
)
,
x
1
≠
0
f(x_1,x_2,...,x_n)=\begin{cases}f(x_2,x_3,x_4,...,x_n),\ x_1=0 \\1+f(x_2+x_1,x_3,x_4,...,x_n) ,\ x_1\ne 0\end{cases}
f(x1,x2,...,xn)={f(x2,x3,x4,...,xn), x1=01+f(x2+x1,x3,x4,...,xn), x1=0证明如下:若
x
1
=
0
x_1=0
x1=0,那么任何使得
(
x
2
,
.
.
.
,
x
n
)
(x_2,...,x_n)
(x2,...,xn)归零的操作都可以使得
(
x
1
,
x
2
,
.
.
.
,
x
n
)
(x_1,x_2,...,x_n)
(x1,x2,...,xn)归零,所以
f
(
x
1
,
x
2
,
.
.
.
,
x
n
)
≤
f
(
x
2
,
x
3
,
x
4
,
.
.
.
,
x
n
)
f(x_1,x_2,...,x_n)\le f(x_2,x_3,x_4,...,x_n)
f(x1,x2,...,xn)≤f(x2,x3,x4,...,xn),反之,把任何使得
(
x
1
,
x
2
,
.
.
.
,
x
n
)
(x_1,x_2,...,x_n)
(x1,x2,...,xn)归零的操作中形如
(
s
,
−
s
,
0
,
0
,
.
.
.
,
0
)
(s,-s,0,0,...,0)
(s,−s,0,0,...,0)都删掉,就得到了使得
(
x
2
,
.
.
.
,
x
n
)
(x_2,...,x_n)
(x2,...,xn)归零的操作序列(理由是要使得第一个分量归零,形如那样的操作必须互相抵消,即和为
0
⃗
\vec{0}
0),所以
f
(
x
1
,
x
2
,
.
.
.
,
x
n
)
≥
f
(
x
2
,
x
3
,
x
4
,
.
.
.
,
x
n
)
f(x_1,x_2,...,x_n)\ge f(x_2,x_3,x_4,...,x_n)
f(x1,x2,...,xn)≥f(x2,x3,x4,...,xn),所以
f
(
x
1
,
x
2
,
.
.
.
,
x
n
)
=
f
(
x
2
,
x
3
,
x
4
,
.
.
.
,
x
n
)
f(x_1,x_2,...,x_n)= f(x_2,x_3,x_4,...,x_n)
f(x1,x2,...,xn)=f(x2,x3,x4,...,xn)。而若
x
1
≠
0
x_1\ne0
x1=0,那么任何使得
(
x
1
,
x
2
,
.
.
.
,
x
n
)
(x_1,x_2,...,x_n)
(x1,x2,...,xn)归零的操作序列中,显然第一个分量非零的操作之和必须等于
(
−
x
1
,
x
1
,
0
,
.
.
.
,
0
)
(-x_1,x_1,0,...,0)
(−x1,x1,0,...,0),而操作序列的先后次序是可以交换的(本质上是
n
n
n维向量的加法交换律),所以可以先进行操作第一个分量的操作,然后再进行其余,所以
f
(
x
1
,
x
2
,
.
.
.
,
x
n
)
≤
1
+
f
(
x
2
+
x
1
,
x
3
,
x
4
,
.
.
.
,
x
n
)
f(x_1,x_2,...,x_n)\le 1+f(x_2+x_1,x_3,x_4,...,x_n)
f(x1,x2,...,xn)≤1+f(x2+x1,x3,x4,...,xn),反之,对于任何一个使得
(
x
1
,
x
2
,
.
.
.
,
x
n
)
(x_1,x_2,...,x_n)
(x1,x2,...,xn)归零的操作序列,删掉和为
(
−
x
1
,
x
1
,
0
,
.
.
.
,
0
)
(-x_1,x_1,0,...,0)
(−x1,x1,0,...,0)的操作,就得到了把
(
x
2
+
x
1
,
x
3
,
x
4
,
.
.
.
,
x
n
)
(x_2+x_1,x_3,x_4,...,x_n)
(x2+x1,x3,x4,...,xn)归零的操作,所以
f
(
x
1
,
x
2
,
.
.
.
,
x
n
)
≥
1
+
f
(
x
2
+
x
1
,
x
3
,
x
4
,
.
.
.
,
x
n
)
f(x_1,x_2,...,x_n)\ge 1+f(x_2+x_1,x_3,x_4,...,x_n)
f(x1,x2,...,xn)≥1+f(x2+x1,x3,x4,...,xn),所以
f
(
x
1
,
x
2
,
.
.
.
,
x
n
)
=
1
+
f
(
x
2
+
x
1
,
x
3
,
x
4
,
.
.
.
,
x
n
)
f(x_1,x_2,...,x_n)= 1+f(x_2+x_1,x_3,x_4,...,x_n)
f(x1,x2,...,xn)=1+f(x2+x1,x3,x4,...,xn)。事实上这个递推式,就是算法在执行的过程。
由递推式可以知道,若
n
=
min
{
j
:
∑
i
=
1
j
x
i
=
0
}
=
min
{
j
:
∑
i
=
1
j
p
i
=
j
p
ˉ
}
n=\min\{j:\ \sum_{i=1}^{j}x_i=0\}=\min\{j:\ \sum_{i=1}^{j}p_i=j\bar{p}\}
n=min{j: ∑i=1jxi=0}=min{j: ∑i=1jpi=jpˉ},则
f
(
x
1
,
x
2
,
.
.
.
,
x
n
)
=
n
−
1
f(x_1,x_2,...,x_n)=n-1
f(x1,x2,...,xn)=n−1。我们只需证明,对于这样的向量
x
⃗
\vec{x}
x,存在长度为
n
−
1
n-1
n−1的合法的操作序列就可以了。为了方便理解,我们先看几个简单的情况:
1、如果
x
⃗
\vec{x}
x是递增的,那么我们可以这样做:把
p
n
p_n
pn多于
p
ˉ
\bar{p}
pˉ的部分全移到
p
n
−
1
p_{n-1}
pn−1上去,接着再把
p
n
−
1
p_{n-1}
pn−1多于
p
ˉ
\bar{p}
pˉ的部分全移到
p
n
−
2
p_{n-2}
pn−2上去,如此操作下去,就得到了一个
n
−
1
n-1
n−1的合法操作序列。
2、如果
x
⃗
\vec{x}
x是单峰的,也就是先增到最大值,然后又递减,那么我们可以把最大的那个峰,给前后分别分一点,然后再应用第一个情况的方法,这样也能构造出一个
n
−
1
n-1
n−1的合法操作序列。
一般的情况怎么证明呢?用数学归纳法!
显然当 n = 1 n=1 n=1时, p 1 = p ˉ p_1=\bar{p} p1=pˉ,此时不需要任何操作,也就是操作序列长度为 n − 1 = 0 n-1=0 n−1=0。假设当 n ≤ k − 1 n\le k-1 n≤k−1时结论正确,当 n = k n=k n=k时,若 p 1 > p ˉ p_1>\bar{p} p1>pˉ,那么就把 p 1 p_1 p1中大于 p ˉ \bar{p} pˉ的部分分到 p 2 p_2 p2上去,显然这个操作是合法的。接着根据归纳法,对于后面 k − 1 k-1 k−1个牌堆,存在长度为 k − 2 k-2 k−2的合法操作序列,加上第一个操作,就得到了长度为 k − 1 k-1 k−1的合法操作序列。 p 1 = p ˉ p_1=\bar{p} p1=pˉ是不可能的。若 p 1 < p ˉ p_1<\bar{p} p1<pˉ,考虑 t = min { j : ∑ i = 1 j p i ≥ j p ˉ } t=\min\{j:\sum_{i=1}^{j}p_i\ge j\bar{p}\} t=min{j:∑i=1jpi≥jpˉ},由于 ∑ i = 1 n x i = n p ˉ \sum_{i=1}^{n}x_i= n\bar{p} ∑i=1nxi=npˉ,所以这样的 t t t是肯定存在的。容易知道 p t > p ˉ p_t>\bar{p} pt>pˉ(可以这么理解,就像往杯子里倒盐水,只有倒浓度更高的盐水,才能让杯中原有的盐水浓度升高),于是乎由 t t t的定义,我们得到两个不等式: ∑ i = 1 t − 1 p i < ( t − 1 ) p ˉ ∑ i = 1 t p i ≥ t p ˉ \sum_{i=1}^{t-1}p_i<(t-1)\bar{p}\\\sum_{i=1}^{t}p_i\ge t\bar{p} i=1∑t−1pi<(t−1)pˉi=1∑tpi≥tpˉ可以验证: ( t − 1 ) p ˉ − ∑ i = 1 t − 1 p i ≤ p t − p ˉ (t-1)\bar{p}-\sum_{i=1}^{t-1}p_i\le p_t-\bar{p} (t−1)pˉ−i=1∑t−1pi≤pt−pˉ所以我们只需要把 p t p_t pt里的 ( t − 1 ) p ˉ − ∑ i = 1 t − 1 p i (t-1)\bar{p}-\sum_{i=1}^{t-1}p_i (t−1)pˉ−∑i=1t−1pi的部分移到第 t − 1 t-1 t−1堆里,这样前 t − 1 t-1 t−1堆的牌的平均值就等于 p ˉ \bar{p} pˉ,由第二数学归纳法知道,前 t − 1 t-1 t−1堆存在长度为 t − 2 t-2 t−2的合法操作序列,而移动完毕之后,第 t t t堆以及以后的牌堆平均值也变成了 p ˉ \bar{p} pˉ,又可以用第二数学归纳法,存在长度为 n − t n-t n−t的合法操作序列。所以对于整个牌堆来说,存在长度为 1 + t − 2 + n − t = n − 1 1+t-2+n-t=n-1 1+t−2+n−t=n−1的合法操作序列。证明完毕。
回到当初的假设,若 n > k = min { j : ∑ i = 1 j x i = 0 } = min { j : ∑ i = 1 j p i = j p ˉ } n>k=\min\{j:\ \sum_{i=1}^{j}x_i=0\}=\min\{j:\ \sum_{i=1}^{j}p_i=j\bar{p}\} n>k=min{j: ∑i=1jxi=0}=min{j: ∑i=1jpi=jpˉ},我们可以把整个牌堆分成连续的满足 k = min { j : ∑ i = 1 j x i = 0 } k=\min\{j:\ \sum_{i=1}^{j}x_i=0\} k=min{j: ∑i=1jxi=0}的几段,然后分别用上面证明的结果,就可以构造出长度为 f ( x 1 , x 2 , . . . , x n ) f(x_1,x_2,...,x_n) f(x1,x2,...,xn)的合法操作序列。这就回答了为什么一开始我们可以不考虑操作序列的合法性。算法正确性证明到此结束。
我们稍微理一理证明过程。我们先证明了,如果不考虑操作的合法性,我们得到了一个递推式,这个递推式算出的结果,就是按照上面的代码算出的一个最小操作次数。接下来我们证明了,的确存在合法的操作序列,它的长度正好可以达到算出的最小操作次数。所以这样的贪心策略算出的结果就是对的。
由算法正确性的证明过程,我们其实发现了另一个“神奇”的算法。以分为两段为例, f ( x 1 , x 2 , . . . , x n ) = k − 1 + f ( x k + 1 , x k + 2 , . . . , x n ) = k − 1 + n − k − 1 = n − 2 f(x_1,x_2,...,x_n)=k-1+f(x_{k+1},x_{k+2},...,x_n)\\=k-1+n-k-1=n-2 f(x1,x2,...,xn)=k−1+f(xk+1,xk+2,...,xn)=k−1+n−k−1=n−2我们发现事实上,如果 ( p 1 , p 2 , . . . , p n ) (p_1,p_2,...,p_n) (p1,p2,...,pn)能分成“浓度”为 p ˉ \bar{p} pˉ的 m m m段子串,那么答案就是 n − m n-m n−m。该算法如下:
#include <iostream>
using namespace std;
int pile[105];
int main() {
int N;
cin >> N;
int tot = 0;
for (int i = 0; i < N; i++) {
cin >> pile[i];
tot += pile[i];
}
int avg = tot / N;
int count = 0; // 记录段数
int cur_sum = 0, n = 0;
for (int i = 0; i < N; ++i) {
cur_sum += pile[i];
n++;
if (cur_sum == avg * n) {
count++;
cur_sum = 0;
n = 0;
}
}
cout << N - count << endl;
return 0;
}
时空复杂度一样。