一、可最佳化的地方
上一節實現的代碼從運行效率上看,有兩個重大缺陷:
1、每次遞迴調用前,需要將當前的狀態矩陣拷貝一份,然後刪除和當前行相交的所有行和列,得到新的矩陣,當矩陣非常大時,拷貝操作所需的時間和空間都很大。
2、在實際情況中,矩陣M一般是疏鬆陣列,0的個數遠遠多於1的個數,如果我們能只處理含1的儲存格的話,將大大提高啟動並執行時空效率。
二、最佳化所用到的資料結構
以下最佳化演算法是Knuth提出來的,其主要思想是用十字鏈表格儲存體矩陣中的所有1.具體敘述如下
1、首先定義一個通用的十字鏈表節點類
struct CNode{CNode* Left; //左節點指標CNode* Right; //右節點指標CNode* Up; //上節點指標,對列節點,則為本列最後一個元素的指標CNode* Down; //下節點指標,對列節點,則為本列第一個元素的指標int name; //對普通節點,為所在子集的編號,即行號;//對列節點,為列編號};
2、然後從上面的節點類派生出一個欄位標題類,這個欄位標題多了一個size資料域,用於儲存本列元素的個數,即
struct CColumnHead:public CNode
{
int size; //本列元素的個數
};
3、資料節點類,儲存原矩陣裡的每個1,
struct CGrid:public CNode{CColumnHead* columnHead; //列前端節點的指標};
4、建立一個前端節點,指向欄位標題雙向鏈表,以及存放可行解的變數。
set<int> solution; //存放可行解CColumnHead* master; //列頭鏈表的前端節點
三、核心函數
演算法的核心是兩個函數。
1、cover函數
cover函數,輸入是一個列號c,函數流程的虛擬碼為
(1)將列c整個的從列頭鏈表中摘除
(2)從列c的第一個資料節點開始向下遍曆列c的所有資料節點r,都每個r都執行如下操作:
a、從r 的右側開始向右遍曆r 所在行的其他節點cell,對每個cell執行如下操作
b、設cell的所在列號為n,將cell從其所在列n中摘除,欄位標題n的size域的值減1
總體來說cover函數的目的是將和這個列相關的所有節點從矩陣中刪除。
2、cover的執行執行個體
下面看一個執行個體,假設
矩陣的目前狀態為,括弧中的數字表示該列的含1個數,h為列頭鏈表的前端節點,開始指向第一列
h-> 1(2) 2(2) 3(2) 4(3) 5(2) 6(2) 7(4) A 1 0 0 1 0 0 1 B 1 0 0 1 0 0 0 C 0 0 0 1 1 0 1 D 0 0 1 0 1 1 0 E 0 1 1 0 0 1 1 F 0 1 0 0 0 0 1
矩陣1 ——原始矩陣
現在要將列1及其相關節點從矩陣中摘除,則具體過程是
(1)將列1從矩陣中整體摘除,此時h指向列2,矩陣的狀態為
h-> 2(2) 3(2) 4(3) 5(2) 6(2) 7(4) A 0 0 1 0 0 1 B 0 0 1 0 0 0 C 0 0 1 1 0 1 D 0 1 0 1 1 0 E 1 1 0 0 1 1 F 1 0 0 0 0 1
矩陣2——摘除列1之後的矩陣
註:列1雖然從矩陣中整體摘除,但它的自身資訊並未發生變化,列1的原始資訊仍然為
1(2)A 1 B 1 C 0 D 0 E 0 F 0
可見,和列1相關的行有A和B
下一步的任務就是將行A和B的其他節點從矩陣2中刪除,
(2) 首先在矩陣2中遍曆行A的所有資料節點(從第一列的右側向右開始遍曆),顯然,A和列4、列7中有交點,於是刪除這兩個節點,第4列和第7列的計數器值減1,
這樣整個行A就被從矩陣2中移走,現在得到的矩陣為
h-> 2(2) 3(2) 4(2) 5(2) 6(2) 7(3) B 0 0 1 0 0 0 C 0 0 1 1 0 1 D 0 1 0 1 1 0 E 1 1 0 0 1 1 F 1 0 0 0 0 1
類似的,將行B的所有資料節點(只有第4列)從矩陣中摘除,各資料節點所在列的計數器值減1。此時矩陣M的新狀態為
h->2(2) 3(2) 4(1) 5(2) 6(2) 7(3) C 0 0 1 1 0 1 D 0 1 0 1 1 0 E 1 1 0 0 1 1 F 1 0 0 0 0 1
矩陣4 - 移走行B之後的狀態
從上面的過程可知,cover並未切斷被刪除元素與其他元素的橫向和縱向聯絡,這樣就被恢複矩陣的狀態打下了基礎。
3、uncpver函數
uncover 函數的輸入也是一個列號c,它的功能與cover相反,是將列c插入到矩陣中,也就是將矩陣恢複到執行
cover(c)以前的狀態。
uncover的處理流程也正好和cover相反。
函數流程的虛擬碼為
(1)從列c的最後一個資料節點開始,向上遍曆列c的所有資料節點r,都每個r都執行如下操作:
a、從r的左側開始向左遍曆其 所在行的其他節點cell,對每個cell執行如下操作
b、設cell的所在列號為n,將cell加入到列n中,欄位標題n的size域的值加1。
(2)將列c整個加入到從列頭鏈表中
4、uncover的執行執行個體
設現在的矩陣狀態為
2(2) 3(2) 4(1) 5(2) 6(2) 7(3) C 0 0 1 1 0 1 D 0 1 0 1 1 0 E 1 1 0 0 1 1 F 1 0 0 0 0 1
要插入的列號為1,且列1的資訊為
1(2)A 1 B 1 C 0 D 0 E 0 F 0
行A和B的資訊為
2 3 4 5 6 7
A 0 0 1 0 0 1
B 0 0 1 0 0 0
那麼uncover的具體執行過程是
(1)首先將列4的最後一行B恢複到矩陣中,即將B的節點4恢複到矩陣中,且列4的計數器加1,矩陣狀態為
h-> 2(2) 3(2) 4(2) 5(2) 6(2) 7(3) B 0 0 1 0 0 0 C 0 0 1 1 0 1 D 0 1 0 1 1 0 E 1 1 0 0 1 1 F 1 0 0 0 0 1
(2)然後將行A恢複到矩陣中,A的最後一個節點是7,所以先恢複列7的狀態,矩陣為
h-> 2(2) 3(2) 4(2) 5(2) 6(2) 7(4) A 0 0 0 0 0 1 B 0 0 1 0 0 0 C 0 0 1 1 0 1 D 0 1 0 1 1 0 E 1 1 0 0 1 1 F 1 0 0 0 0 1
然後將節點4恢複到矩陣中,矩陣的狀態為
h-> 2(2) 3(2) 4(3) 5(2) 6(2) 7(4) A 0 0 1 0 0 1 B 0 0 1 0 0 0 C 0 0 1 1 0 1 D 0 1 0 1 1 0 E 1 1 0 0 1 1 F 1 0 0 0 0 1
(3)最後將列1插入到矩陣中,矩陣為
1(2) 2(2) 3(2) 4(3) 5(2) 6(2) 7(4) A 1 0 0 1 0 0 1 B 1 0 0 1 0 0 0 C 0 0 0 1 1 0 1 D 0 0 1 0 1 1 0 E 0 1 1 0 0 1 1 F 0 1 0 0 0 0 1
至此,矩陣已完全恢複到cover(1)之前的狀態。
四、最佳化演算法的基本步驟
演算法的函數為search
1、如果矩陣為空白,即 master->Right==master,則函數返回true
2、選擇含1數目最少的列,設其欄位標題為c
3、執行cover(c)
4、 遍曆列c的每一個資料節點r ,對每個r都執行(a)到(d)中的操作
(a) 從r的右側開始向右遍曆r所在行的所有其他節點cell,設cell所在裡的列前端節點為n,執行操作cover(n)
(b) 遞迴調用函數search,結果存到變數flag中
(c) 如果上一步的結果為真,將行r加入到解集中,然後返回true
(d) 如果步驟(b)的結果為false,則向從r的左側開始向左遍曆r所在行的所有其他節點cell,設cell所在裡的列前端節點為n
,執行操作uncover(n)
5、執行uncover(c)
6、函數返回false