1. 程式人生 > >矩陣與動態規劃相關

矩陣與動態規劃相關

目錄

54/59螺旋矩陣

思路:

  • 設定上下左右四個邊界變數
  • 新建ArrayList儲存結果
  • 迴圈:四個迴圈,左->右,上->下,右->左,下->上。每次迴圈新增結果,迴圈後判斷邊界是否相等了,是的話就退出
// 設定邊界變數
int m = matrix.length, n = matrix[0].length;
int left = 0, right = n - 1, up = 0, down = m - 1;

// 迴圈,注意right = n - 1,所以遍歷範圍i <= right
while (true){
    for (int i = left; i <= right; i++){
        res.add(matrix[up][i]);
        // 獲取迴旋矩陣的話用 res[up][i] = val++;
    }
    if (++up > down) break;
    
    for (int i = up; i <= down; i++){
        res.add(matrix[i][right]);
    }
    if (--right < left) break;
    
    for (int i = right; i >= left; i--){
        res.add(matrix[down][i]);
    }
    if (--down < up) break;
    
    for (int i = down; i >= up; i--){
        res.add(matrix[i][left]);
    }
    if (++left > right) break;
}

62不同路徑

一個機器人位於一個 m x n 網格的左上角 。機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角。

思路:畫圖

int[] dp = new int[n];
dp[0] = 1;
for (int i = 0; i < m; ++i) {
    for (int j = 1; j < n; ++j) {
        dp[j] += dp[j - 1]; 
    }
}
return dp[n-1];

64最小路徑和

與上面未經優化的程式碼有點像。

思路:

  • 新建dp,填充起步數值。
    • 第一個值為grid[0][0]
    • 第一行和列通過前一個格子和grid的對應值的和來填充
  • 遍歷
    • dp[i][j]的值為grid的對應值加上上一步dp的最小值
int m = grid.length, n = grid[0].length;
int[][] dp = new int[m][n];
dp[0][0] = grid[0][0];
for (int i = 1; i < m; ++i) dp[i][0] = grid[i][0] + dp[i - 1][0];
for (int i = 1; i < n; ++i) dp[0][i] = grid[0][i] + dp[0][i - 1];
for (int i = 1; i < m; ++i) {
    for (int j = 1; j < n; ++j) {
        dp[i][j] = grid[i][j] + Math.min(dp[i - 1][j], dp[i][j - 1]);
    }
}
return dp[m - 1][n - 1];

120三角形最小路徑和

思路:

  • 新建陣列,複製最後一行
  • 迴圈
    • 將第一個數改為第一個數與第二個數的最小值加下上一行的第一個數,第二個數改為第二個數與第三個數的最小值加上一行第二個數,如此類推。

要注意的是下面j的範圍j <= i

for (int i = n - 2; i >= 0; i--){
    for (int j = 0; j <= i; j++){
        dp[j] = Math.min(dp[j], dp[j+1]) + m.get(i).get(j);
    }
}

695島嶼的最大面積

思路:

  • 遍歷二維陣列
    • 如果當前數值==1,說明是島嶼,而且沒有遍歷過,此時呼叫helper計算這個島嶼的面積,並根據返回結果更新res
  • helper函式
    • 檢查島嶼座標的合法性,沒有超邊界和數值==1(後續呼叫需要檢查)
    • 把當前島嶼值變為0,說明已經遍歷
    • return 1+對四個方位呼叫helper

547朋友圈

思路:

  • 新建一個visited資料記錄已被遍歷的人
  • 開始遍歷,如果這個人還沒被遍歷過,那麼就是一個新的朋友圈,cnt++,然後呼叫helper遞迴搜尋這個人的朋友。
  • helper,先把遍歷到的人在visited中設定為true,然後遍歷這個人的行,如果是朋友,且這個人沒有被遍歷過,那麼就遞迴呼叫helper。
// 設定人數變數、朋友圈數計算器
int n = M.length, cnt = 0;

// 記錄visited的人的陣列
boolean[] visited = new boolean[n];

// 按順序遍歷每個人,如果沒有訪問過才開始呼叫helper
for (int i = 0; i < n; i++){
    // 如果未訪問過,就呼叫helper
    if (!visited[i]){
        helper(M, i, visited);
        // 遍歷一次,朋友圈數 +1
        cnt++;
    }
}
return cnt;

// helper
visited[k] = true;
// 遍歷這個人的朋友
for (int i = 1; i < m.length; i++){
    // 如果這個人是朋友,而且沒有訪問過,那就呼叫helper
    if (m[k][i] == 1 && !visited[i]){
        helper(m, i, visited);
    }
}

718最長重複陣列

輸入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
輸出: 3
解釋: 
長度最長的公共子陣列是 [3, 2, 1]。

思路:

  • 遞推公式為dp[j] = A[i] == B[j] ? dp[j-1] + 1 : 0。然後畫圖,先畫二維,再到一維。

221最大正方形

思路:

  • 新建與引數一樣大小的dp
  • 遍歷填充
if (i == 0 || j == 0) dp[i][j] = matrix[i][j] - '0';
else if (matrix[i][j] == '1') {
    dp[i][j] = 當前dp左上,上,左的最小值 + 1;
}
res = Math.max(res, dp[i][j]);

return res * res;

121/122/123/714/188買賣股票的最佳時機

121:只允許一筆交易

122:允許多筆交易

123:只允許兩筆交易

714:允許多筆交易,包含手續費

188:只允許k筆交易

int sell = 0, buy = Integer.MIN_VALUE + free; // free為714,防止第一步溢位
for (int price : prices){
    // 第二個 sell 表示第i天前最後一個操作是賣,此時的最大收益。第一步的sell肯定得到0
    sell = Math.max(sell, price + buy - free); // free為714
    // 121
//    buy = Math.max(buy, 0 - price);
    // 122
    buy = Math.max(buy, sell - price); 
}
return sell;

// 123
int buy1 = Integer.MIN_VALUE, sell1 = 0;
int buy2 = Integer.MIN_VALUE, sell2 = 0;
for (int price : prices) {
    // 這裡要倒排,因為sell2是依賴之前的buy2資料,buy2依賴之前的sell1資料,如此類推
    sell2 = Math.max(sell2, buy2 + price);
    buy2 = Math.max(buy2, sell1 - price);
    sell1 = Math.max(sell1, buy1 + price);
    buy1 = Math.max(buy1, 0 - price); // max尋找最低購入價,第一步buy的sell都是0
}

return sell2;

// 188
if (k > prices.length) {
    // 122程式碼
}
// 下面程式碼把k改為2,就是123的答案
int[] buyArr = new int[k+1];
int[] sellArr = new int[k+1];
Arrays.fill(buyArr,Integer.MIN_VALUE);
for (int price : prices) {
    for (int i = k; i > 0; i--) {
        sellArr[i] = Math.max(sellArr[i], price + buyArr[i]);
        buyArr[i] = Math.max(buyArr[i], sellArr[i-1] - price);
    }
}
return sellArr[k];

416分割等和子集

判斷陣列中的數是否可以被分割成兩份和相等的子集

思路:

  • 累加判斷子集和是否為偶數,不是偶數就已經不可能等分了
  • 總和/2作為目標值,並新建boolean dp[target+1]
  • 遍歷
    • 遞推思想:當前dp是否為true,取決於之前是否已經可以組成i,或者考慮上當前遍歷的num就能等於i。公式為:dp[i] = dp[i] || dp[i - num]
int sum = 0;
for (int num : nums) sum += num;
if ((sum & 1) != 0) return false;

int target = sum >> 1;

// dp[i]表示上面解法的 j ,即前面所有元素的組合的"和"是否能夠等於i
boolean[] dp = new boolean[target + 1];
dp[0] = true;
for (int num : nums) {
    // 直接從num開始,因為考慮了num,那麼其組合最小也等於num
    for (int i = target; i >= num ; i--) {
        dp[i] = dp[i] || dp[i - num];
    }
}

return dp[target];

01揹包/最近等分子集

01揹包

思路:

遞推公式f[i][j] = Math.max(f[i - 1][j - w[i - 1]] + p[i - 1], f[i - 1][j]);取和不取第i個物品的最大值作為f[i][j]。從公式中可知,沒有j-1,所以dp在j的維度上不需要1。另外,公式中考慮了i-1,所以dp在i維度上要+1,而i=1時所考慮的0,即沒有考慮任何物品,所以dp值自然都是0,不需要另外初始化。由於只考慮i-1,所以可以用一維的dp,每次更新dp時,其本身的值就是之前的值。要注意的是j的起始值為當前物品的重量,因為小於這個重量,就不可能考慮加入這個物品了。最後,由於j - w[i - 1],說明j要考慮之前的值,所以1維的覆蓋要從後往前。

int[] dp = new int[capacity];

for (int i = 0; i < n; i++) {
    for (int j = capacity-1; j >= w[i]; j--) {
        dp[j] = Math.max(dp[j], dp[j - w[i]] + p[i]);
    }
}
return dp[capacity-1];

最近等分子集

思路:

  • 累加子集然後除以二得target。目標是讓選取的子集的和儘可能接近targe。類比上題可得優化後的遞推公式為dp[i] = Math.max(dp[i - num] + num, dp[i]);與“416分割等和子集”一樣的方式得出target,由於取最接近,所以不需要判斷可能性。dp也類似,選取的是i代表子集中所考慮的與
for (int num : arr) {
    for (int i = target-1; i >= num; i--) {
        dp[i] = Math.max(dp[i - num] + num, dp[i]);
    }
}

return sum - 2 * dp[target-1];

70爬樓梯

f(x) = f(x-1) + f(x-2)