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)//先前做的是mkdir,这里要rm
{
Dictionary* p = now_flag->rm(command_solve->command_data);
if (p != NULL)
{
undo_or_not = true;
}
break;
}
else if (command_solve->command_id == 2)//之前做的是rm,这里要把删掉的文件重新放到子目录下
{
bool redir_p = false;
redir_p = now_flag->redir(command_solve->record_cmd_pos);
if (redir_p == true)
{
undo_or_not = true;
}
break;
}
else if (command_solve->command_id == 3)//之前做的是cd
{
now_flag = command_solve->record_cmd_pos;
undo_or_not = true;
break;
}
}
if (undo_or_not == true)
printf("OK\n");
else
printf("ERR\n");
}
}
}
}
2、TREE
好了我们解决了UNDO这个棘手的问题,只剩下查看操作中的TREE了。由于整个数据结构是一种近似于树形的组织方式,TREE便是一种类似于图论中树遍历的查看方式。但是这道题不同的是,并不是任何时刻都要求输出全部的节点。该操作分为两种情况:
1、该目录的孩子<=10个时,输出全部子节点
2、该目录的孩子>10个时,输出树形遍历的前5个子节点和后五个子节点
这样的要求,无异于凭空增加了近一倍的时间复杂度(在孩子节点>10个时,需要从前面跑一遍DFS再反向跑一遍DFS才能满足要求)
如果你觉得浪费一点时间无足轻重,那么下面是这道题最大的坑点来了:
通过计算你会发现,如果接近所有操作都是大于十个点的查看操作,也就是每次跑两遍DFS,会导致超时(TLE)!
这需要我们找到一种在DFS过程中,进行剪枝的方法来缩小时间复杂度。
懒更新法
对于某次求解,如果是第一次更新,那毋庸置疑,完全没有剪枝的余地,只能老老实实正反DFS。
但是如果出现连续的TREE查看,且在两次查看之间的操作,并不会对目录和位置产生影响,那么我们完全可以将先前DFS的结果直接输出而剪掉跑DFS的时间。
这种满足条件更新,不满足保持的方式成为懒更新法。利用这种方法,我们可以将题目的时间复杂度压缩在合理的范围内。(毕竟出题人也不会出完全不可能过的数据集)
懒更新法的实现:
懒更新的关键点有三个:
1、在每个节点加入更新标志status和答案数组child_tankl
struct Dictionary
{
string dic_name;
map<string, Dictionary*> child;
Dictionary* father;
int child_num;
bool updating;
vector<string>* child_tank;
}
2、在进行类型1操作(MKDIR\RM\CD)时,将懒更新的标志修改,其余时间懒更新标志不变
3、在进行TREE操作时,查看当前节点的状态标识,如果为未更新状态直接输出child_tank,如果已经更新则需要重新进行正反DFS并更新child_tank。
具体实现见源代码部分
题目源码(C++)
#include<iostream>
#include<stdio.h>
#include<map>
#include<string>
#include<vector>
#include<queue>
#include<set>
#include<algorithm>
using namespace std;
const string command_list[7] = {
"MKDIR","RM","CD","SZ","LS","TREE","UNDO" };
struct Dictionary
{
string dic_name;
map<string, Dictionary*> child;
Dictionary* father;
int child_num;//孩子节点的数量(包括这个点)
bool updating;
vector<string>* child_tank;
Dictionary(Dictionary* father_node, string name)
{
this->dic_name = name;
this->father = father_node;
this->child_num = 1;
this->child.clear();
this->updating = false;
child_tank = new vector<string>;
child_tank->clear();
}
void update_all(vector<string>* tank)
{
tank->push_back(this->dic_name);
map<string, Dictionary*>::iterator it;
for (it = this->child.begin(); it != this->child.end(); it++)
it->second->update_all(tank);
}
void update_front(int number, vector<string> * tank)
//该分支下的更新子节点个数
{
tank->push_back(this->dic_name);
number--;
if (number == 0) return;
//对孩子遍历
int child_num = this->child.size();//直接孩子数
map<string, Dictionary*>::iterator it;
it = this->child.begin();
while (child_num--)
{
int now_child_cnt = it->second->child_num;
if (now_child_cnt >= number)//这个孩子节点的数目已经够number个
{
it->second->update_front(number, tank);
return;
}
else//这个孩子节点不够多,把这个孩子节点的都用上再去用下一个
{
it->second->update_front(now_child_cnt, tank);
number -= now_child_cnt;
}
it++;
}
}
void update_back(int number, vector<string> * tank)
{
int child_num = this->child.size();
map<string, Dictionary*>::iterator it;
it = this->child.end();
while (child_num--)
{
it--;
int now_child_cnt = it->second->child_num;
if (now_child_cnt >= number)
{
it->second->update_back(number, tank);
return;
}
else
{
it->second->update_back(now_child_cnt, tank);
number -= now_child_cnt;
}
}
tank->push_back(this->dic_name);
}
void change_tree_num(int change)//向上维护每一层的子树大小
{
this->updating = true;
this->child_num += change;
if (this->father != NULL)
this->father->change_tree_num(change);
}
//按照给定的目录名,在当前的目录下找到该目录名对应的地址
Dictionary* find_child(string child_s)
{
auto tmp = this->child.find(child_s);
if (tmp == this->child.end())
return NULL;
return tmp->second;
}
bool redir(Dictionary * re_dir)//恢复函数
{
//如果要恢复的子文件在原目录下已经存在,那么就无法再恢复了
if (this->child.find(re_dir->dic_name) != this->child.end())
return false;
this->child[re_dir->dic_name] = re_dir;
this->change_tree_num(+1 * (re_dir->child_num));
return true;
}
Dictionary * mkdir(string new_dir)//创建子文件的函数,返回指向子文件的指针地址
{
if (this->child.find(new_dir) != child.end())
return NULL;//如果在寻找时找到了这个子目录,则建立失败
Dictionary * tmp = new Dictionary(this, new_dir);
this->child[new_dir] = tmp;
this->change_tree_num(+1);
return tmp;
}
Dictionary * rm(string rm_dir)//删除子文件的函数,返回指向删除的文件的指针地址
{
auto tmp = this->child.find(rm_dir);
Dictionary* tmp_p = tmp->second;
if (tmp == this->child.end())//找到的位置是end,说明map中没有这个string
return NULL;
this->change_tree_num(-1 * (tmp->second->child_num));
this->child.erase(tmp);
return tmp_p;
}
Dictionary * cd(string cd_dir)//打开子文件的函数,返回值是指向文件的指针
{
if (cd_dir == "..")
return this->father;
return find_child(cd_dir);
}
void sz()
{
printf("%d\n", this->child_num);
}
void ls()//输出直接子节点
{
int child_sz = this->child.size();
if (child_sz == 0) printf("EMPTY\n");
else if (child_sz <= 10 && child_sz > 0)//输出全部
{
for (map<string, Dictionary*>::iterator it = this->child.begin(); it != this->child.end(); it++)
printf("%s\n",it->first.c_str());
}
else//输出前五个和后五个
{
map<string, Dictionary*>::iterator it = this->child.begin();
for (int i = 0; i < 5; i++)
{
printf("%s\n",it->first.c_str());
it++;
}
printf("...\n");
it = this->child.end();
for (int i = 0; i < 5; i++) it--;
for (int i = 0; i < 5; i++)
{
printf("%s\n",it->first.c_str());
it++;
}
}
}
void tree()
{
if (this->child_num == 1)
printf("EMPTY\n");
else if (this->child_num <= 10)//只有不到十个,全部更新并输出
{
if (this->updating == true)
{
child_tank->clear();
update_all(this->child_tank);
updating = false;
}
for (int i = 0; i < this->child_tank->size(); i++)
{
printf("%s\n",this->child_tank->at(i).c_str());
}
}
else
{
if (this->updating == true)
{
this->child_tank->clear();
this->update_front(5, this->child_tank);
this->update_back(5, this->child_tank);
this->updating;
}
for (int i = 0; i < 5; i++)
{
printf("%s\n",this->child_tank->at(i).c_str());
}
printf("...\n");
for (int i = 9; i >= 5; i--)
{
printf("%s\n",this->child_tank->at(i).c_str());
}
}
}
//UNDO不可以封装在内部,因为他不止和当前的目录有关,还和进行的操作等有关
};
struct Command
{
int command_id;//操作符标识(1-7)
string command_data;//在前三种有操作目标的操作中记录操作的目标文件
Dictionary* record_cmd_pos;//在完成了操作后,记录该操作的目的目录地址,如果==NULL,说明该步骤就失败了
Command(string com_s)
{
string data_s;
for (int i = 0; i < 7; i++)
{
if (com_s == command_list[i])
{
this->command_id = i + 1;
if (command_id <= 3)
{
cin >> data_s;
this->command_data = data_s;
}
break;
}
}
}
};
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)//先前做的是mkdir,这里要rm
{
Dictionary* p = now_flag->rm(command_solve->command_data);
if (p != NULL)
{
undo_or_not = true;
}
break;
}
else if (command_solve->command_id == 2)//之前做的是rm,这里要把删掉的文件重新放到子目录下
{
bool redir_p = false;
redir_p = now_flag->redir(command_solve->record_cmd_pos);
if (redir_p == true)
{
undo_or_not = true;
}
break;
}
else if (command_solve->command_id == 3)//之前做的是cd
{
now_flag = command_solve->record_cmd_pos;
undo_or_not = true;
break;
}
}
if (undo_or_not == true)
printf("OK\n");
else
printf("ERR\n");
}
}
}
}
int main()
{
int datagroup = 0;
scanf("%d", &datagroup);
while (datagroup--)
solve();
return 0;
}