用javascript分类刷leetcode3.动态规划(图文视频讲解)

news2024/11/29 18:39:48

什么是动态规划

动态规划,英文:Dynamic Programming,简称DP,将问题分解为互相重叠的子问题,通过反复求解子问题来解决原问题就是动态规划,如果某一问题有很多重叠子问题,使用动态规划来解是比较有效的。

求解动态规划的核心问题是穷举,但是这类问题穷举有点特别,因为这类问题存在「重叠子问题」,如果暴力穷举的话效率会极其低下。动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。另外,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,只有列出**正确的「状态转移方程」**才能正确地穷举。重叠子问题、最优子结构、状态转移方程就是动态规划三要素

动态规划和其他算法的区别

  1. 动态规划和分治的区别:动态规划和分治都有最优子结构 ,但是分治的子问题不重叠
  2. 动态规划和贪心的区别:动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优解,所以它永远是局部最优,但是全局的解不一定是最优的。
  3. 动态规划和递归的区别:递归和回溯可能存在非常多的重复计算,动态规划可以用递归加记忆化的方式减少不必要的重复计算

动态规划的解题方法

  • 递归+记忆化(自顶向下)
  • 动态规划(自底向上)

ds_135

解动态规划题目的步骤

  1. 根据重叠子问题定义状态
  2. 寻找最优子结构推导状态转移方程
  3. 确定dp初始状态
  4. 确定输出值

斐波那契的动态规划的解题思路

ds_3

动画过大,点击查看

暴力递归
//暴力递归复杂度O(2^n)
var fib = function (N) {
    if (N == 0) return 0;
    if (N == 1) return 1;
    return fib(N - 1) + fib(N - 2);
};
递归 + 记忆化
var fib = function (n) {
    const memo = {}; // 对已算出的结果进行缓存

    const helper = (x) => {
        if (memo[x]) return memo[x];
        if (x == 0) return 0;
        if (x == 1) return 1;
        memo[x] = helper(x - 1) + helper(x - 2);
        return memo[x];
    };

    return helper(n);
};
动态规划
const fib = (n) => {
    if (n <= 1) return n;
    const dp = [0, 1];
    for (let i = 2; i <= n; i++) {
        //自底向上计算每个状态
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
};
滚动数组优化
const fib = (n) => {
    if (n <= 1) return n;
    //滚动数组 dp[i]只和dp[i-1]、dp[i-2]相关,只维护长度为2的滚动数组,不断替换数组元素
    const dp = [0, 1];
    let sum = null;
    for (let i = 2; i <= n; i++) {
        sum = dp[0] + dp[1];
        dp[0] = dp[1];
        dp[1] = sum;
    }
    return sum;
};
动态规划 + 降维,(降维能减少空间复杂度,但不利于程序的扩展)
var fib = function (N) {
    if (N <= 1) {
        return N;
    }
    let prev2 = 0;
    let prev1 = 1;
    let result = 0;
    for (let i = 2; i <= N; i++) {
        result = prev1 + prev2; //直接用两个变量就行
        prev2 = prev1;
        prev1 = result;
    }
    return result;
};

509. 斐波那契数(easy)

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n ,请计算 F(n) 。

示例 1:

输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:

输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:

输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3

提示:

0 <= n <= 30

方法1.动态规划
  • 思路:自底而上的动态规划
  • 复杂度分析:时间复杂度O(n),空间复杂度O(1)

Js:

var fib = function (N) {
    if (N <= 1) {
        return N;
    }
    let prev2 = 0;
    let prev1 = 1;
    let result = 0;
    for (let i = 2; i <= N; i++) {
        result = prev1 + prev2;
        prev2 = prev1;
        prev1 = result;
    }
    return result;
};

62. 不同路径 (medium)

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:

输入:m = 3, n = 7
输出:28
示例 2:

输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。

  1. 向右 -> 向下 -> 向下
  2. 向下 -> 向下 -> 向右
  3. 向下 -> 向右 -> 向下

示例 3:

输入:m = 7, n = 3
输出:28
示例 4:

输入:m = 3, n = 3
输出:6

提示:

1 <= m, n <= 100
题目数据保证答案小于等于 2 * 109

方法1.动态规划

动画过大,点击查看

  • 思路:由于在每个位置只能向下或者向右, 所以每个坐标的路径和等于上一行相同位置和上一列相同位置不同路径的总和,状态转移方程:f[i][j] = f[i - 1][j] + f[i][j - 1];
  • 复杂度:时间复杂度O(mn)。空间复杂度O(mn),优化后O(n)

js:

var uniquePaths = function (m, n) {
    const f = new Array(m).fill(0).map(() => new Array(n).fill(0)); //初始dp数组
    for (let i = 0; i < m; i++) {
        //初始化列
        f[i][0] = 1;
    }
    for (let j = 0; j < n; j++) {
        //初始化行
        f[0][j] = 1;
    }
    for (let i = 1; i < m; i++) {
        for (let j = 1; j < n; j++) {
            f[i][j] = f[i - 1][j] + f[i][j - 1];
        }
    }
    return f[m - 1][n - 1];
};

//状态压缩
var uniquePaths = function (m, n) {
    let cur = new Array(n).fill(1);
    for (let i = 1; i < m; i++) {
        for (let r = 1; r < n; r++) {
            cur[r] = cur[r - 1] + cur[r];
        }
    }
    return cur[n - 1];
};

312. 戳气球 (hard)

有 n 个气球,编号为0 到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。

现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1或 i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。

求所能获得硬币的最大数量。

示例 1:
输入:nums = [3,1,5,8]
输出:167
解释:
nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 315 + 358 + 138 + 181 = 167
示例 2:

输入:nums = [1,5]
输出:10

提示:

n == nums.length
1 <= n <= 300
0 <= nums[i] <= 100

方法1:动态规划

ds_112

  • 思路:dp[i][j] 表示开区间 (i,j) 能拿到的的金币,k是这个区间 最后一个 被戳爆的气球,枚举ij,遍历所有区间,i-j能获得的最大数量的金币等于 戳破当前的气球获得的金钱加上之前i-kk-j区间中已经获得的金币
  • 复杂度:时间复杂度O(n^3),n是气球的数量,三层遍历。空间复杂度O(n^2),dp数组的空间。

js:

var maxCoins = function (nums) {
    const n = nums.length;
    let points = [1, ...nums, 1]; //两边添加虚拟气球
    const dp = Array.from(Array(n + 2), () => Array(n + 2).fill(0)); //dp数组初始化
    //自底向上转移状态
    for (let i = n; i >= 0; i--) {
        //i不断减小
        for (let j = i + 1; j < n + 2; j++) {
            //j不断扩大
            for (let k = i + 1; k < j; k++) {
                //枚举k在i和j中的所有可能
                //i-j能获得的最大数量的金币等于 戳破当前的气球获得的金钱加上之前i-k,k-j区间中已经获得的金币
                dp[i][j] = Math.max(
                    //挑战最大值
                    dp[i][j],
                    dp[i][k] + dp[k][j] + points[j] * points[k] * points[i]
                );
            }
        }
    }
    return dp[0][n + 1];
};

63. 不同路径 II(medium)

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 1 和 0 来表示。

示例 1:

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:

  1. 向右 -> 向右 -> 向下 -> 向下
  2. 向下 -> 向下 -> 向右 -> 向右

示例 2:

输入:obstacleGrid = [[0,1],[0,0]]
输出:1

提示:

m == obstacleGrid.length
n == obstacleGrid[i].length
1 <= m, n <= 100
obstacleGrid[i][j] 为 0 或 1

方法1.动态规划
  • 思路:和62题一样,区别就是遇到障碍直接返回0
  • 复杂度:时间复杂度O(mn),空间复杂度O(mn),状态压缩之后是o(n)

Js:

var uniquePathsWithObstacles = function (obstacleGrid) {
    const m = obstacleGrid.length;
    const n = obstacleGrid[0].length;
    const dp = Array(m)
        .fill()
        .map((item) => Array(n).fill(0)); //初始dp数组

    for (let i = 0; i < m && obstacleGrid[i][0] === 0; ++i) {
        //初始列
        dp[i][0] = 1;
    }

    for (let i = 0; i < n && obstacleGrid[0][i] === 0; ++i) {
        //初始行
        dp[0][i] = 1;
    }

    for (let i = 1; i < m; ++i) {
        for (let j = 1; j < n; ++j) {
            //遇到障碍直接返回0
            dp[i][j] = obstacleGrid[i][j] === 1 ? 0 : dp[i - 1][j] + dp[i][j - 1];
        }
    }

    return dp[m - 1][n - 1];
};

//状态压缩
var uniquePathsWithObstacles = function (obstacleGrid) {
    let m = obstacleGrid.length;
    let n = obstacleGrid[0].length;
    let dp = Array(n).fill(0); //用0填充,因为现在有障碍物,当前dp数组元素的值还和obstacleGrid[i][j]有关
    dp[0] = 1; //第一列 暂时用1填充
    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (obstacleGrid[i][j] == 1) {
                //注意条件,遇到障碍物dp[j]就变成0,这里包含了第一列的情况
                dp[j] = 0;
            } else if (j > 0) {
                //只有当j>0 不是第一列了才能取到j - 1
                dp[j] += dp[j - 1];
            }
        }
    }
    return dp[n - 1];
};

120. 三角形最小路径和(medium)

给定一个三角形 triangle ,找出自顶向下的最小路径和。

每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。

示例 1:

输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
示例 2:

输入:triangle = [[-10]]
输出:-10

提示:

1 <= triangle.length <= 200
triangle[0].length == 1
triangle[i].length == triangle[i - 1].length + 1
-104 <= triangle[i][j] <= 104

方法1.动态规划

ds_72

  • 思路:从三角形最后一层开始向上遍历,每个数字的最小路径和是它下面两个数字中的较小者加上它本身
  • 复杂度分析:时间复杂度O(n^2),空间复杂O(n)

Js:

const minimumTotal = (triangle) => {
    const h = triangle.length;
    // 初始化dp数组
    const dp = new Array(h);
    for (let i = 0; i < h; i++) {
        dp[i] = new Array(triangle[i].length);
    }

    for (let i = h - 1; i >= 0; i--) { //自底而上遍历
        for (let j = 0; j < triangle[i].length; j++) { //同一层的
            if (i == h - 1) {  // base case 最底层
                dp[i][j] = triangle[i][j];
            } else { // 状态转移方程,上一层由它下面一层计算出
                dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle[i][j];
            }
        }
    }
    return dp[0][0];
};


//状态压缩
const minimumTotal = (triangle) => {
    const bottom = triangle[triangle.length - 1];
    const dp = new Array(bottom.length);
    // base case 是最后一行
    for (let i = 0; i < dp.length; i++) {
        dp[i] = bottom[i];
    }
    // 从倒数第二列开始迭代
    for (let i = dp.length - 2; i >= 0; i--) {
        for (let j = 0; j < triangle[i].length; j++) {
            dp[j] = Math.min(dp[j], dp[j + 1]) + triangle[i][j];
        }
    }
    return dp[0];
};

0-1背包问题

0-1背包问题指的是有n个物品和容量为j的背包,weight数组中记录了n个物品的重量,位置i的物品重量是weight[i],value数组中记录了n个物品的价值,位置i的物品价值是vales[i],每个物品只能放一次到背包中,问将那些物品装入背包,使背包的价值最大。

举例:

ds_214

我们用动态规划的方式来做

  • 状态定义:dp[i][j] 表示从前i个物品里任意取,放进容量为j的背包,价值总和最大是多少

  • 状态转移方程: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 每个物品有放入背包和不放入背包两种情况

    1. j - weight[i]<0:表示装不下i号元素了,不放入背包,此时dp[i][j] = dp[i - 1][j],dp[i] [j]取决于前i-1中的物品装入容量为j的背包中的最大价值
    2. j - weight[i]>=0:可以选择放入或者不放入背包。
      放入背包则:dp[i][j] = dp[i - 1][j - weight[i]] + value[i]dp[i - 1][j - weight[i]] 表示i-1中的物品装入容量为j-weight[i]的背包中的最大价值,然后在加上放入的物品的价值value[i]就可以将状态转移到dp[i][j]
      不放入背包则:dp[i][j] = dp[i - 1] [j],在这两种情况中取较大者。
  • 初始化dp数组:dp[i][0]表示背包的容积为0,则背包的价值一定是0,dp[0][j]表示第0号物品放入背包之后背包的价值

    ds_137

  • 最终需要返回值:就是dp数组的最后一行的最后一列

循环完成之后的dp数组如下图

ds_138

js:

function testWeightBagProblem(wight, value, size) {
    const len = wight.length,
        dp = Array.from({ length: len + 1 }).map(//初始化dp数组
            () => Array(size + 1).fill(0)
        );
    //注意我们让i从1开始,因为我们有时会用到i - 1,为了防止数组越界
    //所以dp数组在初始化的时候,长度是wight.length+1
    for (let i = 1; i <= len; i++) {
        for (let j = 0; j <= size; j++) {
            //因为weight的长度是wight.length+1,并且物品下标从1开始,所以这里i要减1
            if (wight[i - 1] <= j) {
                dp[i][j] = Math.max(
                    dp[i - 1][j],
                    value[i - 1] + dp[i - 1][j - wight[i - 1]]
                )
            } else {
                dp[i][j] = dp[i - 1][j];
            }
        }
    }

    return dp[len][size];
}

function test() {
    console.log(testWeightBagProblem([1, 3, 4], [15, 20, 30], 4));
}

test();

状态压缩

根据状态转移方程dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]),第i行只与第i-1行状态相关,所以我们可以用滚动数组进行状态压缩,其次我们注意到,j只与j前面的状态相关,所以只用一个数组从后向前计算状态就可以了。

动画过大,点击查看

function testWeightBagProblem2(wight, value, size) {
    const len = wight.length,
        dp = Array(size + 1).fill(0);
    for (let i = 1; i <= len; i++) {
        //从后向前计算,如果从前向后的话,最新的值会覆盖老的值,导致计算结果不正确
          //dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - wight[i - 1]] + value[i - 1])
        for (let j = size; j >= wight[i - 1]; j--) {
            dp[j] = Math.max(dp[j], dp[j - wight[i - 1]] + value[i - 1] );
        }
    }
    return dp[size];
}

416. 分割等和子集 (medium)

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:

输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

提示:

1 <= nums.length <= 200
1 <= nums[i] <= 100

ds_140

  • 思路:本题可以看成是0-1背包问题,给一个可装载重量为 sum / 2 的背包和 N 个物品,每个物品的重量记录在 nums 数组中,问是否在一种装法,能够恰好将背包装满?dp[i][j]表示前i个物品是否能装满容积为j的背包,当dp[i][j]为true时表示恰好可以装满。每个数都有放入背包和不放入两种情况,分析方法和0-1背包问题一样。
  • 复杂度:时间复杂度O(n*sum),n是nums数组长度,sum是nums数组元素的和。空间复杂度O(n * sum),状态压缩之后是O(sum)

js:

//可以看成是0-1背包问题,给一个可装载重量为 sum / 2 的背包和 N 个物品,
//每个物品的重量记录在 nums 数组中,问是否在一种装法,能够恰好将背包装满?
var canPartition = function (nums) {
    let sum = 0
    let n = nums.length
    for (let i = 0; i < n; i++) {
        sum += nums[i]
    }
    if (sum % 2 !== 0) {//如果是奇数,那么分割不了,直接返回false
        return false
    }
    sum = sum / 2
    //dp[i][j]表示前i个物品是否能装满容积为j的背包,当dp[i][j]为true时表示恰好可以装满
    //最后求的是 dp[n][sum] 表示前n个物品能否把容量为sum的背包恰好装满
    //dp数组长度是n+1,而且是二维数组,第一维表示物品的索引,第二个维度表示背包大小
    let dp = new Array(n + 1).fill(0).map(() => new Array(sum + 1).fill(false))
    //dp数组初始化,dp[..][0] = true表示背包容量为0,这时候就已经装满了,
    //dp[0][..] = false 表示没有物品,肯定装不满
    for (let i = 0; i <= n; i++) {
        dp[i][0] = true
    }
    for (let i = 1; i <= n; i++) {//i从1开始遍历防止取dp[i - 1][j]的时候数组越界
        let num = nums[i - 1]
        //j从1开始,j为0的情况已经在dp数组初始化的时候完成了
        for (let j = 1; j <= sum; j++) {
            if (j - num < 0) {//背包容量不足 不能放入背包
                dp[i][j] = dp[i - 1][j];//dp[i][j]取决于前i-1个物品是否能前好装满j的容量
            } else {
                //dp[i - 1][j]表示不装入第i个物品
                //dp[i - 1][j-num]表示装入第i个,此时需要向前看前i - 1是否能装满j-num
                //和背包的区别,这里只是返回true和false 表示能否装满,不用计算价值
                dp[i][j] = dp[i - 1][j] || dp[i - 1][j - num];
            }
        }
    }
    return dp[n][sum]
};

//状态转移方程 F[i, target] = F[i - 1, target] || F[i - 1, target - nums[i]]
//第 n 行的状态只依赖于第 n-1 行的状态
//状态压缩
var canPartition = function (nums) {
    let sum = nums.reduce((acc, num) => acc + num, 0);
    if (sum % 2) {
        return false;
    }
    sum = sum / 2;
    const dp = Array.from({ length: sum + 1 }).fill(false);
    dp[0] = true;

    for (let i = 1; i <= nums.length; i++) {
        //从后向前计算,如果从前向后的话,最新的值会覆盖老的值,导致计算结果不正确
        for (let j = sum; j > 0; j--) {
            dp[j] = dp[j] || (j - nums[i] >= 0 && dp[j - nums[i]]);
        }
    }

    return dp[sum];
};

198. 打家劫舍 (medium)

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
  偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
  偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:

1 <= nums.length <= 100
0 <= nums[i] <= 400

ds_148

  • 思路:dp[i]表示0-i能偷的最大金额,dp[i]由两种情况中的最大值转移过来
    1. dp[i - 2] + nums[i] 表示偷当前位置,那么i-1的位置不能偷,而且需要加上dp[i-2],也就是前i-2个房间的金钱
    2. dp[i - 1]表示偷当前位置,只偷i-1的房间
  • 复杂度:时间复杂度O(n),遍历一次数组,空间复杂度O(1),状态压缩之后是O(1),没有状态压缩是O(n)

js:

//dp[i]表示0-i能偷的最大金额
const rob = (nums) => {
    const len = nums.length;
    const dp = [nums[0], Math.max(nums[0], nums[1])]; //初始化dp数组的前两项
    for (let i = 2; i < len; i++) {
        //从第三个位置开始遍历
        //dp[i - 2] + nums[i] 表示偷当前位置,那么i-1的位置不能偷,
          //而且需要加上dp[i-2],也就是前i-2个房间的金钱
        //dp[i - 1]表示偷当前位置,只偷i-1的房间
        dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
    }
    return dp[len - 1]; //返回最后最大的项
};

//状态压缩
var rob = function (nums) {
    if(nums.length === 1) return nums[0]
    let len = nums.length;
    let dp_0 = nums[0],
        dp_1 = Math.max(nums[0], nums[1]);
    let dp_max = dp_1;
    for (let i = 2; i < len; i++) {
        dp_max = Math.max(
            dp_1, //不抢当前家
            dp_0 + nums[i] //抢当前家
        );
        dp_0 = dp_1; //滚动交换变量
        dp_1 = dp_max;
    }
    return dp_max;
};

343. 整数拆分 (medium)

给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积 。

示例 1:

输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:

输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

提示:

2 <= n <= 58

ds_136

  • 思路:dp[i]为正整数i拆分之后的最大乘积,循环数字n,对每个数字进行拆分,取最大的乘积,状态转移方程:dp[i] = Math.max(dp[i], dp[i - j] * j, (i - j) * j)j*(i-j)表示把i拆分为j和i-j两个数相乘,j * dp[i-j]表示把i拆分成j和继续把(i-j)这个数拆分,取(i-j)拆分结果中的最大乘积与j相乘
  • 复杂度:时间复杂度O(n^2),两层循环。空间复杂度O(n)dp数组的空间

js:

var integerBreak = function (n) {
    //dp[i]为正整数i拆分之后的最大乘积
    let dp = new Array(n + 1).fill(0);
    dp[2] = 1;

    for (let i = 3; i <= n; i++) {
        for (let j = 1; j < i; j++) {
            //j*(i-j)表示把i拆分为j和i-j两个数相乘
            //j*dp[i-j]表示把i拆分成j和继续把(i-j)这个数拆分,取(i-j)拆分结果中的最大乘积与j相乘
            dp[i] = Math.max(dp[i], dp[i - j] * j, (i - j) * j);
        }
    }
    return dp[n];
};

64. 最小路径和 (medium)

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

示例 1:

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:

输入:grid = [[1,2,3],[4,5,6]]
输出:12

提示:

m == grid.length
n == grid[i].length
1 <= m, n <= 200
0 <= grid[i][j] <= 100

ds_205

  • 思路:dp[i][j]表示从矩阵左上角到(i,j)这个网格对应的最小路径和,只要从上到下,从左到右遍历网格,当前最小路径和就是当前的数值加上上面和左边左小的。
  • 复杂度:时间复杂度O(mn),m、n分别是矩阵的长和宽。空间复杂度如果原地修改是O(1),如果新建dp数组就是O(mn)

js:

var minPathSum = function(dp) {
    let row = dp.length, col = dp[0].length

    for(let i = 1; i < row; i++)//初始化第一列
        dp[i][0] += dp[i - 1][0]

    for(let j = 1; j < col; j++)//初始化第一行
        dp[0][j] += dp[0][j - 1]

    for(let i = 1; i < row; i++)
        for(let j = 1; j < col; j++)
            dp[i][j] += Math.min(dp[i - 1][j], dp[i][j - 1])//取上面和左边最小的

    return dp[row - 1][col - 1]
};

152. 乘积最大子数组 (medium)

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

测试用例的答案是一个 32-位 整数。

子数组 是数组的连续子序列。

示例 1:

输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:

输入: nums = [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。

提示:

1 <= nums.length <= 2 * 104
-10 <= nums[i] <= 10
nums 的任何前缀或后缀的乘积都 保证 是一个 32-位 整数

方法1.动态规划

ds_73

  • 思路:

    1. 状态定义:dp[i][0]表示从第 0 项到第 i 项范围内的子数组的最小乘积,dp[i][1]表示从第 0 项到第 i 项范围内的子数组的最大乘积

    2. 初始状态:dp[0][0]=nums[0], dp[0][1]=nums[0]

    3. 分情况讨论:

      • 不和别人乘,就 nums[i]自己
      • num[i] 是负数,希望乘上前面的最大积
      • num[i] 是正数,希望乘上前面的最小积
    4. 状态转移方程:

      • dp[i] [0]=min(dp[i−1] [0]∗num[i] , dp[i−1] [1] ∗ num[i], num[i])
      • dp[i] [1]=max(dp[i−1] [0]∗num[i] , dp[i−1] [1] ∗ num[i], num[i])
    5. 状态压缩:dp[i][x]只与dp[i][x]-1,所以只需定义两个变量,prevMin = nums[0]prevMax = nums[0]

    6. 状态压缩之后的方程:

      • prevMin = Math.min(prevMin * num[i], prevMax * num[i], nums[i])
      • prevMax = Math.max(prevMin * num[i], prevMax * num[i], nums[i])
  • 复杂度:时间复杂度O(n),空间复杂度O(1)

js:

var maxProduct = (nums) => {
    let res = nums[0]
    let prevMin = nums[0]
    let prevMax = nums[0]
    let temp1 = 0, temp2 = 0
    for (let i = 1; i < nums.length; i++) {
        temp1 = prevMin * nums[i]
        temp2 = prevMax * nums[i]
        prevMin = Math.min(temp1, temp2, nums[i])
        prevMax = Math.max(temp1, temp2, nums[i])
        res = Math.max(prevMax, res)
    }
    return res
}

视频讲解:传送门

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/87784.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

测量温度的优选模块——新型设备M-THERMO3 16

| 具有16个自由选择通道的新型温度测量设备M-THERMO3 16 IPETRONIK推出了新的温度测量设备——M-THERMO3 16。作为新一代设备的首个模块&#xff0c;它为模块化测量技术确立了标准。该模块具有16个通道&#xff0c;并且各通道不仅分别率高达24位ADC&#xff0c;而且可以自由选…

git初学(二)

git如何进行版本控制&#xff1f; 如何做版本控制呢&#xff1f;其实就是让git管理文件夹&#xff0c;比如我们有一个项目叫学生考试管理系统&#xff0c;首先我们创建一个文件夹student&#xff0c;这个文件夹当中保存所开发的所有代码 进入要管理的文件夹 初始化 git ini…

FTX创始人被警方逮捕:身价曾超150亿美元 坑惨红杉

雷递网 雷建平 12月13日“加密货币大王”、加密货币交易所FTX创始人、前首席执行官&#xff08;CEO&#xff09;萨姆班克曼-弗里德&#xff08;Sam Bankman-Fried&#xff0c; 简称“SBF”&#xff09;日前在巴哈马被逮捕&#xff0c;可能被引渡至美国。巴哈马总检察长办公室和…

访问者模式

一、访问者模式 1、定义 访问者模式&#xff08;Visitor Pattern&#xff09;是一种将数据结构与数据操作分离的设计模式&#xff0c;指封装一些作用于某种数据结构中的各元素的操作&#xff0c;可以在不改变数据结构的前提下定义作用于这些元素的新的操作&#xff0c;属于行为…

漫画 | 这个北欧小国发明的编程技术,竟然占领全世界了!

上世纪60年代 &#xff0c;挪威计算中心。一个新来员工刚上班&#xff0c;发现有两个人居然在一楼的黑板前打架新员工立刻跑到电话接线员那里报告什么样的编程语言&#xff0c;能让两人大动肝火&#xff1f;当时Nygaard正在编写复杂系统的模拟程序&#xff0c;模拟程序要求先定…

【猿如意】中的『XMind』工具详情介绍

目录 一、工具名称 二、下载安装渠道 2.1 什么是猿如意&#xff1f; 2.2 如何下载猿如意&#xff1f; 2.3 如何在猿如意中下载开发工具&#xff1f; 三、XMind工具功能简介 四、XMind的下载和安装 4.1下载 4.2安装 五、XMind的基本使用 5.1新建项目 5.2系统模板的使…

【java】程序员基础能力测试33问,持续整理中

Java基础&#xff1a; 1&#xff1a;八大基本数据类型&#xff0c;及所占字节数&#xff1f; 2&#xff1a;讲下对面向对象的理解&#xff1f; 特征:封装、继承、多态; 基础:抽象 面向对象&#xff0c;主要就是将现实中的对象抽象成一个类&#xff0c;这个对象具有一定的属性…

[附源码]Python计算机毕业设计服装商城平台Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

项目管理之Git---submodule

0. 简介 在面对复杂系统时&#xff0c;所有的模块不可能同时开发在一个project下的&#xff0c;而更多的可能就是每个人开发不同的模块&#xff0c;并通过一个模块将这些模块都整合到一起&#xff0c;这时候submodule的作用就非常明显了。通过设置submodule可以轻易地对不同的…

通信基站安装步骤

基站设备安装步骤(移动通信基站施工过程),并说明施工要点和注意事项。 安装机柜流程。安装电源线和系统接地。 安装电源机柜时应直流配电柜接出-48V直流电源至RNC810机柜和NodeB机柜顶端配线盒。 将保护地线接至机柜接地螺栓上并紧固螺栓。 天馈系统安装。 天馈系统安装前的…

大数据学习:压缩与打包

文章目录任务一&#xff1a;压缩文件任务二&#xff1a;解压文件任务三&#xff1a;生成打包文件任务四&#xff1a;将打包文件解压到当前文件任务五&#xff1a;将打包的文件解压缩到指定目录任务六&#xff1a;解压打包文件里的某个目录任务一&#xff1a;压缩文件 在/tmp目…

redis地理位置和MongoDB地理索引的使用

比较 经度纬度都要在有效区间。经度范围介于 -180 到 180&#xff0c;纬度范围大致介于-90和到90。redis使用Zset结构存储&#xff0c;将经度值、纬度值转换为一个值&#xff0c;二维量变成一维量找附近的位置&#xff0c;效率极高&#xff0c;不过限于平面&#xff0c;且无法…

测量学:水准和导线测量实验报告+详细解析

目录 00 说明 实验1 闭合导线测量 实习目的 实习任务和内容 控制点的布置和测量技术要求&#xff08;绘制导线略图&#xff09; 导线略图 外业测量数据和记录相关表格&#xff08;附原始观测记录&#xff09; 原始观测数据记录如下&#xff1a; 记录表格如下&#xff…

Web3中文|星巴克拥抱Web3,新项目Odyssey开启数字旅程

12月8日&#xff0c;成立用于1971年&#xff0c;全球82个市场拥有超过32,000家门店的美国咖啡公司星巴克对其备受期待的Odyssey体验进行了测试&#xff0c;该体验将客户忠诚度奖励与NFT以及其他游戏元素相结合。 早在9月12日&#xff0c;星巴克宣布将推出Web3平台“Starbucks …

openEuler社区开源项目:CPDS(容器故障检测系统)介绍

容器故障检测系统 CPDS (Container Problem Detect System) 是由北京凝思软件股份有限公司&#xff08;以下简称“凝思软件”&#xff09;设计并开发的容器集群故障检测系统&#xff0c;该软件系统实现了对容器TOP故障、亚健康状态的监测与识别。 2022年11月&#xff0c;凝思软…

isp,iap,sw-jtag

https://blog.csdn.net/weixin_45905650/article/details/107707858?ops_request_misc%257B%2522request%255Fid%2522%253A%2522167098526816800180634199%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id167098526816800180634199&biz_i…

JS:通过setTimeout和promise来了解js代码执行机制(面试题讲解)

目录 1.setTimeout定时器 2.promise函数 补充&#xff1a;1.什么是宏任务与微任务 补充&#xff1a;2.宏任务和微任务的执行顺序 补充&#xff1a;3.js的执行顺序 补充&#xff1a;4.答案揭晓 前几天碰到一个关于js执行顺序的面试题&#xff0c;一时间竟然有点模糊&#…

BCN-PEG-Folate 环丙烷环辛炔聚乙二醇叶酸 BCN-PEG-FA

双环[6,1,0]壬炔 (BCN) &#xff08;环丙烷环辛炔&#xff09;可以通过无铜的点击化学与叠氮化物标记的分子或生物分子反应生成稳定的三氮唑连接。 产品名称 BCN-PEG-Folate 环丙烷环辛炔聚乙二醇叶酸 中文名称 环丙烷环辛炔聚乙二醇叶酸 英文名称 BCN-PEG-Folate BCN…

统信软件高级系统研发工程师:sysOM 在系统可靠性与安全上实践

一、系统可靠性 SRE是判断系统是否可靠、可用、有效重要标准&#xff0c;它包括&#xff1a; 服务水平指标SLI&#xff1a;衡量服务使用情况量化指标。 比如IO读写速率、网络延迟。通常量化指标会转换为比率、平均值或百分比。服务水平目标SLO&#xff1a;一段时间、区间内的目…

花1块钱让你的网站支持 ChatGPT

点击上方卡片“前端司南”关注我您的关注意义重大原创前端司南最近 ChatGPT 在技术圈子可太火了&#xff0c;票圈也被刷屏。我也决定来凑个热闹&#xff0c;给自己的博客加一个 ChatGPT 对话功能。先附上体验链接[1]&#xff0c;源码在底部也可以找到。体验 ChatGPTChatGPT[2] …