第一个令人激(qu)动(shi)的实验,竟然是做一个资本主义的银行的服务模拟(即VIP可以插队!! 球球了不要VIP )
做这个实验可以说是心情十分复杂,这这这完全没法debug,因为顾客数据是随机数生成的,不要想着能100%重现刚刚出现的问题。我会在后文中给出几个常见问题,想验证自己思路和实现正确性的话可以着重试一试这几个方面。
下面进入正文吧,带大家三进三出我的破烂银行模拟系统
为书上的银行排队系统,添加VIP客户功能。要求如下:
1)任意VIP客户优先于任意普通客户办理业务;
2)VIP客户之间保持先来后到的顺序服务;
3)普通客户排队顺序原则不变。
4)附加题:设计一套自动引导顾客进行排队的机制,让无论是否是VIP的顾客体验都能最好。此题为开放题,言之成理,实验结果能够支撑论点即可。
这里呢是书上的银行排队系统原始的代码压缩包,想要尝试这个小小的银行实验的小伙伴自取~
要在原有代码上添加功能,最重要的一步是读懂原始代码。我知道读别人的代码真的有亿点点痛苦♂️,所以下面我会讲一讲这个代码中的关键实现部分,让想要尝试的伙伴们可以更快明白模拟的原理,上手设计自己的功能。
理解“事件”,是理解原始代码思路的关键之一。
事件分为两种:arrival 和 departure
// specifies the two kinds of events
enum EventType {arrival, departure};
class Event
{
private:
// members that identify both customer and teller, as
// well as maintain information on time of the event,
// the event type, the length of service required by
// the customer, and the amount of time customer is
// forced to wait for service
int time;
EventType etype;
int customerID; // customers numbered 1, 2, 3,...
int tellerID; // tellers numbered 1, 2, 3,...
int vip;
int waittime;
int servicetime;
public:
// constructors
Event(void);
Event(int t,EventType et,int v,int cn,int tn,
int wt,int st);
// methods to retrieve private data
int GetTime(void) const;
EventType GetEventType(void) const;
int GetVip(void) const;
int GetCustomerID(void) const;
int GetTellerID(void) const;
int GetWaitTime(void) const;
int GetServiceTime(void) const;
void SetTime(int ntime);
void SetWaitTime(int ntime);
};
到达事件,主要关注time(该事件发生时间),customerID(此事件是哪号顾客的到达事件),vip(这是我自己设的flag, 标志此顾客是否为VIP)
此时,servicetime, waittime, tellerID 都不清楚,也不必关心
离开事件,主要通过计算servicetime,waittime来确定事件的time成员变量,计算过程是一个模拟重点。
// delete an element from the priority queue and return its value
DataType PQueue::PQDelete(void)
{
DataType min;
int i, minindex = 0;
if (count > 0)
{
// find the minimum value and its index in pqlist
min = pqlist[0]; // assume pqlist[0] is the minimum
// visit remaining elments, updating minimum and index
for (i = 1; i < count; i++){
if (pqlist[i] < min)
{
// new minimum is pqlist[i]. new minindex is i
min = pqlist[i];
minindex = i;
}
}
// move rear element to minindex and decrement count
pqlist[minindex] = pqlist[count-1];
count--;
}
// qlist is empty, terminate the program
else
{
cerr << "Deleting from an empty priority queue!" << endl;
exit(1);
}
// return minimum value
return min;
}
优先级队列的入队与普通的队列并无区别,区别就发生在出队的过程。银行实验中,出队的优先级以event类的成员变量time为关键码进行比较,time小的先出队,相等的以先进先出原则出队。
优先级队列的含义:事件发生的时间先后。
特别注意!!!这里一定要理解到:优先级队列反映的是事件发生的时间先后顺序。出队的比较关键码与VIP标志无直接关联。如果要实现VIP优先出队,我们所需要实现的,是想办法将VIP的departure类型事件的成员time进行缩减。
我们的优先出队一定是忠实依据成员变量time
e = pq.PQDelete();
// handle an arrival event
if (e.GetEventType() == arrival)
{
// compute time for next arrival.
nexttime = e.GetTime() + NextArrivalTime();
if (nexttime > simulationLength)
// process events but don't generate any more
continue;
else
{
// generate arrival for next customer. put in queue
nextCustomer++;
newevent = Event(nexttime, arrival,nextVip,
nextCustomer, 0, 0, 0);
pq.PQInsert(newevent);
}
cout << "Time: " << setw(2) << e.GetTime()
<< " " << "arrival of customer "
<< e.GetCustomerID()<<endl;
// generate departure event for current customer
// time the customer takes
servicetime = GetServiceTime();
// teller who services customer
tellerID = NextAvailableTeller();
if(e.GetVip())
{
tstat[tellerID].finishService-=pq.MinusTime(tellerID,e.GetTime());
pq.ChangeTime(tellerID,e.GetTime(),servicetime);
}
// if teller free, update sign to current time
if (tstat[tellerID].finishService == 0)
tstat[tellerID].finishService = e.GetTime();
// compute time customer waits by subtracting the
// current time from time on the teller's sign
waittime = tstat[tellerID].finishService -
e.GetTime();
// update teller statistics
if(e.GetVip())
{
tstat[tellerID].totalCustomerWait += (waittime+servicetime*pq.Many(tellerID,e.GetTime()));
tstat[tellerID].totalCustomerCount++;
tstat[tellerID].totalService += servicetime;
tstat[tellerID].finishService += servicetime;
}
else
{
tstat[tellerID].totalCustomerWait += waittime;
tstat[tellerID].totalCustomerCount++;
tstat[tellerID].totalService += servicetime;
tstat[tellerID].finishService += servicetime;
}
// create a departure object and put in the queue
if(waittime>serviceHigh*2)
{
if(e.GetVip())
{
tstat[tellerID].totalCustomerWait -= (waittime+servicetime*pq.Many(tellerID,e.GetTime()));
tstat[tellerID].totalCustomerCount--;
tstat[tellerID].totalService -= servicetime;
tstat[tellerID].finishService -= servicetime;
}
else
{
tstat[tellerID].totalCustomerWait -= waittime;
tstat[tellerID].totalCustomerCount--;
tstat[tellerID].totalService -= servicetime;
tstat[tellerID].finishService -= servicetime;
}
newevent = Event(e.GetTime(),
departure,e.GetVip(),e.GetCustomerID(),0,
0,0);
if(e.GetVip())
{
pq.ChangeTime(tellerID,e.GetTime(),servicetime*(-1));
tstat[tellerID].finishService+=pq.MinusTime(tellerID,e.GetTime());
}
lose++;
}
else{
if(e.GetVip())
{
newevent = Event(tstat[tellerID].finishService,
departure,e.GetVip(),e.GetCustomerID(),tellerID,
waittime,servicetime);
tstat[tellerID].finishService+=pq.MinusTime(tellerID,e.GetTime());
}
else
newevent = Event(tstat[tellerID].finishService,
departure,e.GetVip(),e.GetCustomerID(),tellerID,
waittime,servicetime);
}
pq.PQInsert(newevent);
}
如果此时出队的是一个到达事件,那么产生一个新的到达事件(发生时间为晚于此发生事件,早于模拟结束时间的随机时间),入队。
同时要生成出队的到达事件所对应的离开事件。如何生成离开事件:
- 通过函数选取等待时间最短的窗口,确定TellerID.
- 通过该窗口类的finishservice成员,确定waittime.
- 通过随机函数计算servicetime,确定servicetime
- 根据waittime 以及servicetime,确定time
// handle a departure event
else
{
cout << "Time: " << setw(2) << e.GetTime()
<< " " << "departure of customer "
<< e.GetCustomerID() <<" "<<"the vip id: "<<e.GetVip()<<endl;
cout << " Teller " << e.GetTellerID()
<< " Wait " << e.GetWaitTime()
<< " Service " << e.GetServiceTime()
<< endl;
tellerID = e.GetTellerID();
// if nobody waiting for teller, mark teller free
if (tellerID!=0 && e.GetTime() == tstat[tellerID].finishService)
tstat[tellerID].finishService = 0;
}
出队事件为departure类型就简单多啦对不对!只要打印相应的信息,就可以在模拟输出中看到该顾客对应的等待时间和接受服务的时间啦!
唯一要注意的点就是如果此时是teller的最后一位顾客,把finishService置零,意味着后续算顾客能够接受服务的时间将是顾客的到达时间【就是说一到达就可以接受服务啦,不用排队】!
长篇大论预警
event类中加入成员变量vip //0表示普通顾客,1表示vip顾客
event类中加入成员函数int GetVip() //用于获取顾客vip状态
Simulation类中将原先的numTellers改为 numTellers_common 与numTellers_vip //设置普通窗口和vip窗口
约定:前 n u m T e l l e r s _ v i p numTellers\_vip numTellers_vip个为VIP窗口,后面为普通窗口
Simulation类中加入成员函数int NextAvailableTeller_vip(void) //用于vip顾客选择窗口
Simulation类的构造函数和成员函数RunSimulation中加入变量NextVip //计算下一个客户的vip状态(0 or 1)
更改 v o i d S i m u l a t i o n : : P r i n t S i m u l a t i o n R e s u l t s ( v o i d ) void Simulation::PrintSimulationResults(void) voidSimulation::PrintSimulationResults(void)
event类中加入成员函数 v o i d s e t T i m e ( ) void\ \ setTime() void setTime()以及 v o i d s e t W a i t T i m e ( ) void\ setWaitTime() void setWaitTime()//用于vip插队时更改后面顾客的离开时间
PQueue类中加入成员函数ChangeTime() //用于更改相应条件顾客的等待时间
PQueue类加入成员函数MinusTime()//用于解决vip顾客插队时Service Finish的更改
PQueue类加入成员函数Many()//用于计算vip插队的时候前面有几个人
Simulation类加入成员变量 int lose//用于计算流失客户(发现等待时间过长,直接离开)
①函数 i n t N e x t A c a i l a b l e T e l l e r _ v i p ( v o i d ) int NextAcailableTeller\_vip(void) intNextAcailableTeller_vip(void)
int Simulation::NextAvailableTeller_vip(void)
{
// initially assume all tellers finish at closing time
int minfinish = simulationLength;
// assign random teller to customer who arrives
// before closing but obtains service after closing
int minfinishindex = 1;
int average;
double sum;
for(int i=numTellers_vip+1;i<=numTellers_common+numTellers_vip;i++)
{
sum+=tstat[i].finishService;
}
average=sum/numTellers_common;
// find teller who is free first
for (int i = 1; i <= numTellers_vip; i++)
if (tstat[i].finishService < minfinish)
{
minfinish = tstat[i].finishService;
minfinishindex = i;
}
if(minfinish>average)
{
for(int i=numTellers_vip+1;i<=numTellers_common+numTellers_vip;i++)
{
if(tstat[i].finishService<minfinish)
{
minfinishindex=i;
minfinish = tstat[i].finishService;
}
}
}
else;
return minfinishindex;
}
②变量 N e x t V i p NextVip NextVip
#include
srand(time(NULL));
int nextVip=(rand()%100-1)>20?0:1;//用随机数生成vip状态,三元表达式可以调整vip顾客和普通顾客的比例
③对窗口选择计算的修改以及生成相应departure事件板块修改
if(e.GetVip()==1)
{
tellerID = NextAvailableTeller_vip();
}
else
{
tellerID = NextAvailableTeller();
}
cout<<" "<<"teller: "<<tellerID<<" the vip id: "<<e.GetVip()<<endl;
// if teller free, update sign to current time
if (tstat[tellerID].finishService == 0)
tstat[tellerID].finishService = e.GetTime();
// compute time customer waits by subtracting the
// current time from time on the teller's sign
if(e.GetVip())
{
if(tellerID>=numTellers_vip+1)
{
tstat[tellerID].finishService-=pq.MinusTime(tellerID,e.GetTime());
pq.ChangeTime(tellerID,e.GetTime(),servicetime);
}
}
waittime = tstat[tellerID].finishService-e.GetTime();
// update teller statistics
if(e.GetVip() && tellerID>=numTellers_vip+1)
{
tstat[tellerID].totalCustomerWait +=(waittime+servicetime*pq.Many(tellerID,e.GetTime()));
tstat[tellerID].totalCustomerCount++;
tstat[tellerID].totalService += servicetime;
tstat[tellerID].finishService += servicetime;
}
else
{
tstat[tellerID].totalCustomerWait += waittime;
tstat[tellerID].totalCustomerCount++;
tstat[tellerID].totalService += servicetime;
tstat[tellerID].finishService += servicetime;
}
// create a departure object and put in the queue
if(waittime>serviceHigh*2)
{
if(e.GetVip() && tellerID>=numTellers_vip+1)
{
tstat[tellerID].totalCustomerWait -=(waittime+servicetime*pq.Many(tellerID,e.GetTime()));
tstat[tellerID].totalCustomerCount--;
tstat[tellerID].totalService -= servicetime;
tstat[tellerID].finishService -= servicetime;
}
else
{
tstat[tellerID].totalCustomerWait -= waittime;
tstat[tellerID].totalCustomerCount--;
tstat[tellerID].totalService -= servicetime;
tstat[tellerID].finishService -= servicetime;
}
newevent = Event(e.GetTime(),
departure,e.GetVip(),e.GetCustomerID(),0,
0,0);
if(e.GetVip() && tellerID>=numTellers_vip+1)
{
pq.ChangeTime(tellerID,e.GetTime(),servicetime*(-1));
tstat[tellerID].finishService+=pq.MinusTime(tellerID,e.GetTime());
}
lose++;
}
else{
if(e.GetVip() && tellerID>=numTellers_vip+1)
{
newevent = Event(tstat[tellerID].finishService,
departure,e.GetVip(),e.GetCustomerID(),tellerID,
waittime,servicetime);
tstat[tellerID].finishService+=pq.MinusTime(tellerID,e.GetTime());
}
else
newevent = Event(tstat[tellerID].finishService,
departure,e.GetVip(),e.GetCustomerID(),tellerID,
waittime,servicetime);
}
pq.PQInsert(newevent);
}
④函数 v o i d S i m u l a t i o n : : P r i n t S i m u l a t i o n R e s u l t s ( v o i d ) void Simulation::PrintSimulationResults(void) voidSimulation::PrintSimulationResults(void)
void Simulation::PrintSimulationResults(void)
{
int cumCustomers = 0, cumWait = 0, i;
int avgCustWait, tellerWorkPercent;
int avgVipWait,vipWait=0,vipCustomers=0;
float tellerWork;
for (i = numTellers_vip+1; i <= numTellers_common+numTellers_vip; i++)
{
cumCustomers += tstat[i].totalCustomerCount;
cumWait += tstat[i].totalCustomerWait;
}
for (i = 1; i <= numTellers_vip; i++)
{
vipCustomers += tstat[i].totalCustomerCount;
vipWait += tstat[i].totalCustomerWait;
}
//evaluate the waittime of common window and vip window respectivly
cout << endl;
cout << "******** Simulation Summary ********" << endl;
cout << "Simulation of " << simulationLength
<< " minutes" << endl;
cout << " No. of common Customers: " << (cumCustomers) << endl;
cout << " No. of vip Customers: " << (vipCustomers) << endl;
cout << " No. of Customers: " << (vipCustomers+cumCustomers) << endl;
cout << " Average Customer Window Wait: ";
avgCustWait = int(float(cumWait)/cumCustomers + 0.5);
avgVipWait =(vipCustomers!=0)?int(float(vipWait)/vipCustomers + 0.5):0;
cout << avgCustWait << " minutes" << endl;
cout << " Average Vip Window Wait: ";
cout << avgVipWait << " minutes" << endl;
for(i=1;i <= numTellers_common+numTellers_vip;i++)
{
cout << " Teller #" << i << " % Working: ";
// display percent rounded to nearest integer value
tellerWork = float(tstat[i].totalService)/simulationLength;
tellerWorkPercent =int( tellerWork * 100.0 + 0.5);
cout << tellerWorkPercent << endl;
}
}
⑤ 函数 S e t T i m e & S e t W a i t T i m e SetTime\ \ \&\ \ SetWaitTime SetTime & SetWaitTime
void SetTime(int ntime);
void SetWaitTime(int ntime);
void Event::SetTime(int ntime)
{
time+=ntime;
}
void Event::SetWaitTime(int ntime)
{
waittime+=ntime;
}
⑥函数 C h a n g e T i m e ChangeTime ChangeTime
void PQueue::ChangeTime(int ID,int arrive,int wait)
{
int i=0;
for(i=0;i<count;i++)
{
if(pqlist[i].GetTellerID()==ID)
{
if(pqlist[i].GetEventType()==departure)
{
if(!(pqlist[i].GetVip()) && (pqlist[i].GetTime()-pqlist[i].GetServiceTime())>arrive && (pqlist[i].GetTime()-pqlist[i].GetServiceTime()-pqlist[i].GetWaitTime())<arrive)
{
pqlist[i].SetTime(wait);
pqlist[i].SetWaitTime(wait);
}
}
}
}
}
⑦函数MinusTime
int PQueue::MinusTime(int tellerID,int arrive)
{
int i;
int minus=0;
for(i=0;i<count;i++)
{
if(pqlist[i].GetEventType()==departure && pqlist[i].GetTellerID()==tellerID)
{
if(!(pqlist[i].GetVip()) && (pqlist[i].GetTime()-pqlist[i].GetServiceTime())>arrive && (pqlist[i].GetTime()-pqlist[i].GetServiceTime()-pqlist[i].GetWaitTime())<arrive)
{
minus+=(pqlist[i].GetServiceTime());
}
}
}
return minus;
}
⑧函数 i n t P Q u e u e : : M a n y ( i n t I D , i n t a r r i v e ) int\ PQueue::Many(int\ ID,int\ arrive) int PQueue::Many(int ID,int arrive)
int PQueue::Many(int ID,int a)
{
int i;
int count=0;
for(i=0;i<count;i++)
{
if(pqlist[i].GetEventType()==departure && pqlist[i].GetTellerID()==ID)
{
if(!(pqlist[i].GetVip()) && (pqlist[i].GetTime()-pqlist[i].GetServiceTime())>a && (pqlist[i].GetTime()-pqlist[i].GetServiceTime()-pqlist[i].GetWaitTime())<a)
{
count+=1;
}
}
}
return count;
}
重要的插队功能通过暂时更改窗口FinishService, 生成该vip的对应离开事件后,再恢复减去的那一段时间来实现(参考下图),具体代码见上述第3条代码块。
在银行设立vip窗口和普通窗口,VIP窗口只有vip可以排队,但也必须遵循先来者先接受服务的原则。 v i p vip vip顾客进行窗口选择时,如果发现vip窗口人员过多,甚至超出了普通窗口等待时间平均值,可以选择普通窗口插队接受服务。但是请注意vip窗口另外开设和从中间划分vip窗口不同
讨论 { 另 外 增 加 v i p 窗 口 可 以 减 少 等 待 时 间 , 且 让 大 家 都 开 心 从 原 有 窗 口 划 分 v i p 窗 口 人 流 量 少 , 时 间 差 距 不 明 显 ; 人 流 量 大 , 客 户 等 待 时 间 明 显 上 升 , 容 易 造 成 用 户 流 失 \begin{cases}另外增加vip窗口\ \ \ \ \ \ \ \ 可以减少等待时间,且让大家都开心\\从原有窗口划分vip窗口\ \ \ \ \ \ \ 人流量少,时间差距不明显;人流量大,客户等待时间明显上升,容易造成用户流失\end{cases} {另外增加vip窗口 可以减少等待时间,且让大家都开心从原有窗口划分vip窗口 人流量少,时间差距不明显;人流量大,客户等待时间明显上升,容易造成用户流失
选择窗口通过函数 i n t N e x t A c a i l a b l e T e l l e r _ v i p ( v o i d ) int\ NextAcailableTeller\_vip(void) int NextAcailableTeller_vip(void)实现,vip客户插队思想通过以下代码实现
// when vip comes to the common window, they can jump into the queue
if(e.GetVip())
{
if(tellerID>=numTellers_vip+1)
{
tstat[tellerID].finishService-=pq.MinusTime(tellerID,e.GetTime());
pq.ChangeTime(tellerID,e.GetTime(),servicetime);
}
}
当vip插队时,finishService直接变成在他到达之前正在接受服务的人的结束时间,并把他后面人的时间进行增加,其中如果有多个vip插队时,按照先来后到,不改变之前到达的vip的相应信息
此外,我们还对系统设定了功能:如果等待时间超过服务时间上限的n倍,则顾客选择离开(注意,已经排队但因被插队而等待时间超过的不会离开),记录流失的顾客lose
if(waittime>serviceHigh*2)
{
tstat[tellerID].totalCustomerWait -= waittime;
tstat[tellerID].totalCustomerCount--;
tstat[tellerID].totalService -= servicetime;
tstat[tellerID].finishService -= servicetime;
newevent = Event(e.GetTime(),
departure,e.GetVip(),e.GetCustomerID(),0,
0,0);
if(e.GetVip() && tellerID>=numTellers_vip+1)
{
pq.ChangeTime(tellerID,e.GetTime(),servicetime*(-1));
tstat[tellerID].finishService+=pq.MinusTime(tellerID,e.GetTime());
}
lose++;
}
通过下面的案例可以看出,4号窗口的16号普通顾客被22号vip顾客插队,vip插队功能实现。
因为没办法逐条语句debug(巨有耐心和时间的小伙伴也是可以一试,不过我瞄一眼那些个代码想想调试就有一点绝望)
我推荐的方法是:使用极端的初始情况参数,在打印出来的结果中仔细分析出现的不科学不合理问题,思考可能出错的环节,优化自己的代码
错误类型 | 可能原因 |
---|---|
等待时间出现负数 | 在操作FinishService时出现问题;改顾客等待时间时不小心加上了VIP顾客的等待时间 |
窗口服务率高于100% | totalService的操作出现问题 |
VIP插不了队 | emmm大概就是哪哪儿都可能有问题 |
时间线混乱 | ServiceTime, 顾客离开时间没调等 |
Tips: 这些问题是在我们做实验中碰到的,因为时间有些久了,当时没有立刻记录下来具体的解决方法(有的错误真的是自己太傻了),所以只能给出一些可能的问题原因,具体的还是得自己仔细分析啦。
这个银行,时间战线拉的非常久,为什么呢?问就是自己过于自信以及测试的初始值设置的还不够极端。
第一次,根本就是整个就不对(没改动顾客的任何东西,也没改FinishService,反而是改了优先级队列的出队),但因为没有具体的模拟答案,就以为自己写的贼好。结果在听同学讲自己思路的时候顿悟自己写了一堆垃圾,遂回去重构代码
第二次,思路终于整明白了,开始着手改上文中提到的那些药改的数据。但是因为各种各样的疏漏,以及本人通常是在改错中逐渐完善思路的,这个阶段中我主要关注到了负数的等待时间问题(此时虽然窗口工作率已经超过了100%,但我不以为然)
第三次,已经接近ddl了,我的室友给我再一次提出了工作率的问题(赵赵,我的超人!!)我们仔细分析了工作率计算的代码,发现根本不可能超过100%啊!好是我的问题,经过一段紧张的抢救工作,好像是哪里的totalService算错了(我真忘了,好几个月前的实验了),终于解决!至此我的银行才算能开业
果然老话说的好啊:当你的代码能跑出点什么东西的时候,就不要再碰它了