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<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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章