火车票余票问题的算法解析

作者: Phill King

原创文章,转载请注明出处。

我们每次购买火车票,在查询的时候都可以及时的看到余票数。这个余票数是怎么计算出来的?有没有好的算法可以很快的计算出余票数目。本文对这个问题做了详细的分析,并给出了具体的代码。

声明:

本文详细分析了火车票的余票的算法,不考虑现实中一列火车包含不同等级座位和站票,以及预分配等情况,采用一个简化的模型来做基本分析。最后用一个高效率的算法来获取所有站点的余票数。

本文和12306无关。

-----------------------------------------------------------------------------------------------------

火车票余票问题的描述:

假设有一列火车,一共有M个座位,沿途经过S个站点。在出售一定数量的车票后,求该火车在沿途各站点的余票数。

首先我们对购票的情况和余票的关系做一个分析。

以M=5个座位, S=6站 为例

 

该表中的列代表所购票对应的站点段,一共有6站,对应的站点段数为5. 用户购买某张票如(3-6)站,即意味者占用了(3-4),(4-5),(5-6)的站点段。

该表中的行代表了座位号,对应购票时的座位。下文的总票数即为总的座位数。

对于(1-2)段来说,余票数就是总票数减去被占用的座位数。 示例中是 5-2 = 3;

对于(1-3)段来说,余票数就是总票数减去  (1-2) ∪ (2-3)的占用座位数。 示例中是 5-3 = 2;

对于(2-4)段来说,  余票数就是总票数减去  (2-3) ∪ (3-4)的占用座位数。  示例中是 5-4 = 1;

...

对应的余票表:

(行表头对应出发站,列表头对应抵达站)

 

因为余票即为总票数减去占用座位数,下面我们用更直观的占用座位表格来分析。

座位占用表: ( Si代表 从i到i+1站的座位占用集合)

我们可以看到对于某两站的座位占用集合有相应的规律,如

S(1-3) = S1  S2; 

S(2-5) = S2 ∪ S3 U S4

S(i-j) = Si ∪ S(i+1) ... ∪ S(j-1)

我们可以根据座位占用的情况推导出余票计算的公式:

对于某两站i,j的余票计算可推出公式:

        T(i,j) = 总票数 – (Si ∪ S(i+1)   … S(j -1))

或者 T(i,j) = 总票数 – (S ∪ S)

测试代码采用stl的Set来存储座位号,在100个站点和1000个座位下,更新购买2000张票计算整张余票表大约需要0.8秒。

可以看出性能不够理想。

性能优化:

由于集合运算比较耗时,我们考虑用位图法提升效率。

如果某个座位被出售,既标识相应的位(bit)1.

比如(1-2)段的2座和5座已售,则可以用10010来表示

假设火车共有1000个座位,如果用64位整数代表,则只需 大小为 1000/64 +1 = 16的数组即可表示。

uint64_t seat[16]

每两个站点之间的座位占用情况都需要一个数组来存储。一个数组的大小为128bytes。

这样计算集合只需要做或运算。大大提高效率。

用bit来表示购票的座位占用情况:

(1-2)站, 座位的分布对应的值是 二进制 10010

(2-3)站, 座位的分布对应的值是 二进制 10110

(3-4)站, 座位的分布对应的值是 二进制 10111

(4-5)站, 座位的分布对应的值是 二进制 01011

(5-6)站, 座位的分布对应的值是 二进制 01101

(1-3)站, 座位的分布对应的值是 二进制 10010 | 10110 = 10110  。 占用的座位数是3, 所以余票数是5-3= 2

以此类推

 

对应的余票计算公式:

站对应的座位占用用一组64位整形数组表示,设Bi

i,j余票数T(i,j) = 总票数  –  Bi|B(i+1)  | B(j-1) 1的个数

座位占用示意表:

采用位图(bitmap)法,运行效率提高很多,同时占用的内存空间也大大降低。

在100个站点,1000个座位的情况下测试更新购买2000张票之后的余票表格只需要5毫秒。

如果只是计算某两个站点的余票数更是只需要几微秒。

算法复杂度分析:

设购票数为T, 站点数为S, 时间复杂度为 T*S + S*(S+1)/2. 因为S是固定的,所以时间复杂度是O(N)

T*S的部分是更新购票信息,如果单独计算更新余票的时间复杂度,则为O(1);

空间复杂度:

设总座位数为M, 每两个站点的售票信息占用M/64*8 bytes. 整个个余票表占用M/8 * S(S+1)/2 bytes空间。

 

结合实际情况的一些测试数据(在本人的机器上):

50个站点,1000个座位的火车,批量更新2000张票,共耗时1.2毫秒。任意两个站点的座位占用数据可以压缩到大小为16的64位整形数组里,占用空间为128bytes。 整张表格的占用空间为128*50*49/2 = 154k bytes. 

所以火车票更新余票按照此算法从时间和空间上分析,是非常快和省空间的。

最后是示例代码:

#include <vector>
#include <iostream>
#include <unordered_set>
#include <iomanip>
#include <chrono>
#include <ctime>
#include <cstdlib>
#include <set>
#include <fstream>

using namespace std;

#define BITS_WIDTH 64

const int MAX_STATION = 100;
const int TOTAL_SEAT = 1000; // 总座位数,即每一站的总票数

struct ticket_info
{
	int start = -1;
	int end = -1;
	int number = -1;
};

typedef struct seat_type
{
	uint64_t & operator[](int i){return seat[i];};
	uint64_t seat[32] = {0};

	int get_bits(uint64_t n)
	{
		int bits=0;
		while(n>0)
		{
			n= n&(n-1);
			bits++;
		}
		return bits;

	}
	int get_count()
	{
		int count = 0;
		for(int i=0; i<32;i++)
			count += get_bits(seat[i]);
		return count;
	}


}seat_type;





class ticket_box
{
	std::vector< std::vector<seat_type> > tickets_count;
	const int ticket_total = TOTAL_SEAT ;
	const int station_number = MAX_STATION;
public:

    
    //逐票更新
	int buy_ticket(ticket_info& new_ticket)
	{
		int start = new_ticket.start;
		int end = new_ticket.end ;
		int number = new_ticket.number;
		int base = number/BITS_WIDTH;
		int offset = number%BITS_WIDTH;
		uint64_t update_bit = 1<<offset;

		for(int i=0; i<=start; i++)
			for(int j=start;j<station_number;j++)
			{
				tickets_count[i][j][base] |= update_bit;
			}

		for(int i=start+1;i<=end;i++)
			for(int j=i;j<station_number;j++)
			{
				tickets_count[i][j][base] |= update_bit;

			}
		return 0;
	}
     
    //批量更新余票
	int buy_tickets(std::vector<ticket_info>& tickets)
	{
		for(auto ticket:tickets)
		{
			int base = ticket.number/BITS_WIDTH;
			int offset = ticket.number%BITS_WIDTH;
			uint64_t update_bit = 1<<offset;
			for(int i=ticket.start; i<=ticket.end;i++)
			{
				tickets_count[i][i][base] |= update_bit;
			}
		}

		for(int i=0; i<station_number-1; i++)
			for(int j=i+1, index=1; j<station_number; j++)
			{
				for(int k=0; k<32; k++)
				{
					tickets_count[i][j][k] = tickets_count[i][j-1][k] | tickets_count[i+index][j][k];
				}
				index++;
			}

		return 0;
	}

    //输出余票
	int output_remain_tickets()
	{
		std::cout<<std::setw(4)<<" ";
		for(int i=0; i< station_number; i++)
		{
			std::cout<<std::setw(4)<<i+2<<" ";
		}
		std::cout<<endl;
		for(int i=0; i< station_number; i++)
		{
			std::cout<<std::setw(4)<<"-----";
		}
		std::cout<<endl;

		for(int i=0; i< station_number&&i<12; i++)
		{
			std::cout<<std::setw(3)<<i+1<<" ";
			for(int k=0; k<i; k++)
			{
				std::cout<<std::setw(4)<<"     ";
			}
			for(int j=i; j<station_number&&j<12; j++)
			{
				std::cout<<std::setw(4)<<TOTAL_SEAT - tickets_count[i][j].get_count()<<" ";
			}
			std::cout<<endl;
		}

		return 0;
	}

};

以下给出一个test case和对应的输出

此case中站点数为12,总座位数为1000。

(起始站,终点站,座位号)
7,10,1
10,12,1
3,6,2
6,8,2
2,4,3
4,6,3
1,5,4
5,8,4
2,4,5
4,5,5
8,9,6
4,6,7
6,7,7
5,7,8
7,9,8
10,11,9
1,3,10
3,4,10
       2    3    4    5    6    7    8    9   10   11   12 
-------------------------------------------------------
  1  998  996  995  994  993  993  992  991  991  990  990 
  2       996  995  994  993  993  992  991  991  990  990 
  3            995  994  993  993  992  991  991  990  990 
  4                 995  994  994  993  992  992  991  991 
  5                      995  995  994  993  993  992  992 
  6                           996  995  994  994  993  993 
  7                                996  995  995  994  994 
  8                                     997  997  996  996 
  9                                          999  998  998 
 10                                               998  998 
 11                                                    999 

 

 

本文考虑了有座位号的余票计算,可以涵盖不同类型有座位号的余票更新, 比如卧铺,一等座,二等座等。但是对于站票等没有座位号的情况,余票计算的方式会略有不同。如果用户购买的是没有座位的站票,购票只有出发站和到达站的信息,相应的余票应该如何计算呢?这个简单的问题就留给读者思考吧。

  • 14
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值