问题描述
请实现一个铁路购票系统的简单座位分配算法,来处理一节车厢的座位分配。
  假设一节车厢有20排、每一排5个座位。为方便起见,我们用1到100来给所有的座位编号,第一排是1到5号,第二排是6到10号,依次类推,第20排是96到100号。
  购票时,一个人可能购一张或多张票,最多不超过5张。如果这几张票可以安排在同一排编号相邻的座位,则应该安排在编号最小的相邻座位。否则应该安排在编号最小的几个空座位中(不考虑是否相邻)。
  假设初始时车票全部未被购买,现在给了一些购票指令,请你处理这些指令。
样例说明
输入格式
  输入的第一行包含一个整数n,表示购票指令的数量。
  第二行包含n个整数,每个整数p在1到5之间,表示要购入的票数,相邻的两个数之间使用一个空格分隔。
输出格式
  输出n行,每行对应一条指令的处理结果。
  对于购票指令p,输出p张车票的编号,按从小到大排序。
样例输入
4
2 5 4 2
样例输出
1 2 1) 购2张票,得到座位1、2。
6 7 8 9 10 2) 购5张票,得到座位6至10。
11 12 13 14 3) 购4张票,得到座位11至14。
3 4 4) 购2张票,得到座位3、4。    
  
---------------------------------------------------------------请思考的分割线----------------------------------------------------------------------

  读者看到这里的时候,想必心里已经有自己的想法了。无论可行性如何,有想法才是最重要的。接下来我们来看一下这个问题的解决思路吧。(个人能力有限,所以贡献的算法也难免局限,如果大家有更好的想法,欢迎讨论)

  首先,看到问题的时候第一思路是建立一个数组来记录100个座位的占用情况,对于每一次的购票行为,只需要对数组进行遍历,找到满足要求的位置并占用即可。当然这种方法的缺陷也很明显,每一次的购票行为都需要对数组进行遍历,而且由于每一排有5个座位的限制,所以在遍历的时候仍然要考虑连续的座位是否在同一排中,这无疑会增加算法的复杂性。

  进一步想,既然题目的座位是20排,那我们可以直接选择二维数组作为我们记录占用情况的数据结构。针对二维数组而言,我们只需要按行遍历,对于每一行判断是否可以容纳本次的购票人数。这种方法的遍历情况跟上一个方法是一样的,优势在于每一次的子数组长度都是5,不需要另行判断。

  上面的方法都局限在同一个问题,即对座位数组的反复遍历。尤其是对于那些已经占用的座位,每一次的遍历都是白白地消耗时间,如果,(敲黑板,重点来了)对于已经被占用的位置,我们可以减少访问,那么我们的运行时间就可以缩短!!!

  针对上面的两种思路我们来进行改进,我们先从一维数组开始考虑(因为二维数组的想留给读者自己试验)。在购票过程中,我们会尽量地把同一批客户安排在连续的同一排座位。那么我们在每次安排座位的时候,在被占用的数组中标示连续占用的座位数,这样的话,在我们遍历的时候就可以直接利用这个数字跳过这段连续的区域,减少访问次数。

  看完上面这段,读者可能对方法还是很疑惑。语言描述实在是不够直观,我们不妨看一下示意图。

          【刷题笔记】火车购票-----java方案-LMLPHP

  如果读者看图已经明白了原理,不妨先去实现一下。如果还是觉得抽象,我们就来细细地看图说话吧。

  上图显示的就是示例中的购票过程,首先将数组seats初始化为0,表示座位空置。

  第一次购票2张,占用位置(数组片段)是1、2,把seats[1]的数字置为2,表示由此开始的连续两个座位被占用。等到遍历数组时,在这个位置可以对下标进行+2操作,直接跳过这一段被占用的座位。

  第二次购票是5张,从头开始遍历数组,访问第一个位置,被占用且数字为2,则跳转到1+2=3号位置,3号座位为0表示空置,但是3--5之间只有三个空位,不能容纳5个人,所以跳转到下一排,即6号位置,6号位满足要求,在此处落座并更改seats[6] = 5。

  第三次购票是4张,依然从头开始,1号位非0,跳转至1+2=3号位;3号位为0,但是空间不足,跳转至6号位;6号位非0,跳转至6+5=11号位;11号位为0且空间足够,落座并更改seats[11] = 4。

  第四次购票是2张,遍历过程如上,1号位非0,转至1+2=3号位;3号位为0且空间足够,落座并更改seats[3]=2。

  到此为止,例程部分的购票已经结束。如果某次购票行为中发现没有足够的连续空位,就要选择散座,即重新从头开始遍历,并且每次只坐一个人。方法同上。

  总结一下,在这个版本的算法中,我们合理利用了开辟的数组,记录了被连续占用的段落长度,从而减少了遍历的次数,好像已经前进了一步。但是这个算法仍然可能需要进行两次遍历,即第一次找不到连续座位然后重新找散座。因为题目中的要求是先尽量连续分配,失败则分配散座,所以两次遍历操作是难免的。

  考虑到以上的情况,再来想一想能不能在遍历过程中再精简一些。

---------------------------------------------------------------请思考的分割线----------------------------------------------------------------------

  

  仔细分析一下,上面的过程中对于每一排座位,是在发现空位之后才去判断空位数是否足够。从二维数组的角度考虑一下,如果每一行的第一个位置标示了这一排已经坐下的人数x,那么我们就可以很直接的通过5-x得到空余的座位。这样的话我们就不必要先跳转到空位,而可以直接判断是否在这一排落座,如果选择落座的话,则利用排首坐标+x跳转至座位。
  相比之下,二维数组的情况跟一维是相似的,甚至更简单,我们在每一行的第一个位置记录下本行已经被占用的座位数,这样的话每次只需要访问seats[*][1]的数字,就可以得知该行是否可以坐并且可以直接找到空位,落座的时候也只需要让seats[*][1]加上本次坐下的人数。

PS:读者如果注意到上面seats[*][1]里的*,应该明白其实完全可以只用一个20位的数组解决。哇塞,进步好大啊!!问题不难,读者请动手试试吧。(注意,文中的数组起始下标是1,请根据实际情况修正)

PPS:俗话说得好,talk is cheap,show me the code。下面贴上对于一维数组的相对比较复杂的算法实现。大家试着实现其它的吧。

 Scanner fin = new Scanner(System.in);      //有一些语句是为了对应提交题目的格式,读者自动选择有价值的部分参观
int N = fin.nextInt(); int [] seat = new int[100]; for(int i=0;i<N;i++){
int num = fin.nextInt();
int pos = 0;
while(pos < 100){
if(seat[pos] == 0){
if(pos%5 + num <= 5) break;
pos = pos + 5 - pos%5;
}else{
pos += seat[pos];
}
}
if(pos < 100){
seat[pos] = num;
while(num > 0){
String cc = Integer.toString(++pos);
System.out.print(cc + " ");
num --;
}
}else{
pos = 0;
while(num > 0){
if (seat[pos] == 0){
String cc = Integer.toString(pos+1);
System.out.print(cc + " ");
seat[pos++] = 1;
num--;
}else{
pos += seat[pos];
}
}
}
System.out.println("");
}
05-08 08:02