A-咕咕东的目录管理器
题目描述
咕咕东的雪梨电脑的操作系统在上个月受到宇宙射线的影响,时不时发生故障,他受不了了,想要写一个高效易用零bug的操作系统 —— 这工程量太大了,所以他定了一个小目标,从实现一个目录管理器
开始。前些日子,东东的电脑终于因为过度收到宇宙射线的影响而宕机,无法写代码。他的好友TT正忙着在B站看猫片,另一位好友瑞神正忙着打守望先锋。现在只有你能帮助东东!
初始时,咕咕东的硬盘是空的,命令行的当前目录为根目录 root。
目录管理器可以理解为要维护一棵有根树结构,每个目录的儿子必须保持字典序。
现在咕咕东可以在命令行下执行以下表格中描述的命令:
时空限制
Time limit 6000 ms
Memory limit 1048576 kB
Sample
Input
1
22
MKDIR dira
CD dirb
CD dira
MKDIR a
MKDIR b
MKDIR c
CD …
MKDIR dirb
CD dirb
MKDIR x
CD …
MKDIR dirc
CD dirc
MKDIR y
CD …
SZ
LS
TREE
RM dira
TREE
UNDO
TREE
Output:
OK
ERR
OK
OK
OK
OK
OK
OK
OK
OK
OK
OK
OK
OK
OK
9
dira
dirb
dirc
root
dira
a
b
c
dirb
x
dirc
y
OK
root
dirb
x
dirc
y
OK
root
dira
a
b
c
dirb
x
dirc
y
解题思路
- 准备工作
- 将文件目录看成一棵树,对每个文件夹/文件看成树上的一个节点。建立节点的结构体Directory。
包含节点名称(即文件夹名),子节点名称映射,父节点编号,子树大小sz(包括自己),pre,bck用于root输出,更新标签flag。 - 对每个命令建立命令的结构体Command,包括命令名,命令类型,文件名(若命令类型为“操作”的话)。
- 我们如何标识一个文件呢?——每个文件/文件夹有唯一的节点编号,建立Directory数组node,node[i]中存放每个节点的信息。
- 命令执行
- now存贮当前目录编号,cnt表示总的文件编号
- “MKDIR s”:在当前目录下新建子目录s,即增加子节点——用install_node先创建子节点,并将其加到当前节点的子节点映射mp中,同时更新子节点所有祖先直到root的sz
- “RM s”:删除当前目录下的子目录s,涉及两个问题:1.在当前节点的子节点映射mp中删除该子节点 2.更新该子节点所有祖先直到root的sz
- “CD s”:若cmd的str为“…”,直接返回父节点目录,now=node[now].par;否则,查找名称为cmd.str的子节点编号,更新now
- “SZ” :对当前目录,输出节点的sz,在O(1)的复杂度下直接查询当前目录大小!
- “LS”:输出“直接子目录名”,因此使用当前节点的子节点映射mp即可,这里要注意当其子节点数>10时,迭代器反向遍历的写法
- “TREE”:如果节点个数大于10的时候,需要整个子树的前序遍历结果。而当每次都遍历一遍的话,树的最大规模5000,一次前序遍历计算量为5000,TREE命令最多有1e5-5000≈1e5条,显然会TLE!!!
1.每个节点中的pre,bck向量存储他们的前序遍历结果(当后代目录数+1>10时,pre只存储前序遍历的前五个子节点名,bck只存储后五个,当<10时,bck与pre相同),每次遍历子代后节点flag置1
2.flag标记该节点是否在上次遍历后被更新过(每次"MKDIR"和"RM"操作都会使经过的祖宗节点的flag置0),若未被更新过,那么下次需要输出时直接按要求输出即可,不需再次遍历对pre和bck更新
3.这里注意到节点数远少于TREE的操作数,而且说不定还有重复询问,对目录相同期间询问过的相同问题,理应只进行一次遍历过程 - “UNDO”:我们用了一个向量vector<pair<string,pair<int,int>>> v记录命令的执行次序,string表示命令,pair的first为该命令下的当前目录now,second为命令中所指明文件s(新建、删除或转到)对应节点的编号。pop出最近一次成功执行的命令,判断该命令的类型并更新数据,其中"MKDIR"用"RM"实现"UNDO",反之亦然,"CD"直接更改now反向行走即可。一定要注意成功执行的指令
注意
- 多组数据注意初始化now,cnt,v和node[0]为root,root的父节点设置为-1
- cmd为每次命令实例
- "RM"操作只是删除两个节点间的边,而并非完全把子树删掉
总结
本题的关键点:
- 树形结构、动态创建、删除
- 字典序(map恰好在这道题中派上用场,且查找的复杂度为log(n)!)
- 撤销操作“UNDO”
- 输出数据与数量有关
对这道复杂的模拟题,要分别实现这七种命令。可以先分而治之,独立实现各个命令,好的封装可以理清思路。再寻求各个命令之间的联系,不要一上来就写细节,而要先避免细节,假装已经封装好,整体设计。同时要特别注意的是利用时限计算复杂度设计数据结构,例如此题的map和懒更新(记忆化)的技巧。
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<map>
#include<vector>
#include<algorithm>
using namespace std;
const int maxn=1e5+100;
const string cmd_string[]={"MKDIR","RM","CD","SZ","LS","TREE","UNDO"};
struct Command{
string name,str;
int type;
void init(string s) {
name=s;
for(int i=0;i<7;++i) {
if(s==cmd_string[i]) {
type=i;
if(i<3) cin>>str;
break;
}
}
}
}cmd;
//目录
struct Directory{
//名称
string name;
//后代名称->编号映射
map<string,int> mp;
//父亲节点,子树规模
int par,sz;
vector<string> pre,bck;
bool flag;//更新标签
void init(string s,int p) {
flag=0;par=p;
name=s;sz=1;
pre.clear();
bck.clear();
mp.clear();
}
}node[maxn];
int cnt,now;//节点数,当前节点
vector<pair<string,pair<int,int>>> v;//存储顺序
int T,Q;
void install_node(string s,int p) {
node[++cnt].init(s,p);
node[p].mp[s]=cnt;
}
void update(int no,int num) {
while(no!=-1) {
node[no].flag=0;
node[no].sz+=num;
no=node[no].par;
}
}
void mkdir() {//创建
if(node[now].mp.count(cmd.str)) {
cout<<"ERR"<<endl;
return;
}
install_node(cmd.str,now);
update(now,1);
v.push_back({"MKDIR",{now,cnt}});
cout<<"OK"<<endl;
}
void rm() {//删除
if(!node[now].mp.count(cmd.str)) {
cout<<"ERR"<<endl;
return;
}
int x=node[now].mp[cmd.str];
update(now,-node[x].sz);
node[now].mp.erase(node[x].name);
v.push_back({"RM",{now,x}});
cout<<"OK"<<endl;
}
void cd() {//切换
if(cmd.str=="..") {
if(node[now].par==-1) {//根
cout<<"ERR"<<endl;
return;
}
v.push_back({"CD",{now,node[now].par}});
now=node[now].par;
cout<<"OK"<<endl;
return;
}
if(!node[now].mp.count(cmd.str)) {
cout<<"ERR"<<endl;
return;
}
int x=node[now].mp[cmd.str];
v.push_back({"CD",{now,x}});
now=x;
cout<<"OK"<<endl;
}
void sz() {//大小
cout<<node[now].sz<<endl;
}
void ls() {//子目录列表
int t=node[now].mp.size();
if(t==0) {
cout<<"EMPTY"<<endl;
return;
}
auto pos=node[now].mp.begin();
if(t>=1&&t<=10) {
while(pos!=node[now].mp.end()) {
cout<<pos->first<<endl;
pos++;
}
return;
}
if(t>10) {
for(int i=1;i<=5;++i) {
cout<<pos->first<<endl;
pos++;
}
cout<<"..."<<endl;
pos=node[now].mp.end();
//反向遍历
for(int i=1;i<=5;++i) pos--;
for(int i=1;i<=5;++i) {
cout<<pos->first<<endl;
pos++;
}
}
}
void undo() {
if(!v.size()) {
cout<<"ERR"<<endl;
return;
}
auto m=v[v.size()-1];
v.pop_back();
cout<<"OK"<<endl;
int temp=now;
if(m.first=="MKDIR") {
cmd.name="RM";
now=m.second.first;
int x=m.second.second;
cmd.str=node[x].name;
update(now,-node[x].sz);
node[now].mp.erase(node[x].name);
now=temp;
}else if(m.first=="RM") {
//cmd.name="MKDIR";
now=m.second.first;
int x=m.second.second;
//cmd.str=node[x].name;
update(now,node[x].sz);
node[now].mp[node[x].name]=x;
now=temp;
}
else now=m.second.first;
}
void pushdown(int x);
void pretrack(int x) {
node[x].pre.push_back(node[x].name);
if(node[x].sz==1) return;
if(node[x].sz<=10) {
for(auto i:node[x].mp) {
if(!node[i.second].flag) pushdown(i.second);
node[x].pre.insert(node[x].pre.end(),node[i.second].pre.begin(),node[i.second].pre.end());
}
return;
}
int cnt=1;
for(auto i:node[x].mp) {
if(!node[i.second].flag) pushdown(i.second);
for(auto j:node[i.second].pre) {
node[x].pre.push_back(j);
cnt++;
if(cnt>=5) break;
}
if(cnt>=5) break;
}
}
void bcktrack(int x) {
int cnt=0;
auto it=node[x].mp.end();it--;
for(;;--it) {
int u=it->second;
if(!node[u].flag) pushdown(u);
for(int i=node[u].bck.size()-1;i>=0;--i) {
node[x].bck.push_back(node[u].bck[i]);
cnt++;
if(cnt>=5) {
reverse(node[x].bck.begin(),node[x].bck.end());
break;
}
}
if(cnt>=5) break;
if(it==node[x].mp.begin()) break;
}
}
void pushdown(int x){
//构造pre和bak
node[x].pre.clear();
node[x].bck.clear();
pretrack(x);//正向找前几个
if(node[x].sz>10) bcktrack(x);//反向找后几个
else node[x].bck=node[x].pre;
node[x].flag=1;
}
void tree() {
if(!node[now].flag) pushdown(now);
int t=node[now].sz;
if(t==1) cout<<"EMPTY"<<endl;
else if(t>1&&t<=10) {
for(int i=0;i<node[now].pre.size();++i)
cout<<node[now].pre[i]<<endl;
}else {
for(int i=1;i<=5;++i)
cout<<node[now].pre[i-1]<<endl;
cout<<"..."<<endl;
for(int i=5;i>=1;--i)
cout<<node[now].bck[node[now].bck.size()-i]<<endl;
}
}
void init() {
cnt=0;now=0;
v.clear();
node[0].init("root",-1);
}
int main() {
cin>>T;
while(T--) {
cin>>Q;
init();
for(int i=1;i<=Q;++i) {
string s;
cin>>s;
cmd.init(s);
int t=cmd.type;
if(t==0) mkdir();
else if(t==1) rm();
else if(t==2) cd();
else if(t==3) sz();
else if(t==4) ls();
else if(t==5) tree();
else if(t==6) undo();
}
}
return 0;
}
hint:附英文原版题面
B-东东学打牌
题目描述
最近,东东沉迷于打牌。所以他找到 HRZ、ZJM 等人和他一起打牌。由于人数众多,东东稍微修改了亿下游戏规则:
- 所有扑克牌只按数字来算大小,忽略花色。
- 每张扑克牌的大小由一个值表示。A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J,Q, K 分别指代 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13。
- 每个玩家抽得 5张扑克牌,组成一手牌!(每种扑克牌的张数是无限的,你不用担心,东东家里有无数副扑克牌)
理所当然地,一手牌是有不同类型,并且有大小之分的。
举个栗子,现在东东的 “一手牌”(记为 α),瑞神的 “一手牌”(记为 β),要么 α > β,要么 α < β,要么 α = β。
那么这两个 “一手牌”,如何进行比较大小呢?首先对于不同类型的一手牌,其值的大小即下面的标号;对于同类型的一手牌,根据组成这手牌的 5 张牌不同,其值不同。下面依次列举了这手牌的形成规则:
- 大牌:这手牌不符合下面任一个形成规则。如果 α 和 β 都是大牌,那么定义它们的大小为组成这手牌的 5 张牌的大小总和。
- 对子:5 张牌中有 2 张牌的值相等。如果 α 和 β 都是对子,比较这个 “对子” 的大小,如果 α 和 β 的"对子"大小相等,那么比较剩下 3 张牌的总和。
- 两对:5 张牌中有两个不同的对子。如果 α 和β都是两对,先比较双方较大的那个对子,如果相等,再比较双方较小的那个对子,如果还相等,只能比较 5 张牌中的最后那张牌组不成对子的牌。
- 三个:5 张牌中有 3 张牌的值相等。如果 α 和 β 都是 “三个”,比较这个 “三个” 的大小,如果 α 和 β 的 "三个"大小相等,那么比较剩下 2 张牌的总和。
- 三带二:5 张牌中有 3 张牌的值相等,另外 2 张牌值也相等。如果 α 和 β 都是 “三带二”,先比较它们的"三个"的大小,如果相等,再比较 “对子” 的大小。
- 炸弹:5 张牌中有 4 张牌的值相等。如果 α 和 β 都是 “炸弹”,比较 “炸弹” 的大小,如果相等,比较剩下那张牌的大小。
- 顺子:5 张牌中形成 x, x+1, x+2, x+3, x+4。如果 α 和 β 都是 “顺子”,直接比较两个顺子的最大值。
- 龙顺:5 张牌分别为 10、J、Q、K、A。
作为一个称职的魔法师,东东得知了全场人手里 5 张牌的情况。他现在要输出一个排行榜。排行榜按照选手们的 “一手牌” 大小进行排序,如果两个选手的牌相等,那么人名字典序小的排在前面。
不料,此时一束宇宙射线扫过,为了躲避宇宙射线,东东慌乱中清空了他脑中的 Cache。请你告诉东东,全场人的排名
输入格式
输入包含多组数据。每组输入开头一个整数 n (1 <= n <= 1e5),表明全场共多少人。
随后是 n 行,每行一个字符串 s1 和 s2 (1 <= |s1|,|s2| <= 10), s1 是对应人的名字,s2 是他手里的牌情况。
输出格式
对于每组测试数据,输出 n 行,即这次全场人的排名。
样例
Input
3
DongDong AAA109
ZJM 678910
Hrz 678910
Output
Hrz
ZJM
DongDong
解题思路
- 设置player结构体,包含玩家姓名,手上“一手牌”的类型,该类型下的得分
- 结构体重写<,类型优先,分数其次,名字字典序最后(注意char*要用strcmp来比较字符串)
- 对“一手牌”的字符串用string输入,遇到2-9不做处理直接存入temp数组(存放每个玩家手中的牌,用int表示),遇到特殊的’A’,‘J’,‘Q’,‘K’直接按1,11,12,13存储,遇到’1’说明这里想表示’10’,直接存储10,同时枚举位置后移一位j++,跳过此后的’0’
- 对每个player的temp中的牌排序,小牌在前,根据优先级(牌型值的大小)顺序判断这手牌的牌型
- 判断牌型时的打分时关键,这里我给牌型相同时的优先比较的那些牌的值更大的“权重”,比如炸弹时,炸弹的大小的权重为100,而剩下的牌权重为1;三带二时,“三个”的权重为100,剩下的两张牌权重为1(这里设置100是因为牌的值最大为13,设为10不足以满足优先级判定大小的加权需求)…最终当牌型相同时,可直接根据它的“分数”来比较两手牌的大小
- 最终对每位player的牌排序后输出姓名即可
这里注意多组数据,且每个选手的信息输入前要初始化temp数组
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<string>
#include<cstring>
using namespace std;
const int maxn=100100;
struct player{
char name[20];
int type;
int score;
bool operator<(const player &p) const {
if(type!=p.type) return type>p.type;
else if(score!=p.score) return score>p.score;
else if(strcmp(name,p.name)<0) return true;
else return false;
}
};
player rank_p[maxn];
int n,temp[10];
int main() {
while(cin>>n){
for(int i=1;i<=n;++i) {
memset(temp,0,sizeof(temp));
char s1[20];string s2;
int cnt=0;
scanf("%s",s1);cin>>s2;
strcpy(rank_p[i].name,s1);
for(int j=0;j<s2.size();j++) {
char c=s2[j];
if(c=='A') temp[++cnt]=1;
else if(c=='J') temp[++cnt]=11;
else if(c=='Q') temp[++cnt]=12;
else if(c=='K') temp[++cnt]=13;
else if(c=='1') {
temp[++cnt]=10;
j++;
}else temp[++cnt]=c-'0';
}
sort(temp+1,temp+6);
//顺子
if(temp[1]==1&&temp[2]==10&&temp[3]==11&&temp[4]==12&&temp[5]==13)
rank_p[i].type=8,rank_p[i].score=100;
else if((temp[1]+1==temp[2])&&(temp[2]+1==temp[3])&&(temp[3]+1==temp[4])&&
(temp[4]+1==temp[5])) rank_p[i].type=7,rank_p[i].score=temp[5];
//炸弹
else if(temp[2]==temp[5])
rank_p[i].type=6,rank_p[i].score=temp[2]*100+temp[1];
else if(temp[1]==temp[4])
rank_p[i].type=6,rank_p[i].score=temp[1]*100+temp[5];
//三带二
else if(temp[1]==temp[3]&&temp[4]==temp[5])
rank_p[i].type=5,rank_p[i].score=temp[1]*100+temp[5];
else if(temp[1]==temp[2]&&temp[3]==temp[5])
rank_p[i].type=5,rank_p[i].score=temp[3]*100+temp[1];
//三个
else if(temp[1]==temp[3])
rank_p[i].type=4,rank_p[i].score=temp[1]*100+temp[4]+temp[5];
else if(temp[2]==temp[4])
rank_p[i].type=4,rank_p[i].score=temp[2]*100+temp[1]+temp[5];
else if(temp[3]==temp[5])
rank_p[i].type=4,rank_p[i].score=temp[3]*100+temp[1]+temp[2];
//两对
else if(temp[1]==temp[2]&&temp[3]==temp[4])
rank_p[i].type=3,rank_p[i].score=temp[3]*1e4+temp[1]*1e2+temp[5];
else if(temp[1]==temp[2]&&temp[4]==temp[5])
rank_p[i].type=3,rank_p[i].score=temp[4]*1e4+temp[1]*1e2+temp[3];
else if(temp[2]==temp[3]&&temp[4]==temp[5])
rank_p[i].type=3,rank_p[i].score=temp[4]*1e4+temp[2]*1e2+temp[1];
//对子
else if(temp[1]==temp[2])
rank_p[i].type=2,rank_p[i].score=temp[1]*1e2+temp[3]+temp[4]+temp[5];
else if(temp[2]==temp[3])
rank_p[i].type=2,rank_p[i].score=temp[2]*1e2+temp[1]+temp[4]+temp[5];
else if(temp[3]==temp[4])
rank_p[i].type=2,rank_p[i].score=temp[3]*1e2+temp[1]+temp[2]+temp[5];
else if(temp[4]==temp[5])
rank_p[i].type=2,rank_p[i].score=temp[4]*1e2+temp[1]+temp[2]+temp[3];
else rank_p[i].type=1,rank_p[i].score=temp[1]+temp[2]+temp[3]+temp[4]+temp[5];
}
sort(rank_p+1,rank_p+n+1);
for(int i=1;i<=n;++i)
cout<<rank_p[i].name<<endl;
}
return 0;
}
C-签到题
题目描述
SDUQD 旁边的滨海公园有 x 条长凳。第 i 个长凳上坐着 a_i 个人。这时候又有 y 个人将来到公园,他们将选择坐在某些公园中的长凳上,那么当这 y 个人坐下后,记k = 所有椅子上的人数的最大值,那么k可能的最大值mx和最小值mn分别是多少。
输入格式
第一行包含一个整数 x (1 <= x <= 100) 表示公园中长椅的数目
第二行包含一个整数 y (1 <= y <= 1000) 表示有 y 个人来到公园
接下来 x 个整数 a_i (1<=a_i<=100),表示初始时公园长椅上坐着的人数
输出格式
输出 mn 和 mx
Example
Input
3
7
1
6
1
Output
6 13
样例解释
最初三张椅子的人数分别为 1 6 1
接下来来了7个人。
可能出现的情况为{1 6 8},{1,7,7},…,{8,6,1}
相对应的k分别为8,7,…,8
其中,状态{1,13,1}的k = 13,为mx
状态{4,6,5}和状态{5,6,4}的k = 6,为mn
解题思路
- 找k的最大值mx很容易,在输入a[i]时,找到当前椅子上人数的最大值temp,再让新来的y个人全部坐到该椅子上去,mx即=temp+y
- 找k的最小值,首先我们要明白无论再来多少人,mn一定>=当前人数最大值temp,现在考虑新来的y个人的坐法,如果存在某种分配方法使新来的y人坐到人数!=temp的椅子(即小于最大值)上,且最终满足坐好后这些椅子上的人数不大于temp,那么k的最小值即为temp。若无法满足temp*x>=sum(已有的人数)+y,说明仍有新来的人未坐下,则设定的人数最大值的最小值小了,temp++,每轮重新执行上述判定,直到满足,此时的temp即为k的最小值mn。
代码
#include<iostream>
#include<cstdio>
using namespace std;
int x,y,a[110],temp,mn,mx,sum;
int main() {
scanf("%d%d",&x,&y);
for(int i=1;i<=x;++i) {
scanf("%d",a+i);
if(temp<a[i]) temp=a[i];
sum+=a[i];
}
mx=temp+y;
while(1) {
if(temp*x>=(sum+y)) break;
else temp++;
}
mn=temp;
printf("%d %d\n",mn,mx);
return 0;
}