0.动态规划与暴力递归基础
基础理论知识请参考我的算法课程学习之动态规划与暴力递归
1.线性DP
3.递推型DP
所有线性递推关系都可以用矩阵快速幂做,可以达到O(logN),最典型是斐波那契数列。
斐波那契数列
递推公式遵守F(N) = F(N-1) + F(N-2)
, 对于第N项,有矩阵乘法的方式可以将时间复杂度降到O(logN)。可以用矩阵的形式,表达递推公式:
根据前四列的值,可以推导出:
多推倒几次后,可以得到如下通式:
求数列就转化成了,状态矩阵的乘法和幂运算。
矩阵乘法和幂运算
矩阵乘法,要遵守[m,n] * [n,m']
, 也就是维度要匹配。新矩阵的(i,j)
项是m矩阵
的第i
行和m'矩阵
第j
列的内积,所以两个矩阵的维度要匹配,不然无法在这里求内积。
public static long[][] matrixMulti(long[][] m1, long[][] m2) { // 矩阵乘法: [m*n] * [n*m'] => m*m' int row = m1.length; int col = m2[0].length; long[][] res = new long[row][col]; // 填充每一个元素 for (int i=0; i<row; i++){ for(int j=0; j<col; j++){ // res[i][j] = m1中i行的每个元素和m2中j列的每个元素,相乘并相加 for(int k=0; k<m2.length; k++){ res[i][j] += m1[i][k] * m2[k][j]; } } } return res; }
矩阵的幂运算,就是多次乘法运算,这里先解释如何快速求10的75次方?
- 75的二进制形式是1001011
- 10的75次方=10ˆ64 * 10ˆ8 * 10ˆ2 * 10ˆ1。
在过程一中,先求出10ˆ1, 再根据它平方求出10ˆ2, 再由10ˆ2的平方求出10ˆ4,..., 75的二进制有多少位,就使用几次乘法。
在过程二中,把75对应的二进制上为1的位,累乘所得到的数相乘即可,比如10ˆ64,10ˆ8,10ˆ2, 10ˆ1应该累乘;而32,16,4上对应的累乘数,不相乘。
// temp 被 matrixMulti的结果所更新 public static long[][] matrixPower(long[][] m, long power){ // 只有方阵有幂运算,结果也是方阵 long[][] res = new long[m.length][m[0].length]; // 运算子,每次都要自乘,也就是一次幂已经计算了 long[][] temp = m; // 先把res初始化为单位阵 for (int i=0; i<m.length; i++){ res[i][i] = 1; } // 一个数二进制有多少位: 右移位直到为0的移动次数 for (; power!=0; power >>= 1){ // 二进制最低位上是否为0 // 不为0,则累乘 if ( (power&1) != 0 ){ // 这里的temp是n次幂 res = matrixMulti(res, temp);// 单位矩阵的作用就类似1 } // 更新n次幂,为下次循环做准备 temp= matrixMulti(temp, temp); } return res; }
定义好了上述两个运算,求Fabonacci第N项就是,现将状态矩阵进行N-2次幂运算,再左乘初始状态矩阵[F(2), F(1)]
// 计算Fibonacci数列的第n项 public static long getFibonacciNth(long N){ // base case: if (N == 1 || N == 2){ return 1; } long[][] base = { {1,1}, {1,0} }; long[][] res = matrixPower(base, N-2); // 1*res[0][0] + 1*res[1][0]; return res[0][0] + res[1][0]; }
跳台阶(12型求方法数)
奶牛繁殖
4. 贪心策略DP
跳跃游戏2
题号45
给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。
示例:
请在这里输入引用内容
输入: [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
题解思路:
每走出一步中找到最远能达到边界,如果当前位置大于这个边界了,就说明我们不得不跳跃一次,每次跳跃必定是最远的那个。
// 每次尝试走一步,如果走到超出最大边界了,则步数+1,也就是被迫走的每一步都是能达到最大 // 边界的 public static int jump(int[] nums) { // 跳的步数 int step = 0; // 跳当前步,最远能达到的地方 int curRange = 0; // 多跳一步,最远能达到的地方,为下一次跳做准备 int nextRange = -1; for (int i = 0; i < nums.length; i++){ // 不得不跳一步,从之前准备好的最远的,跳那一步 if ( curRange < i){ ++step; curRange = nextRange; } // 比如,在i=0的位置上,跳出第一步能达到的最远距离就是 i+nums[i] nextRange = Math.max(nextRange, i + nums[i]); } return step; }
5. 计数型DP
计数型DP可以以组合数学的方法写出组合数,然后dp求组合数,比如“不同路径I”
不同路径
Nr. 62
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
请在这里输入引用内容
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
解题思路:
创建DP数组,dp[i][j]表示到达(i,j)的方法数,它是由子问题dp[i-1][j]和dp[i][j-1]
注意初始化条件,第一行和第一列都是1,方法数都是1,因为只能直行。
// dp[i][j] 就表示到达(i,j)位置的方法数,它是由子问题,左边和上边的方法数 public static int uniquePaths_DP(int m, int n){ // 棋盘其实是 n*m int[][] dp = new int[n][m]; int rows = dp.length; int cols = dp[0].length; // base case: 因为只能向下或者向右,所以第一行,第一列的值都是1,只有一种走法 // 第一行 for (int i = 0; i < m; i++){ dp[0][i] = 1; } for (int i = 0; i < n; i++){ dp[i][0] = 1; } for (int i=1; i<rows; i++){ for (int j=1; j<cols; j++){ dp[i][j] = dp[i-1][j] + dp[i][j-1]; } } // 想求board[n-1][m-1]的值 return dp[rows-1][cols-1]; }
这种依赖左边和上边的可以空间压缩成一个数组。
public static int uniquePaths_DP3(int m, int n){ int[] dp = new int[m]; int len = m; // 空间压缩 Arrays.fill(dp, 1); for (int row=1; row<n; row++){ for (int i=1; i<m; i++){ // In-place 修改空间压缩 // 等号右边的 dp[i]表示上一次的结果,也就是二维中上面的结果 // 等号右边的 dp[i-1]表示左边的结果,也就是二维中左面的结果 dp[i] = dp[i] + dp[i-1]; } } return dp[m-1]; }
不同路径II
Nr. 63
一一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
请在这里输入引用内容
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
请在这里输入引用内容
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
解题思路:
和62类似,也是建立DP数组,不过要注意边界条件。第一行和第一列如果遇到障碍物,后面的格子都不可达,所以都是0。如果(i,j)不是障碍物,则dp[i][j]的公式和62一致。
public static int getPaths_DP(int[][] obstacleGrid) { if (obstacleGrid == null || obstacleGrid.length==0){ return 0; } int rows = obstacleGrid.length; int cols = obstacleGrid[0].length; // 初始化为0 int[][] dp = new int[rows][cols]; // 第一列中,如果不是障碍物有一种方法达到 // 一旦遇到障碍物后,后续的点都不可到达都为0 for (int i=0; i<rows && obstacleGrid[i][0] == 0; i++){ dp[i][0] = 1; } // 第一行中, 如果不是障碍物有一种方法达到 // 一旦遇到障碍物后,后续的点都不可到达都为0 for (int i=0; i<cols && obstacleGrid[0][i] == 0; i++){ dp[0][i] = 1; } // general case for (int i=1; i<rows; i++){ for (int j=1; j<cols; j++){ // 判断是否是障碍物,是障碍物自动置0 if (obstacleGrid[i][j] == 0){ dp[i][j] = dp[i][j-1] + dp[i-1][j]; } } } return dp[rows-1][cols-1]; }
这个空间优化,一定注意边界条件,也就是每次row循环时的第一个数!
并且要先处理第一行。循环从第二行开始。
public static int getPaths_DP2(int[][] obstacleGrid){ if (obstacleGrid == null || obstacleGrid.length==0){ return 0; } int rows = obstacleGrid.length; int cols = obstacleGrid[0].length; int[] dp = new int[cols]; // 第一行中, 如果不是障碍物有一种方法达到 // 一旦遇到障碍物后,后续的点都不可到达都为0 for (int i=0; i<cols && obstacleGrid[0][i] == 0; i++){ dp[i] = 1; } // general case for (int i=1; i<rows; i++){ // 单独处理第一个节点 // 当上一个是0 或者 当前位置是障碍物,那么第一个位置就是0 dp[0] = (dp[0]==0 || obstacleGrid[i][0] == 1) ? 0 : 1; for (int j=1; j<cols; j++){ // 如果当前j=0是障碍物,就要手动置零,二维方法中初始化为0 if (obstacleGrid[i][j] == 1){ dp[j] = 0; //continue; }else { // 如果不是障碍物,继承上一个0位置的值 dp[j] += dp[j-1]; } } } return dp[cols-1]; }