题目描述
分析
这道题算法已经给出,主要考察数据结构和字符串的处理。代码量比较大,实现起来比较繁琐,数据结构的设计也比较困难,需要有一定的编码能力才能在规定的时间内做完。并且这种代码量很大的题目,也需要细心和较强的debug能力,能够较好地检测出做题人平时的代码积累。
读完题目后首先看一下数据范围,数据还是比较大的。如果使用string来处理IP地址可能会超时,所以本题适合使用整数来处理IP地址。题目中已经说明IP地址是32位无符号整数,所以不用担心会爆long long int。而为了方便输出,可以使用一个数组来存储每个点分十进制的整数,另外使用一个变量存储前缀值。数据结构设计如下:
struct IP
{
int addr[4];//点分十进制整数
int length;//前缀值
long long int lli;//IP地址的整数表示
IP():length(8), lli(0){}//构造函数,把length初始化为8,与后面处理输入的函数有关
};
然后需要考虑使用什么数据结构来存储输入的IP地址。因为后续操作会大量的删除和增加元素,并且不需要随机访问元素,所以采用链表来存储比较合适。STL库中有list双向链表可以使用,不熟悉的读者可以先去看一下文档,熟悉里面有哪些函数。其实大多数操作都和其他STL容器相同,这里主要提一些不同的点。首先是容器的排序,其他常见的容器直接使用sort进行排序,而list的排序是使用容器自带的sort函数,这一点需要注意。另外,比较容易出错的是元素的删除erase函数。list的erase函数是有返回值的,并且返回的是删除元素的下一个元素。erase函数的返回值需要使用迭代器保存,否则会出现链表断裂的现象,这一点很容易出错,一定要注意。
erase函数的常见使用如下:
#include<list>
using namespace std;
int main(void)
{
list<int> test;
test.push_back(1);
test.push_back(2);
test.push_back(3);
for(list<int>::iterator it = test.begin(); it != test.end();)
{
if(*it == 2)
{
it = test.erase(it);
}
else
{
++it;
}
}
return 0;
}
另一种常见用法如下:
#include<list>
using namespace std;
int main(void)
{
list<int> test;
test.push_back(1);
test.push_back(2);
test.push_back(3);
for(list<int>::iterator it = test.begin(); it != test.end();)
{
if(*it == 2)
{
test.erase(it++);
}
else
{
++it;
}
}
return 0;
}
一种常见的错误使用如下:
#include<list>
using namespace std;
int mian(void)
{
list<int> test;
test.push_back(1);
test.push_back(2);
test.push_back(3);
for(list<int>::iterator it = test.begin(); it != test.end(); ++it)
{
if(*it == 2)
{
test.erase(it);
}
}
return 0;
}
具体的错误分析详见《Effective STL》一书。
主要的数据结构已经说完了,另外还需要一个全局数组,倒序保存着2的幂次,方便后面是否进行合并的判断。如果想要更加快速地计算,可以在判断时使用移位运算,这个后面会具体说明。这样所有的数据结构已经说完了,下面就进行算法的实现。
首先是输入的处理,这个应该是这道题最为繁琐的步骤,因为要能够处理三种不同的输入。从最复杂的输入入手,也就是标准型输入。因为有四个不同的整数代表点分十进制,所以想到可能会循环处理四次。在每次处理时使用字符串查找和分割,string的find函数和substr函数。每次查找IP地址的分隔符,然后将原始字符串更新为后面的子串。接着考虑前缀值的处理,同样使用find函数进行查找“/”,然后使用substr函数进行分割处理。另外还需要考虑其他两种输入的处理,大体思路是使用某个标志来判断输入的类型,然后根据不同的输入类型进行不同的输入处理。在此过程中还要填充数据结构中的值,这又会导致情况的多样性,需要仔细考虑处理逻辑和特殊情况,这里也是bug经常出现的地方。还需要了解string库中的stoi函数能够方便地将string转化为int,这个函数会经常用到,需要掌握。
输入处理完成后,就真正开始算法的实现。一个好的输入处理思路,能够大大降低算法的实现难度。首先是排序,这个很简单,调用list的sort函数即可。这个函数有两种形式,无参类型和有参类型。有参类型的参数可以是函数指针或者是lambda表达式。有关这两种参数的说明,读者可以自行查看文档,这里不再说明。
排序完成后,就开始第一次合并。经过了第一步的排序以后,能够保证链表中前面的元素不可能是后面元素的子集,因此只需要判断后面的元素是否是前面元素的子集即可。第一次合并的难点在于判断子集关系,这里使用IP地址的整数形式表示能够快速地判断。判断B是否是A的子集,只需要判断到A的前缀值为止的每一位的值,A和B是否相同。注意,如果A和B相等,那么B也是A的子集。只有这样,B才可能是A的子集。如果使用整数来判断,只需要将A和B同除以某个2的幂次,或者同时右移某个相同的位数即可。相当于将前缀值后面的位去除掉,判断前缀值前面的位所表示的整数值是否相等。这样进行比较更加方便快捷,如果使用字符串进行比较,需要一位一位地进行,速度较慢。判断是否是子集后,就可以进行删除操作或者继续遍历。list的删除操作比较简单,注意到前面提到的问题即可。
接着是第二次合并,第二次合并的难点在于判断是否是并集,这个就需要判断两个范围能否进行合并。这个使用整数来判断也比较简单,和上一步判断子集是差不多的,只需要比上一步判断的位数少一位即可。因为每一个二进制位只能表示0和1,而两个IP地址进行比较,判断所表示的范围能否进行合并,就是前面的所有二进制位都相同,前缀值所在的位不同。前缀值所在的位只能一个是0,一个是1。所以这里也是将A和B同时除以某个2的幂次,或者同时右移某个位数即可判断。判断并集以后就可以进行链表的插入和删除,这里可以只删除后面的一个元素,然后修改前面元素的值,就省去了插入元素的步骤。这里还有一点需要注意,题目中也说明了,如果插入的元素前面还有元素,那么需要从前面一个元素开始继续遍历。
最后遍历链表进行输出即可。因为在设计数据结构时使用了一个数组来存储点分十进制的整数,所以输出时不需要进行处理,直接输出即可。
代码如下:
#include<iostream>
#include<string>
#include<list>
#include<algorithm>
#include<cmath>//pow函数
using namespace std;
long long int A[33];//倒序存放2的幂次
struct IP
{
int addr[4];//点分十进制整数
int length;//前缀值
long long int lli;//IP地址表示的整数
IP():length(8), lli(0){}//构造函数,前缀值初始化为8,为了处理省略前缀值的输入
};
struct IP InputHandle(string str)//处理输入的函数
{
struct IP ip;
//标志,判断剩下的string中的值代表的是IP地址还是前缀值
//因为有缺省前缀值的输入,这时输入中就没有前缀值
bool flag = false;
for(int i = 0; i < 4; ++i)//点分十进制是4位整数,所以循环4次
{
int index = str.find(".");
if(index != -1)//有.字符
{
string temp = str.substr(0, index);//.前面的数字
ip.addr[i] = stoi(temp);//转化为整数
//length初始化为8,有一个.就说明前缀值至少为16
//所以最开始length需要初始化为8
//因为有可能前缀值会省略,这时需要自己计算
ip.length += 8;
//计算IP地址所表示的整数,注意这里需要强转为long long int
//因为pow函数的返回值是double
ip.lli += (long long int)pow(256, 3-i) * ip.addr[i];
str = str.substr(index+1);//截取后面的子串
}
else//没有.字符
{
index = str.find("/");
if(index != -1)//有前缀值
{
string temp = str.substr(0, index);
ip.addr[i] = stoi(temp);//前缀值前面剩下的数字
ip.lli += (long long int)pow(256, 3-i) * ip.addr[i];
str = str.substr(index+1);//后面还剩的前缀值
flag = true;//表示有前缀值
}
else//没有找到/,不代表没有前缀值,有可能前面已经处理了字符串,所以剩下的字符串中没有/
{
if(flag)//有前缀值
{
ip.length = stoi(str);
//注意需要将输入设置为0
//因为如果输入缺省,需要将点分十进制后面的值设置为0
str = "0";
flag = false;//表明前缀值已经处理
}
else//没有前缀值
{
ip.addr[i] = stoi(str);//设置点分十进制后面的值
//程序运行到这里,说明str中已经没有有效值了
//所以需要设置为0
str = "0";
if(ip.addr[i] != 0)//计算IP地址的整数值
{
ip.lli += (long long int)pow(256, 3-i) * ip.addr[i];
}
}
}
}
}
return ip;
}
bool compare(const struct IP& a, const struct IP& b)//排序使用的比较函数
{
if(a.lli != b.lli)
{
return a.lli < b.lli;
}
else
{
return a.length < b.length;
}
}
bool IsChildSet(const struct IP& a, const struct IP& b)//判断子集
{
if(a.length > b.length)
{
return false;
}
else if(a.lli/A[a.length] != b.lli/A[a.length])//相当于比较前缀值及其之前的二进制位是否相同
{
return false;
}
return true;
}
int merge1(list<struct IP>& list)//第一次合并
{
//i,j分别表示链表的前后两个元素
auto i = list.begin(), j = list.begin();
++j;
for(;j != list.end();)
{
if(IsChildSet(*i, *j))//是子集
{
j = list.erase(j);//删除j所指向的元素
}
else//不是子集,继续遍历
{
++i;
++j;
}
}
return 0;
}
bool CanMerge(const struct IP& a, const struct IP& b)//判断并集
{
if(a.length != b.length)
{
return false;
}
else if(a.lli/A[a.length-1] != b.lli/A[a.length-1])//相当于比较前缀值之前的二进制位是否相同
{
return false;
}
return true;
}
int merge2(list<struct IP>& list)//第二次合并
{
//i,j分别表示链表的前后两个元素
auto i = list.begin(), j = list.begin();
++j;
for(;j != list.end();)
{
if(CanMerge(*i, *j))//判断并集
{
j = list.erase(j);//删除后一个后元素
--(*i).length;//修改前一个元素的值
if(i != list.begin())//相当于判断插入的元素前是否还有元素
{
--i;
--j;
}
}
else//不能合并,继续遍历
{
++i;
++j;
}
}
return 0;
}
int main(void)
{
A[32] = 1;//计算2的幂次,倒序存放
for(int i = 31; i >= 0; --i)
{
A[i] = 2 * A[i+1];
}
long long int n = 0;
cin >> n;
string str;//输入
list<struct IP> IpList;
for(int i = 0; i < n; ++i)
{
cin >> str;
IpList.push_back(InputHandle(str));//插入链表
}
IpList.sort(compare);//排序
merge1(IpList);//第一次合并
merge2(IpList);//第二次合并
//输出
for(list<struct IP>::iterator it = IpList.begin(); it != IpList.end(); ++it)
{
cout << it->addr[0] << "." << it->addr[1] << "." << it->addr[2] << "." << it->addr[3] << "/" << it->length << endl;
}
return 0;
}