PAT1026 TableTennis

PAT终于刷完了,很后悔没有一边刷一边写博客,希望以后空闲的时候能慢慢补齐。这道题是我完成的最后一道题,也是我最不擅长的模拟题,卡了许久,最后终于理清了逻辑,AC掉。

其实模拟题最重要的是处理逻辑,只要逻辑理顺,代码实现自然不是问题。

完成这道题后,我也搜了许多其他人的代码,发现讲思路的很少,或者我觉得不够直观,所以在此将自己的思路记录下来,一方面备忘,一方面希望帮助更多的人。

先分析题意,总结要点如下:

1.一共有N张桌子,编号1到N,其中部分为VIP桌。

2.玩家分为普通玩家和VIP玩家。

3.这类模拟题需要抽象出一个队列,按到达先后顺序进行入队,然后根据当前队列中玩家的身份,可以分情况讨论:

(1)当前有VIP桌空余且队列中有VIP玩家在等待,优先处理VIP玩家,处理规则是为VIP玩家分配最小VIP桌;

(2)当前没有VIP桌空余,则按照排队顺序依次处理,先到先得,且先得的桌子编号是空余桌中最小的编号。(即此时VIP的身份并没有显示出来,VIP身份只有在有VIP桌空余的时候才发挥作用)

(3)还有一种情况,当前有VIP桌空余,但是队伍中并没有VIP玩家,这时也可以按照先到者得到最小编号桌的原则分配桌子,即使这个编号最小的桌子是VIP桌。(没办法,谁让现在没有VIP大佬来打球呢,总不能浪费资源吧)

4.还有一些小细节:

(1)每个玩家玩的时间不能超过120分钟,超过的按120分钟处理
(2)对于在关门之前还没有排上桌的玩家不作处理

考虑完以上几点后,就应该分析程序中应该怎么处理这个问题,主要是需要考虑:
1.存储哪些信息
2.使用什么数据结构

很显然,我们需要一个结构体,保存每个玩家的到达时间(arriveTime),玩耍时间(playTime),是否是VIP;
另外,玩家在分配球桌时会产生一些信息,比如玩家是何时分配到桌子(即开始玩的时间,startTime),什么时候结束玩耍把桌子空出来(endTime),在哪个桌子玩的(serveTable),另外我们还可以顺便记录下该玩家等待了多久,即startTime减去arriveTime,最后需要输出这个信息,因此为了记录以上信息,我们定义如下结构体player

struct player
{
    int arriveTime;
    int waitTime;
    int playTime;
    int startTime;
    int endTime;
    int serveTable;
    int VIP;
    player(int arrive, int play, int vip) :arriveTime(arrive), playTime(play), VIP(vip), startTime(-1){}
};

为了保存所有玩家的信息,由于排队需要按照时间先后顺序,且所有球员都是在8点到21点之间到达,时间精确到秒,并且没有同一秒到达的人,因此可以直接定义一个以时间为索引的结构体数组保存所有玩家信息(将8点设为基准下标0,每一秒占一个元素位置),此处我定义的是指针数组,先初始化为NULL,后面读取每个时间点的玩家信息时再更新,使用时只需要判断某个元素是否为NULL,即可知道该时间点有无玩家到来。

按照时间点存储信息并且更新状态是队列模拟题的核心!!!因为按照时间点处理时,我们每次只要关注一个点的状态变化,这样考虑问题就更加简单明了,能将一个连续的复杂问题分解为每个点的瞬时状态处理,这种思考方式很重要!!不容易混乱!

vector<player*> players;
players.resize(14 * 60 * 60, NULL);

另外我们还需要定义一个数据结构保存球桌信息(是否是VIP桌),因为球桌编号是1-N,因此可以用数组保存,当然也可以用map来保存,此处我用的是map,将普通球桌编号映射为0,VIP球桌编号映射为1,便于后面查询

map<int, int> tableKind;

由于每次都分配最小编号的球桌,不论是普通球桌还是VIP球桌,所以为了每次都能查询到最小值,自然想到了优先队列,但是由于C++默认的大顶堆,所以需要自己写一个比较函数,改为小顶堆;并且普通球桌和VIP球桌分开存储:

struct TableCmp{
    bool operator()(const int i1, const int i2)const{
        return i1 > i2;
    }
};
priority_queue<int, vector<int>, TableCmp> commonTablePQ;
priority_queue<int, vector<int>, TableCmp> vipTablePQ;

前面已经提及,我们是按照时间点从小到达依次增长的方式处理问题,因此我们需要知道在某个时间点,是否有玩家刚刚打完球,把桌子空出来,如果有,就得把空出来的桌子加入队列,供后面的玩家选用,如何知道某个时间点是否有人打完呢,很简单,由于我们的时间点是逐渐增长的,并且player结构体中已经存储了endTime,只要当前的时间点等于所有在玩的玩家中endTime最小的,那么这个endTime最小的玩家就得结束玩耍,归还桌子了。所以我们得知道已经得到桌子的玩家他们结束的最早时间,即(endTime最小),又是一个获取最值的问题,自然用到优先队列:

struct QueueCmp{
    bool operator()(const player *p1, const player *p2)const{
        return p1->endTime > p2->endTime;
    }
};

priority_queue<player*, vector<player*>, QueueCmp> inSerivePQ;

最后,我们需要两个队列来模拟当前的排队状况,而且我们还需要知道当前队列中有没有VIP玩家,所以需要两个队列存储,一个队列1存储所有在排队的玩家,一个队列2 存储在所有在排队的玩家中的VIP玩家,很显然,队列1属于队列2 ,这样存储的目的是为了在有VIP桌空余时,检查是否有VIP玩家在排队,如果队列2非空,则说明有VIP,那就赶紧的把VIP桌分配给VIP玩家,需要注意的是,分配给VIP玩家后,除了需要把这名VIP从队列2的队首移除,还需要将其从队列1中移除,同样如果没有VIP桌,只有普通桌,当按照先来先得的顺序VIP玩家分配到了普通桌,那同样需要将这名VIP玩家从队列1的队首移除,并且从队列2中移除,为了方便移除操作,在这里使用双端队列存储:

deque<player*> currentQueue;
deque<player*> currentVipQueue;

最后需要一个数组记录每个桌子被使用的次数,初始化为0,在分配桌子时更新相应编号桌子的次数即可;

vector<int> tableUsed;

以上就是需要的数据结构,总之处理逻辑就是:

  1. 从8点扫描到21点,以每一秒为间隔,首先检查是否有桌子空出,有的话记录空出的桌子是什么类型,普通就加入普通桌优先队列,VIP就加入VIP优先队列;
  2. 然后看看当前时间点有没有玩家到来,有的话就加入队列1,如果是VIP,就也加入队列2;
  3. 然后看看是否有VIP在排队以及是否有VIP桌空余,两者都满足,就为VIP分配VIP桌;
  4. 最后再按先来后到顺序处理排队的队列,为队首玩家分配最小编号桌,直到没有桌子或者队列中没人;

完整代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#include <vector>
#include <string>
#include <cstring>
using namespace std;

struct player
{
    int arriveTime;
    int waitTime;
    int playTime;
    int startTime;
    int endTime;
    int serveTable;
    int VIP;
    player(int arrive, int play, int vip) :arriveTime(arrive), playTime(play), VIP(vip), startTime(-1){}
};

bool cmp(const player* p1, const player* p2){

    return p1->startTime < p2->startTime;

}//输出答案的比较函数,按照开始打球的时间排序

struct QueueCmp{
    bool operator()(const player *p1, const player *p2)const{
        return p1->endTime > p2->endTime;
    }
};

struct TableCmp{
    bool operator()(const int i1, const int i2)const{
        return i1 > i2;
    }
};

int timeIndex(string s){
    int h = stoi(s.substr(0, 2));
    int m = stoi(s.substr(3, 5));
    int ss = stoi(s.substr(6));
    int time = h * 3600 + m * 60 + ss;
    return time;
}//将时间转化为秒数

void printTime(int t){
    int hh = t / 3600;
    int mm = (t - 3600 * hh) / 60;
    int ss = t - 3600 * hh - 60 * mm;
    printf("%02d:%02d:%02d", hh, mm, ss);
}//将秒数转化为时间打印


int main(){
    //freopen("D:\\C++\\in.txt", "r", stdin);
    vector<player*> players;//所有玩家
    players.resize(13* 60 * 60, NULL);
    deque<player*> currentQueue;//当前队列
    deque<player*> currentVipQueue;//当前队列中的VIP
    priority_queue<player*, vector<player*>, QueueCmp> inSerivePQ;//正在玩的玩家
    priority_queue<int, vector<int>, TableCmp> commonTablePQ;//当前普通桌
    priority_queue<int, vector<int>, TableCmp> vipTablePQ;//当前VIP桌
    map<int, int> tableKind;
    int N;
    scanf("%d", &N);
    for (int i = 0; i < N; i++){
        char s[9];
        int playTime, vipTag;
        scanf("%s%d%d", &s, &playTime, &vipTag);
        string str = s;
        int arrive = timeIndex(s);
        if (playTime>120)
            playTime = 120;
        players[arrive - 8 * 3600] = new player(arrive, playTime, vipTag);
    }
    int K, M;
    scanf("%d%d", &K, &M);
    for (int i = 1; i <= K; i++){
        tableKind[i] = 0;
    }

    for (int i = 0; i < M; i++){
        int t;
        scanf("%d", &t);
        tableKind[t] = 1;
    }//记录VIP桌
    for (int i = 1; i <= K; i++){
        if (tableKind[i]){
            vipTablePQ.push(i);
        }
        else{
            commonTablePQ.push(i);
        }
    }//普通桌和VIP桌分别存储到各自的优先队列,便于取最小
    vector<int> tableUsed;
    tableUsed.resize(K + 1, 0);
    for (int i = 0; i < 13 * 3600; i++){//按照时间点处理
        int currenTime = i + 8 * 3600;//因为存储时将8点设为0基准,所以转换为具体时间需要加上8点的秒数
        while (!inSerivePQ.empty() && inSerivePQ.top()->endTime == currenTime){
            player* temp = inSerivePQ.top();
            inSerivePQ.pop();
            int curTab = temp->serveTable;
            if (tableKind[curTab]){
                vipTablePQ.push(curTab);
            }
            else{
                commonTablePQ.push(curTab);
            }
        }//看看当前时间点有没有桌子空余出
        if (players[i] != NULL){
            currentQueue.push_back(players[i]);
            if (players[i]->VIP == 1){
                currentVipQueue.push_back(players[i]);
            }
        }//看看当前时间点有没有玩家到来,有的话,先让其入队,VIP也要进入VIP队伍

        while (!currentVipQueue.empty() && !vipTablePQ.empty()){//当前有VIP桌并且有VIP在排队
            player* curPlayer = currentVipQueue.front();
            currentVipQueue.pop_front();
            int curT = vipTablePQ.top();
            curPlayer->startTime = currenTime;
            curPlayer->serveTable = curT;
            curPlayer->waitTime = currenTime - curPlayer->arriveTime;
            curPlayer->endTime = currenTime + 60 * curPlayer->playTime;
            inSerivePQ.push(curPlayer);
            tableUsed[curT]++;
            vipTablePQ.pop();//分配桌子,更新信息
            deque<player*>::iterator itr = currentQueue.begin();
            for (; itr != currentQueue.end(); itr++){
                if (*itr == curPlayer){
                    currentQueue.erase(itr);
                    break;
                }
            }//把这个VIP从队伍中移除
        }

        while (!currentQueue.empty()){//vip处理完后,看看剩下的队伍
            player* curPlayer = currentQueue.front();
            int commonT = INT_MAX;
            int vipT = INT_MAX;//因为当前可能还有VIP桌子没用完,但是需要分配最小编号的桌子,所以需要都拿出来比较一下
            int curT = -1;
            if (!commonTablePQ.empty()){
                commonT = commonTablePQ.top();
            }
            if (!vipTablePQ.empty()){
                vipT = vipTablePQ.top();
            }
            curT = min(commonT, vipT);
            if (curT == INT_MAX)
                break;//等于最大值,说明没有桌子了,直接跳出循环
            if (curT == commonT){
                commonTablePQ.pop();
            }//从普通桌拿出来的最小,所以更新普通桌队列,选了这个桌子就得把它拿出来pop掉
            else{
                vipTablePQ.pop();
            }//vip桌同理


            curPlayer->startTime = currenTime;
            curPlayer->serveTable = curT;
            curPlayer->waitTime = currenTime - curPlayer->arriveTime;
            curPlayer->endTime = currenTime + 60 * curPlayer->playTime;
            inSerivePQ.push(curPlayer);
            tableUsed[curT]++;
            currentQueue.pop_front();//更新各种信息
            if (curPlayer->VIP){//此处值得注意,如果这个人是VIP玩家,但是前面没有分配到VIP桌,在此处分配到了桌子,同样将其从VIP队列中移除,总之就是得到了桌子后就不能在相应的队伍中再存在下去了。
                deque<player*>::iterator itr = currentVipQueue.begin();
                for (; itr != currentVipQueue.end(); itr++){
                    if (*itr == curPlayer){
                        currentVipQueue.erase(itr);
                        break;
                    }
                }
            }
        }
    }

//后面就是所有的信息都更新后,记录和输出答案了
    vector<player*> pans;
    for (int i = 0; i< 13*3600; i++){
        if (players[i] && players[i]->startTime != -1){
            pans.push_back(players[i]);
        }
    }
    sort(pans.begin(), pans.end(), cmp);
    for (int i = 0; i < pans.size(); i++){

        printTime(pans[i]->arriveTime);
        printf(" ");
        printTime(pans[i]->startTime);
        int w = pans[i]->waitTime / 60;
        if (pans[i]->waitTime - 60 * w >= 30){
            w++;
        }
        printf(" %d\n", w);

    }
    for (int i = 1; i <= K; i++){
        if (i == 1)
            printf("%d", tableUsed[i]);
        else{
            printf(" %d", tableUsed[i]);
        }
    }
    return 0;
}

以上就是我的思考和分析过程。
题目不难,没有高超的算法,就是逻辑相对较为复杂,或许在实际工作中还是这种情况比较多吧。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值