第三十二章、最小操作數
? ? 題目詳情如下:
? ? 給定一個單詞集合Dict,其中每個單詞的長度都相同。現從此單詞集合Dict中抽取兩個單詞A、B,我們希望通過若干次操作把單詞A變成單詞B,每次操作可以改變單詞的一個字母,同時,新產生的單詞必須是在給定的單詞集合Dict中。求所有行得通步數最少的修改方法。
? ?舉個例子如下:
Given:
? ?A = "hit"
? ?B = "cog"
? ?Dict = ["hot","dot","dog","lot","log"]
Return
?[
? ?["hit","hot","dot","dog","cog"],
? ?["hit","hot","lot","log","cog"]
?]
? ? 即把字符串A = "hit"轉變成字符串B = "cog",有以下兩種可能:
"hit" -> "hot" -> ?"dot" -> ?"dog" -> "cog";
"hit" -> ?"hot" -> ?"lot" -> ?"log" ?->"cog"。
? ??詳解:本題是一個典型的圖搜索算法問題。此題看似跟本系列的第29章的字符串編輯距離相似,但其實區別特別大,原因是最短編輯距離是讓某個單詞增加一個字符或減少一個字符或修改一個字符達到目標單詞,來求變換的最少次數,但此最小操作數問題就只是改變一個字符。?
? ? 通過此文:http://blog.csdn.net/v_JULY_v/article/details/6111353,我們知道,在圖搜索算法中,有深度優先遍歷DFS和廣度優先遍歷BFS,而題目中并沒有給定圖,所以需要我們自己建立圖。
? ? 涉及到圖就有這么幾個問題要思考,節點是什么?邊如何建立?圖是有方向的還是無方向的?包括建好圖之后,如何記錄單詞序列等等都是我們要考慮的問題。
? ? 解法一、單向BFS法
? ??1、建圖
? ? 對于本題,我們的圖的節點就是字典里的單詞,兩個節點有連邊,對應著我們可以把一個單詞按照規則變為另外一個單詞。比如我們有單詞hat,它應該與單詞cat有一條連邊,因為我們可以把h變為c,反過來我們也可以把c變為h,所以我們建立的連邊應該是無向的。
? ? ? ? ? ? 如何建圖?有兩種辦法,
- 第一種方法是:我們可以把字典里的任意兩個單詞,通過循環判斷一下這兩個單詞是否只有一個位置上的字母不同。即假設字典里有n個單詞,我們遍歷任意兩個單詞的復雜度是O(n2),如果每個單詞長度為length,我們判斷兩個單詞是否連邊的復雜度是O(length),所以這個建圖的總復雜度是O(n2*length)。但當n比較大時,這個復雜度非常高,有沒有更好的方法呢?
- 第二種方法是:我們把字典里地每個單詞的每個位置的字母修改一下,從字典里查找一下(若用基于red-black tree的map查找,其查找復雜度為O(logn),若用基于hashmap的unordered_map,則查找復雜度為O(1)),修改后的單詞是否在字典里出現過。即我們需要遍歷字典里地每一個單詞O(n),嘗試修改每個位置的每個字母,對每個位置我們需要嘗試26個字母(其實是25個,因為要改得和原來不同),因此這部分復雜度是O(26*length),總復雜度是O(26 * n * length) ?(第二種方法優化版:這第二種方法能否更優?在第二種方法中,我們對每個單詞每個位置嘗試了26次修改,事實上我們可以利用圖是無向的這一特點,我們對每個位置試圖把該位置的字母變到字典序更大的字母。例如,我們只考慮cat變成hat,而不考慮hat變成cat,因為再之前已經把無向邊建立了。這樣,只進行一半的修改次數,從而減少程序的運行時間。當然這個優化從復雜度上來講是常數的,因此稱為常數優化,此雖算是一種改進,但不足以成為第三種方法,原因是我們經常忽略O背后隱藏的常數)。
? ? OK,上面兩種方法孰優孰劣呢?直接比較n2*length 與?26 * n * length的大小。很明顯,通常情況下,字典里的單詞個數非常多,也就是n比較大,因此第二種方法效果會好一些,稍后的參考代碼也會選擇上述第二種方法的優化。
? ??2、記錄單詞序列
? ? 對于最簡單的bfs,我們是如何記錄路徑的?如果只需要記錄一條最短路徑的話,我們可以對每個走到的位置,記錄走到它的前一個位置。這樣到終點后,我們可以不斷找到它的前一個位置。我們利用了最短路徑的一個特點:即第二次經過一個節點的時候,路徑長度不比第一次經過它時短。因此這樣的路徑是沒有圈的。
? ? 但是本題需要記錄全部的路徑,我們第二次經過一個節點時,路徑長度可能會和第一次經過一個節點時路徑長度一樣。這是因為,我們可能在第i層中有多個節點可以到達第(i + 1)層的同一個位置,這樣那個位置有多條路徑都是最短路徑。
? ? 如何解決呢?——我們記錄經過這個位置的前面所有位置的集合。這樣一個節點的前驅不是一個節點,而是一個節點的集合。如此,當我們第二次經過一個第(i+ 1)層的位置時,我們便保留前面那第i層位置的集合作為前驅。
? ??3、遍歷
? ? ? ? ? ? 解決了以上兩個問題,我們最終得到的是什么?如果有解的話,我們最終得到的是從終點開始的前一個可能單詞的集合,對每個單詞,我們都有能得到它的上一個單詞的集合,直到起點。這就是bfs分層之后的圖,我們從終點開始遍歷這個圖的到起點的所有路徑,就得到了所有的解,這個遍歷我們可以采用之前介紹的dfs方法(路徑的數目可能非常多)。
? ? ? ? ? ? 其實,為了簡單起見,我們可以從終點開始bfs,因為記錄路徑記錄的是之前的節點,也就是反向的。這樣最終可以按順序從起點遍歷到終點的所有路徑。
參考代碼如下:
?? ?? class?Solution???? {???? public:???? ?????? ????void?help(intx,vector<int>?&d,?vector<string>?&word,vector<vector<int>?>?&next,vector<string>?&path,vector<vector<string>?>?&answer)?{???? ????????path.push_back(word[x]);???? ????????if?(d[x]?==?0)?{????? ????????????answer.push_back(path);???? ????????}???? ????????else?{???? ????????????int?i;???? ????????????for?(i?=?0;?i?<next[x].size();?++i)?{???? ????????????????help(next[x][i],d,?word,?next,path,answer);???? ????????????}???? ????????}???? ????????path.pop_back();????? ????}???? ?? ????vector<vector<string>>?findLadders(string?start,?string?end,?set<string>&?dict)???? ????{???? ?? ????????vector<vector<string>?>?answer;???? ????????if?(start?==?end)?{????? ????????????return?answer;???? ????????}???? ?????????? ????????dict.insert(start);???? ????????dict.insert(end);???? ????????set<string>::iterator?dt;???? ????????vector<string>?word;???? ????????map<string,int>allword;???? ?????????? ????????for?(dt?=?dict.begin();?dt!=?dict.end();?++dt)?{???? ????????????word.push_back(*dt);???? ????????????allword.insert(make_pair(*dt,?allword.size()));???? ????????}???? ?? ?????????? ????????vector<vector<int>?>?con;???? ????????int?i,j,n?=word.size(),temp,len?=?word[0].length();???? ????????con.resize(n);???? ????????for?(i?=?0;?i?<?n;?++i){???? ????????????for?(j?=?0;?j?<len;?++j)?{???? ????????????????char?c;???? ????????????????for?(c?=word[i][j]?+?1;?c?<=?'z';?++c)?{???? ????????????????????char?last?=word[i][j];???? ????????????????????word[i][j]?=c;???? ????????????????????map<string,int>::iterator?t?=?allword.find(word[i]);???? ????????????????????if?(t?!=allword.end())?{???? ????????????????????????con[i].push_back(t->second);???? ????????????????????????con[t->second].push_back(i);???? ????????????????????}???? ????????????????????word[i][j]?=last;???? ????????????????}???? ????????????}???? ?? ????????}???? ?? ?????????? ????????queue<int>?q;???? ????????vector<int>?d;???? ????????d.resize(n,?-1);???? ????????int?from?=?allword[start],to?=?allword[end];???? ????????d[to]?=?0;???? ????????q.push(to);???? ????????vector<vector<int>?>?next;???? ????????next.resize(n);???? ????????while?(!q.empty())?{???? ????????????int?x?=?q.front(),?now=?d[x]?+?1;???? ?????????????? ?????????????? ????????????if?((d[from]?>=?0)&&?(now?>?d[from]))?{???? ????????????????break;???? ????????????}???? ????????????q.pop();???? ????????????for?(i?=?0;?i?<con[x].size();?++i)?{???? ????????????????int?y?=?con[x][i];???? ?????????????????? ????????????????if?(d[y]?<?0)?{?????? ????????????????????d[y]?=?now;???? ????????????????????q.push(y);???? ????????????????????next[y].push_back(x);???? ????????????????}???? ?????????????????? ????????????????else?if?(d[y]?==now)?{???? ????????????????????next[y].push_back(x);???? ????????????????}???? ?? ????????????}???? ????????}???? ????????if?(d[from]?>=?0)?{???? ????????????vector<string>path;???? ????????????help(from,?d,word,next,?path,answer);???? ????????}???? ????????return?answer;???? ????}???? };????
? ??解法二、雙向BFS法
? ? BFS需要把每一步搜到的節點都存下來,很有可能每一步的搜到的節點個數越來越多,但最后的目的節點卻只有一個。后半段的很多搜索都是白耗時間了。
? ? 上面給出了單向BFS的解法,但看過此前blog中的這篇文章“A*、Dijkstra、BFS算法性能比較演示”可知:http://blog.csdn.net/v_JULY_v/article/details/6238029,雙向BFS性能優于單向BFS。
? ? 舉個例子如下,第1步,是起點,1個節點,第2步,搜到2個節點,第3步,搜到4個節點,第4步搜到8個節點,第5步搜到16個節點,并且有一個是終點。那這里共出現了31個節點。從起點開始廣搜的同時也從終點開始廣搜,就有可能在兩頭各第3步,就相遇了,出現的節點數不超過(1+2+4)*2=14個,如此就節省了一半以上的搜索時間。
? ? 下面給出雙向BFS的解法,參考代碼如下:
?? class?Solution?? {?? public:?? ????vector<vector<string>>?findLadders(string?start,?string?end,?set<string>&?dict)?? ????{?? ????????vector<vector<string>>?result,?result_temp;?? ????????if?(dict.erase(start)?==?1?&&?dict.erase(end)?==?1)??? ????????{?? ????????????map<string,?vector<string>>?kids_from_start;?? ????????????map<string,?vector<string>>?kids_from_end;?? ?? ????????????set<string>?reach_start;?? ????????????reach_start.insert(start);?? ????????????set<string>?reach_end;?? ????????????reach_end.insert(end);?? ?? ????????????set<string>?meet;?? ????????????while?(meet.empty()?&&?!reach_start.empty()?&&?!reach_end.empty())?? ????????????{?? ????????????????if?(reach_start.size()?<?reach_end.size())?? ????????????????{?? ????????????????????search_next_reach(reach_start,?reach_end,?meet,?kids_from_start,?dict);?? ????????????????}?? ????????????????else?? ????????????????{?? ????????????????????search_next_reach(reach_end,?reach_start,?meet,?kids_from_end,?dict);?? ????????????????}?? ????????????}?? ?? ????????????if?(!meet.empty())?? ????????????{?? ????????????????for?(set<string>::iterator?it?=?meet.begin();?it?!=?meet.end();?++it)?? ????????????????{?? ????????????????????vector<string>?words(1,?*it);?? ????????????????????result.push_back(words);?? ????????????????}?? ?? ????????????????walk(result,?kids_from_start);?? ????????????????for?(size_t?i?=?0;?i?<?result.size();?++i)?? ????????????????{?? ????????????????????reverse(result[i].begin(),?result[i].end());?? ????????????????}?? ????????????????walk(result,?kids_from_end);?? ????????????}?? ????????}?? ?? ????????return?result;?? ????}?? ?? private:?? ????void?search_next_reach(set<string>&?reach,?const?set<string>&?other_reach,?set<string>&?meet,?map<string,?vector<string>>&?path,?set<string>&?dict)?? ????{?? ????????set<string>?temp;?? ????????reach.swap(temp);?? ?? ????????for?(set<string>::iterator?it?=?temp.begin();?it?!=?temp.end();?++it)?? ????????{?? ????????????string?s?=?*it;?? ????????????for?(size_t?i?=?0;?i?<?s.length();?++i)?? ????????????{?? ????????????????char?back?=?s[i];?? ????????????????for?(s[i]?=?'a';?s[i]?<=?'z';?++s[i])?? ????????????????{?? ????????????????????if?(s[i]?!=?back)?? ????????????????????{?? ????????????????????????if?(reach.count(s)?==?1)?? ????????????????????????{?? ????????????????????????????path[s].push_back(*it);?? ????????????????????????}?? ????????????????????????else?if?(dict.erase(s)?==?1)?? ????????????????????????{?? ????????????????????????????path[s].push_back(*it);?? ????????????????????????????reach.insert(s);?? ????????????????????????}?? ????????????????????????else?if?(other_reach.count(s)?==?1)?? ????????????????????????{?? ????????????????????????????path[s].push_back(*it);?? ????????????????????????????reach.insert(s);?? ????????????????????????????meet.insert(s);?? ????????????????????????}?? ????????????????????}?? ????????????????}?? ????????????????s[i]?=?back;?? ????????????}?? ????????}?? ????}?? ?? ????void?walk(vector<vector<string>>&?all_path,?map<string,?vector<string>>?kids)?? ????{?? ????????vector<vector<string>>?temp;?? ????????while?(!kids[all_path.back().back()].empty())?? ????????{?? ????????????all_path.swap(temp);?? ????????????all_path.clear();?? ????????????for?(vector<vector<string>>::iterator?it?=?temp.begin();?it?!=?temp.end();?++it)?? ????????????{?? ????????????????vector<string>&?one_path?=?*it;?? ????????????????vector<string>&?p?=?kids[one_path.back()];?? ????????????????for?(size_t?i?=?0;?i?<?p.size();?++i)?? ????????????????{?? ????????????????????all_path.push_back(one_path);?? ????????????????????all_path.back().push_back(p[i]);?? ????????????????}?? ????????????}?? ????????}?? ????}?? };??
第三十三章、木塊砌墻
題目:用 1×1×1, 1× 2×1以及2×1×1的三種木塊(橫綠豎藍,且綠藍長度均為2),
搭建高長寬分別為K × 2^N × 1的墻,不能翻轉、旋轉(其中,0<=N<=1024,1<=K<=4)
有多少種方案,輸出結果
對1000000007取模。
舉個例子如給定高度和長度:N=1 K=2,則
答案是7,即有7種搭法,如下圖所示:
?
? ??詳解:此題很有意思,涉及的知識點也比較多,包括動態規劃,快速矩陣冪,狀態壓縮,排列組合等等都一一考察了個遍。而且跟一個比較經典的矩陣乘法問題類似:即用1 x 2的多米諾骨牌填滿M x N的矩形有多少種方案,M<=5,N<2^31,輸出答案mod p的結果
OK,回到正題。下文使用的圖示說明(所有看到的都是橫切面):
首先說明“?方塊”的作用
“?方塊”,表示這個位置是空位置,可以任意擺放。
上圖的意思就是,當右上角被綠色木塊占用,此位置固定不變,其他位置任意擺放,在這種情況下的堆放方案數。
? ? 解法一、窮舉遍歷
? ? 初看此題,你可能最先想到的思路便是窮舉:用二維數組模擬墻,從左下角開始擺放,從左往右,從下往上,最后一個格子是右上角那個位置;每個格子把每種可以擺放木塊都擺放一次,每堆滿一次算一種用擺放方法。為了便于描述,為木塊的每個格子進行編號:
下面演示當n=1,k=2的算法過程(7種情況):
? ? 窮舉遍歷在數據規模比較小的情況下還撐得住,但在0<=N<=1024這樣的數據規模下,此方法則立刻變得有心無力,因此我們得尋找更優化的解法。
? ? 解法二、遞歸分解
遞歸求解就是把一個大問題,分解成小問題,逐個求解,然后再解決大問題。
2.1、算法演示
假如有墻規模為(n,k),如果從中間切開,被分為規模問(n-1,k)的兩堵墻,那么被分開的墻和原墻有什么關系呢?我們首先來看一下幾組演示。
2.1.1、n=1,k=2的情況
首先演示,n=1,k=2時的情況,如下圖2-1:
圖 2-1
上圖2-1中:
?表示,左邊墻的所有堆放方案數 * 右邊墻所有堆放方案數 = 2 * 2 = 4
表示,當切開處有一個橫條的時候,空位置存在的堆放方案數。左邊*右邊 = 1*1 = 2;剩余兩組以此類推。
這個是排列組合的知識。
2.1.2、n=2,k=3的情況
其次,我們再來演示下面更具一般性的計算分解,即當n=2,k=3的情況,如下圖2-2:
?圖 2-2
再從分解的結果中,挑選一組進行分解演示:
?圖 2-3
通過圖2-2和圖2-3的分解演示,可以說明,最終都是分解成一列求解。在逐級向上匯總。
2.1.3、n=4,k=3的情況
我們再假設一堵墻n=4,k=3,也就是說,寬度是16,高度是3時,會有以下分解:
圖2-4
根據上面的分解的一個中間結果,再進行分解,如下:
圖 2-5
? ? 通過上面圖2-1~圖2-5的演示可以明確如下幾點:
假設f(n)用于計算問題,那么f(n)依賴于f(n-1)的多種情況。切開處有什么特殊的地方呢?通過上面的演示,我們得知被切開的兩堵墻從沒有互相嵌入的木塊(綠色木塊)到全是互相連接的木塊,相當于切口綠色木塊的全排列(即有綠色或者沒有綠色的所有排列),即有2^k種狀態(比如k=2,且有綠色用1表示,沒有綠色用0表示,那么就有00、01、10、11這4種狀態)。根據排列組合的性質,把每一種狀態下左右木墻堆放方案數相乘,再把所有乘積求和,就得到木墻的堆放結果數。以此類推,將問題逐步往下分解即可。此外,從圖2-5中可以看出,除了需要考慮切口綠色木塊的狀態,還需要考慮最左邊一列和最右邊一列的綠色木塊狀態。我們把這兩種邊界狀態稱為左邊界狀態和右邊界狀態,分別用leftState和rightState表示。?
且在觀察圖2-5被切分后,所有左邊的墻,他們的左邊界ls狀態始終保持不變,右邊界rs狀態從0~maxState, maxState = 2^k-1(有綠色方塊表示1,沒有表示0;ls表示左邊界狀態,rs表示右邊狀態):
圖 2-6
?同樣可以看出右邊的墻的右邊界狀態保持不變,而左邊界狀態從0~maxState。要堆砌的木墻可以看做是左邊界狀態=0,和右邊界狀態=0的一堵墻。
? ??有一點可能要特別說明下,即上文中說,有綠色方塊的狀態表示標為1,無綠色方塊的狀態表示標為0,特意又拿上圖2-6標記了一些數字,以讓絕大部分讀者能看得一目了然,如下所示:
? ? ? ?
? 圖2-7
這下,你應該很清楚的看到,在上圖中,左邊木塊的狀態表示一律為010,右邊木塊的狀態表示則是000~111(即從下至上開始計數,右邊木塊rs的狀態用二進制表示為:000 001 010 011 100 101 110 111,它們各自分別對應整數則是:0 1 2 3 4 5 6 7)。
2.2、計算公式
通過圖2-4、圖2-5、圖2-6的分解過程,我們可以總結出下面公式(leftState=最左邊邊界狀態,rightState=最右邊邊界狀態):
即:
? ? 接下來,分3點解釋下上述公式:
? ??1、上述函數返回結果是當左邊狀態為=leftState,右邊狀態=rightState時木墻的堆砌方案數,相當于直接分解的左右狀態都為0的情況,即直接分解f(n,k,0,0)即可
。看到這,讀者可能便有疑問了,既然直接分解f(n,k,0,0)即可,為何還要加leftstate和leftstate兩個變量呢?回顧下2.1.3節中n=4,k=3的演示例子,即當n=4,k=3時,其分解過程即如下圖(
上文2.1.3節中的圖2-4
)
? ? 也就是說,剛開始直接分解f(4,3,0,0),即n=4,k=3,leftstate=0,rightstate=0,但分解過程中leftstate和rightstate皆從0變化到了maxstate,故才讓函數的第3和第4個參數采用leftstate和rightstate這兩個變量的形式,公式也就理所當然的寫成了f(n,k,leftstate,rightstate)。
? ??2、然后我們再看下當n=4,k=3分解的一個中間結果,即給定如上圖最下面部分中紅色框框所框住的木塊時
:
?
它用方程表示即為?f(2,3,2,5),怎么得來的呢?其實還是又回到了上文2.1.3節中,當n=2,k=3 時(下圖即為上文2.1.3節中的圖2-5和圖2-6)
? ? 左邊界ls狀態始終保持不變時,右邊界rs狀態從0~maxState;右邊界狀態保持不變時,而左邊界狀態從0~maxState。
? ? 故上述分解過程用方程式可表示為:
f(2,3,2,5) = f(1,3,2,0) * f(1,3,0,5)
? ? ? ? ? ? + f(1,3,2,1) * f(1,3,1,5)
? ? ? ? ? ?+ f(1,3,2,2) * f(1,3,2,5)
? ? ? ? ? ? + f(1,3,2,3) * f(1,3,3,5)
? ? ? ? ? ?+ f(1,3,2,4) * f(1,3,4,5)
? ? ? ? ? ?+ f(1,3,2,5) * f(1,3,5,5)
? ? ? ? ? ? + f(1,3,2,6) * f(1,3,6,5)
? ? ? ? ? ? + f(1,3,2,7) * f(1,3,7,5)
? ? 說白了,我們曾在2.1節中從圖2-2到圖2-6正推推導出了公式,然上述過程中,則又再倒推推了一遍公式進行了說明。
?
3、最后,作者是怎么想到引入 leftstate 和rightstate 這兩個變量的呢?如紅色標記所說:"因為切開后,
發現綠色條,在分開出不斷的變化,當時也進入了死胡同,我就在想,藍色的怎么辦。
后來才想明白,與藍色無關。每一種變化就是一種狀態,所以就想到了引入leftstate 和rightstate這兩個變量。"
2.3、參考代碼
下面代碼就是根據上面函數原理編寫的。最終執行效率,n=1024,k=4 時,用時0.2800160秒(之前代碼用的是字典作為緩存,用時在1.3秒左右,后來改為數組結果,性能大增)。
?? ?? using?System;???? using?System.Collections.Generic;???? using?System.Text;???? using?System.Collections;???? ?? namespace?HeapBlock???? {???? ????public?class?WoolWall???? ????{???????????? ????????private?int?n;???? ????????private?int?height;???? ????????private?int?maxState;???? ????????private?int[,?,]?resultCache;????? ?? ????????public?WoolWall(int?n,?int?height)???? ????????{???? ????????????this.n?=?n;???? ????????????this.height?=?height;???? ????????????maxState?=?(1?<<?height)?-?1;???? ????????????resultCache?=?new?int[n?+?1,?maxState?+?1,?maxState?+?1];????? ????????}???? ?? ?????????? ?????????? ?????????? ?????????? ?????????? ?????????? ????????public?static?int?Heap(int?n,?int?k)???? ????????{???? ????????????return?new?WoolWall(n,?k).Heap();???? ????????}???? ?? ?????????? ?????????? ?????????? ?????????? ????????public?int?Heap()???? ????????{???? ????????????return?(int)Heap(n,?0,?0);???? ????????}???? ?? ????????private?long?Heap(int?n,?int?lState,?int?rState)???? ????????{???? ?????????????? ?????????????? ????????????if?(resultCache[n,?lState,?rState]?!=?0)???? ????????????{???? ????????????????return?resultCache[n,?lState,?rState];???? ????????????}???? ?? ?????????????? ?????????????? ????????????if?(n?==?0)???? ????????????{???? ????????????????return?CalcOneColumnHeapCount(lState);???? ????????????}???? ?? ????????????long?result?=?0;???? ????????????for?(int?state?=?0;?state?<=?maxState;?state++)???? ????????????{???? ????????????????if?(n?==?1)???? ????????????????{???? ?????????????????????? ????????????????????if?(!StateIsAvailable(n,?lState,?rState,?state))???? ????????????????????{???? ????????????????????????continue;???? ????????????????????}???? ????????????????????result?+=?Heap(n?-?1,?state?|?lState,?state?|?lState)???? ????????????????????????*?Heap(n?-?1,?state?|?rState,?state?|?rState);???? ????????????????}???? ????????????????else???? ????????????????{???? ????????????????????result?+=?Heap(n?-?1,?lState,?state)?*?Heap(n?-?1,?state,?rState);????? ????????????????}???? ????????????????result?%=?1000000007;?? ????????????}???? ?? ????????????resultCache[n,?lState,?rState]?=?(int)result;????? ????????????resultCache[n,?rState,?lState]?=?(int)result;????? ????????????return?result;???? ????????}???? ?? ?????????? ?????????? ?????????? ?????????? ?????????? ????????private?int?CalcOneColumnHeapCount(int?state)???? ????????{???? ????????????int?sn?=?0;??? ????????????int?result?=?1;???? ????????????for?(int?i?=?0;?i?<?height;?i++)???? ????????????{???? ????????????????if?((state?&?1)?==?0)???? ????????????????{???? ????????????????????sn++;???? ????????????????}???? ????????????????else???? ????????????????{???? ????????????????????if?(sn?>?0)???? ????????????????????{???? ????????????????????????result?*=?CalcAllState(sn);???? ????????????????????}???? ????????????????????sn?=?0;???? ????????????????}???? ????????????????state?>>=?1;???? ????????????}???? ????????????if?(sn?>?0)???? ????????????{???? ????????????????result?*=?CalcAllState(sn);???? ????????????}???? ?? ????????????return?result;???? ????????}???? ?? ?????????? ?????????? ?????????? ?????????? ?????????? ?????????? ?????????? ?????????? ?????????? ????????private?static?int?CalcAllState(int?k)???? ????????{???? ????????????return?k?<=?2???k?:?CalcAllState(k?-?1)?+?CalcAllState(k?-?2);???? ????????}???? ?? ?????????? ?????????? ?????????? ?????????? ?????????? ?????????? ?????????? ?????????? ?????????? ?????????? ?????????? ?????????? ????????private?bool?StateIsAvailable(int?n,?int?lState,?int?rState,?int?state)???? ????????{???? ????????????return?(n?>?1)?||?((lState?|?state)?==?lState?+?state?&&?(rState?|?state)?==?rState?+?state);???? ????????}???? ????}???? }????
上述程序中,
- WoolWall.Heap(1024,4); //直接通過靜態方法獲得結果
- new??WoolWall(n, k).Heap();//通過構造對象獲得結果
2.3.1、核心算法講解
? ??因為它最終都是分解成一列的情況進行處理,這就會導致很慢。為了提高速度,本文使用了緩存機制來提高性能。緩存原理就是,n,k,leftState,rightState相同的墻,返回的結果肯定相同。利用這個特性,每計算一種結果就放入到緩存中,如果下次計算直接從緩存取出。剛開始緩存用字典類實現,有網友給出了更好的緩存方法——數組。這樣性能好了很多,也更加簡單。程序結構如下圖所示:
? ? 上圖反應了Heep調用的主要方法調用,在循環中,result 累加 lResult 和 rResult。
①在實際代碼中,首先是從緩存中讀取結果,如果沒有緩存中讀取結果在進行計算。?
分解法分解到一列時,不在分解,直接計算機過
if?(n?==?0)?? {?? ?????return?CalcOneColumnHeap(lState);?? }??
②下面是整個程序的核心代碼,通過for循環,求和state=0到state=2^k-1的兩邊木墻乘積:
for?(int?state?=?0;?state?<=?maxState;?state++)?? {?? ????if?(n?==?1)?? ????{?? ????????if?(!StateIsAvailable(n,?lState,?rState,?state))?? ????????{?? ????????????continue;?? ????????}?? ????????result?+=?Heap(n?-?1,?state?|?lState,?state?|?lState)?*?? ????????????Heap(n?-?1,?state?|?rState,?state?|?rState);?? ????}?? ????else?? ????{?? ????????result?+=?Heap(n?-?1,?lState,?state)?? ????????????*?Heap(n?-?1,?state,?rState);?? ????}?? ????result?%=?1000000007;?? }??
? ? 當n=1切分時,需要特殊考慮。如下圖:
圖2-8
? ? 看上圖中,因為左邊墻中間被綠色方塊占用,所以在(1,0)-(1,1)這個位置(位置的標記方法同解法一)不能再放綠色方塊。所以一些狀態需要排除,如state=2需要排除。同時在還需要合并狀態,如state=1時,左邊墻的狀態=3。
? ??特別說明下:依據我們上文2.2節中的公式,如果第i行有這種木塊,state對應2^(i-1),加上所有行的貢獻就得到state(0就是沒有這種橫跨木塊,2^k-1就是所有行都是橫跨木塊),然后遍歷state,還記得上文中的圖2-7么?
? ??當第i行被這樣的木塊或這樣的木塊占據時,其各自對應的state值分別為:
當第1行被占據,state=1;當第2行被占據,state=2;當第1和第2行都被占據,state=3;當第3行被占據,state=4;當第1和第3行被占據,state=5;當第2和第3行被占據,state=6;當第1、2、3行全部都被占據,state=7。
? ? 至于原因,即如2.1.3節節末所說:二進制表示為:000 001 010 011 100 101 110 111,它們各自分別對應整數則是:0 1 2 3 4 5 6 7。
? ? 具體來說,下面圖中所有框出來的位置,不能有綠色的:
③CalcOneColumnHeap(int state)函數用于計算一列時擺放方案數。
? ? 計算方法是, 求和被綠色木塊分割開的每一段連續方格的擺放方案數。每一段連續的方格的擺放方案通過CalcAllState方法求得。經過分析,可以得知CalcAllState是類似斐波那契序列的函數。
? ? 舉個例子如下(分步驟講述):
令state = 4546(state=2^k-1,k最大為4,故本題中state最大在15,而這里取state=4546只是為了演示如何計算),二進制是:1000111000010。位置上為1,表示被綠色木塊占用,0表示空著,可以自由擺放。1000111000010? 被分割后 1? 000? 111? 0000? 1? 0, 那么就有 000=3個連續位置, 0000=4個連續位置?, 0=1個連續位置。堆放結果=CalcAllState(3) + CalcAllState(4) + CalcAllState(1) = 3 + 5 + 1 = 9。
2.4、再次優化
? ? 上面程序因為調用性能的樹形結構,形成了大量的函數調用和緩存查找,所以其性能不是很高。 為了得到更高的性能,可以讓所有的運算直接依賴于上一次運算的結果,以防止更多的調用。即如果每次運算都算出所有邊界狀態的結果,那么就能為下一次運算提供足夠的信息。后續優化請查閱此文第3節:http://blog.csdn.net/dw14132124/article/details/9038417#t2。
? 解法三、動態規劃
相信讀到上文,不少讀者都已經意識到這個問題其實就是一個動態規劃問題,接下來咱們換一個角度來分析此問題。
3.1、暴力搜索不可行
首先,因為木塊的寬度都是1,我們可以想成2維的問題。也就是說三種木板的規格分別為1* 1, 1 * 2, 2 * 1。
? ? ?通過上文的解法一,我們已經知道這個問題最直接的想法就是暴力搜索,即對每個空格嘗試放置哪種木板。但是看看數據規模就知道,這種思路是不可行的。因為有一條邊范圍長度高達21024,普通的電腦,230左右就到極限了。于是我們得想想別的方法。
3.2、另辟蹊徑
? ? 為了方便,我們把墻看做有2n行,k列的矩形。這是因為雖然矩形木塊不能翻轉,但是我們同時擁有1*2和2*1的兩種木塊。
? ? 假設我們從上到下,從左到右考慮每個1*1的格子是如何被覆蓋的。顯然,我們每個格子都要被覆蓋住。木塊的特點決定了我們覆蓋一個格子最多只會影響到下一行的格子。這就可以讓我們暫時只考慮兩行。
? ? 假設現我們已經完全覆蓋了前(i– 1)行。那么由于覆蓋前(i-1)行導致第i行也不“完整”了。如下圖:
xxxxxxxxx
ooxooxoxo
我們用x表示已經覆蓋的格子,o表示沒覆蓋的格子。為了方便,我們使用9列。
? ? 我們考慮第i行的狀態,上圖中,第1列我們可以用1*1的覆蓋掉,也可以用1*2的覆蓋前兩列。第4、5列的覆蓋方式和第1、2列是同樣的情況。第7列需要覆蓋也有兩種方式,即用1*1的覆蓋或者用2*1的覆蓋,但是這樣會導致第(i+1)行第7列也被覆蓋。第9列和第7列的情況是一樣的。這樣把第i行覆蓋滿了之后,我們再根據第(i+1)行被影響的狀態對下一行進行覆蓋。
? ? 那么每行有多少種狀態呢?顯然有2k,由于k很小,我們只有大約16種狀態。如果我們對于這些狀態之間的轉換制作一個矩陣,矩陣的第i行第j列的數表示的是我們第m行是狀態i,我們把它完整覆蓋掉,并且使得第(m + 1)行變成狀態j的可能的方法數,這個矩陣我們可以暴力搜索出來,搜索的方式就是枚舉第m行的狀態,然后嘗試放木板,用所有的方法把第m行覆蓋掉之后,下一行的狀態。當然,我們也可以認為只有兩行,并且第一行是2k種狀態的一種,第二行起初是空白的,求使得第一行完全覆蓋掉,第二行的狀態有多少種類型以及每種出現多少次。
3.3、動態規劃
? ? 這個矩陣作用很大,其實我們覆蓋的過程可以認為是這樣:第一行是空的,我們看看把它覆蓋了,第2行是什么樣子的。根據第二行的狀態,我們把它覆蓋掉,看看第3行是什么樣子的。
? ? 如果我們知道第i行的狀態為s,怎么考慮第i行完全覆蓋后,第(i+1)行的狀態?那只要看那個矩陣的狀態s對應的行就可以了。我們可以考慮一下,把兩個這樣的方陣相乘得到得結果是什么。這個方陣的第i行第j個元素是這樣得到的,是第i行第k個元素與第k行第j個元素的對k的疊加。它的意義是上一行是第m行是狀態i,把第m行和第(m+ 1)行同時覆蓋住,第(m+2)行的狀態是j的方法數。這是因為中間第(m+1)行的所有狀態k,我們已經完全遍歷了。
? ? 于是我們發現,每做一次方陣的乘法,我們相當于把狀態推動了一行。那么我們要坐多少次方陣乘法呢?就是題目中墻的長度2n,這個數太大了。但是事實上,我們可以不斷地平方n次。也就是說我們可以算出A2,A4, A8, A16……方法就是不斷用結果和自己相乘,這樣乘n次就可以了。
? ? 因此,我們最關鍵的問題就是建立矩陣A。我們可以這樣表示一行的狀態,從左到右分別叫做第0列,第1列……覆蓋了我們認為是1,沒覆蓋我們認為是0,這樣一行的狀態可以表示維一個整數。某一列的狀態我們可以用為運算來表示。例如,狀態x第i列是否被覆蓋,我們只需要判斷x & (1 << i) 是否非0即可,或者判斷(x >> i) & 1, 用右移位的目的是防止溢出,但是本題不需要考慮溢出,因為k很小。 接下來的任務就是遞歸嘗試放置方案了
3.4、參考代碼
? ? 最終結果,我們最初的行是空得,要求最后一行之后也不能被覆蓋,所以最終結果是矩陣的第[0][0]位置的元素。另外,本題在乘法過程中會超出32位int的表示范圍,需要臨時用C/C++的long long,或者java的long。
參考代碼如下:
?? #ifdef?WIN32?? #define?ll?__int64??? #else?? #define?ll?long?long?? #endif?? ?? ?? ?? void?cal(int?a[6][32][32],int?n,int?col,int?laststate,int?nowstate)?{?? ????if?(col?>=?n)?{?? ????????++a[n][laststate][nowstate];?? ????????return;?? ????}?? ?????? ????cal(a,n,?col?+?1,?laststate,?nowstate);?? ????if?(((laststate?>>?col)?&?1)?==?0)?{?? ????????cal(a,n,?col?+?1,?laststate,?nowstate?|?(1?<<?col));?? ????????if?((col?+?1?<?n)?&&?(((laststate?>>?(col?+?1))?&?1)?==?0))?{?? ????????????cal(a,n,?col?+?2,?laststate,?nowstate);?? ????????}?? ????}?? }?? ?? inline?int?mul(ll?x,?ll?y)?{?? ????return?x?*?y?%?1000000007;?? }?? ?? void?multiply(int?n,int?a[][32],int?b[][32])?{??? ????int?i,j,?k;?? ????for?(i?=?0;?i?<?n;?++i)?{?? ????????for?(j?=?0;?j?<?n;?++j)?{?? ????????????for?(k?=?b[i][j]?=?0;?k?<?n;?++k)?{?? ????????????????if?((b[i][j]?+=?mul(a[i][k],a[k][j]))?>=?1000000007)?{?? ????????????????????b[i][j]?-=?1000000007;?? ????????????????}?? ????????????}?? ????????}?? ????}?? }?? ?? int?calculate(int?n,int?k)?{?? ????int?i,?j;?? ????int?a[6][32][32],mat[2][32][32];?? ????memset(a,0,sizeof(a));?? ????for?(i?=?1;?i?<=?5;?++i)?{?? ????????for?(j?=?(1?<<?i)?-?1;?j?>=?0;?--j)?{?? ????????????cal(a,i,?0,?j,?0);?? ????????}?? ????}?? ????memcpy(mat[0],?a[k],sizeof(mat[0]));?? ????k?=?(1?<<?k);?? ????for?(i?=?0;?n;?--n)?{?? ????????multiply(k,?mat[i],?mat[i?^?1]);?? ????????i?^=?1;?? ????}?? ????return?mat[i][0][0];?? }??
參考鏈接及推薦閱讀
caopengcs,最小操作數:http://blog.csdn.net/caopengcs/article/details/9919341;caopengcs,木塊砌墻:http://blog.csdn.net/caopengcs/article/details/9928061;紅色標記,木塊砌墻:http://blog.csdn.net/dw14132124/article/details/9038417;LoveHarvy,木塊砌墻:http://blog.csdn.net/wangyan_boy/article/details/9131501;
在線編譯測試木塊砌墻問題:http://hero.pongo.cn/Question/Details?ID=36&ExamID=36;編程藝術第29章字符串編輯距離:http://blog.csdn.net/v_july_v/article/details/8701148#t4;matrix67,十個利用矩陣乘法解決的經典題目:http://www.matrix67.com/blog/archives/276;
leetcode上最小操作數一題:http://leetcode.com/onlinejudge#question_126;hero上木塊砌墻一題:http://hero.pongo.cn/Question/Details?ExamID=36&ID=36&bsh_bid=273040296;超然煙火,http://blog.csdn.net/sunnianzhong/article/details/9326289;
總結
以上是生活随笔為你收集整理的最小操作数,木块砌墙问题的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。