[离散事件模拟] 银行窗口模拟 - C语言

数据结构与算法 同时被 2 个专栏收录
52 篇文章 10 订阅

这种抽象问题的思维蛮巧妙的,看完一定会有不小的收获!

问题

编制一个程序以模拟银行窗口接待客户的排队业务活动,并计算一天中客户在银行的逗留的平均时间

  1. 每个窗口在某个时刻只能接待一个客户
  2. 窗口空闲,则可上前办理业务
  3. 窗口均被占,则新客户便会排在人数少的队伍前面

【运行结果】采用离散事件模拟,输入30多运行几次,可以好好体会这种离散事件模拟的思想

  • 这里营业时间选小一点,一共营业30分钟
  • 用户来的间隔时间nextTime,nextTime∈[0,5]
  • 银行一个业务的办理时间段durtime,durtime∈[1,15]

在这里插入图片描述

思考

【问题1】为了对最终的编程结果有一个感性的认识,我们要搞清楚:客户在银行逗留的平均时间meanWaitTime和什么有关?
【答】银行办理一件业务的平均时间、窗口总数、用户间隔到达的时间等
【例如】

  • ①银行办理一件业务需要1min-15min②银行办理一件业务需要1min-30min,那么后者的meanWaitTime会比较长 --> 所以我们这里假定用户办理业务的时间是1min-15min
  • ①两个用户间隔0-5min的时间到达②两个用户相隔5-10min到达 --> 后者的间隔时间大,自然meanWaitTime比较小 --> 所以我们这里假设两个用户相隔时间为0-5min

【问题2】如何来模拟这个问题?
【答】容易想到的是,根据时间主线来刻画这个问题

  1. 设置一个currentTime表示当前的时间,根据currentTime来处理每一个用户,然后算的meanWaitTime
  2. 但这种抽象方法有接问题
    • 其一:currentTime的最小单元设置为多少?是秒?分?时?一般是设置成分钟(业务处理的最小单元是min),但是从早上7:00遍历到下午6:00,一分钟一分钟的推移,这个遍历的规模也是很大的
    • 其二:每到下一分钟,都要去遍历用户表,去判断这个点是不是有用户来
    • 其三:每到下一分钟,都要去遍历每个窗口当前的事件,去判断这个点是不是有用户办理完事情了
    • 其四:事先我们需要知道今天要来多少用户,然后随机生成他们的到达时间、处理业务的时间。虽然我们事先可以通过随机值来确定今天的用户量,但这种方法有它的局限性

【问题3】以客户为主线模拟这个问题,行不行?
【答】

  • 问题2中分析了以时间为主线有很多弊端,但以客户为主线行吗?确实,用户正是一个一个来到的店里,线性的抽象方法,有助于我们单线程编程的实现,值得考虑
  • 但仔细考虑,客户的行为分为“刚到店里”、“办理业务”和“离开银行”,三种事件,所以用户不是最小原子单元,它还可以继续再分,可以分成好几个事件,单线程也要分成3个部分。所以本篇介绍了以事件为主线,来模拟这个问题。

离散事件模拟

【背景】上述谈到以时间为主线来模拟这个问题,虽说思维清晰简单,但时间复杂度大,而且需要事先知道有多少个用户量

【较好的解决方案】离散事件模拟:以事件为主线,作为最小单元来处理。
把用户到达、等待、离开看成一个事件,而且这个时间在时间维度上是离散分布的

  1. 如何生成用户?
    【答】以时间为主线的方法是事先把所有用户都确定下来。而离散事件模拟的思路是:每到一个用户,就新增下一个用户,预计他所到达的时间
    • 而现实中,也是如此,用户是一个接着一个来到的店里,不考虑一起跨入店门(只要不一起跨入店门,最小划分单元为秒时,都可以抽象成用户是一个接着一个来的),这也是离散事件模拟的好处之一
    • 基于上一点,假设银行客户间隔来店的时间time,time∈[0min, 5min]
  2. 离散事件是以什么为主线的?
    【答】主线是事件,创建一个事件的列表eventList,按照事件event的发生时间来进行从小到大排序,并从小到大来处理事件

【数据结构】

  1. 客户:customer
    • 客户到达时间:arrivalTime
    • 办理业务所需时间:duration
      用伪随机数生成该用户的到达时间、办业务的所需的持续时间
  2. 事件:event
    • 该事件的类型:type
      • type=0:预计有下一个用户到达
      • type=value:表示有一个用户正在窗口value上办理业务
        例子:type=1:表示有一个用户正在窗口1上办理业务
    • 该事件发生的时刻:occurTime
  3. 事件链表:eventList
    • 把当前所有事件都串起来
  4. 窗口数组:windows[窗口数]
    • 每个窗口是一个队列
    • 队列的元素时一个用户customer
  5. 总时间totalTime
  6. 客户数customerNum

流程图

在这里插入图片描述
在这里插入图片描述

主要逻辑

[主要逻辑] 函数main()

int totalTime=0, customerNum=0; //累计客户逗留时间,客户数
EventList eventList; //事件表
Event event; //事件
CustomerQueue windows[WINDOWS_NUM+1]; //窗口,从1开始存储
Customer customer; //客户记录
int main() {
	int closeTime; //关门时间

	srand(time(NULL)); //设置随机种子,注意:一定要在main函数里,不能放在Random()里,否则无效
	
	printf("输入营业的总分钟数:\n>>> ");
	scanf("%d", &closeTime);
	BankSimulation(closeTime);

	return 0;
}
// 银行模拟
void BankSimulation(int closeTime) {
	OpenForDay(); //开门,初始化工作
	while ( !ListEmpty(eventList) ) {
		// 事件队列还有事件没有处理完
		DelFirst(&eventList, &event); //取出第一个事件,并删除
		if (event.type==0) //预计有新用户达到
			CustomerArriving(closeTime); //生成这个用户几点来
		else //用户正在办理业务
			CustomerDeparture(closeTime); //用户离开事件
	}
	CloseForDay(); //关门,计算总结果
}

[预计用户到达] 函数CustomerArriving()

// 预测用户到达事件
void CustomerArriving(int closeTime) {
	long durtime, intertime;
	int minWindow;
	
	customerNum++; //客户量+1
	printf("\t预测第%d客户", customerNum);

	// 创建用户
	durtime = rand()%15 +1; //一个业务的时间在1-15分钟
	intertime = rand()%6; //用户间隔0-5分钟来一个
	customer.id = customerNum; // 这是今天第几个用户了
	customer.arrivalTime = event.occurTime + intertime; //到达时间
	customer.duration = durtime;						//客户办事的持续时间
	if ( customer.arrivalTime >= closeTime ) { //用户来的时候已经关门了

		printf("\t× 生成的下一个用户将在第%dmin到达,那时候已经关门了\n", customer.arrivalTime);

	} else { //用户来的时候还没有关门
		
		// 插入最短队
		minWindow = GetMin(windows);						//得到人数最少的队列
		EnQueue(&windows[minWindow], customer);				//插入最短的队伍
		printf("将在第%dmin到达,办理业务需要%dmin,到窗口%d排队", customer.arrivalTime, customer.duration, minWindow);
		
		// 插入离开事件
		event.occurTime = customer.arrivalTime + durtime; //预计离开时间
		printf(",预计离开时间%dmin\n", event.occurTime);
		event.type = minWindow; //窗口
		if ( QueueLength(windows[minWindow]) ==1 ) //当前队伍只有他一个人
			OrderInsert(&eventList, event); //插入离开事件,让这个人离开
		
		// 预计下一个用户的到达
		event.occurTime = customer.arrivalTime; //创建下一个用户到达的事件
		event.type = 0;
		if ( event.occurTime < closeTime ) //如果预计时间已经关门了,就退出
			OrderInsert(&eventList, event);

	}
}


[用户离开逻辑] 函数CustomerDeparture()

// 事件处理完成,用户离开
void CustomerDeparture(int closeTime) {
	int type; //窗口号
	QNode *p;
	QElemType qe;

	type = event.type; //窗口号
	DelQueue(&windows[type], &customer); //得到出队的用户
	printf("窗口%d %d号用户离开 ", type, customer.id);
	if (event.occurTime > closeTime) { //用户办理业务时已经关门了
		printf("× 他是%dmin到达的,预计办理业务所花费的时间为%dmin,预计离开时间是%dmin。但排到队时已经是%d了,只好改天再来\n", customer.arrivalTime, customer.duration, customer.arrivalTime+customer.duration ,event.occurTime);
		customerNum--; //去掉这个用户
		return ;
	} else {
		printf("√ 离开时间%d(即当前时钟的时间)\n", event.occurTime);
		totalTime += event.occurTime - customer.arrivalTime; //客户等待的时间=当前时间-客户到达店里的时间
		if ( QueueLength(windows[type]) ) {
			// 当前窗口还有人
			
			// 开始处理下一位
			p = windows[type].front->next;
			qe = p->data;
			customer.arrivalTime = qe.arrivalTime; //开始时间
			customer.duration = qe.duration;       //持续时间
			event.occurTime = event.occurTime + customer.duration; //事件的发生时间
			event.type = type;	//type号窗口开始处理该用户
			OrderInsert(&eventList, event); //插入到事件链表中等待离队
		}
	}
}

总结:离散事件模拟思想的本质

  1. 以上逻辑其实是有模拟时间的,但你也许搞不明白,并没有设置一个currentTime变量存储当前的时间点
  2. 没错,确实是没有。但是我们有一个全局变量event事件,我们模拟的是事件的处理,当前处理的时间event,它所发生的时间event.occurTime就是目前时间啊!
  3. 所以,离散事件的模拟实质就是
    • 随机出很多个事件eventList,然后按时间发生event.occurTime的先后来处理这些事件。处理每个时间event时,event.occurTime就是当前的时间点。其时间的步长正是一个事件!具体分析看下图右侧文字

在这里插入图片描述

完整代码

#include<stdio.h>
#include<stdlib.h>
#include<time.h>

#define MAX 10000
#define WINDOWS_NUM 4 //银行的窗口数

// 链表类型:有头的单链表

/****** 事件 ******/
typedef struct{
	long occurTime; //事件发生时刻
	int type;	    //事件的类型
		// type = 0:预计会有下一个用户到达
		// type != 0:为其他值时,表示该用户在type窗口已经正在办理业务(type=1,表示该用户在窗口1正在处理业务)
}LElemType;
typedef LElemType Event; //事件
typedef struct LNode{
	Event data;
	struct LNode *next;
}LNode, *LinkList; 
typedef LinkList EventList; //事件链表类型(有序链表)

/****** 客户 ******/
typedef struct{
	long arrivalTime; //到达时间
	long duration;    //办理业务所需时间
	int id; //用户的id
}QElemType;
typedef QElemType Customer; //客户

/****** 客户队列 ******/ 
typedef struct QNode{
	QElemType data;
	struct QNode *next;
}QNode, *QueuePtr;
typedef struct{
	QueuePtr front; //队头
	QueuePtr rear;  //队尾
}LinkQueue;
typedef LinkQueue CustomerQueue; //客户队列

void BankSimulation(int closeTime); //银行模拟
void OpenForDay();			//开店
void CloseForDay();			//关店
int GetMin(LinkQueue q[]);  //得到人最少的窗口
void CustomerArriving(int closeTime); //预计用户到来
void CustomerDeparture(int closeTime); //用户离开

/***** 链表操作 ******/
void InitList(LinkList *pL);
int ListEmpty(LinkList L);
void OrderInsert(LinkList *pL, LElemType en);
void DelFirst(LinkList *pL, LElemType *e);


/***** 队列操作 ******/
void InitQueue(LinkQueue *Q);
int DelQueue(LinkQueue *pQ, QElemType *e);
int EnQueue(LinkQueue *pQ, QElemType e);
int QueueLength(LinkQueue Q);


int totalTime=0, customerNum=0; //累计客户逗留时间,客户数
EventList eventList; //事件表
Event event; //事件
CustomerQueue windows[WINDOWS_NUM+1]; //窗口,从1开始存储
Customer customer; //客户记录
int main() {
	int closeTime; //关门时间

	srand(time(NULL)); //设置随机种子,注意:一定要在main函数里,不能放在Random()里,否则无效
	
	printf("输入营业的总分钟数:\n>>> ");
	scanf("%d", &closeTime);
	BankSimulation(closeTime);

	return 0;
}

// 银行模拟
void BankSimulation(int closeTime) {
	OpenForDay(); //开门
	while ( !ListEmpty(eventList) ) {
		// 事件队列还有事件没有处理完
		DelFirst(&eventList, &event); //取出第一个事件,并删除
		if (event.type==0) //预计有新用户达到
			CustomerArriving(closeTime); //生成这个用户几点来
		else //用户正在办理业务
			CustomerDeparture(closeTime); //用户离开事件
	}
	CloseForDay();
}

// 银行开门:初始化
void OpenForDay() {
	int i;
	totalTime = 0;  //总时间
	customerNum = 0; //客户数

	InitList(&eventList); //初始化事件列表
	//银行一开门,就预计有下一个用户到来
	event.occurTime = 0;
	event.type = 0;
	OrderInsert(&eventList, event); //插入到事件列表
	for (i=1; i<=WINDOWS_NUM; i++) {
		InitQueue(&windows[i]); //初始化银行窗口
	}
	printf("\n△ start 预计下一个用户会来(生成下一个用户到达的事件)\n");
}

void CloseForDay() {
	printf("\n△ 客户数=%ld,累计客户逗留时间%ld,平均逗留时间%ld\n", customerNum, totalTime, totalTime/customerNum);
}

// 预测用户到达事件
void CustomerArriving(int closeTime) {
	long durtime, intertime;
	int minWindow;
	
	customerNum++; //客户量+1
	printf("\t预测第%d客户", customerNum);

	// 创建用户
	durtime = rand()%15 +1; //一个业务的时间在1-15分钟
	intertime = rand()%6; //用户间隔0-5分钟来一个
	customer.id = customerNum; // 这是今天第几个用户了
	customer.arrivalTime = event.occurTime + intertime; //到达时间
	customer.duration = durtime;						//客户办事的持续时间
	if ( customer.arrivalTime >= closeTime ) { //用户来的时候已经关门了

		printf("\t× 生成的下一个用户将在第%dmin到达,那时候已经关门了\n", customer.arrivalTime);

	} else { //用户来的时候还没有关门
		
		// 插入最短队
		minWindow = GetMin(windows);						//得到人数最少的队列
		EnQueue(&windows[minWindow], customer);				//插入最短的队伍
		printf("将在第%dmin到达,办理业务需要%dmin,到窗口%d排队", customer.arrivalTime, customer.duration, minWindow);
		
		// 插入离开事件
		event.occurTime = customer.arrivalTime + durtime; //预计离开时间
		printf(",预计离开时间%dmin\n", event.occurTime);
		event.type = minWindow; //窗口
		if ( QueueLength(windows[minWindow]) ==1 ) //当前队伍只有他一个人
			OrderInsert(&eventList, event); //插入离开事件,让这个人离开
		
		// 预计下一个用户的到达
		event.occurTime = customer.arrivalTime; //创建下一个用户到达的事件
		event.type = 0;
		if ( event.occurTime < closeTime ) //如果预计时间已经关门了,就退出
			OrderInsert(&eventList, event);

	}
}

// 事件处理完成,用户离开
void CustomerDeparture(int closeTime) {
	int type; //窗口号
	QNode *p;
	QElemType qe;

	type = event.type; //窗口号
	DelQueue(&windows[type], &customer); //得到出队的用户
	printf("窗口%d %d号用户离开 ", type, customer.id);
	if (event.occurTime > closeTime) { //用户办理业务时已经关门了
		printf("× 他是%dmin到达的,预计办理业务所花费的时间为%dmin,预计离开时间是%dmin。但排到队时已经是%d了,只好改天再来\n", customer.arrivalTime, customer.duration, customer.arrivalTime+customer.duration ,event.occurTime);
		customerNum--; //去掉这个用户
		return ;
	} else {
		printf("√ 离开时间%d(即当前时钟的时间)\n", event.occurTime);
		totalTime += event.occurTime - customer.arrivalTime; //客户等待的时间=当前时间-客户到达店里的时间
		if ( QueueLength(windows[type]) ) {
			// 当前窗口还有人
			
			// 开始处理下一位
			p = windows[type].front->next;
			qe = p->data;
			customer.arrivalTime = qe.arrivalTime; //开始时间
			customer.duration = qe.duration;       //持续时间
			event.occurTime = event.occurTime + customer.duration; //事件的发生时间
			event.type = type;	//type号窗口开始处理该用户
			OrderInsert(&eventList, event); //插入到事件链表中等待离队
		}
	}
}

// 得到人数最少的队列
int GetMin(LinkQueue q[]) {
	int i,k,min;
	int cnt;
	QNode *p;

	min = MAX;
	for (i=1; i<=WINDOWS_NUM; i++) {
		if ( q[i].front == q[i].rear ) { // 该窗口没有人
			cnt = 0; //窗口人数=0
		} else { //该窗口有人
			// 计算目前窗口的人数
			for (cnt=1,p=q[i].front->next; p!=q[i].rear; p=p->next) { 
				cnt++;
			}
		}
		if (min>cnt) {
			min = cnt;
			k = i;
		}
	}
	return k;
}

/***** 链表操作 ******/
// 有头结点的单链表
void InitList(LinkList *pL) { //链表初始化
	*pL = (LNode *)malloc(sizeof(LNode));
	if (!*pL) exit(0);
	(*pL)->next = NULL;
}

int ListEmpty(LinkList L) {
	return L->next==NULL ? 1 : 0; //L的下一个为空 ? 是空 : 不空
}

void OrderInsert(LinkList *pL, LElemType en) { //按occurTime从小到大的顺序插入
	LNode *p, *q, *s;

	for (p=*pL,q=p->next; q && q->data.occurTime<en.occurTime; p=q,q=p->next) ; //找到插入位置
	s = (LNode *)malloc(sizeof(LNode)); if (!s) exit(0);
	s->data.type = en.type; s->data.occurTime = en.occurTime;
	p->next = s;
	s->next = q;
}

void DelFirst(LinkList *pL, LElemType *e) {
	LNode *p;
	p = (*pL)->next;
	(*pL)->next = p->next;
	e->occurTime = p->data.occurTime;
	e->type = p->data.type;
	free(p);
}


/***** 队列操作 ******/
// 有头结点的单链表
void InitQueue(LinkQueue *Q) {
	Q->front = Q->rear = (QNode *)malloc(sizeof(QNode));
	Q->front->next = NULL;
}

int DelQueue(LinkQueue *pQ, QElemType *e) {
	QNode *p;
	if ( (*pQ).front == (*pQ).rear ) return 0; //空
	p = (*pQ).front->next;
	e->arrivalTime = p->data.arrivalTime;
	e->duration = p->data.duration;
	e->id = p->data.id;
	(*pQ).front->next = p->next;
	if ( (*pQ).rear == p ) //删除一个后,队列变空了
		(*pQ).rear = (*pQ).front;
	free(p);
	return 1;
}

int EnQueue(LinkQueue *pQ, QElemType e) {
	QNode *p;
	p = (QNode *)malloc(sizeof(QNode)); if (!p) exit(0);
	p->data.arrivalTime = e.arrivalTime;
	p->data.duration = e.duration;
	p->data.id = e.id;
	p->next = NULL;
	(*pQ).rear->next = p;
	(*pQ).rear = p;
	return 1;
}

int QueueLength(LinkQueue Q) {
	int cnt=0;
	QNode *p;
	if ( Q.front==Q.rear ) cnt=0;
	else {
		for ( p=Q.front; p!=Q.rear; p=p->next ) cnt++;
	}
	return cnt;
}
  • 21
    点赞
  • 0
    评论
  • 57
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
自己亲手做出来的东西。 银行业务模拟 【问题描述】客户业务分为两种,第一种是申请从银行得到一笔资金,即取款或者借款。第二种是向银行投入一笔资金,即存款或者还款。银行有两个服务窗口,相应的有两个队列。客户到达银行后先排第一个队。处理每个客户业务时,如果属于第一种,且申请额超出银行现存资金总额而得不到满足,则立即排入第二个队等候,直至满足时才离开银行;否则业务处理完后立即离开银行。每接待完一个第二种业务的客户,则顺序检查和处理(如果可能)第二个队列中的客户,对能满足的申请者予以满足,不能满足的者重新排到第二个队列的队尾。 注意:在此检查过程中,一旦银行的资金总额少于或者等于刚才第一个队列中最后一个客户(第二种业务)被接待之前的数额,或者本次已将第二个队列检查或处理了一遍,就停止检查(因为此时已不可能还有能满足者)转而继续接待第一个队列客户。任何时刻都只开一个窗口。假设检查不需要时间。营业时间结束时所有客户立即离开银行。 【基本要求】利用动态存储结构实现模拟。 【测试数据】一天营业开始时银行拥有的款额为10000(元),营业时间为600(分钟)。其他模拟参量自定,注意测定两种极端的情况:一是两个到达事件之间的间隔时间很短,而客户的交易时间很长,另一个恰好相反,设置两个到达事件的间隔时间很长,而客户的交易时间很短。 【实现提示】事件有两类:到达银行的和离开银行。初始时银行现存资金总额为total。开始营业后的第一个事件是客户到达,营业时间从0到closetime。到达事件发生时随机地设置此客户的交易时间和距下一到达事件之间的时间间隔。每一个客户要办理的款额也是随机确定的,用负值和正值分别表示第一类和第二类业务。变量total,closetime以及上述两个随机量的上下界均交互地从终端读入,作为模拟参数。 两个队列和一个事件表均要用动态存储结构实现。注意弄清应该在什么条件下设置离开事件,以及第二个队列用怎么样的存储结构实现时可以获得较高的效率。注意:事件表是按时间顺序有序的。
©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值