巧用遞迴解決矩陣最大序列和問題
之前同事問了一道需要點腦洞的演演算法題,我覺得蠻有意思的,思路可能會給大家帶來一些啟發,特意在此記錄一下
題目
現有一個元素僅為 0,1 的 n 階矩陣,求連續相鄰(水平或垂直,不能有環)元素值為 1 的序列和的最大值 假設有如下矩陣
則此矩陣連續相鄰元素為 1 的序列和分別為 4, 3,(如圖示),可知這個矩陣序列和的最大值為 4
解題思路
要算序列和的最大值,我們可以先找出所有可能的序列和,然後取其中的最大值,那怎麼找這些序列呢? 首先我們發現,每個序列的起點和終點必然是 1,我們可以遍歷矩陣的每一個元素,如果元素值為 1,則將其作為序列的起點開始查詢所有以這個元素為起點的序列,我們知道序列是可以向垂直和水平方向延伸的,所以我們可以以這個元素為起點,查詢它的上下左右值為 1 的元素,再以找到的這些元素為起點,繼續在元素的上下左右查詢值為1的元素(遞迴),如果找不到符合條件的值,則序列終止,在遍歷過程中儲存每條序列遍歷的元素,即可知曉每條序列的元素和,從而求得序列和的最大值
文字說得有點繞,接下來我們就以查詢以下矩陣的最大序列和為例來詳細看一下如何查詢最大序列和
-
從左到右,從上到下遍歷所有值為 1 的元素,第一個符合條件的元素在右上角,所以以這個元素為起點來查詢序列
-
以這個元素為起點,查詢這個元素上下左右為值為 1 的元素,發現只有這個元素下面的元素符合條件
-
再以這個元素為起點查詢這個元素前後左右值為 1 的元素,可以看到這個元素的上 ,左元素值為 1,左邊的元素顯然符合條件,而上面的元素由於是當前正在遍歷序列中遍歷過的元素,所以不符合條件(假設上面的元素符合條件,會發生什麼?接下來會尋找以上面元素為起始點的序列,又回到了第一步,陷入無限迴圈,所以元素的下一個值為 1 的元素不能是當前正在遍歷的序列中的元素!
-
再尋找此元素上下左右都為 1 的元素,可以看到這個元素的左右下的元素都為 1,根據上一步的分析可知,右元素是當前正在遍歷序列中已遍歷過的元素,所以不符合條件,那麼只剩下左,下元素符合條件
-
再次尋找這兩個元素上下左右皆為 1 的元素,可知符合條件的元素為步驟 3 中的紅框元素,由於此元素是當前正在遍歷序列中已遍歷過的元素,所以不符合條件,序列的遍歷到此終止,至此我們可以知道,從右上角元素為起點的序列和的最大值為 4,連線遍歷過的元素,如圖示
或
-
同理接下來再按照以上的步驟依次遍歷剩餘的值為 1 的元素,可知以這些元素為起點的序列和的最大值分別為 4,3,4(如下圖)
(紅圈的元素代表序列遍歷的起始點) -
綜上可知,此矩陣連續相鄰值為 1 的元素的序列和的最大值為 4
程式碼實現
好了知道了解題思路,現在我們來看下程式碼該如何實現 首先我們要用一個資料結構來表示矩陣,顯然矩陣用陣列表示很合適,這裡我們用一維陣列來表示矩陣,Java 程式碼如下
public class Matrix {
/**
* @param matrix 矩陣
* @param dimension 代表 dimension 階矩陣
* @return 矩陣序列的最大值
*/
private static Integer getMaxSequetialSum(int[] matrix,int dimension) {
int count = matrix.length; // 矩陣的元素個數
int maxSequentialSum = 0; // 矩陣序列的最大值
// 逐個遍歷元素
for (int index = 0; index < count; index++) {
int elementValue = matrix[index];
// 如果當前元素為1,則以此元素為起點,查詢以此元素為起點的序列的和的最大值
if (elementValue == 1) {
// 記錄以下標為 index 的元素為起點的序列遍歷過的元素位置,以防元素被重複遍歷
Set<Integer> traverseElementSet = new HashSet<>();
traverseElementSet.add(index);
// 以下標值為 index 的元素為起點的序列的最大值
int currentSequetialSum = getCurrentVerticeSequetialSum(matrix,traverseElementSet,index,dimension);
maxSequentialSum = Math.max(maxSequentialSum,currentSequetialSum);
}
}
return maxSequentialSum;
}
/**
* @param matrix 矩陣
* @param traverseElementSet 序列中已遍歷過的元素的位置
* @param index 元素的位置,序列的起點
* @param dimension dimension 階矩陣
* @return 以位置為 index 的元素為起點的序列的最大值
*/
private static Integer getCurrentVerticeSequetialSum(int[] matrix,Set<Integer> traverseElementSet,int index,int dimension) {
// 查詢 矩陣中位置為 index 的元素上下左右元素對應的位置
int left = index - 1;
int right = index + 1;
int up = index - dimension;
int down = index + dimension;
// 以左元素為起點的序列的值
int leftIndexSum = 0;
// 以右元素為起點的序列的值
int rightIndexSum = 0;
// 以上元素為起點的序列的值
int upIndexSum = 0;
// 以下元素為起點的序列的值
int downIndexSum = 0;
/**
* 以下四個if else 旨在檢查每一個元素位置的有效性,值必須為 1
* 需要注意的是元素不能是序列已遍歷過的元素!
* 如果上下左右元素不合法,則序列終止,打點此遍歷序列的元素和
*/
if (left >= 0 && matrix[left] == 1 && !traverseElementSet.contains(left)) {
Set<Integer> leftTraverseElementSet = new HashSet<>(traverseElementSet);
leftTraverseElementSet.add(left);
leftIndexSum = getCurrentVerticeSequetialSum(matrix,leftTraverseElementSet,left,dimension);
} else {
leftIndexSum = traverseElementSet.size();
}
// 右元素必須與位置為index的元素在同一行上
if (right / dimension == index / dimension && matrix[right] == 1 && !traverseElementSet.contains(right)) {
traverseElementSet.add(right);
Set<Integer> rightTraverseElementSet = new HashSet<>(traverseElementSet);
rightTraverseElementSet.add(right);
rightIndexSum = getCurrentVerticeSequetialSum(matrix,rightTraverseElementSet,right,dimension);
} else {
rightIndexSum = traverseElementSet.size();
}
if (up >= 0 && matrix[up] == 1 && !traverseElementSet.contains(up)) {
Set<Integer> upTraverseElementSet = new HashSet<>(traverseElementSet);
upTraverseElementSet.add(up);
upIndexSum = getCurrentVerticeSequetialSum(matrix,upTraverseElementSet,up,dimension);
} else {
upIndexSum = traverseElementSet.size();
}
if (down < matrix.length && matrix[down] == 1 && !traverseElementSet.contains(down)) {
Set<Integer> downTraverseElementSet = new HashSet<>(traverseElementSet);
downTraverseElementSet.add(down);
downIndexSum = getCurrentVerticeSequetialSum(matrix,downTraverseElementSet,down,dimension);
} else {
downIndexSum = traverseElementSet.size();
}
// 查詢以位置為 index 的元素為起點各向上下左右延伸的序列的最大值
return Collections.max(Arrays.asList(leftIndexSum,rightIndexSum,upIndexSum,downIndexSum));
}
public static void main(String[] args) {
// 初始化矩陣,假設此矩陣為 5 x 5 矩陣
int[] matrix1 = {
0,0,1,};
int max = Matrix.getMaxSequetialSum(matrix1,5);
System.out.println(max); // 列印4
int[] matrix2 = {
0,};
max = Matrix.getMaxSequetialSum(matrix2,5);
System.out.println(max); // 列印6
}
}
複製程式碼
時間複雜度與空間複雜度分析
任何演演算法,如果不談時間複雜度與空間複雜度都是耍流氓,接下來我們看下以上解法的時間複雜度和空間複雜度 1.首先來看空間複雜,由於在在遍歷過程中我們用了記錄遍歷序列元素位置的 traverseElementSet,所以空間複雜度顯然是 O(n) 2.這道題用了遞迴,時間複雜度確實挺複雜的,也比較考驗程式設計師的水平,直觀上看不出來,那我們看下怎麼推導,我們用 f(n) 來表示以位置為 n 的元素為起點的序列和的計算次數,從以上的推導可知,只要計算出以此元素的上下左右元素為起點的序列和的最大值,也自然知道了 f(n)。即計算以位置 n 為起點的序列和次數換算成計算以此元素的上下左右元素為起點的序列和的次數
**f(n) = f(左) + f(右) + f(上) + f(下) **
仔細考慮一下可知以上下左右四個元素為起點的序列和的計算次數可以認為是一樣的 從而有 f(n) = 4f(左) 假設矩陣元素個數為N,則 f(n) = 4N 由於有 N 個元素,所以可知總的時間複雜度為 O(4N2),即 O(n2)
總結
這道題乍一看確實沒什麼頭緒,無法像反轉二叉樹那樣比較容易地看出使用遞迴的思路去解決,所以我們需要耐心地去分析,學會把問題分解,分解思路如下 求序列的最大和轉化為求所有序列的和 ----> 轉化成如何找尋所有的序列 ----> 觀察到序列的起點的元素必須是 1 ----> 想到如何找尋以此元素為起點的所有序列 ----> 只要找到以這個元素上下左右值為 1 的元素為起點的所有序列和 ----> 再以上下左右元素值為 1 的元素為起點遞迴找尋以它們各自的上下左右值為 1 的元素為起點的所有序列和 ----> 找到所有的序列和後自然就找到了最大序列和
個人微訊號「geekoftaste」,歡迎加微信一起交流,共同進步