CSP-聊聊大模拟
大模拟的求解思维
大模拟题,也就是复杂模拟题,是ACM比赛和程序设计中不可或缺的题目类型,同时也是CSP中T3的固定模式题目。这种题目虽然对具体算法的要求不高,但是由于其题目情景设置复杂,数据结构种类繁杂,且由于其题目庞杂可能造成理解或认知上的障碍,很容易在做的时候产生退避的心理导致题目求解未果。因此面对大模拟,建立一套有效的分析问题,建立思路,实现需求的方法论是至关重要的。在正式开始引入题目之前,先谈谈大模拟的求解思路和规划方法论。
题目简述
咕咕东的雪梨电脑的操作系统在上个月受到宇宙射线的影响,时不时发生故障,他受不了了,想要写一个高效易用零bug的操作系统 —— 这工程量太大了,所以他定了一个小目标,从实现一个目录管理器开始。前些日子,东东的电脑终于因为过度收到宇宙射线的影响而宕机,无法写代码。他的好友TT正忙着在B站看猫片,另一位好友瑞神正忙着打守望先锋。现在只有你能帮助东东!
初始时,咕咕东的硬盘是空的,命令行的当前目录为根目录 root。
目录管理器可以理解为要维护一棵有根树结构,每个目录的儿子必须保持字典序。
命令 | 说明 |
---|---|
MKDIR s | 在当前目录下创建一个子目录 s,s 是一个字符串 创建成功输出 “OK”;若当前目录下已有该子目录则输出 “ERR” |
RM s | 在当前目录下删除子目录 s,s 是一个字符串 删除成功输出 “OK”;若当前目录下该子目录不存在则输出 “ERR” |
CD s | 进入一个子目录 s,s 是一个字符串(执行后,当前目录可能会改变) 进入成功输出 “OK”;若当前目录下该子目录不存在则输出 "ERR"特殊地,若 s 等于 “…” 则表示返回上级目录,同理,返回成功输出 “OK”,返回失败(当前目录已是根目录没有上级目录)则输出 “ERR” |
SZ | 输出当前目录的大小 也即输出 1+当前目录的子目录数 |
LS | 输出多行表示当前目录的 “直接子目录” 名 若没有子目录,则输出 “EMPTY”;若子目录数属于 [1,10] 则全部输出;若子目录数大于 10,则输出前 5 个,再输出一行 “…”,输出后 5 个。 |
TREE | 输出多行表示以当前目录为根的子树的前序遍历结果 若没有后代目录,则输出 “EMPTY”;若后代目录数+1(当前目录)属于 [1,10] 则全部输出;若后代目录数+1(当前目录)大于 10,则输出前 5 个,再输出一行 “…”,输出后 5 个。若目录结构如上图,当前目录为 “root” 执行结果如下, |
UNDO | 撤销操作 撤销最近一个 “成功执行” 的操作(即MKDIR或RM或CD)的影响,撤销成功输出 “OK” 失败或者没有操作用于撤销则输出 “ERR” |
input及输入样例
输入文件包含多组测试数据,第一行输入一个整数表示测试数据的组数 T (T <= 20);
每组测试数据的第一行输入一个整数表示该组测试数据的命令总数 Q (Q <= 1e5);
每组测试数据的 2 ~ Q+1 行为具体的操作 (MKDIR、RM 操作总数不超过 5000);
输入样例:
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
框架思路概述
根据对上述的题意进行分析理解,我们可以得出题目中的文件系统组织形态如下图所示:
结构特点是一个父节点有多个字节点,整个系统的祖先节点是root节点。
再对要求的操作进行剖析,可以将操作分为三个种类,用如下的图来表示这种关系比较直观:
1、MKDIR\RM\CD三种操作会对子目录产生修改或改变当前目录的位置。
2、SZ\LS\TREE三种操作是在该目录的位置下做相关的查看操作,但并不会对目录和位置有修改。
3、UNDO不是一种固定的操作,而是一个相对于前一步操作而进行的撤销。要注意的是,UNDO只能作用于1类操作,并做上一个1类操作的反操作(MKDIR和RM反操作,CD和退回父目录反操作)。如果上一个1类反操作为空,则操作失败。
分析完这样的数据组织方式和操作类型,可以写出每个节点的数据结构组成:
struct Dictionary
{
string dic_name;//该节点的文件名
map<string, Dictionary*> child;//孩子节点的map
Dictionary* father;//父亲节点指针
int child_num;//孩子节点的数量(包括这个点)
}
使用这种数据结构可以通过father指针访问父亲节点,通过map child根据孩子姓名为key查到的孩子指针访问孩子节点,同时利用child_num统计孩子的数量,花上一定的耐心就可以实现操作1-操作5。剩下的TREE和UNDO,就需要我们动动脑筋找一些新方式来实现了,我们放在难点剖析中叙述。
难点剖析
根据上文,我们已经能够便捷的实现MKDIR–LS的操作,但是TREE和UNDO还没有头绪,这里考察的是对数据结构的设计能力而非纯算法能力了,希望通过这道题的两个难点,能够为你以后做大模拟题带来新的思维。
1、UNDO
前面的操作叙述中我们说过,UNDO不是一种固定操作,而是一种相对一类型1操作(MKDIR\RM\CD)的反操作。这种特点决定了它的具体实现无法在节点结构中封装,而是随着程序的运行,不断变化。这里我们可能会想到:能不能把系统做过的类型1操作存下来呢?
这是一个合理的想法,但是由于记录的对象是操作指令而非节点,我们需要建立一个指令结构,来存放每个指令,再利用vector进行种类1的指令存放就通了!
Command数据结构:
struct Command
{
int command_id;//操作符标识(1-7)
string command_data;//在前三种有操作目标的操作中记录操作的目标文件
Dictionary* record_cmd_pos;//在完成了操作后,记录该操作的目的目录地址,如果==NULL,说明该步骤就失败了
}
针对几种操作的代码:
void solve()
{
int move_num = 0;
string s;
string move_data;
cin >> move_num;
Dictionary* now_flag = new Dictionary(NULL, "root");//在开始操作之前,先建立一个根节点,flag是操作的定位节点
vector<Command*> cmdList;//在每组数据进行操作时,使用vec来记录完成过的所有指令记录
while (move_num--)
{
cin >> s;
Command* command_solve = new Command(s);
switch (command_solve->command_id)
{
case 1://MKDIR操作的操作指针扔指向删除这一层
{
//在mkdir时,指令内的地址指针指向新建的目录
command_solve->record_cmd_pos = now_flag->mkdir(command_solve->command_data);
if (command_solve->record_cmd_pos == NULL) printf("ERR\n");
else
{
printf("OK\n");
cmdList.push_back(command_solve);
}
break;
}
case 2://RM操作的操作指针扔指向删除这一层
{
command_solve->record_cmd_pos = now_flag->rm(command_solve->command_data);
if (command_solve->record_cmd_pos == NULL) printf("ERR\n");
else
{
printf("OK\n");
cmdList.push_back(command_solve);
}
break;
}
case 3:
{
Dictionary* tmp_p = now_flag->cd(command_solve->command_data);
if (tmp_p == NULL) printf("ERR\n");
else
{
printf("OK\n");
command_solve->record_cmd_pos = now_flag;//这条指令指针指向打开的上级目录
now_flag = tmp_p;//实时的目录指针指向被打开的目录
cmdList.push_back(command_solve);
}
break;
}
case 4://SZ
now_flag->sz();
break;
case 5://LS
now_flag->ls();
break;
case 6://TREE
now_flag->tree();
break;
case 7://UNDO
{
bool undo_or_not = false;
while (undo_or_not == false && !cmdList.empty())
{
//从尾部取出一个操作并且进行undo
command_solve = cmdList.back();
cmdList.pop_back();
if (command_solve->command_id == 1