这是我的第二篇博客,小学期的程序设计课程设计告一段落,内容是完成201912的CSP认证考题,我完成了前三题。攻克难题还是非常有成就感的,所以发博客纪念一下。最近也是在进行ACM的训练,希望自己能再接再厉,不断提高编程能力。
题目内容:判断化学方程式是否配平
[输入格式]
从标准输入读入数据。
输入的第一行包含一个正整数n,表示输入的化学方程式个数。接下来n行,每行是一个符合定义的化学方程式。
[输出格式]
输出到标准输出。
输出共n行,每行是一个大写字母Y或N,回答输入中相应的化学方程式是否配平。
[样例输入]
11
H2+O2=H2O
2H2+O2=2H2O
H2+Cl2=2NaCl
H2+Cl2=2HCl
CH4+2O2=CO2+2H2O
CaCl2+2AgNO3=Ca(NO3)2+2AgCl
3Ba(OH)2+2H3PO4=6H2O+Ba3(PO4)2
3Ba(OH)2+2H3PO4=Ba3(PO4)2+6H2O
4Zn+10HNO3=4Zn(NO3)2+NH4NO3+3H2O
4Au+8NaCN+2H2O+O2=4Na(Au(CN)2)+4NaOH
Cu+As=Cs+Au
[样例输出]
N
Y
N
Y
Y
Y
Y
Y
Y
Y
N
①主要数据结构设计:
map<string,int> leftcnt,rightcnt;
②算法设计:
1、输入化学方程式fangcheng,找到等号的位置,将fangcheng分成左右两部分left和right,对两部分各自进行处理,过程相同。
2、对等号一边进行处理,首先在最前面添加“+”,这样所有含系数的分子式就统一跟在“+”后面,便于进一步对每个分子式进行处理。
3、对每个含系数的分子式,首先提取它的系数xishu,再提取不含系数的分子式fenzishi,然后调用函数void deal(int lr,int
xishu,string fenzi)进行处理。
4、deal函数当中,lr参数代表这是化学方程式的左半部分还是右半部分。提取分子式中所含的所有原子,存入map类型的tempcnt中。然后遍历tempcnt,对每个原子回到原来的分子式中从头到尾查找,每次找到包含该原子的位置以后,从分子式的右端往左端查找将其包含在内的圆括号,提取括号后的后缀下标,以及原子后的后缀下标,由此计算出每个原子的数量。最终再乘上之前提取的系数,加到总的计数器leftcnt或rightcnt当中。
5、最终判断leftcnt和rightcnt两个map是否完全相同,相应地输出“Y”或“N”,就实现了题目的要求。
这道题目的思路其实就是把一个复杂的大问题逐步分解成简单的小问题,然后由小及大,一步步地解决问题,代码当中我也作了详细的注释,总共有30多行注释,我也正是这样逐步将这道题目完成的。思路其实不难想到,但真正写代码实现还是相当有挑战性的,还要充分考虑各种特殊情况。后面的小结中我会写一些自己做题的感悟。
100分代码:
#include <bits/stdc++.h>
using namespace std;
map<string,int> leftcnt,rightcnt;//将等号左右两边所有原子个数分别存入map中进行计数
void deal(int lr,int xishu,string fenzi)//对分子式(去掉前面的系数)处理
{
map<string,int> tempcnt;
for(int i=0;i<fenzi.length();i++)
{
if(isupper(fenzi[i]))
{
string t;
t+=fenzi[i];
if(i+1<fenzi.length())
if(islower(fenzi[i+1]))
t+=fenzi[i+1];
tempcnt[t]=1;
}//将所有原子存入tempcnt中
}
map<string,int>::iterator it;
for(it=tempcnt.begin();it!=tempcnt.end();it++)
{
string yuanzi=it->first;
int pos=0,sum=0,wz=0;
while(pos<fenzi.length()&&wz!=-1)
{
int geshu=1;
wz=fenzi.find(yuanzi,pos);
if(wz!=-1)
if(yuanzi.length()==2||(yuanzi.length()==1&&!islower(fenzi[wz+1])))//排除比如要查找的原子是N,而分子式中是Na的情况
{
for(int i=fenzi.length()-1;i>=wz+yuanzi.length();i--)
{
if(fenzi[i]==')')
{
int flag=1,kuohao=1;
for(int j=i-1;j>=0;j--)
{
if(fenzi[j]=='(')
kuohao--;
else if(fenzi[j]==')')
kuohao++;
if(kuohao==0)
{
if(j>wz)
flag=0;
break;
}
}//判断当前查找到的原子是否在这对括号内
if(flag==1)
{
int houzhui=0;
string hz;
if(i<fenzi.length()-1)//样例中有如Na(Au(CN)2)最后括号后面没有后缀的分子式
{
for(int k=i+1;k<fenzi.length();k++)
{
if(fenzi[k]>='0'&&fenzi[k]<='9')
hz+=fenzi[k];
else break;
}
for(int k=0;k<hz.length();k++)
{
houzhui*=10;
houzhui+=(hz[k]-'0');
}//提取右括号后的后缀,用houzhui记录
}
if(houzhui==0)
houzhui=1;//若右括号后没有后缀,则后缀为1
geshu*=houzhui;//原子个数乘以后缀
}
}//从分子式右端往左查找“)”
}
int hou=0;
string h;
if(wz+yuanzi.length()<fenzi.length())
{
for(int k=wz+yuanzi.length();k<fenzi.length();k++)
{
if(fenzi[k]>='0'&&fenzi[k]<='9')
h+=fenzi[k];
else break;
}
for(int k=0;k<h.length();k++)
{
hou*=10;
hou+=(h[k]-'0');
}//提取原子后的后缀,用hou记录
}
if(hou==0)
hou=1;//若原子后没有后缀,则后缀为1
geshu*=hou;//原子个数乘以后缀
sum+=geshu;
}
pos=wz+1;
}//原子可能在分子式中多次出现,所以要挨个查找
sum*=xishu;
if(lr==1)
leftcnt[yuanzi]+=sum;
else if(lr==2)
rightcnt[yuanzi]+=sum;
}//对tempcnt中每个原子在原分子式中进行查找和处理
return;
}
int main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int n;
cin>>n;
while(n--)
{
leftcnt.clear(),rightcnt.clear();//清空map
string fangcheng,left,right;
cin>>fangcheng;//输入化学方程式
int equal;
for(int i=0;i<fangcheng.length();i++)
if(fangcheng[i]=='=')
{
equal=i;
break;
}//找到等号的位置,用equal记录
for(int i=0;i<equal;i++)
left+=fangcheng[i];
for(int i=equal+1;i<fangcheng.length();i++)
right+=fangcheng[i];//将方程式分为左右两部分,分别用left、right保存
left.insert(0,"+");//使每个分子式(含系数)前都是“+”,便于处理
for(int i=0;i<left.length();i++)
{
if(left[i]=='+')
{
int xishu=0,p;
string xs;
for(int j=i+1;j<left.length();j++)
{
if(left[j]>='0'&&left[j]<='9')
xs+=left[j];
else
{
p=j;
break;
}
}
for(int j=0;j<xs.length();j++)
{
xishu*=10;
xishu+=(xs[j]-'0');
}//提取每个分子式前的系数,用xishu记录
if(xishu==0)
xishu=1;//若分子式前不含系数,则系数为1
string fenzishi;
for(int j=p;j<left.length()&&left[j]!='+';j++)
fenzishi+=left[j];//提取不含系数的分子式
deal(1,xishu,fenzishi);
}//对每个分子式进行处理
}//处理等号左半边
right.insert(0,"+");//使每个分子式(含系数)前都是“+”,便于处理
for(int i=0;i<right.length();i++)
{
if(right[i]=='+')
{
int xishu=0,p;
string xs;
for(int j=i+1;j<right.length();j++)
{
if(right[j]>='0'&&right[j]<='9')
xs+=right[j];
else
{
p=j;
break;
}
}
for(int j=0;j<xs.length();j++)
{
xishu*=10;
xishu+=(xs[j]-'0');
}//提取每个分子式前的系数,用xishu记录
if(xishu==0)
xishu=1;//若分子式前不含系数,则系数为1
string fenzishi;
for(int j=p;j<right.length()&&right[j]!='+';j++)
fenzishi+=right[j];//提取不含系数的分子式
deal(2,xishu,fenzishi);
}//对每个分子式进行处理
}//处理等号右半边
bool flag=1;
if(leftcnt.size()!=rightcnt.size())
flag=0;
else
{
map<string,int>::iterator it;
for(it=leftcnt.begin();it!=leftcnt.end();it++)
{
map<string,int>::iterator itt;
itt=rightcnt.find(it->first);
if(itt==rightcnt.end())
flag=0;//没找到相应的原子
if(itt->second!=it->second)
flag=0;//相应的原子个数不同
}
}//最后判断leftcnt和rightcnt是否完全相等,即是否配平
if(flag)
cout<<"Y"<<endl;
else
cout<<"N"<<endl;
}
return 0;
}
小结
真的要感叹一句“功夫不负有心人”啊。周一晚上我足足用了有四个小时,按照自己的思路写了两百多行代码,然后还遇到各种各样的问题。我在纸上打草稿,发现只对原程序进行调试还不够,并不能解决一些问题,我又分块编写了多个程序,进行多组样例的测试,分步检验程序能否得到正确的结果。最终竟然真的满分通过了!
前几天我做了一定的思考,但还没有进行写代码的尝试。晚上我静下心来挑战,终于攻克了一道目前而言还比较难的题目。我的思路大致是这样的:输入化学方程式以后,首先找到等号的位置,分成左右两部分,分别处理;它们各自又可以被加号分成几个小部分,每个小部分又可以分成前缀系数及分子式,分子式又能分成原子,当然还带有括号和后缀下标数字。其实就是一个不断地把复杂问题简单化,然后从小到大逐步来解决问题。
我又上网看了一些别人写的题解,看看其他人的思路是怎么样的,有些内容其实看起来还是有些费劲的。一篇经典的题解当中说这道题类似编译原理中的递归下降法,其实思路和我的差不多,不过可以采用层层调用函数的方式,使代码更加简洁清晰。这也是我下一阶段练习所要努力的方向。这道题可以说是我进入大一到现在写过的算法题里最难的题之一了,做出来真的非常有成就感。虽然花的时间有点多,如果是正式考试可能还有一定的困难,但至少以后碰到类似的题目不会再有害怕的想法。
整个做题的过程必须思维缜密,而且要非常细心,很多细节一旦没考虑到就会导致错误。比如原子在分子式中可能多次出现;存在例如要查找的原子是N、而分子式中是Na这样的情况;存在形如Na(Au(CN)2)这样最后括号后面没有后缀下标的分子式;前缀系数及后缀下标不存在时要默认设置为1;处理括号时要判断当前原子是否包含在这对括号当中,等等。从有了完整的思路到最终完成题目还是需要相当有耐心的,这也正是题目的考验所在。