1. 程式人生 > >動態規劃中初識狀態壓縮(入門)

動態規劃中初識狀態壓縮(入門)

想必很多人還不知道動態規劃是可以狀態壓縮的吧,通俗的講就是把維數變小,一般就是把二維陣列降為一維。維數變小意味著空間變小,速度還不變,不用空間換時間,這就是狀態壓縮的強大之處。 以leetcode64題最小路徑和為例,帶大家一步一步見識一下狀態壓縮這個小技巧 題意:給定一個包含非負整數的 m x n 網格 grid ,請找出一條從左上角到右下角的路徑,使得路徑上的數字總和為最小 說明:每次只能向下或者向右移動一步 示例1 ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4e637988f57f424086e31b2b7a080474~tplv-k3u1fbpfcp-watermark.image) 輸入:grid = [[1,3,1],[1,5,1],[4,2,1]] 輸出:7 解釋:因為路徑 1→3→1→1→1 的總和最小 函式名: ``` public int minPathSum(int[][] grid) ``` 題目的最基本的狀態轉移方程是 dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]; 意思是如果我們在i, j 這個位置的話從可以從兩個方向推出dp[i][j]的值,題目說明了每次只能向下或者向右移動一步,如下圖 ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/144658908a3c43b1b924ca8078b8004e~tplv-k3u1fbpfcp-watermark.image) 完整程式碼如下 ``` class Solution { public int minPathSum(int[][] grid) { if (grid.length == 0) return 0; int n = grid.length; int m = grid[0].length; int[][] dp = new int[n][m]; dp[0][0] = grid[0][0]; //初始化 //從(0,0)一直向下走 for (int i = 1; i < n; i++) dp[i][0] = dp[i - 1][0] + grid[i][0]; //從(0,0)一直向右走 for (int j = 1; j < m; j++) dp[0][j] = dp[0][j - 1] + grid[0][j]; //狀態轉移 for (int i = 1; i < n; i++) for (int j = 1; j < m; j++) dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]; return dp[n - 1][m - 1]; } } ``` 上面的測試用例執行後的dp陣列如下 ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/084950c99aa447c5a848b963957f5289~tplv-k3u1fbpfcp-watermark.image) 上面的程式碼很直觀吧 下面的程式碼是為了推出狀態壓縮而寫的,原理和上面一樣,只是上面的dp[0][0]換成下面的dp[1][1] 我這樣子寫不是說要這樣子做,只是為了方便大家理解狀態壓縮而已,基本思路完全沒有變 先貼出dp陣列結果如下 ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a93133bd282d44bb84a50846855d431c~tplv-k3u1fbpfcp-watermark.image) 程式碼如下 ``` class Solution { public int minPathSum(int[][] grid) { if (grid.length == 0) return 0; int n = grid.length; int m = grid[0].length; int[][] dp = new int[n + 1][m + 1]; //初始化 for (int i = 0; i <= n; i++) dp[i][0] = Integer.MAX_VALUE; dp[1][1] = grid[0][0]; //初始狀態 //從(1,1)一直往右走 for (int j = 2; j <= m; j++) dp[1][j] = dp[1][j - 1] + grid[0][j - 1]; //狀態轉移 for (int i = 2; i <= n; i++) for (int j = 1; j <= m; j++) dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1]; return dp[n][m]; } } ``` 接下來開始進行狀態壓縮 把二維降為一維結果如下,採用的是**直接**投影的方法 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cc341e8d9eda420c94e220ef9add2d48~tplv-k3u1fbpfcp-watermark.image) 投影也就是**直接把dp[i][j]中i所在的那一維去掉** 程式碼的轉變過程如下 由 ``` dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1] ``` 變成 ``` dp[j] = Math.min(dp[j], dp[j - 1]) + grid[i - 1][j - 1] ``` 解釋一下上面為什麼這樣子就完成了 想要求出dp[i][j]需要先求出dp[i - 1][j]和dp[i][j - 1] 我們發現二維的dp[i - 1][j]對映成了一維的dp[j],dp[i][j - 1]對映成了dp[j - 1] 所需要的資訊都能直接用一維來表示 哪怕dp[j]被覆蓋了也沒事,原因看下圖 ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cc22e723a3b04b3988fd3f8624e68d04~tplv-k3u1fbpfcp-watermark.image) 所以 ``` dp[j] = Math.min(dp[j], dp[j - 1]) + grid[i - 1][j - 1] ``` 完整程式碼如下: ``` class Solution { public int minPathSum(int[][] grid) { if (grid.length == 0) return 0; int n = grid.length; int m = grid[0].length; int[] dp = new int[m + 1]; //初始化 dp[0] = Integer.MAX_VALUE; dp[1] = grid[0][0]; //從(1,1)一直向右走 真的只是把上面的程式碼中dp[i][j]的i那一維去掉 for (int j = 2; j <= m; j++) dp[j] = dp[j - 1] + grid[0][j - 1]; //這裡也是直接去掉i中的那一維 for (int i = 2; i <= n; i++) for (int j = 1; j <= m; j++) dp[j] = Math.min(dp[j], dp[j - 1]) + grid[i - 1][j - 1]; return dp[m]; } } ``` 這題到這裡也就結束了 這題的狀態壓縮之所以簡單是因為沒有多個元素對映到同一個位置 如果如下圖所示的話 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0bd7db8ac2994e25b7bbf8a547dbe49e~tplv-k3u1fbpfcp-watermark.image) 一旦對映就會有兩個狀態對映到同一個位置,這裡提示一下,此時也很簡單,只需要**定義一個變數**,來**儲存**那兩個對映到同一個位置的**兩個變數中的其中一個變數**就行了 後面我有空也會寫一下兩個狀態對映到同一位置的這種情況 這裡只是大概幫助你們入一下門。以後你看到有兩個狀態對映到同一位置的狀態壓縮的程式碼時,也能很輕鬆的讀懂 這或許也是為什麼面試官那麼看重計算機基礎,不就是因為計算機基礎是一切的基石,計算機基礎穩了,上手其他那不是手到擒來 圖片目前做的不是很美觀,以後會慢慢優化。 如果覺得有收穫,不妨花個幾秒鐘點個贊,歡迎關注我的公眾號**玩程式設計地碼農**,目前會不斷寫與java、資料結構與演算法和計算機基礎相關的知識等。 ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/186661b7be4e4a1fa7f5e23f253b2707~tplv-k3u1fbpfcp-watermar