【洛谷】P1031 均分纸牌(配数学证明)

题目地址:

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 N1的堆上;其他堆上取的纸牌,可以移到相邻左边或右边的堆上。现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。例如 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 1N100
第二行为: 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 1Ai10000

输出格式:
一行:即所有堆均达到相等时的最少移动次数。

思路如下:一边扫描,一边一步一步直接调整每堆牌数,然后在过程中做计数。也就是贪心算法(正确性需要证明)。具体代码如下:

#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=pipˉ,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)=n1。我们只需证明,对于这样的向量 x ⃗ \vec{x} x ,存在长度为 n − 1 n-1 n1的合法的操作序列就可以了。为了方便理解,我们先看几个简单的情况:
1、如果 x ⃗ \vec{x} x 是递增的,那么我们可以这样做:把 p n p_n pn多于 p ˉ \bar{p} pˉ的部分全移到 p n − 1 p_{n-1} pn1上去,接着再把 p n − 1 p_{n-1} pn1多于 p ˉ \bar{p} pˉ的部分全移到 p n − 2 p_{n-2} pn2上去,如此操作下去,就得到了一个 n − 1 n-1 n1的合法操作序列。
2、如果 x ⃗ \vec{x} x 是单峰的,也就是先增到最大值,然后又递减,那么我们可以把最大的那个峰,给前后分别分一点,然后再应用第一个情况的方法,这样也能构造出一个 n − 1 n-1 n1的合法操作序列。

一般的情况怎么证明呢?用数学归纳法!

显然当 n = 1 n=1 n=1时, p 1 = p ˉ p_1=\bar{p} p1=pˉ,此时不需要任何操作,也就是操作序列长度为 n − 1 = 0 n-1=0 n1=0。假设当 n ≤ k − 1 n\le k-1 nk1时结论正确,当 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 k1个牌堆,存在长度为 k − 2 k-2 k2的合法操作序列,加上第一个操作,就得到了长度为 k − 1 k-1 k1的合法操作序列。 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=1jpijpˉ},由于 ∑ 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=1t1pi<(t1)pˉi=1tpitpˉ可以验证: ( 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} (t1)pˉi=1t1piptpˉ所以我们只需要把 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 (t1)pˉi=1t1pi的部分移到第 t − 1 t-1 t1堆里,这样前 t − 1 t-1 t1堆的牌的平均值就等于 p ˉ \bar{p} pˉ,由第二数学归纳法知道,前 t − 1 t-1 t1堆存在长度为 t − 2 t-2 t2的合法操作序列,而移动完毕之后,第 t t t堆以及以后的牌堆平均值也变成了 p ˉ \bar{p} pˉ,又可以用第二数学归纳法,存在长度为 n − t n-t nt的合法操作序列。所以对于整个牌堆来说,存在长度为 1 + t − 2 + n − t = n − 1 1+t-2+n-t=n-1 1+t2+nt=n1的合法操作序列。证明完毕。

回到当初的假设,若 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)=k1+f(xk+1,xk+2,...,xn)=k1+nk1=n2我们发现事实上,如果 ( 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 nm。该算法如下:

#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;
}

时空复杂度一样。

  • 11
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值