請設計乙個函式,用來判斷在乙個矩陣中是否存在一條包含某字串所有字元的路徑。路徑可以從矩陣中任意一格開始,每一步可以在矩陣中向左、右、上、下移動一格。如果一條路徑經過了矩陣的某一格,那麼該路徑不能再次進入該格仔。
例如在下面的3×4的矩陣中包含一條字串「bfce」的路徑(路徑中的字母用下劃線標出)。但矩陣中不包含字串「abfb」的路徑,因為字串的第乙個字元b佔據了矩陣中的第一行第二個格仔之後,路徑不能再次進入這個格仔。
題意比較容易理解,場景也比較直觀,沒有需要特別考慮的地方。
一般而言,暴力窮舉是可以實現的,把窮舉的結果封裝成字典樹,直接查即可。但是如果不提前知道字串中的字元的位數,需要考慮所有位數可能的情況,那麼代價是非常非常大的,情況實在是太多了。
既然要暴力,那這裡比較好的迂迴方案是 回溯法, 我比較喜歡稱它「「溫柔的暴力」
用回溯法解題的乙個顯著特徵是在搜尋過程中動態產生問題的解空間。在任何時刻,演算法只儲存從根結點到當前擴充套件結點的路徑。
基本思想如下:
針對所給問題,定義問題的解空間;
確定易於搜尋的解空間結構;
以深度優先方式搜尋解空間,並在搜尋過程中用剪枝函式避免無效搜尋
剪枝函式有下面兩種常見的型別:下面依據演算法思想去分析這個問題:用
約束函式
在擴充套件結點處剪去不滿足約束的子樹;用
限界函式
剪去得不到最優解的子樹。
假如從 f 開始,我們下一步有 4 種搜尋可能,如果先搜尋 d,需要將 d 標記為已經使用,防止重複使用。在這一次搜尋結束之後,需要將 d 的已經使用狀態清除,並搜尋 b。
**如下:
/**
** @param matrix 字元陣列
* @param rows 矩陣的行數
* @param cols 矩陣的列數
* @param str 查詢的字串的字元陣列
* @return 判斷結果
*/public boolean haspath(char matrix, int rows, int cols, char str)
// 用於記錄狀態的陣列 預設為false,也就是沒有訪問過
boolean isvisited = new boolean[rows * cols];
arrays.fill(isvisited, false);
// pathlength記錄已經搜尋時的字串長度
int pathlength = 0;
for (int row = 0; row < rows; row++)
}return false;
}// 解空間
private boolean haspathcore(char matrix, int rows, int cols, int row, int col, char str, int pathlength,
boolean isvisited)
// 判斷是否滿足結束條件
if (pathlength == str.length - 1)
// 記錄元素已經被訪問
isvisited[row * cols + col] = true;
boolean haspath = haspathcore(matrix, rows, cols, row - 1, col, str, pathlength + 1, isvisited)
|| haspathcore(matrix, rows, cols, row + 1, col, str, pathlength + 1, isvisited)
|| haspathcore(matrix, rows, cols, row, col - 1, str, pathlength + 1, isvisited)
|| haspathcore(matrix, rows, cols, row, col + 1, str, pathlength + 1, isvisited);
// 不滿足(但是可能之後滿足,這裡消除已訪問狀態)
if (!haspath)
// 回溯
return haspath;
}
如果對遞迴不熟悉,這套題又有點陌生。那你看是有點難看出來門道的,動筆畫畫,用ide除錯一下,才能理解更好點。
這裡需要說明一下:回溯是深度優先搜尋的一種特例,它在一次搜尋過程中需要設定一些本次搜尋過程的區域性狀態,並在本次搜尋結束之後清除狀態。而普通的深度優先搜尋並不需要使用這些區域性狀態,雖然還是有可能設定一些全域性狀態。
記得當年,我第一次接觸回溯法的時候,對解空間非常懵。這裡記錄一下,我是如何認識解空間的。
解空間 是 解的空間結構的意思。
對於問題的解的空間結構通常以樹或圖的形式表示,比較常見的兩類典型的解空間是子集樹和排列樹。
當所給的問題是從n個元素的集合s中找到s滿足某種性質的子集時,相應的解空間樹稱為子集樹。
我們上面做的這道題用到的解空間就是子集樹。
上圖是一棵n為3的子集樹。從根到每乙個葉結點的路徑表示乙個可行解。從根結點出發,以深度優先的方式搜尋整棵樹。用回溯法搜尋子集樹的一般演算法 偽** 可描述為:
void backtrack(int t)
else }}
當所給問題是確定n個元素滿足某種性質的排列時,相應的解空間樹稱為排列樹。
排列樹從根到葉結點表示一條可選路徑。與子集樹不同的是,每乙個當前結點的搜尋策略是選擇剩下的元素中的乙個,而子集樹是選擇或不選擇當前元素。排列樹通常有n!個葉子節點。因此遍歷排列樹需要o(n!)的計算時間。
(圖示為旅行員售貨)
用回溯法搜尋排列樹的一般演算法為(偽** 參考旅行員售貨問題):
void backtrack(int t)
else//與子集樹不同,搜尋 剩下的結點
swap(x[t], x[i]);}}
劍指Offer對答如流系列 醜數
我們把只包含質因子2 3和5的數稱作醜數 ugly number 求按從小到大的順序的第n個醜數。例如6 8都是醜數,但14不是,因為它包含質因子7。習慣上我們把1當做是第乙個醜數。判斷乙個數是不是醜數,最容易想到的方法就是讓這個數不斷除以2,3,5。對於第n個醜數,只要從1開始,依次判斷每個數是不...
劍指Offer對答如流系列 剪繩子
給你一根長度為n繩子,請把繩子剪成m段 m n都是整數,n 1並且m 1 每段的繩子的長度記為k 0 k 1 k m k 0 k 1 k m 可能的最大乘積是多少?例如當繩子的長度是8時,我們把它剪成長度分別為2 3 3的三段,此時得到最大的乘積18。遇到問題,先分析問題,由分析的結果確定所運用的演...
劍指Offer對答如流系列 包含min函式的棧
定義棧的資料結構,請在該型別中實現乙個能夠得到棧的最小元素的min函式。在該棧中,呼叫min push及pop的時間複雜度都是o 1 push 和 pop均容易實現。主要就是min函式的定義,如果要通過操作push和pop操作獲取最小元素時間複雜度為o 1 基本上是不可能的。如果我們另外定義乙個成員...