这是CSP认证 第三题的模拟题
题目背景
小 H 同学发现,他维护的存储系统经常出现有人用机器学习的训练数据把空间占满的问题,十分苦恼。
查找了一阵资料后,他想要在文件系统中开启配额限制,以便能够精确地限制大家在每个目录中最多能使用的空间。
文件系统概述
文件系统,是一种树形的文件组织和管理方式。在文件系统中,文件是指用名字标识的文件系统能够管理
的基本对象,分为普通文件和目录文件两种,目录文件可以被简称为 目录。目录中有一种特殊的目录被叫做根目录 。
除了根目录外,其余的文件都有名字,称为文件名。合法的文件名是一个由若干数字([0-9])、
大小写字母([A-Za-z])组成的非空字符串。普通文件中含有一定量的数据,占用存储空间;目录不占用存储空间。
文件和目录之间存在含于关系。上述概念满足下列性质:
有且仅有一个根目录;
对于除根目录以外的文件,都含于且恰好含于一个目录;
含于同一目录的文件,它们的文件名互不相同;
对于任意不是根目录的文件 ,若 不含于根目录,那么存在有限个目录 ,
使得 含于 , 含于 ,, 含于根目录。
结合性质 4 和性质 2 可知,性质 4 中描述的有限多个目录,即诸 ,是唯一的。再结合性质 3,
我们即可通过从根目录开始的一系列目录的序列,来唯一地指代一个文件。我们记任意不是根目录且不含于
根目录的文件 的文件名是 ,那么 的路径 是:,其中符号 表示字符串的连接;对于含于根目录的文件 ,
它的路径是:;根目录的路径是:。不符合上述规定的路径都是非法的。例如:/A/B
是合法路径,但 /A//B、/A/、A/、A/B 都不是合法路径。
若文件 含于目录 ,我们也称 是 的孩子文件。 是 的双亲目录。
我们称文件 是目录 的后代文件,如果满足:(1) 是 的孩子文件,或(2)
含于 的后代文件。
如图所示,该图中绘制的文件系统共有 8 个文件。其中,方形表示目录文件,圆形表示普通文件,它们之间
的箭头表示含于关系。在表示文件的形状上的文字是其文件名;各个形状的左上方标记了序号,以便叙述。
在该文件系统中,文件 5 含于文件 2,文件 5 是文件 2 的孩子文件,文件 5 也是文件 2 的后代文件。
文件 8 是文件 2 的后代文件,但不是文件 2 的孩子文件。文件 8 的路径是 /D1/D1/F2。
配额概述
配额是指对文件系统中所含普通文件的总大小的限制。
对于每个目录 ,都可以设定两个配额值:目录配额和
后代配额。我们称目录配额 是满足的,当且仅当 的孩子文件中,
全部普通文件占用的存储空间之和不大于该配额值。我们称后代配额
是满足的,当且仅当 的后代文件中,全部普通文件占用的存储空间之和不大于该配额值。
我们称文件系统的配额是满足的,当且仅当该文件系统中所有的配额都是满足的。
很显然,若文件系统中仅存在目录,不存在普通文件,那么该文件系统的配额一定是满足的。随着配额和文件
的创建,某个操作会使文件系统的配额由满足变为不满足,这样的操作会被拒绝。例如:试图设定少于目前已有
文件占用空间的配额值,或者试图创建超过配额值的文件。
题目描述
在本题中,假定初始状态下,文件系统仅包含根目录。你将会收到若干对文件系统的操作指令。对于每条指令,
你需要判断该指令能否执行成功,对于能执行成功的指令,在成功执行该指令后,文件系统将会被相应地修改。
对于不能执行成功的指令,文件系统将不会发生任何变化。你需要处理的指令如下:
创建普通文件
创建普通文件指令的格式如下:
创建普通文件的指令有两个参数,是空格分隔的字符串和一个正整数,分别表示需要创建的普通文件的路径和
文件的大小。
对于该指令,若路径所指的文件已经存在,且也是普通文件的,
则替换这个文件;若路径所指文件已经存在,但是目录文件的,则该指令不能执行成功。
当路径中的任何目录不存在时,应当尝试创建这些目录;若要创建的目录文件与已有的同一双亲目录下的孩子
文件中的普通文件名称重复,则该指令不能执行成功。
另外,还需要确定在该指令的执行是否会使该文件系统的
配额变为不满足,如果会发生这样的情况,则认为该指令不能执行成功,反之则认为该指令能执行成功。
移除文件
移除文件指令的格式如下:
移除文件的指令有一个参数,是字符串,表示要移除的文件的路径。
若该路径所指的文件不存在,则不进行任何操作。
若该路径所指的文件是目录,则移除该目录及其所有后代文件。
在上述过程中被移除的目录(如果有)上设置的配额值也被移除。
该指令始终认为能执行成功。
设置配额值
设置配额值的指令有三个参数,是空格分隔的字符串和两个非负整数,分别表示需要设置配额值的目录
的路径、目录配额和后代配额。
该指令表示对所指的目录文件,
分别设置目录配额和后代配额。若路径所指的文件不存在,或者不是目录文件,则
该指令执行不成功。
若在该目录上已经设置了配额,则将原配额值替换为指定的配额值。
特别地,若配额值为 0,
则表示不对该项配额进行限制。若在应用新的配额值后,该文件系统配额变为不满足,那么该指令执行不成功。
输入格式
从标准输入读入数据。
输入的第一行包含一个正整数 ,表示需要处理的指令条数。
输入接下来会有 行,每一行一个指令。指令的格式符合前述要求。输入数据保证:对于所有指令,
输入的路径是合法路径;对于创建普通文件和移除文件指令,输入的路径不指向根目录。
输出格式
输出到标准输出。
输出共有 行,表示相应的操作指令是否执行成功。若成功执行,则输出字母 Y;否则输出 N。
样例1输入
10
C /A/B/1 1024
C /A/B/2 1024
C /A/B/1/3 1024
C /A 1024
R /A/B/1/3
Q / 0 1500
C /A/B/1 100
Q / 0 1500
R /A/B
Q / 0 1
Data
样例1输出
Y
Y
N
N
Y
N
Y
Y
Y
Y
Data
样例1解释
输入总共有 10 条指令。其中前两条指令可以正常创建两个普通文件。第三条指令试图创建 /A/B/1/3,
但是 /A/B/1 已经存在,且不是目录,而是普通文件,不能再进一步创建孩子文件,因此执行不成功。
第四条指令试图创建 /A,但是 /A 已经存在,且是目录,因此执行不成功。第五条指令试图删除 /A/B/1/3,
由于该文件不存在,因此不对文件系统进行修改,但是仍然认为执行成功。第六条指令试图在根目录
增加后代配额限制,但此时,文件系统中的文件总大小是 2048,因此该限制无法生效,执行不成功。
第七条指令试图创建文件 /A/B/1,由于 /A/B/1 已经存在,且是普通文件,因此该指令实际效果是
将原有的该文件替换。此时文件总大小是 1124,因此第八条指令就可以执行成功了。
第九条指令递归删除了 /A/B 目录和它的所有后代文件。此时文件系统中已经没有普通文件,因此第十条命令
可以执行成功。
样例2输入
9
Q /A/B 1030 2060
C /A/B/1 1024
C /A/C/1 1024
Q /A/B 1024 0
Q /A/C 0 1024
C /A/B/3 1024
C /A/B/D/3 1024
C /A/C/4 1024
C /A/C/D/4 1024
Data
样例2输出
N
Y
Y
Y
Y
N
Y
N
N
Data
样例2解释
输入共有 9 条指令。第一条指令试图为 /A/B 创建配额规则,然而该目录并不存在,因此执行不成功。
接下来的两条指令创建了两个普通文件。再接下来的两条指令分别在目录 /A/B 和 /A/C 创建了
两个配额规则。其中前者是目录配额,后者是后代配额。接下来的两条指令,创建了
两个文件。其中,/A/B/3 超出了在 /A/B 的目录配额,因此执行不成功;但 /A/B/D/3
不受目录配额限制,因此执行成功。最后两条指令,创建了两个文件。虽然在 /A/C
没有目录配额限制,但是无论是 /A/C 下的孩子文件还是后代文件,都受到后代配额的限制,因此两条指令
执行都不成功。
子任务
本题目各个测试点的数据规模如下:
表格中,目录层次是指各指令中出现的路径中,/ 字符的数目。
题解
这题纯粹就是一个恶心的模拟,复杂的模拟其实最好把代码结构组织清除一些,我尽可能把代码改造得易于理解。
我们不妨这么考虑,先不思考文件路径是如何分解的和多级目录怎么组织,假设我们只有一级目录,来思考怎么解决问题。
文件节点的定义
不论是普通文件还是目录,都可以看成一个文件,目录是特殊的文件
所以可以定义文件节点,父节点可以方便向上回溯,dic是一个基于map数据结构的目录,键为文件名,值就是Node对象的指针:
class Node{
public:
map<string,Node*> dic; // 目录键值对 文件名/节点
LL LR,LD;
LL v_LR,v_LD;
Node* father;//父节点
bool type; //0为普通文件 1为目录文件
LL fileSize;
};
文件树的操作日志
其实整个问题中最麻烦的插入文件,插入文件时,如果有同名文件,就做更新。这导致我们没法预先更新上级目录的后代文件大小。最清晰的方法是,我们先尝试在最后一层目录尝试插入这个文件,然后回溯检查每个目录是否满足限额,最后再更新每级目录的后代文件大小。这时候就要有一个容器来记录文件插入过程中创建的目录的日志,这样可以在创建失败后回退
//下面是日志的定义和目录的回退
vector<pair<Node*,string>> steps;
void step_back(){
for(int i=0;i<steps.size();i++){
steps[i].first->dic.erase(steps[i].second);
}
}
这样就可以定义最后一层目录创建文件的方法函数。具体代码注释应该也很清晰
// Node类方法
//增加文件
bool insertFile(string fileName,LL fs){
if(type==0)// 只有目录文件才可以添加文件
return false;
//判断是否已经存在文件
if(this->dic[fileName]){
//如果是目录文件就会创建失败
if(this->dic[fileName]->type == 1)
return false;
else{
//否则更新文件
LL old_size = this->dic[fileName]->fileSize;
LL diff = fs - this->dic[fileName]->fileSize ;
if((LR!=0 && v_LR+diff > LR)||(LD!=0 && v_LD+diff > LD)){
//检查容量限制
step_back();
return false;
}
// cout<<"'diff'"<<diff<<endl;
this->dic[fileName]->fileSize = fs;
//回溯检查合法性
Node* p = this->father;
while(p){
if(p->LR==0 || p->v_LR+diff <= p->LR){
p = p->father;
continue;
}
//出现错误 将所有操作退回
this->dic[fileName]->fileSize = old_size;
step_back();
return false;
}
//如果都没有问题,就回溯更新
this->v_LR+=diff;
this->v_LD+=diff;
p = this->father;
while(p){
p->v_LR += diff;
p = p->father;
}
return true;
}
}else{
// 创建新的文件
if((LR!=0 && v_LR+fs > LR)||(LD!=0 && v_LD+fs > LD)){
//检查容量限制
step_back();
return false;
}
//回溯检查合法性
Node* p = this->father;
while(p){
if(p->LR==0 || p->v_LR+fs <= p->LR){
p = p->father;
continue;
}
//出现错误 将所有操作退回
step_back();
return false;
}
//如果都没有问题,就回溯更新
this->v_LR+=fs;
this-