Princeton Algorithms, WordNet

Princeton Algorithms, Part II, WordNet

普林斯頓大學算法課 WordNet 題解與代碼

首先要理解什麼是 WordNet,這裏定義了同義詞集、下位詞、上位詞、邏輯門等計算語言學的複雜概念,確實不太好懂,但是總體上說,它是一個有根的有向無環圖 a rooted DAG,但是它不一定是樹。

同義詞集列表中第一個字段是 id,第二個字段是同義詞集,構成同義詞集的各個名詞之間用空格分隔,第三個字段與本次作業無關。

上位詞列表中第一個字段表示同義詞的 id,後續字段是改同義詞的上位詞 id 號,一個詞可以有多個上位詞,所以可能有多個字段,這就相當於是確定了 DAG 中的邊的關係。

在 WordNet 的類的構造中,參數爲 null 或者單詞不是 WordNet 中有效的單詞,給出異常,這個比較好做。難以理解的是 The input to the constructor does not correspond to a rooted DAG. 這個要求,我們如何才能確定自己構造的是不是一個有根的有向無環圖?實際上,algs4.jar 中的 Digraph(有向圖)給出了 2 個函數,indegree() 和 outdegree() 可以很方便地計算一個頂點的入度和出度,我們可以通過這個來判斷是不是一個 rooted DAG。

除了是不是單根,還需要調用 DirectedCycle 或者 Topological 去檢查是不是有環。

private void validate(Digraph g) {
	assert g != null;
	int vertexNumber = g.V();
	int rootNumber = 0;
	for (int i = 0; i < vertexNumber; i++) {
		// 出度爲 0 的點是根節點(沒有上位詞的同義詞集)
		if (g.outdegree(i) == 0) {
			rootNumber++;
		}
	}
	// 根節點不足 1 或者大於 1 都不滿足條件
	if (rootNumber != 1) {
		throw new IllegalArgumentException();
	}
	// The program uses neither 'DirectedCycle' nor 'Topological' to check whether the digraph is a DAG.
	DirectedCycle dc = new DirectedCycle(g);
	if (dc.hasCycle()) {
		throw new IllegalArgumentException();
	}
}

最近公共祖先比較好理解,推廣到頂點的集合也很簡單,就是找到所有 SAP 的最短的。

求公共祖先的過程是一個廣度優先搜索的過程。

while (!q.isEmpty()) {
	int x = q.poll();
	Iterable<Integer> bag = g.adj(x);
	// 加入後面的點
	for (int vv : bag) {
		if (!visited[vv]) {
			q.add(vv);
			visited[vv] = true;
			// 更新距離
			int d = distanceV.get(x);
			int dd = distanceV.getOrDefault(vv, d + 1);
			distanceV.put(vv, dd);
		}
	}
}

具體的策略是,先對 v 點做一次廣搜,直到根結點,在每一次搜索的時候記錄下 depth。

然後對 w 做一次廣搜,搜索過程中遇到符合條件的(搜索過的),都是祖先,記錄下 depth 並相加,取最小的 depth 就是最近公共祖先。

最多對所有的點訪問 2 次(從 v 出發一次,從 w 出發一次),所以時間複雜度只與點的個數有關。

if (distanceV.containsKey(x)) {
	// 更新最短的 LCA
	int minDistance = distanceV.get(x) + distanceW.get(x);
	if (sap[0] == -1 || minDistance < sap[0]) {
		sap[0] = minDistance;
		sap[1] = x;
	}
}
// 這裏不是 else 的關係,要繼續往上找
Iterable<Integer> bag = g.adj(x);
for (int vv : bag) {
	if (!visited[vv]) {
		q.add(vv);
		visited[vv] = true;
		// 更新距離
		int d = distanceW.get(x);
		int dd = distanceW.getOrDefault(vv, d + 1);
		distanceW.put(vv, dd);
	}
}

另外在實現的時候需要注意性能和安全,例如有些可以在構造的時候就緩存出來的值就在構造的時候緩存好,不要等用的時候再去遍歷。

validate(word);
// 直接查緩存就可以了
return synsetToIdMap.containsKey(word);

以及有些返回類型是 Iterable 的,一定要確保返回類型不可變,不要返回原來的那個,要 new 一個新的去返回。

// 時刻記得做成不可變的
this.g = new Digraph(g);
// 時刻注意這種類型返回的時候一定要不可變,所以這裏返回的時候返回一個新的,不返回原來那個
return new ArrayList<>(synsetToIdMap.keySet());

Princeton 的作業質量高還在於它的異常處理,需要注意所有不合法情況的判斷,本題中對應的是單詞爲 null,更進一步的是單詞不存在於單詞集中。而對於圖,則需要滿足 DAG 和 rooted 這兩個條件。

封裝的情況也需要考慮,例如本題,在搜索 sap(int, int) 和 sap(Iterable, Iterable) 的時候,可以將這兩個功能抽象出來,做一次封裝。

最簡單的當然是寫 sap(int, int),然後對於 sap(Iterable, Iterable) 的情況,做一次雙重循環,對於每一個 i 對於每一個 j 做一次 sap(i, j) 並取最小值。

但是這樣的寫法是不是真的足夠好?在搜索的時候是不是重複搜索了很多?

所以我們可以按下面代碼所述,進行更合理的封裝和優化,對於已經搜索過的,就不再搜索了。

需要注意的是本題存在的映射關係有:“已知 id 獲取同義詞集”,“已知單詞獲取該單詞對應的同義詞集”這兩種。其中,“已知單詞獲取該單詞對應的同義詞集”可以由“已知單詞獲取 id”和“已知 id 獲取同義詞集”完成,所以我們只需要保存“已知單詞獲取 id”和“已知 id 獲取同義詞集”這樣兩個 HashMap 即可。

Outcast 就非常簡單了,WordNet 寫好了以後直接調用,取最大值就好。

完整代碼參見 https://gitlab.jxtxzzw.com/jxtxzzw/coursera_assignments 或者 https://www.jxtxzzw.com/archives/5263

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章