1. 程式人生 > >清華OJ:PA3-3 重名剔除(Deduplicate)難題精解

清華OJ:PA3-3 重名剔除(Deduplicate)難題精解

分析:

題目涉及字串,提示用雜湊。不難想到雜湊碼轉換,根據鄧公教材上的方法,字串"x_{0}x_{1}...x_{n-1}"的雜湊碼取作x_{0}a^{n-1}+x_{1}a^{n-2}+...x_{n-2}a^{1}+x_{n-1},其中常數a>=2,但若用這一方法,得到的值會超出int範圍上限,不值得。故將上式改為x_{0}\times 1+x_{1}\times 2+...+x_{n-1}\times n+x_{n}\times (n+1),這樣遠遠不會達到int範圍上限。至於雜湊函式的設計,一般用除餘法即可,故可在得到雜湊碼後,直接除餘,餘數即散列表長,一般選用不小於輸入規模上限的素數即可,此題是600001。轉換後的雜湊碼可能重複,等效於不同關鍵碼(字串)對映到同一地址,故需要解決衝突。

解決衝突的方法很多,常用的是開放定址法的線性探測法,但是這一方法會導致超時,原因在於當出現衝突時,最壞情況下需要遍歷之前所有的非空桶單元,因此此法不妥。此題一般適用的方法是獨立鏈法,即將衝突的關鍵碼組織成列表放在對映到的桶單元中,發生衝突時,最壞情況下只需遍歷當前桶單元的所有衝突關鍵碼,相較於前一方法,時間成本得到了極大降低。

除了上述方法正確之外,還需要結合題目,此題要求重複的字串只輸出1次,故需要增設額外標誌,一般是在桶單元的每個槽位(衝突單元)中設定,可以設為布林型,初始化為假,若當前輸入的字串與對映到的字串重複,且當前對應的槽位標誌為假,則將其改為真,輸出這個字串,這樣之後重複的字串不會被輸出。

有了正確的基本方法和結合題目的特殊方法,一般可以通過。至於每個槽位中的資料域的選取,一般選取字串,而為了賦值方便,一般使用C++的string型別,但是這樣做通過後,會發現最壞用例耗時1200+ms。不難發現,其原因在於每次字串的賦值導致時間成本的迅速升高。於是,將資料域取字元指標,於是資料域的每次賦值只需O(1)的時間。經測試,此方法最壞用例僅耗時200+ms,相較於前一方法,效率提升了近5倍!

若槽位資料域選用字元指標,則需要注意,你需要在主函式之外定義一個二維字元陣列,原因有兩點,一是字元(串)指標需要取一個字元二維陣列的第1維,這樣就指向了一個字串,而這個字串的首字元地址是唯一的,這樣可以保證每個槽位字元指標不至於重複。二是陣列若在主函式之內定義,執行會出錯。最後注意陣列第2維度長度即字串最大長度,需至少為41,原因是字串需要有結束符。

掌握基本方法,利用好題目條件,注意和處理好和題目相關的所有細節,定能AC!

程式碼:

#include<cstdio>
#include<cstring>
#define HASHSIZE 600001
using namespace std;
struct Slot {//每個桶對應的槽位,儲存衝突,即對映到同一地址且不重複的字串(實則字元指標) 
	char* data;//資料項,儲存字元指標 
	bool repeat;//標誌,判別字串是否重複
	Slot* succ;//後繼 
}buckets[HASHSIZE];//桶陣列(散列表) 
char name[HASHSIZE][41];//字元二維陣列(必須開頭定義),儲存輸入字串,注意二維長度為40+1個結束符=41 
void Insert(int addr, char* s) {//在相應地址中插入衝突的字串(實則字元指標) 
	Slot* t = new Slot;
	t->data = s; t->repeat = false;//初始化當前字串從未重複 
	t->succ = buckets[addr].succ;//連結串列頭插法 
	buckets[addr].succ = t;
}
int HashCode(char* s) {//雜湊碼轉換(字串轉數字) 
	int sum = 0, len = strlen(s);
	for (int i = 0; i<len; i++)//多項式求和 
		sum += (i + 1)*(s[i] - 'a'+1);
	return sum;
}
int main() {
	int n;
	scanf("%d", &n);
	for (int i = 0; i < n; i++){		
		scanf("%s", name[i]);
		int addr = HashCode(name[i]) % HASHSIZE;//獲得對映到的地址 
		Slot* p = buckets[addr].succ;//從當前桶的第1個槽位開始 
		while (p)//遍歷所有槽位(衝突的單元) 
		if (!strcmp(p->data, name[i])) {//若當前槽位的字串重複 
			if (!p->repeat) {//檢查當前槽位的字串是否重複
				p->repeat = true;//若未重複,則標誌為已重複過 
				puts(name[i]);//輸出重複字串 
			}break;//若重複過,則忽略,無論是否重複過,皆終止遍歷 
		}
		else p = p->succ;
		if (!p)Insert(addr, name[i]);//若當前槽位空,則進行插入 
	}
	return 0;
}