帖一个排队模拟问题的可行答案

跑马溜溜的山上 2018-07-02 12:49:40
前面某个帖子讲到一个排队模拟问题,觉得很适合作为作业,于是布置下去。题干稍微修改:
题目:要求模拟一次超市排队结帐过程。每个客户购买了数量不等的东西,导致他们的结帐时间不同。
1、共有5条队列,客户在各个队列排队等候。
2、新到来的客户,挑选距离自己较近的最短队列入队。
3、客户具有耐心属性。一旦在队列里等待太久(超过M秒),则会触发张望,每K秒一次。
4、进入张望状态的用户,一旦发现有更短的队列,随即更换队列。
不要求图形化界面,但要清晰的输出用户的事件和队列的状态。另外,要求模拟5小时内的活动,耗时小于100毫秒。

这个题目的难点在于学生对时间轴的模拟。大部分同学选择了细粒度的时间轴滚动,比如1秒/次向后推移。这种做法,模拟真实度比较高,但是很容易超时。最终,有一位同学的代码脱颖而出,做的不错。他使用的是最近事件时刻驱动,整个程序篇幅控制的很好,性能也不错。在处理概率方面,使用了正态分布,充分把课程学到的知识用活了。

先看代码:

#include <iostream>
#include <list>
#include <algorithm>
#include <random>
#include <cmath>
#include <memory>
#include <functional>
using namespace std;
/**
* @brief normdist_range 正态分布辅助函数
* @param mean 均值
* @param var 方差
* @return 随机数
*/
double normdist_range(const double mean, const double var)
{
static default_random_engine eg; //引擎
normal_distribution<double> n(mean, var); //均值, 方差
double v = n(eg);
if (v < 10 ) v = 10;
if (v > 3600 *4 ) v = 3600 * 4;
return v;
}
/**
* @brief The guest class 是客户类
* 每个客户结帐所需时间不同,性格不同。
* 时间单位为秒
*/
class cGuest{
public:
enum EStatus{
GE_NULL = 0,//未入队
GE_WAIT = 1,//排队
GE_DEAL = 2//正在结帐
};
public:
//构造函数随机的生成这个客户的性格
cGuest():
m_payment_cost(normdist_range(m_payment_cost_mean,m_payment_cost_var)),
m_patient(normdist_range(m_patient_mean,m_patient_var)),
m_check(normdist_range(m_check_mean,m_check_var)),
m_id(++m_guests)
{ }
void enqueue(shared_ptr<cGuest> gu,
list<shared_ptr<cGuest>> & qu,
const double currtime)
{
qu.push_back(gu);
auto it = qu.end();
--it;
m_current_queue = &qu;
m_current_pointer = it;
m_current_order = qu.size()-1;
if (m_current_order)
{
m_status = GE_WAIT;
m_next_evt_time = currtime + m_patient;
cout<<currtime<<":"<<id()<<" start wait for "<<m_patient<<
" seconds until "<< m_next_evt_time << endl;
}
else
{
m_status = GE_DEAL;
m_next_evt_time = currtime + m_payment_cost;
cout<<currtime<<":"<<id()<<" start pay for "<< m_payment_cost<<" seconds until "<< m_next_evt_time << endl;
}
}
void wait_again(const double currtime)
{
m_next_evt_time = currtime + m_check;
cout<<currtime<<":"<<id()<<" will continue wait for "<< m_check<<" seconds until "<< m_next_evt_time << endl;
}

void switchqueue(list<shared_ptr<cGuest>> & qu, const double currtime)
{
//插入新的
shared_ptr<cGuest> gu = * m_current_pointer;
//删除旧的
for_each(m_current_pointer,m_current_queue->end(),
[](shared_ptr<cGuest> p){
p->set_curr_order(p->curr_order()-1);
}
);
m_current_queue->erase(m_current_pointer);
//插入新的
qu.push_back(gu);
auto it = qu.end();
--it;
m_current_queue = &qu;
m_current_pointer = it;
m_current_order = qu.size()-1;

if (m_current_order)
{
m_next_evt_time = currtime + m_check;
cout<<"!!!"<<currtime<<":"<<id()<<" will wait at new queue for "<< m_check
<<" seconds until "<< m_next_evt_time << endl;
}
else
{
m_status = GE_DEAL;
m_next_evt_time = currtime + m_payment_cost;
cout<<currtime<<":"<<id()<<" start pay for "<< m_payment_cost<<" seconds until "<< m_next_evt_time << endl;
}
}

void dequeue(const double currtime)
{
m_current_queue->pop_front();
if (m_current_queue->size())
{
for_each(m_current_queue->begin(),m_current_queue->end(),
[](shared_ptr<cGuest> p){
p->set_curr_order(p->curr_order()-1);
}
);
(*m_current_queue->begin())->start_pay(currtime);
}
cout<<currtime<<":"<<id()<<" is over "<< endl;
}

void start_pay(const double currtime)
{
m_status = GE_DEAL;
m_next_evt_time = currtime + m_payment_cost;
cout<<currtime<<":"<<id()<<" start pay for "<< m_payment_cost<<" seconds until "<< m_next_evt_time << endl;
}

public:
double next_evt_time() const {return m_next_evt_time;}
double payment_cost() const {return m_payment_cost;}
double patient() const {return m_patient;}
double check_period() const {return m_check;}
int id() const {return m_id;}
EStatus status() const {return m_status;}
int curr_order() const {return m_current_order;}

void set_curr_order(const int o){m_current_order = o;}
//客户性格和参数
private:
double m_next_evt_time = -1; //下一事件时刻
const double m_payment_cost; //本客户结账开销
const double m_patient; //客户耐心,超过这个,会轮寻队列
const double m_check; //轮寻的每隔时间。
const int m_id; //客户ID
EStatus m_status = GE_NULL; //当前状态
private:
list<shared_ptr<cGuest> > * m_current_queue = nullptr; //当前队列
list<shared_ptr<cGuest> >::iterator m_current_pointer ; //当前队列位置
size_t m_current_order = 0;//当前顺序
//静态成员,控制仿真的全局参数
public:
static const double m_payment_cost_mean;//客户结帐开销均值
static const double m_payment_cost_var;//客户结帐开销方差
static const double m_patient_mean;//客户耐心均值
static const double m_patient_var;//客户耐心方差
static const double m_check_mean;//客户超过耐心后的巡查时间均值
static const double m_check_var;//客户超过耐心后的巡查时间方差
static int m_guests; //总客户数
};


//静态成员
const double cGuest::m_payment_cost_mean = 120;
const double cGuest::m_payment_cost_var = 100;
const double cGuest::m_patient_mean = 300;
const double cGuest::m_patient_var = 100;
const double cGuest::m_check_mean = 60;
const double cGuest::m_check_var = 10;
int cGuest::m_guests = 0;


int main()
{
const int total_queues = 5;
//新客户到来的概率
const double enq_mean = 120/6;
const double enq_var = 40/5;
//时间轴
double timeline = 0;
const double simu_max = 3600;//1小时模拟最大
//队列
list<shared_ptr<cGuest> > queue [total_queues];
//Output function
auto output_queue = [&]()->void{
cout<<timeline<<": queue status = ";
for (int i=0;i<total_queues;++i)
cout<<queue[i].size()<<" ";
cout<<endl;
};

//下一个新客户到来的时刻
double next_new_guest_time = timeline + normdist_range(enq_mean,enq_var);

//开始仿真
while (timeline<simu_max)
{
//遍历所有队列,找到最近的事件时刻、最新的对象
shared_ptr<cGuest> nextg(nullptr);
for (int i = 0;i < total_queues; ++i)
{
if (queue[i].size()>0)
{
shared_ptr<cGuest> maxg = *min_element(
queue[i].begin(),
queue[i].end(),
[](const shared_ptr<cGuest> &g1,
const shared_ptr<cGuest> &g2)->bool{
return g1->next_evt_time()<g2->next_evt_time();
});
if (nextg.get()==nullptr ||
nextg->next_evt_time() > maxg->next_evt_time() )
nextg = maxg;
}
}

//最后和下一客户到来的时刻比较,处理下一客户入队
if (nextg.get()==nullptr ||
next_new_guest_time < nextg->next_evt_time())
{
//更新时刻
timeline = next_new_guest_time;
next_new_guest_time = timeline + normdist_range(enq_mean,enq_var);

shared_ptr<cGuest> newg(new cGuest());
//位置
const int minpos = rand() % total_queues;
int enqpos = -1;
size_t minvol = 0x7fffffff;
for (int i = 0; i< total_queues; ++i)
{
if (minvol >= queue[i].size())
{
if (minvol > queue[i].size() ||
abs(minpos - i) < abs(enqpos - i) )
enqpos = i;
minvol = queue[i].size();
}
}
//入队
newg->enqueue(newg,queue[enqpos],timeline);
output_queue();
}
else
//处理事件nextg
{
timeline = nextg->next_evt_time();
switch (nextg->status())
{
case cGuest::GE_WAIT:
//如果是等待状态,说明耐心没有了,则查看队列,比较和自己当前位置
{
const int currod = nextg->curr_order();
//位置
int enqpos = -1;
size_t minvol = currod;//当前位置
for (int i = 0; i< total_queues; ++i)
{
if (minvol > queue[i].size())
{
if (minvol > queue[i].size())
enqpos = i;
minvol = queue[i].size();
}
}
//是否交换
if (enqpos>=0)
{
nextg->switchqueue(queue[enqpos],timeline);
output_queue();
}
else
nextg->wait_again(timeline);
}
break;
case cGuest::GE_DEAL:
//如果是结帐,说明结帐结束了
{
nextg->dequeue(timeline);
output_queue();
}
break;
default:
break;
}
}
}
return 0;
}



输出:


19.0243:1 start pay for 123.327 seconds until 142.351
19.0243:0 0 0 1 0
44.4986:2 start pay for 222.096 seconds until 266.595
44.4986:0 1 0 1 0
81.496:3 start pay for 42.726 seconds until 124.222
81.496:0 1 1 1 0
94.0093:4 start pay for 112.626 seconds until 206.635
94.0093:1 1 1 1 0
112.532:5 start pay for 241.152 seconds until 353.684
......
3439.56:5 5 3 5 6
3447.37:158 will continue wait for 73.7181 seconds until 3521.09
3450.36:160 will continue wait for 55.1252 seconds until 3505.48
!!!3450.36:162 will wait at new queue for 51.0903 seconds until 3501.45
3450.36:5 5 4 4 6
3451.7:172 start wait for 155.534 seconds until 3607.23
3451.7:5 5 5 4 6
3457.66:152 will continue wait for 62.8057 seconds until 3520.46
3458.3:155 will continue wait for 57.8758 seconds until 3516.18
3459.59:154 will continue wait for 56.3001 seconds until 3515.89
3463.66:158 start pay for 142.682 seconds until 3606.35
3463.66:151 is over
......


在助教的指导下,该学生基本对STL库的内容达到活学活用的水平。有没有更为精确的模拟呢?准备在后面数学建模竞赛辅导课上寻找更好的答案。

实际上,排队问题是很复杂的。随遍瞎想,就有很多因素。
1、客户对队列的感知与性格有关。有人只关心长度,有人关心前进的速度。这两类人的概率分布如何?
2、在队列长度过长时,客户无法准确数清除每个队列的人数。有的队列长,但客户数目少,有的反之。如何建立模型评估队列的疏密?
3、5个收银员的熟练程度不同。

有兴趣的同学可以试试看!
...全文
253 2 打赏 收藏 转发到动态 举报
写回复
用AI写文章
2 条回复
切换为时间正序
请发表友善的回复…
发表回复
  • 打赏
  • 举报
回复
控制台也可以这样画,太棒了。还以为只能在Linux下这样呢!
赵4老师 2018-07-02
  • 打赏
  • 举报
回复
仅供参考,以实现图示化队列:
#include <windows.h>
#include <stdio.h>

void ConPrint(char *CharBuffer, int len);
void ConPrintAt(int x, int y, char *CharBuffer, int len);
void gotoXY(int x, int y);
void ClearConsole(void);
void ClearConsoleToColors(int ForgC, int BackC);
void SetColorAndBackground(int ForgC, int BackC);
void SetColor(int ForgC);
void HideTheCursor(void);
void ShowTheCursor(void);

int main(int argc, char* argv[])
{
HideTheCursor();
ClearConsoleToColors(15, 1);
ClearConsole();
gotoXY(1, 1);
SetColor(14);
printf("This is a test...\n");
Sleep(5000);
ShowTheCursor();
SetColorAndBackground(15, 12);
ConPrint("This is also a test...\n", 23);
SetColorAndBackground(1, 7);
ConPrintAt(22, 15, "This is also a test...\n", 23);
gotoXY(0, 24);
SetColorAndBackground(7, 1);
return 0;
}

//This will clear the console while setting the forground and
//background colors.
void ClearConsoleToColors(int ForgC, int BackC)
{
WORD wColor = ((BackC & 0x0F) << 4) + (ForgC & 0x0F);
//Get the handle to the current output buffer...
HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
//This is used to reset the carat/cursor to the top left.
COORD coord = {0, 0};
//A return value... indicating how many chars were written
//not used but we need to capture this since it will be
//written anyway (passing NULL causes an access violation).
DWORD count;

//This is a structure containing all of the console info
// it is used here to find the size of the console.
CONSOLE_SCREEN_BUFFER_INFO csbi;
//Here we will set the current color
SetConsoleTextAttribute(hStdOut, wColor);
if(GetConsoleScreenBufferInfo(hStdOut, &csbi))
{
//This fills the buffer with a given character (in this case 32=space).
FillConsoleOutputCharacter(hStdOut, (TCHAR) 32, csbi.dwSize.X * csbi.dwSize.Y, coord, &count);

FillConsoleOutputAttribute(hStdOut, csbi.wAttributes, csbi.dwSize.X * csbi.dwSize.Y, coord, &count);
//This will set our cursor position for the next print statement.
SetConsoleCursorPosition(hStdOut, coord);
}
}

//This will clear the console.
void ClearConsole()
{
//Get the handle to the current output buffer...
HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
//This is used to reset the carat/cursor to the top left.
COORD coord = {0, 0};
//A return value... indicating how many chars were written
// not used but we need to capture this since it will be
// written anyway (passing NULL causes an access violation).
DWORD count;
//This is a structure containing all of the console info
// it is used here to find the size of the console.
CONSOLE_SCREEN_BUFFER_INFO csbi;
//Here we will set the current color
if(GetConsoleScreenBufferInfo(hStdOut, &csbi))
{
//This fills the buffer with a given character (in this case 32=space).
FillConsoleOutputCharacter(hStdOut, (TCHAR) 32, csbi.dwSize.X * csbi.dwSize.Y, coord, &count);
FillConsoleOutputAttribute(hStdOut, csbi.wAttributes, csbi.dwSize.X * csbi.dwSize.Y, coord, &count);
//This will set our cursor position for the next print statement.
SetConsoleCursorPosition(hStdOut, coord);
}
}

//This will set the position of the cursor
void gotoXY(int x, int y)
{
//Initialize the coordinates
COORD coord = {x, y};
//Set the position
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);
}

//This will set the forground color for printing in a console window.
void SetColor(int ForgC)
{
WORD wColor;
//We will need this handle to get the current background attribute
HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO csbi;

//We use csbi for the wAttributes word.
if(GetConsoleScreenBufferInfo(hStdOut, &csbi))
{
//Mask out all but the background attribute, and add in the forgournd color
wColor = (csbi.wAttributes & 0xF0) + (ForgC & 0x0F);
SetConsoleTextAttribute(hStdOut, wColor);
}
}

//This will set the forground and background color for printing in a console window.
void SetColorAndBackground(int ForgC, int BackC)
{
WORD wColor = ((BackC & 0x0F) << 4) + (ForgC & 0x0F);;
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), wColor);
}

//Direct console output
void ConPrint(char *CharBuffer, int len)
{
DWORD count;
WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), CharBuffer, len, &count, NULL);
}

//Direct Console output at a particular coordinate.
void ConPrintAt(int x, int y, char *CharBuffer, int len)
{
DWORD count;
COORD coord = {x, y};
HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(hStdOut, coord);
WriteConsole(hStdOut, CharBuffer, len, &count, NULL);
}

//Hides the console cursor
void HideTheCursor()
{
CONSOLE_CURSOR_INFO cciCursor;
HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);

if(GetConsoleCursorInfo(hStdOut, &cciCursor))
{
cciCursor.bVisible = FALSE;
SetConsoleCursorInfo(hStdOut, &cciCursor);
}
}

//Shows the console cursor
void ShowTheCursor()
{
CONSOLE_CURSOR_INFO cciCursor;
HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);

if(GetConsoleCursorInfo(hStdOut, &cciCursor))
{
cciCursor.bVisible = TRUE;
SetConsoleCursorInfo(hStdOut, &cciCursor);
}
}

64,683

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧