原题链接
先说一下今年的题目特点,那就是并没有考到复杂的算法,而是考了一些应用性较强的内容,比如第三题的字符串处理。很多OI选手对字符串处理都不擅长,导致这道题失分。其实,今年的题目还是很简单的。下面,我们来探讨一下这道题的几种解法。(只介绍字符串处理部分,其他地方太简单,正常人都能写出来)
一、正则表达式匹配
先介绍一下正则表达式:(详细教程看这里)
正则表达式是匹配字符串的一种模式。经常用到的语法有:
字符 | 意义 |
---|---|
+ | 表示前面的字符至少出现1次,无上限。 |
* | 表示前面的字符至少出现0次,无上限。 |
? | 表示前面的字符只能出现0或1次。 |
[] | 匹配括号中的字符之一。如果字符在一个区间里,可以用-连接,如[0-9]表示匹配一个数字,[a-zA-Z]表示匹配一个字母。 |
. | 匹配任意非换行的字符。 |
\ | 转义字符,所有在该表格内的字符(当然还包括其它的),如果想匹配它,必须前面加上一个\。而一些普通字符加上了\,就有了特殊含义。注意,在C++中,\本来就表示转义字符,所以要写两个\,比如匹配一个星号,要写"\\*" 。 |
{m,n} | 表示前面的字符至少出现m次,最多出现n次。 |
{m,} | 表示前面的字符至少出现m次,无上限。(注意,m后面有逗号) |
{m} | 表示前面的字符只能出现m次。 |
\d | 匹配一个数字,即[0-9]。 |
() | 有两个作用,第一个是表示一串字符是一个整体,第二个是匹配后可以查看括号里具体匹配了什么。 |
除了有特殊含义的字符,其它的字符的正则就是它本身,比如想匹配"hello"
,正则就是"hello"
。
要注意的是,*,+,{}都是贪婪的,也就是说会尽可能匹配更多的字符,如对字符串"你好123"
使用正则"(.*)([0-9]+)"
,结果并不是"你好","123"
,而是"你好12","3"
。想避免这种情况,要在它们后面加一个 ? ,如上例,如果用"(.*?)([0-9]+)"
,结果就是"你好","123"
了。但是,这些都有一个前提,那就是必须先保证匹配成功,才考虑贪婪不贪婪的问题。如"(.*?)([0-9]+)"
,尽管它不是贪婪的,但.*还是会匹配"你好"
,而不是一个字符也不匹配,因为那样就无法匹配成功了。
接下来,我们学习一下C++中的正则:C++从C++11开始支持正则,而今年的CSP-J标准是C++14,所以说可以使用。使用正则,首先要包含这个头文件:
#include<regex>//STL的一部分,包含关于正则的内容。
C++中关于正则主要有regex_search和regex_match这两个函数,前者要求字符串部分匹配,后者要求字符串完全匹配。还有一个容器,是smatch容器,用于存储正则匹配的结果。
我们来看一下代码(主函数是我写的测试,包含了我能想到的所有错误情况,并不是题解):
#include <iostream>
#include<regex>
bool match(const std::string& str)
{
std::smatch m;
if (std::regex_match(str, m, std::regex("([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\:([0-9]{1,5})")))
{
for (size_t i = 1; i < m.size(); ++i)//注意,这里的i要从1开始,因为smatch容器第一个是匹配的字符串整体
{
const std::string& s=m.str(i);
if ((s.size() > 1 && s[0] == '0')/*前导0*/ || (i != 5 && s > "255") || (s > "65535")/*超出范围*/)
{
return false;
}
}
return true;
}
return false;
}
int main()
{
const std::string strs[] = {
"127.0.0.1:185","255.255.255.255:65535","0.0.0.0:0","256.1.1.1:1","125.0.0.0:1288a",
"hehe","呵呵","127.0.0:1","7:7:7:7.7","05.04.5.7:8","999999999.99999.9999999.999999:-888"};
for (const auto& s : strs)
{
std::cout << match(s) << '\n';
}
return 0;
}
运行结果:
二、sscanf匹配
sscanf函数大家应该熟悉,就是从字符串输入元素,代码很简单,唯一要注意的是,必须用%d输入,%s无法正确匹配(因为第一个就会把所有字符都匹配)。所以这样就有些麻烦了,判断前导0必须求出每个数字的位数,然后一加,和原字符串长度比较。另外,sscanf返回值是成功读入数据数量。
代码:
#include <iostream>
#include<regex>
#include<functional>
bool match(const std::string& str)
{
std::function<int(int)>func([](int x) {
int i = 1;
while (x /= 10)
i++;
return i;
});
int ip[5];
if (sscanf(str.data(), "%d.%d.%d.%d:%d", &ip[0], &ip[1], &ip[2], &ip[3], &ip[4]) == 5)
{
for (int i = 0; i < 5; i++)
{
if ((i != 4 && ip[i] > 255) || ip[i] > 65535)
{
return false;
}
}
int sum = 4;
for (int i = 0; i < 5; ++i)
{
sum += func(ip[i]);
}
return sum == str.size();
}
return false;
}
int main()
{
const std::string strs[] = {
"127.0.0.1:185","255.255.255.255:65535","0.0.0.0:0","256.1.1.1:1","125.0.0.0:1288a",
"hehe","呵呵","127.0.0:1","7:7:7:7.7","05.04.5.7:8","999999999.99999.9999999.999999:-888"};
for (const auto& s : strs)
{
std::cout << match(s) << '\n';
}
return 0;
}
运行结果:
三、手写
自己写个代码来匹配,很麻烦,不推荐使用。
几种算法的比较
正则和sscanf这两个在时间复杂度上,都差不多,单看匹配的速度,sscanf应该比正则快 (等等,我突然发现sscanf好像也支持正则…) ,但sscanf需要求出位数,所以应该不相上下。
而自己手写的,应该是最快的,一般不会超时,但很浪费答题时间,而且还可能有bug,所以还是老老实实用库函数好…