遇上一个强迫症人——怎么均分纸牌
题目来自:「NOIP 2002」均分纸牌
A.遇到一个问题
阿瓜是一个强迫症人……
有一天,邻居约他出来打牌,但是村子里 没有一副完整的扑克牌 。怎么办? 大家把各自的牌都拿了出来,没人知道这些牌有多少……
\(n\)个人围在一张桌子旁,洗牌的人把牌分成了\(n\)堆,编号分别为\(1,2,…,N\)。每堆上有纸牌数量不等。只是阿瓜见每堆牌不等,很是心烦!于是他决定使每堆上纸牌数都一样多。阿瓜突发奇想,制定下面的移动规则:
在编号为\(1\)堆上取的纸牌,只能移到编号为\(2\)的堆上;
在编号为\(N\)的堆上取的纸牌,只能移到编号为\(N-1\)的堆上;
其他堆上取的纸牌,可以移到相邻左边或右边的堆上。
但是邻居们可不想让阿瓜耽搁太多时间,因此,阿瓜必须在一秒内算出答案!
请你帮帮可怜的阿瓜吧!
输入
\(N\)(\(N\)堆纸牌,\(1\leq N\leq 100\)) \(A_1 A_2 … A_n\)(N堆纸牌,每堆纸牌初始数,\(1\leq Ai\leq 10000\))
输出
所有堆均达到相等时的最少移动次数。
样例输入
4
9 8 17 6
样例输出
3
如果你想读正版题目描述:
有\(N\)堆纸牌,编号分别为\(1,2,…,N\)。每堆上有若干张,但纸牌总数必为\(N\)的倍数。可以在任一堆上取若于张纸牌,然后移动。
移牌规则为:在编号为\(1\)堆上取的纸牌,只能移到编号为\(2\)的堆上;在编号为\(N\)的堆上取的纸牌,只能移到编号为\(N-1\)的堆上;其他堆上取的纸牌,可以移到相邻左边或右边的堆上。
现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。
给个样例解释
例如 N=4,4 堆纸牌数分别为:① 9 ② 8 ③ 17 ④ 6
移动3次可达到目的:
从 3 取 4 张牌放到 4 (9 8 13 10) ->
从 3 取 3 张牌放到 2 (9 11 10 10)->
从 2 取 1 张牌放到 1 (10 10 10 10)。
B.开始建模
当我们能建立一个数学模型,离成功就不远了。
可是,很多新手,抄手甚至高手,老手见了题目都会不知所措,爱往复杂处想,其实这也是每年CCF套路之一😁😁😁,但是呢大家如果仔细读,这是一道很简单的的题目,下面呢我们来分析一下错误的模型构造做题方法。
温馨提示:如果您有点不耐烦,可以跳过。
读题不认真
我才不会告诉你,这道题 **我也没认真读 **。
Maybe有朋友会如此读题:
n个人围在一张桌子旁,洗牌的人把牌分成了n堆,编号分别为1,2,…,N。每堆上有纸牌数量不等。只是阿瓜见每堆牌不等,很是心烦!于是他决定使每堆上纸牌数都一样多。
阿瓜突发奇想,制定下面的移动规则:
在编号为1堆上取的纸牌,只能移到编号为2的堆上;
在编号为N的堆上取的纸牌,只能移到编号为N-1的堆上;
其他堆上取的纸牌,可以移到相邻左边或右边的堆上。但是邻居们可不想让阿瓜耽搁太多时间,因此,阿瓜必须在一秒内算出答案!
请你帮帮可怜的阿瓜吧!
哈哈,我一开始正是如此
于是我会想:
So easy ,我只需要将它们排个序,每次用最大的和最小的互相填充,再算……
紧接着你会写出以下代码:
#include<aldorithm>
#include<cstdio>
#include<iostream>
using namespace std;
int n,run,cnt,a[101];
int bracsort(){
int l=1,r=n;
while(l<r){
if(a[l]==a[r]){
break;
}//当最小的等于最大的时,我们就可以出来了
if(run-a[l]>a[r]-run){//如果a[l]还不满足
a[r]=run;
a[l]+=(a[r]-run);
r--;
cnt++;
}
if(run-a[l]<a[r]-run){//如果a[r]还太满足
a[l]=run;
a[r]-=(run-a[l]);
l++;
cnt++;
}
if(run-a[l]==a[r]-run){//最好还是
a[l]=a[r]=run;
l++;
r--;
cnt++;
}
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);//读入
run+=a[i];
}
run/=n;//我们的目标
sort(a+1,a+n+1);//有可怜的朋友可能看不懂这里,不过没关系,只要知道它是在排序就行了
bracsort();
printf("%d",cnt);
return 0;
}
至于错误的问题嘛,就不用我说了。
让我们思考思考……
见下--->
C.正确分析
既然我们想让他们全部相等,那么何不一一攻破?
确立方向
首先需要的是确立要前进的方向。
及最后它们要成为什么样子。
这里我们可以设一个go for it变量或者run变量:
int n,a[101],run=0;
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
run+=a[i];
}
run/=n;
这里使用的是小学二年级法。
设置标记变量
在这里我们要设计一个标记变量,比如cnt。
int cnt=0;//初始化是一个常忘的问题
紧接着我们需要设计cnt怎么计数。
其实很简单。
如果我请别人给我一点,那么cnt就+1。
for(int i=1;i<=n;i++){
if(/*a[i]请别人借他一点*/)
cnt++;
}
什么时候需要借?
这是一个关键,什么时候需要去接?
你看看题目中的规则:
在编号为1堆上取的纸牌,只能移到编号为2的堆上;
在编号为N的堆上取的纸牌,只能移到编号为N-1的堆上;
其他堆上取的纸牌,可以移到相邻左边或右边的堆上。
既然只能这样,那么我们发现有a[i]不足就借呗。
if(a[i]<run){
//借
cnt++;
}
怎么去借呢?我们这里统一向后借吧:
a[i+1]-=(run-a[i]);
a[i]=run;
然而还有一种情况:手中事情太多!!!
if(a[i]>run){
a[i+1]+=(a[i]-run);
a[i]=run;
}
不知大家发现了吗,这样一来,算到最后,我们的a[n]自然不用接了。
于是循环改写成:
for(int i=1;i<n;i++){
}
循环内部我们也可以改一改:
if(a[i]!=run){
a[i+1]=a[i+1]+a[i]-run;;
a[i]=run;
cnt++;
}
D.代码全套亮相
#include<cstdio>
using namespace std;
int main(){
int n,a[101],run=0;
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
run+=a[i];
}
run/=n;
int cnt=0;
for(int i=1;i<n;i++){
if(a[i]!=run){
a[i+1]=a[i+1]+a[i]-run;
a[i]=run;
cnt++;
}
}
printf("%d",cnt);
return 0;
}
其实一看,还蛮简单的。
根本不像一道贪心题~~。