本题为CCF CSP考试,2017年9月的第三题。具体题目在此不贴。(没做这题的肯定也看不到这篇文章是吧?)这道题属于模拟类的题目,做出它不需要太多的高级算法,只需要照着题目说明一步一步往前走就能写出来。所以很多人认为这样的题很简单,充其量只是对使用STL熟练度的考察。然而当他们熟练使用STL却仍然写出复杂冗长且逻辑不通的代码,写到后来都不知道在写啥,以至于提交分数很低甚至为0的时候,他们才会发现这样的题目,真正考验的是大家对于“分析问题”、“流程”、“逻辑”的理解与控制。我认为这是一个比较有难度的学问。我也并没有掌握这方面的诀窍,即使这题我能分析出来,下一题也未必,不过这次让我们专注于这一题。下面我分享一下做这道题的思路,以及最终提交满分的C++代码。
首先,这道题的意图很直接,就是让我们根据输入数据,产生一系列的键值对用于查询。接下来给出查询的键,让我们输出值。当然这题也可以在每次查询时,遍历整个输入数据,查找出对应的值。但我认为不如前一种方法来得方便与高效。因此我采用前面的方法实现。那么,如何存储键值对呢?——使用C++标准库的map可以轻易做到。
map mp;
确定了主要思路以后,接下来就按流程一个个地去解决吧。第一步,要先根据输入存储键值对。键一定是字符串,而值则可能是字符串,可能是对象。根据题目的要求,值为对象的键也有可能被查询。于是无论值是什么类型,这对键值都应该存到map里。所以我们先不考虑嵌套查询,将对象也视作字符串存进map里。下一步我们就需要思考,对于如此不规则的输入数据(存在各种空格及换行符用于美观),我们如何能从中分离出键值?为了避免空白(包括换行符)带来的影响,我们必须把空白从输入中去除,将输入浓缩为一个字符串,然后才能分离键值。预处理的代码如下:
int n = 0, m = 0;
cin >> n >> m;
string s, json;
// 将所有输入去空格,结合起来,赋给json进行处理
string::iterator it;
getline(cin, s);
for (int i = 0; i != n; ++i){
getline(cin, s);
for (it = s.begin(); it != s.end();)
if (isspace(*it))
it = s.erase(it);
else
++it;
json += s;
}
接下来开始分离键值。在题目中,键值之间用“:”分隔,键值对之间用“,”分隔,对象用“{}”包含。于是我做了一个假定,认为“:”,“,”,“{”,“}”只用于分隔,不会出现在字符串的内容中。(如果不这么考虑,这个分离的逻辑可能会非常复杂,所以抱着即使不能满分也要这么写的念头就这样了23333……)于是,从第一个单引号开始,想找key就去找“:”,再想找value就去找“,”,用string.find()函数查找,而获取key和value则可以用string.substr()函数获取子串。
但这里要考虑两个问题:1、最后一组键值对后是不存在“,”的。2、如果value是个对象,那它里面是会有“,”的,这样找“,”就不好使了。第一个问题好办,既然最后一组了,那value就取到最后好了。第二个问题则促使我们将字符串与对象区分开,如果是字符串,value照常找“,”,但如果是对象,即“:”后面是“{”,那就必须要找到与其匹配的“}”,才算找到了value。至于经典的括号匹配问题,用栈的思想能比较容易理解。当连续遇到“{”时,不断地将其压入栈中,而遇到“}”时则弹出栈顶元素,这样下去直到栈空,即匹配成功。
int count = 1;
while (count != 0){
if (json[i] == '{')
++count;
else if (json[i] == '}')
--count;
++i;
}
到这一步基本上已经能够把第一层键值对都存储好了,那么现在要开始考虑多层查询的问题了:当value是对象的时候怎么办?注意到对象本身是一个键值对的集合,也就是说,对象也是一种json数据。所以处理对象的方式和我们刚才处理整个json数据的方式是一样的,因此“调用自己”就好了,即“递归”。这里注意一下递归的时候要传入当前的key,这样新的key才能是key.key1,否则就只会是key1。
当递归到最底层时,一定是key与value都为字符串了。这时要对字符串进行标准化。什么是标准化呢?注意到题目里说明了字符串中可能含有转义字符" \\ "以及" \" ",而题目要求我们处理掉这个反斜杠。这个过程当然很简单。
void format(string &s)
{
for (int i = 0; i < s.size(); ++i)
// 如果遇到反斜杠,删除它并且跳过下个字符
if (s[i] == '\\')
s.erase(s.begin() + i);
}
最后,map的内容已经成型,接下来的任务就仅仅是map.find(),以及输出了,非常轻松。总结一下我们整个程序的流程吧:
(1)对输入预处理,将多行去空白,整理成一个字符串json用于处理,向deal()函数传入json。
(2)从第一个双引号(第一个字符串的开端)开始,先找“:”获取key,接着再获取value。如果“:”的下一位是双引号,说明value是字符串,找“,”(如果没有“,”直接定位到末尾);反之则下一位一定是“{”(输入保证合法),说明value是对象,去找下一个与其匹配的大括号。
(3)如果value是字符串,对key和value进行标准化;如果value是对象,对该对象递归调用deal()函数。无论value是什么,将
(4)对每个查询,在map中查找对应的键是否存在,输出相应的信息。
下面是完整的代码。大家可以从main函数开始看,这样比较容易理解整个程序的逻辑。这里为了代码的简洁,补充注释一下:1、获取字符串时,会掐掉头尾的双引号,而获取对象时则会保留头尾的花括号;2、各个find()与substr()的具体含义不进行解释,请自行了解C++标准库sting的相关函数;3、每次获取键值对后都能保证 i 的位置在该键值对的后面,也就是表明这个键值对已经处理完毕,之后不会再访问到了。
#include
#include
#include
最后我想提一个小小的建议,就是在做这样流程复杂,代码庞大的程序题时,不妨在完成一些部分时便采用一些测试数据测试你的程序与逻辑是否正确,而不要等到一口气写完才发现错误却无从下手。