CSP-大模拟的求解方法和实战总结

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;
}

猜你喜欢

转载自blog.csdn.net/qq_43942251/article/details/105779344