关于"丢失的牛"这个题的教学反思[原创]
最近在教学中,和学生探讨《丢失的牛》这一问题,引发了我的思考,下面简单谈谈《丢失的牛》问题及引发的思考。
【问题描述】给你一个正整数n,有一个1到n的全排列,现告诉你该全排列中从第二个位置开始到最后一个位置,每个位置的前面的数字中比它小的数的个数,请你求该n的全排列中每个位置上的数字是多少?n<=8000
【输入格式】第一行为一个正整数n;第二行为n-1个整数,表示n的全排列中第二个位置到最后一个位置,每个位置的前面的数字中比它小的数的个数。
【输出格式】一行n个正整数,为n的全排列。
【样例输入】
5
1 2 1 0
【样例输出】
2 4 5 3 1
初步分析样例输入,尝试正向求解。对于输入的第1个数字1,表示5的全排列中的第2个位置上的数字,前面有1个数字比它小。先只考虑第1和第2个位置上的数,数对(1,2),(1,3),(1,4),(1,5),(2,3),(2,4),(2,5),(3,4),(3,5),(4,5)均是满足条件的,仔细算下有c(5,2)个数对满足条件。若分别以这些数对为最初状态来进行后面的推演是很困难的。
正难则反,于是我们倒过来做。设输入数列为ai,结果数列为ansi,ansi的数值其实就是在1~n的数字之间找第ai+1小的数字。每求出一个ansi来后,要将其数从1~n的数中删除掉。以样例输入说明其过程:
对输入数据1,2,1,0,从后往前看,对于数字0,表示前面没有数字比当前位置数字小,问题求解即是在数字1~5之间找第1小的数,明显为1,我们记下这个值并将其从1~5的数列中删除掉。对于倒数第2个数字1,代表要在数字2~5之间找第2小的数字,明显为3,我们记下这个值并将其从原数列中删除掉。对于输入的倒数第3个数字2,代表在数列(2,4,5)中第3小的数字,明显为5。对于输入的倒数第4个数字1,代表在数列(2,4)中找第2小的数字,明显为4。最后还剩下数字2,易知其为结果数列的第1个数字。于是最终找出来的数列为(2,4,5,3,1)。这个题就本质而言就是一个动态求第K小数字的问题,而且数据范围也不大,可以暴力来进行实现。
代码如下:
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
int a[8010],f[8010],ans[8010];
int main()
{
int n;
scanf("%d",&n);
for(int i=2;i<=n;i++)
scanf("%d",&a[i]);
for(int i=n;i>=1;i--)
{
int sum=0;
for(int j=1;j<=n;j++)
{
if(!f[j])sum++;
if(sum==a[i]+1)
{
ans[i]=j;
f[j]=1;
break;
}
}
}
for(int i=1;i<=n;i++)
printf("%d\n",ans[i]);
return 0;
}
接下来,我就安排学生自行书写程序了。但有个学生一直在纸上写写画画,我问他:“有什么疑问吗?”他说:“老师,我觉得这个题正着做,也是可以的。”我有些不耐烦的说:“这个题,我认真研究过,正着做是不太可能的。”学生倔强的说:“老师,你让我试试吧。”过了一小会儿,学生按他的思路成功解决了这一问题。下面仍然以样例输入为例,思路如下:
对于问题求解的结果数列,先不妨设第1个位置上的数字为1。然后从第2个位置依次求解到第5个位置。对于输入1,2,1,0中的第1个数字1,代表第2个位置上的数字前面有1个数字比它小,所以可设第2个位置上的数字为2,目前结果数列为1,2;然后于输入的2来说,代表第3位上的数字前面有2个数字比它小,所以可设之为3,结果数列为1,2,3;于输入的1来说,代表第4个位置上的数前面有1个数字比它小,所以可设之为2,于是对前面的结果数列1,2,3进行调整,将所有大于等于2的数字分别加1,结果数列变成1,3,4,2;最后于输入的0来说,代表第5个位置上的数字前面有0个数字比它小,所以将第5个位置上的数字设置为1,从而对前面的结果数列1,3,4,2进行调整,将所有大于等于1的数字分别加1,于是结果数列变成2,4,5,3,1。
代码如下:
#include<bits/stdc++.h>
using namespace std;
int n,a[8000],b[8000],t;
int main(){
cin>>n;
for(int i=2;i<=n;i++){
cin>>a[i];
}
b[1]=1;
for(int i=2;i<=n;i++)
{
b[i]=a[i]+1;
//对于第i个位置来说,在它前面有a[i]个数字比它小,于是可以不考虑其它因素
//这个位置上应该是a[i]+1.
for(int j=1;j<=i-1;j++)
//对于在其左边的数字,如果其权值大于b[i],则对其进行调整
if(b[j]>=b[i])
b[j]++;
}
for(int i=1;i<=n;i++){
cout<<b[i]<<endl;
}
return 0;
}
仔细思考学生的求解过程 ,他比我最开始的想法更进一步,原因在于以下两点:(1)对于数列的第1个位置上的数来说,它的左边没有别的数字,自然也就没有数字比它小。(2)本题是求某个n的全排列,也就是说当n=1时,这个全排列是唯一的,就是数列1。如果n=2,则样例输入就只需要给出1个数字,我们求解出来的,自然也就是一个2的全排列。
于是他的整个求解就有一个扎实的“初状态”,即可以设结果数列的第1个位置就是数字1。然后根据输入的数列ai,设第i个位置上的数字为ai+1,再对位置i-1前的结果数列进行适当的调整,保证其始终是一个满足题意的n的全排列的子序列。
通过这一教学案例,我有以下几点感怀:首先,学生的创造性是无穷的,永远要去鼓励学生积极探索。我们的学生都是一个个鲜活的个体,他们天生没有束缚,有着无穷的探知欲。反而成年人多了很多条条框框,老师不能以自己的身份去打压学生的探索欲,而是应该去鼓励他们积极探索。当然这种探索应该是以理性思考为基础,进行缜密的分析,层层推进。其次,对于每节课,老师应该给学生一定的自主空间。对于教学来说,最简单最无脑的教法就是老师从头讲到尾,不给学生任何思考的机会。这样的课看似老师很负责任,非常卖力的在讲。但事实上学生接收了多少呢,就算有一定的接收,这种被动的接收,对他的心智的启发又有多大呢?所以对于教学来说,之所以可称之为一门艺术,很大的原因就在于,当面临的学生个体不同,课堂的设计是完全不同的,教学情景如何设计、如何引出问题,引导学生进行分析并进行析疑等等都是非常有讲究的。
题后记:本题用树状数组+二分,可以做到N*Log2N* Log2N的时间复杂度;如果对倍增思想理解得很深的话,可以做到N* Log2N;如果直接用线段树,能较为轻松的做到N* Log2N。

浙公网安备 33010602011771号