本周所讲的内容为T3大模拟,题目特别长,读懂题意,抽取信息是一大难关。读题时应在纸上记录下关键信息,同时可以适当对对象进行封装。看学姐上课时现场写代码,觉得她的思路很清晰。自己动手的时候,虽说copy她的思路,但是实现上显得更加冗长和杂乱,对函数功能的分离也不清晰,这说明道阻且长啊。
A : 化学方程式
化学方程式,也称为化学反应方程式,是用化学式表示化学反应的式子。给出一组化学方程式,请你编写程序判断每个方程式是否配平(也就是方程式中等号左右两边的元素种类和对应的原子个数是否相同)。
本题给出的化学方程式由大小写字母、数字和符号(包括等号=、加号+、左圆括号(和右圆括号))组成,不会出现其他字符(包括空白字符,如空格、制表符等)。化学方程式的格式与化学课本中的形式基本相同(化学式中表示元素原子个数的下标用正常文本,如H2O),用自然语言描述如下:
化学方程式由左右两个表达式组成,中间用一个等号=连接,如2H2+O2=2H2O;
表达式由若干部分组成,每部分由系数和化学式构成,部分之间用加号+连接,如2H2+O2、2H2O;
系数是整数或空串,如为空串表示系数为 1;
整数由一个或多个数字构成;
化学式由若干部分组成,每部分由项和系数构成,部分之间直接连接,如H2O、CO2、Ca(OH)2、Ba3(PO4)2;
项是元素或用左右圆括号括起来的化学式,如 H、Ca、(OH)、(PO4):
元素可以是一个大写字母,也可以是一个大写字母跟着一个小写字母,如H、O、Ca。
输入格式
从标准输入读入数据。
输入的第一行包含一个正整数n,表示输入的化学方程式个数。接下来 n 行,每行是一个符合定义的化学方程式。
输出格式
输出到标准输出。
输出共 n 行,每行是一个大写字母 Y 或 N,回答输入中相应的化学方程式是否配平。
子任务
1≤n≤100
输入的化学方程式都是符合题目中给出的定义的,且长度不超过 1000
系数不会有前导零,也不会有为零的系数
化学方程式的任何一边,其中任何一种元素的原子总个数都不超过 10^9
题目分析
这题是不断把概念拆分的过程。化学方程式可使用等号分为两部分,每部分由若干化学式经过“+”连接而成。每个化学式由元素,括号及系数组成。括号内的内容可视作另一个化学式,从中引出递归的定义。
上课时学姐对系数的处理我觉得十分清楚方便。化学式前可能有系数,事先抽取出来作为整个式子小系数的乘积。小系数这里指的是元素后的系数,表示每个元素的个数。在获取元素和获取系数时,向函数里传递的参数为引用,则随着元素和参数的获取,在化学式中的代表位置的变量自然后移。
并且学姐并不是事先写好函数,而是先在主函数写好框架,再填充函数,逻辑十分清楚。
以下是代码:
#include<bits/stdc++.h>
using namespace std;
struct record{
map<string,int> mp;//写成结构体后重载符号会使操作更加简洁
void operator+=(map<string,int> m2)
{
for(auto it=m2.begin();it!=m2.end();it++)
{
mp[it->first]+=it->second;
}
}
bool operator==(record& r2)
{
return mp==r2.mp;
}
};
vector<string> split(string str,char ch)//根据ch分割字符串
{
//学姐课上写的时候用的是stringstream+getline
//但是我自己对这个用法不是很熟练,就先按照自己的土办法写了
int p1=0,p2=0;
vector<string> res;
while(p2<str.size())
{
while(p2<str.size()&&str[p2]!=ch)
p2++;
res.push_back(str.substr(p1,p2-p1));
p1=++p2;
}
return res;
}
int get_int(string str,int& s)
{
int sum=0;
while(isdigit(str[s]))//以前我总是用阿斯克码的大小
//判断数字,但使用isdigit方便很多
{
sum=sum*10+(str[s++]-'0');
}
if(sum==0)
return 1;
return sum;
}
string get_el(string str,int& s)
{
string res;
if(isupper(str[s]))//同样isupper islower也是新学的方法
{
res+=str[s++];
if(islower(str[s]))
res+=str[s++];
}
return res;
}
map<string,int> elementnum(string str,int& s)
{
int mult=get_int(str,s);//化学式前的系数
map<string,int> res;//计算每个元素的个数
while(s<str.size())
{
if(str[s]=='(')//遇到左括号,递归调用
{
s++;
map<string,int> sub=elementnum(str,s);
for(auto it=sub.begin();it!=sub.end();it++)
res[it->first]+=mult*it->second;//不要忘记前系数
}
else if(str[s]==')')//遇到右括号,退出此层递归
{
s++;
int ss=get_int(str,s);//获取右括号后系数,极易遗漏
if(ss!=1)
{
for(auto it=res.begin();it!=res.end();it++)
it->second*=ss;
}
return res;
}
else
{
string element=get_el(str,s);//先获取元素
int num=get_int(str,s);//再获取系数
res[element]+=mult*num;
}
}
return res;
}
int main()
{
int n=0;
scanf("%d",&n);
while(n>0)
{
n--;
string formula;
cin>>formula;
record r[2];
vector<string> half=split(formula,'=');//先分为两部分
for(int i=0;i<=1;i++)
{
string hf=half[i];
vector<string> chemistry=split(hf,'+');//分为化学式
for(int j=0;j<chemistry.size();j++)
{//计算每个化学式里元素的个数
int s=0;
r[i]+=elementnum(chemistry[j],s);
}
}
if(r[0]==r[1])
printf("Y\n");
else
printf("N\n");
}
}
B : 带配额的文件系统
题目背景
小 H 同学发现,他维护的存储系统经常出现有人用机器学习的训练数据把空间占满的问题,十分苦恼。
查找了一阵资料后,他想要在文件系统中开启配额限制,以便能够精确地限制大家在每个目录中最多能使用的空间。
文件系统概述
文件系统,是一种树形的文件组织和管理方式。在文件系统中,文件是指用名字标识的文件系统能够管理的基本对象,分为普通文件和目录文件两种,目录文件可以被简称为 目录。目录中有一种特殊的目录被叫做 根目录。
除了根目录外,其余的文件都有名字,称为文件名。合法的文件名是一个由若干数字([0-9])、大小写字母([A-Z,a-z])组成的非空字符串。普通文件中含有一定量的数据,占用存储空间;目录不占用存储空间。
文件和目录之间存在含于关系。上述概念满足下列性质:
有且仅有一个根目录;
对于除根目录以外的文件,都含于且恰好含于一个目录;
含于同一目录的文件,它们的文件名互不相同;
对于任意不是根目录的文件 f,若 f 不含于根目录,那么存在有限个目录 d1,d2,…,dn,使得 f 含于 d1,d1 含于 d2…dn,dn 含于根目录。
结合性质 4 和性质 2 可知,性质 4 中描述的有限多个目录,即诸 did_idi ,是唯一的。再结合性质 3,我们即可通过从根目录开始的一系列目录的序列,来唯一地指代一个文件。
我们记任意不是根目录且不含于根目录的文件 f的文件名是 Nf,那么 f 的路径是:‘/’+Ndn+‘/’+⋯+Nd1+‘/’+Nf,其中符号 +表示字符串的连接;对于含于根目录的文件 f,它的路径是:‘/’+Nf;根目录的路径是:‘/’。不符合上述规定的路径都是非法的。
例如:/A/B 是合法路径,但 /A//B,/A/,A/、A/B不是合法路径。
若文件 f含于目录 d,我们也称 f 是 d 的孩子文件。d 是 f 的双亲目录。我们称文件 f 是目录 d 的后代文件,如果满足:(1) f 是 d 的孩子文件,或 (2) f含于 d的后代文件。
如图所示,该图中绘制的文件系统共有 8 个文件。其中,方形表示目录文件,圆形表示普通文件,它们之间的箭头表示含于关系。在表示文件的形状上的文字是其文件名;各个形状的左上方标记了序号,以便叙述。
在该文件系统中,文件 5 含于文件 2,文件 5 是文件 2 的孩子文件,文件 5 也是文件 2 的后代文件。文件 8 是文件 2 的后代文件,但不是文件 2 的孩子文件。文件 8 的路径是 /D1/D1/F2。
配额概述
配额是指对文件系统中所含普通文件的总大小的限制。对于每个目录 d,都可以设定两个配额值:目录配额 和 后代配额。
我们称目录配额 LD 是满足的,当且仅当 d的孩子文件中,全部普通文件占用的存储空间之和不大于该配额值。我们称后代配额 LR是满足的,当且仅当 d 的后代文件中,全部普通文件占用的存储空间之和不大于该配额值。我们称文件系统的配额是满足的,当且仅当该文件系统中所有的配额都是满足的。
很显然,若文件系统中仅存在目录,不存在普通文件,那么该文件系统的配额一定是满足的。随着配额和文件的创建,某个操作会使文件系统的配额由满足变为不满足,这样的操作会被拒绝。例如:试图设定少于目前已有文件占用空间的配额值,或者试图创建超过配额值的文件。
题目描述
在本题中,假定初始状态下,文件系统仅包含根目录。你将会收到若干对文件系统的操作指令。对于每条指令,你需要判断该指令能否执行成功,对于能执行成功的指令,在成功执行该指令后,文件系统将会被相应地修改。对于不能执行成功的指令,文件系统将不会发生任何变化。你需要处理的指令如下:
创建普通文件
创建普通文件指令的格式如下:
C file path file size
创建普通文件的指令有两个参数,是空格分隔的字符串和一个正整数,分别表示需要创建的普通文件的路径和文件的大小。
对于该指令,若路径所指的文件已经存在,且也是普通文件的,则替换这个文件;若路径所指文件已经存在,但是目录文件的,则该指令不能执行成功。
当路径中的任何目录不存在时,应当尝试创建这些目录;若要创建的目录文件与已有的同一双亲目录下的孩子文件中的普通文件名称重复,则该指令不能执行成功。
另外,还需要确定在该指令的执行是否会使该文件系统的配额变为不满足,如果会发生这样的情况,则认为该指令不能执行成功,反之则认为该指令能执行成功。
移除文件
移除文件指令的格式如下:
R file path
移除文件的指令有一个参数,是字符串,表示要移除的文件的路径。
若该路径所指的文件不存在,则不进行任何操作。若该路径所指的文件是目录,则移除该目录及其所有后代文件。在上述过程中被移除的目录(如果有)上设置的配额值也被移除。
该指令始终认为能执行成功。
设置配额值
Q file path LD LR
设置配额值的指令有三个参数,是空格分隔的字符串和两个非负整数,分别表示需要设置配额值的目录的路径、目录配额和后代配额。
该指令表示对所指的目录文件,分别设置目录配额和后代配额。若路径所指的文件不存在,或者不是目录文件,则该指令执行不成功。
若在该目录上已经设置了配额,则将原配额值替换为指定的配额值。
特别地,若配额值为 0,则表示不对该项配额进行限制。若在应用新的配额值后,该文件系统配额变为不满足,那么该指令执行不成功。
输入格式
从标准输入读入数据。
输入的第一行包含一个正整数 n,表示需要处理的指令条数。
输入接下来会有 n 行,每一行一个指令。指令的格式符合前述要求。
输入数据保证:对于所有指令,输入的路径是合法路径;对于创建普通文件和移除文件指令,输入的路径不指向根目录。
输出格式
输出到标准输出。
输出共有 n行,表示相应的操作指令是否执行成功。若成功执行,则输出字母 Y;否则输出 N。
子任务
本题目各个测试点的数据规模如下:
表格中,目录层次是指各指令中出现的路径中,/ 字符的数目。
所有输入的数字均不超过10^18。
终于读完题了,题目超级长,而且信息量很大,需要对其内容进行一定的整理。有个题外话,数据结构课设助教说有一个实验是根据CSP某年T3改的,应该就是这题,要是我在考场上看见这题已经可以原地崩溃了。
题目分析
文件系统以树的结构表示,有两种节点类型,文件和文件夹。文件的属性为文件的大小,文件夹的属性为孩子配额和后代配额。
说到树,我脑海里想到的是指针建立的常规树。BUT学姐用数组存放节点,用map存放每个节点的孩子。文件和文件夹使用同一个结构体,使用type区分。
文件系统结构体内存放数组,路径和操作的目标名。我一开始很不理解为什么要把路径和目标明放在类内,要是让我写我百分之一百会写成参数。但是自己尝试着写成参数以后遇到了很多麻烦,在函数的层层调用时,要不厌其烦地传参数,很难找到一个平衡。
接着来看看操作,对其进行分类:
创建文件
第一个参数为路径,第二个参数为文件大小。
主要分为以下几种情况:
1.文件已经存在且为普通文件,则替换已有文件
2.同目录下有同名目录,创建失败
3.目录缺失,需要创建目录。如果有一个目录名与已存在文件重名则创建失败。
4.若添加文件后配额不匹配则创建失败。添加文件将改变其直接父目录直到根目录的已用配额。
删除文件/文件夹
1.当前路径不存在,不进行操作
2.当前路径存在,若删除的为文件夹,则也应移除其所有后代,若为文件则移除文件。应注意删除会影响目录当前已用配额
更改配额
1.当前路径不存在,更改失败
2.当前路径不是目录,更改失败
3.当前目录已有配额,则改变配额。若改变后的配额小于当前已用配额,则更改失败
4.应注意0意为不限制配额
在课上,我觉得学姐写的函数中较为重要的为寻找路径和定位当前位置的函数。在此做一下记录。
寻找路径
将输入的路径根据“/”进行分割,将结果存入数组中。所有路径的第一个元素都是“/”,先将其读出丢弃。若最终数组中没有元素,则说明当前操作针对根目录。若非空,则将最后一个元素弹出存储,该元素为操作的目标名称。
定位当前位置
每个目录都有一个map<string,int>记录其孩子在数组中的位置。从根节点开始,在map中找到相应孩子的位置进行跳转。若得到的位置为0,说明该孩子不存在。路径一定都是目录,若有一个路径名对应的节点类型为文件,此种路径也是不合法的。
以下为全部代码:
#include<bits/stdc++.h>
#define N 3000000
#define ll long long
using namespace std;
struct file{
int type;
ll ld;//目录配额
ll lr;//后代配额
ll nld;//当前使用的目录配额
ll nlr;//当前使用的后代配额
ll size;//文件的大小
map<string,int> child;//记录孩子的位置
file()
{
type=0;ld=0;lr=0;size=0;nld=0;nlr=0;
}
};
struct fsystem{
int cnt;//计数
file* filelist;//这里记录一个问题,学姐上课直接开的数组,我开数组的时候VS报.s文件出错
//百度了一下,说是爆栈了,建议开成全局变量
//还有一个方法是new一个数组
vector<string> path;
string name;
int root;//根目录位置
fsystem()
{
cnt=1;root=1;
filelist=new file[N];
}
~fsystem()
{
delete[] filelist;
}
void find_path(string str)
{
path.clear();//每次使用前先清空
name="";
stringstream ss(str);
string pa;
getline(ss,pa,'/');
while(getline(ss,pa,'/'))
path.push_back(pa);
if(path.size()>0)
{
string _name=path[path.size()-1];
path.pop_back();
name=_name;
}
}
int cd()
{
if(name=="")
return root;
int now=root;
for(auto it=path.begin();it!=path.end();it++)
{
now=filelist[now].child[*it];//每次都转到其孩子
if(now==0)//目录缺失
return 0;
if(filelist[now].type==1)//路径重名文件
return -1;
}
return filelist[now].child[name];//返回目标名称的位置
}
bool check_size(int p,ll size,bool isfather=false)
{//检查是否能使用配额
if(filelist[p].type==1)//文件没有配额之说
return false;
if(isfather&&filelist[p].ld!=0&&filelist[p].nld+size>filelist[p].ld)//如果是直属父亲目录还应该检查孩子配额
return false;
if(filelist[p].lr!=0&&filelist[p].nlr+size>filelist[p].lr)
return false;
return true;
}
void change_size(int p,ll size,bool isfather=false)
{
filelist[p].nlr+=size;
if(isfather)
filelist[p].nld+=size;
}
bool createfile(ll size)
{//创建文件
int now=root;
for(auto it=path.begin();it!=path.end();it++)
{
if(!check_size(now,size))//逐个检查是否可使用配额
{
return false;
}
if(filelist[now].child[*it]==0)//目录缺失则创建目录
{
filelist[now].child[*it]=++cnt;
}
now=filelist[now].child[*it];
}
if(!check_size(now,size,1))
return false;
filelist[now].child[name]=++cnt;
filelist[cnt].type=1;
filelist[cnt].size=size;
now=root;
for(auto it=path.begin();it!=path.end();it++)
{
change_size(now,size);//逐个使用配额
now=filelist[now].child[*it];
}
change_size(now,size,1);
return true;
}
void deletefile(int pos)
{//删除文件
if(pos>0)
{
ll _size=0;
int now=root;
bool isfather=false;
if(filelist[pos].type==1)//删除的为文件
{_size=filelist[pos].size;isfather=true;}
else _size=filelist[pos].nlr;//删除的为文件夹
for(auto it=path.begin();it!=path.end();it++)
{
change_size(now,-_size);//逐个使用配额
now=filelist[now].child[*it];
}
change_size(now,-_size,isfather);
filelist[now].child.erase(name);//删除该孩子
}
}
bool check_set(int pos,ll _ld,ll _lr)
{//检查是否能改变配额
if(pos<=0)
return false;
if(filelist[pos].type==1)
return false;
if(_ld!=0&&_ld<filelist[pos].nld)
return false;
if(_lr!=0&&_lr<filelist[pos].nlr)
return false;
filelist[pos].ld=_ld;
filelist[pos].lr=_lr;
return true;
}
};
int main()
{
int n=0;
cin>>n;
fsystem fs;
while(n>0)
{
n--;
char ch;
cin>>ch;
string p;
cin>>p;
fs.find_path(p);//我一开始把这两个函数写在每个if里了,事实证明
//这样做非常拉跨,非常混乱,还是跟着学姐做好公共的工作
int pos=fs.cd();
if(ch=='C')
{
ll size;
cin>>size;
if(pos==-1)//路径与文件重名
{printf("N\n");continue;}
else if(pos>0)//已有该文件名
{
if(fs.filelist[pos].type==1)//为普通文件
{
fs.deletefile(pos);
}
else//为文件夹
{printf("N\n");continue;}
}
if(fs.createfile(size))
printf("Y\n");
else
{
if(pos>0)
fs.createfile(fs.filelist[pos].size);
printf("N\n");
}
}
else if(ch=='R')
{
fs.deletefile(pos);
printf("Y\n");
}
else if(ch=='Q')
{
ll ld,lr;
cin>>ld>>lr;
if(fs.check_set(pos,ld,lr))
{
printf("Y\n");
}
else
printf("N\n");
}
}
}
在此记录一个问题。自己写的时候总是WA,对着回放看了几遍,改了一个delete后忘记回撤影响的错误,只多A了一个点。后来觉得自己delete函数过于冗余,精简了一下居然就AC了,看来看去都觉得只有写法的区别,实际上没啥影响来着,现在也还是想不通为什么改写了就对了。
改写前:
void deletefile(int pos)
{
ll _size=0;
int now=0;
if(pos>0)
{
if(filelist[pos].type==1)
{
_size=filelist[pos].size;
now=root;
int c=0;
for(auto it=path.begin();it!=path.end();it++)
{
change_size(now,-_size);
now=filelist[now].child[*it];
}
change_size(now,-_size,1);
}
else
{
_size=filelist[pos].nlr;
now=root;
for(auto it=path.begin();it!=path.end();it++)
{
change_size(now,-_size);
now=filelist[now].child[*it];
}
change_size(now,_size);
}
filelist[now].child.erase(name);
}
}
改写后:
void deletefile(int pos)
{
if(pos>0)
{
ll _size=0;
int now=root;
bool isfather=false;
if(filelist[pos].type==1)
{_size=filelist[pos].size;isfather=true;}
else _size=filelist[pos].nlr;
for(auto it=path.begin();it!=path.end();it++)
{
change_size(now,-_size);
now=filelist[now].child[*it];
}
change_size(now,-_size,isfather);
filelist[now].child.erase(name);
}
}
大模拟耗时间,又容易写错,遇到类似B题的我觉得还是要适当权衡一下,A题可以一试
-end-