CCF认证:CIDR合并

题目描述

分析

这道题算法已经给出,主要考察数据结构和字符串的处理。代码量比较大,实现起来比较繁琐,数据结构的设计也比较困难,需要有一定的编码能力才能在规定的时间内做完。并且这种代码量很大的题目,也需要细心和较强的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

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

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

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
#include
#include
#include
#include//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;
}

你可能感兴趣的:(CCF)