动态规划题集

动态规划解题五部曲

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

01背包 

不同路径

. - 力扣(LeetCode)

题目描述:给定一个m x n 的网格,一机器人的起点在左上角,终点在右下角。机器人每次只能向下和向右走。问从起点到终点有多少种不同路径。

解题思路:确定一个dp数组。

1、dp[i][j]:表示从起点到i,j位置有多少种不同路径。
2、递推公式:机器人每次只能向下和向右走,所以dp[i][j] = dp[i-1][j] + dp[i][j-1]。当确定位置i,j时,需要向该位置的上方和左方去确定。dp[i-1][j]表示i,j位置的上方位置有多少种路径;dp[i][j-1]表示i,j位置的左方位置有多少种路径。
3、dp数组初始化:由递推公式dp[i][j] = dp[i-1][j] + dp[i][j-1]可知,每一个位置的值是由该位置的上方和左方确定的。所以需要初始化dp数组的第一行和第一列。当机器人一直向右走,可得第一行的dp数组的值为1;当机器人一直向下走,可得第一列得dp数组的值为1。
4、遍历顺序:自然是遍历除了第一行和第一列的整个数组了。

class Solution {
public:
    int uniquePaths(int m, int n) {
        int dp[101][101];
        for(int i = 0; i < m; i++) dp[i][0] = 1;
        for(int i = 0; i < n; i++) dp[0][i] = 1;
        for(int i = 1; i < m; i++)
        {
            for(int j = 1; j < n; j++)
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
        }
        return dp[m-1][n-1];
    }
};

优化

dp数组也可以使用一维数组。每一次在dp数组上赋值,dp[i-1][j] = dp[j]; dp[i][j-1] = dp[j-1]

class Solution {
public:
    int uniquePaths(int m, int n) {
        int dp[101];
        for(int i = 0; i < n; i++)
            dp[i] = 1;
        for(int i = 1; i < m; i++)
        {
            for(int j = 1; j < n; j++)
                dp[j] = dp[j-1] + dp[j];
        }
        return dp[n-1];
    }
};

不同路径II

. - 力扣(LeetCode)

题目描述:相对于上一题来说,在网格中添加了障碍,障碍会挡住机器人。

解题思路:思路和上一题差不多,就是多了对于障碍的处理。当网格中有障碍的位置,机器人是到达不了该位置的。对遍历的处理逻辑和dp数组的初始化会有改变

遍历的处理

for(int i = 1; i < n; i++)
   {
     for(int j = 1; j < m; j++)
        {
          if(obstacleGrid[i][j] == 1) // 当该位置有障碍的时候
               dp[i][j] = 0;
          else
               dp[i][j] = dp[i-1][j] + dp[i][j-1];
         }
    }

dp数组初始化

        int n = obstacleGrid.size(), m = obstacleGrid[0].size();
        vector<vector<int>> dp(n, vector<int>(m,0));
        // 先给dp数组整体赋值为0
        for(int i = 0; i < n && obstacleGrid[i][0] == 0; i++) // 当遇到障碍,后面的全部都是0
            dp[i][0] = 1;
        for(int i = 0; i < m && obstacleGrid[0][i] == 0; i++)
            dp[0][i] = 1;
       // 当不给dp数组初始化为0时,也可以这样赋值
        for(int i = 0; i < n; i++) 
        {
            if(obstacleGrid[i][0] == 1)
                dp[i][0] = 0;
            else
            {
                if(i > 0 && dp[i-1][0] == 0)
                    dp[i][0] = 0;
                else
                    dp[i][0] = 1;
            }
        }

        for(int i = 0; i < m; i++) 
        {
            if(obstacleGrid[0][i] == 1)
                dp[0][i] = 0;
            else
            {
                if(i > 0 && dp[0][i-1] == 0)
                    dp[0][i] = 0;
                else
                    dp[0][i] = 1;
            }
        }

整体代码

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int n = obstacleGrid.size(), m = obstacleGrid[0].size();
        vector<vector<int>> dp(n, vector<int>(m,0));
        for(int i = 0; i < n && obstacleGrid[i][0] == 0; i++)
            dp[i][0] = 1;
        for(int i = 0; i < m && obstacleGrid[0][i] == 0; i++)
            dp[0][i] = 1;
        for(int i = 1; i < n; i++)
        {
            for(int j = 1; j < m; j++)
            {
                if(obstacleGrid[i][j] == 1)
                    dp[i][j] = 0;
                else
                    dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[n-1][m-1];
    }
};

优化

同样也可以使用滚动数组,使用一维的dp数组

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        int n = obstacleGrid.size(), m = obstacleGrid[0].size();
        vector<int> dp(m, 0);
        for(int i = 0; i < m && obstacleGrid[0][i] == 0; i++)
            dp[i] = 1;

        for(int i = 1; i < n; i++)
        {
            for(int j = 0; j < m; j++) // 注意这里是从j = 0 开始的
            {
                if(obstacleGrid[i][j] == 1)
                    dp[j] = 0;
                else if(j > 0)
                    dp[j]= dp[j-1] + dp[j];
                // 当j = 0时,并且没有障碍,那么就和上一行的dp[0]是一样的值,不需要修改
            }
        }
        return dp[m-1];
    }
};

整数拆分

. - 力扣(LeetCode)

题目描述:给定一个整数n,要求将整数拆分为k个数的和,求拆分k个数的最大乘积。

解题思路:确定dp数组的含义:dp[i]:表示将i拆分求得的最大乘积dp[i]。

递推公式:对于i,可以分为两种情况拆分:拆分为两项或多项。两项:i * (i - j);多项:i * dp(i - j)
那为什么要这样拆分呢?举个例子:对于n = 3这种情况。拆分为两项是:2 * 1 = 2;拆分为多项式:1 * 1 * 1 = 1。明显拆分为两项是大于拆分为多项的。那为什么拆分多项是:j * dp[i-j]?这需要再联系dp数组的含义。dp[i-j]是将i - j拆分k项所得的最大乘积。那么由此可得递推公式为:dp[i] = max(j * (i - j), j * dp[i-j])

初始化:dp[0]、dp[1] 都为0,没有意义。从dp[2]开始才有意义,dp[2] = 1。

class Solution {
public:
    int integerBreak(int n) {
        vector<int>dp(n + 1);
        dp[2] = 1;
        for(int i = 3; i <= n; i++)
        {
            for(int j = 1; j <= i / 2; j++) 
           // j <= i / 2是一个优化。由数学推导可得:将一个数拆分只有拆得数越相等,得到的乘积才最大
                dp[i] = max(dp[i], max(j * dp[i - j], j * (i - j)));
        }
        return dp[n];
    }
};

贪心

本题还可以使用贪心,对于一个大于3的整数,只有尽可能的拆分成3相乘,最后剩余的整数为4,这样得到的乘积才最大。证明方法需要用到函数极值证明,或数学归纳法

class Solution {
public:
    int integerBreak(int n) {
        if (n == 2) return 1;
        if (n == 3) return 2;
        if (n == 4) return 4;
        int result = 1;
        while (n > 4) {
            result *= 3;
            n -= 3;
        }
        result *= n;
        return result;
    }
};

不同的二叉搜索树

. - 力扣(LeetCode)

题目描述:给定一个结点树n,问由n个结点组成的不同的二叉搜索树有多少颗。

 二叉搜索树的性质是根结点大于左子树结点,右子树结点大于根节点。

当求结点数为n时,需要选1 ~ n 作为根结点,那么选该结点作为根节点时,组成的不同二叉树种数有:左子树组成种类 * 右子树组成种类。
根据上图可知:当n = 3时,我们是通过头节点为1,2,3,然后在计算它们的左右子树,左右子树所组成的种类可以通过前面已经推导的种类所计算出来。 所以当n = 3时可组成的二叉搜索树的种类 =  1为头结点组成的种类 + 2为头结点组成的种类 + 3为头结点组成的种类。那么对于任意n,求组成的二叉搜索树的种类,需要以[1,n]为头节点,根据二叉搜索树的性质可得:头结点的左子树都比头结点小,右子树都比头节点大。

确定dp数组的含义:dp[i]表示由i个结点可以组成dp[i]种不同的二叉搜索树。

递推公式:以j遍历[1,i],用j作为组成的二叉搜索树头节点,那么左子树的结点树有j - 1个,右子树有i - j个。这是通过二叉搜索树的性质得到的。那么递推公式为:dp[i] += dp[j-1] + dp[i-j]。

class Solution {
public:
    int numTrees(int n) {
        vector<int> dp(n + 1);
        dp[0] = 1; // 一颗空树也是一颗二叉搜索树
        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j <= i; j++)
                dp[i] += dp[j-1] * dp[i-j];    // 注意这里+=
        }
        return dp[n];
    }
};
class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        if(n == 1)
        {
            return nums[0];
        }
        int dp[101];
        dp[0] = nums[0];
        dp[1] = max(nums[0], nums[1]);
        for(int i = 2; i < n; i++)
        {
            dp[i] = max(dp[i-1], dp[i-2] + nums[i]); 
            // 选i这个位置前一个位置的值,不选前一个的值,那么就符合没有相邻的情况,就可以加上nums[i]
        }
        return dp[n - 1];
    }
};
class Solution {
public:
    int rob(vector<int>& nums) {
        if(nums.size() == 1) return nums[0]; 
        // 优化空间
        int res1 = nums[0];
        int res2 = max(nums[1], res1);
        for(int i = 2; i < nums.size(); i++)
        {
            int temp = res2;
            res2 = max(res2, res1 + nums[i]);
            res1 = temp;
        }
        return res2;
    }
};

删除并获得点数 

题目链接:. - 力扣(LeetCode)

解题思路:当选择一个元素x,那么x-1和x+1都应该被删除,则应该选择全部的x,这样点数才会最大。我们可以用一个哈希表把数组中所有元素x的和存储在下标x中,那么x-1和x+1的下标不能选择,那么就可以抽象成一个打家劫舍问题了。

class Solution {
public:
    int rob(vector<int>& nums) {
        if(nums.size() == 1) return nums[0]; 
        // 优化空间
        int res1 = nums[0];
        int res2 = max(nums[1], res1);
        int res = res2;
        for(int i = 2; i < nums.size(); i++)
        {
            res = max(res2, res1 + nums[i]);
            res1 = res2;
            res2 = res;
        }
        return res;
    }
    int deleteAndEarn(vector<int>& nums) {
        int n = nums.size();
        vector<int> sum(10001, 0);
        for(int i = 0; i < n; i++)
        {
            sum[nums[i]] += nums[i];
        }
        return rob(sum);
    }
};

分割等和子集

题目链接:. - 力扣(LeetCode)

解题思路:题目要求就是能否在数组中找到一个子集和等于整个数组和的一半。可以使用01背包解决。
dp数组:表示在容量为i的背包中有dp[i]的价值。这个容量就是整个数组和的一半,设为v,最后有dp[v] = v.
递推公式:dp[i] = max(dp[i], dp[i - nums[i]] + nums[i])。这个递推公式已经很熟悉了。就当第i个元素不选时为:dp[i]。当选时为:dp[i - nums[i]] + nums[i]
初始化:显然dp[0] = 0;根据递推公式,dp数组应该初始化为0,因为对于任意dp[i]都是由前面的dp数组的值得到的最大值,自然前面的dp数组的值不能影响到dp[i]的值,所以初始化为0.

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int n = nums.size();
        int sum = 0;
        for(int e : nums)
            sum += e;
        if(sum % 2 != 0) return false; // 当容量为奇数时
        int v = sum / 2;
        vector<int> dp(v+1, 0);
        for(int i = 0; i < n; i++)
        {
            for(int j = v; j >= nums[i]; j--) // 注意这里是倒序,对于一个容量必须要大于 考虑放入的物品的体积。不然就不用放。
                dp[j] = max(dp[j], dp[j-nums[i]] + nums[i]);
        }
        return dp[v] == v;
    }
};

最后一块石头的重量II

题目链接:. - 力扣(LeetCode)

解题思路:这题很难想到用01背包求解,题目要求最后一块石头的最小重量。当两块重量最相似的石头相撞可以得到最小重量。那么将这堆石头分成两堆重量和相似。设总和为sum,那么一堆为sum/2,另外一堆就是sum - sum/2。那么题目就可转换为在这一堆石头中找可以构成重量为sum/2的重量。那么最后结果就(sum/2 - (sum - sum/2))。那么接下来思路和上一题差不多

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        vector<int>dp(1501, 0);
        int n = stones.size();
        int sum = 0;
        for (int i = 0; i < n; i++)
            sum += stones[i];
        int target = sum / 2;
        for(int i = 0; i <= target; i++)
        {
            if(i >= stones[0])
                dp[i] = stones[0];
        }
        for (int i = 1; i < n; i++)
        {
            for (int j = target; j >= stones[i]; j--)
            {
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        return sum - dp[target] - dp[target];
    }
};

目标和

题目链接:. - 力扣(LeetCode)

解题思路:题目要求计算组成target的组合。通过‘+'或‘-’运算。那么可以把数组的元素分成加法集合a和减法集合b,那么可得a - b = target;设数组的和为sum;那么a + b = sum;b = sum - a;那么a - (sum - a) = target;a = (sum + target) / 2。sum 和 target都是确定的,就可以得到a。题目就可转换为在数组中求可以构成a的组合。已知数组都是正整数组成的,所以a肯定不能为小数,所以当2不能整除sum - target时,不能组成target。target是由数组中的元素组成的,那么target不能超过数组的和的范围,也就是 target∈[-sum, sum]。当target超出这个范围,那么也不能构成target。
至此题目就转换为:在数组中求可以构成a的组合。那么可以使用01背包来求解,a就是背包的最大容量,数组的元素就是价值,求用价值组成容量的方法。
dp数组的含义:
dp[j]表示能装满容量为j的背包,有dp[j]种方法。
递推公式:对于一个容量来说,求解能组成它的方法,可以根据已经有的元素,去找能构成减去该元素的容量的方法。换句话说,就是求dp[j],那么对于求j容量的组成方法,可以先确定一个元素(设该元素为x),这个元素可以抽象为物品的价值,我们将这个背包的容量缩小x,那么就是dp[j-x]
然后元素x,就数组中的每一个元素。而dp[j]的前面元素都是根据前面的前面dp数组求出来的。所以累加dp数组,就可以得到一个容量的组成方法数了。举个例子:当a = 5时;求能构成容量为5的组合数。当确定一个元素1,那么它就可以由dp[4]构成;当确定元素2,那么就由dp[3]构成;当确定元素3,那么就由dp[2]构成;当确定元素4,那么就由dp[1]构成;…………最后,当数组中的元素遍历完了,最后dp[5]就等于dp[4]、dp[3]、dp[2]、dp[1]、dp[0]之和。所以递推公式为:dp[j] += dp[j - nums[i]]

初始化:根据递推公式,需要初始化dp[0],那么dp[0]到底为1还是0,当dp[0] = 0,那么后面的dp数组都为0了,所以dp[0] = 1。而0后面的dp数组可以根据dp[0]推导出来,所有0之后的dp数组初始化为0。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int n = nums.size();
        int sum = 0;
        for(int e : nums)
            sum += e;
        if((sum + target) % 2 == 1)
            return 0;
        if(abs(target) > sum)
            return 0;
        int v = (sum + target) / 2;
        vector<int>dp(v+1, 0);
        dp[0] = 1;
        for(int i = 0; i < n; i++)
        {
            for(int j = v; j >= nums[i]; j--)
            {
                dp[j] += dp[j-nums[i]];
            }
        }
        return dp[v];
    }
};

一和零

题目链接:. - 力扣(LeetCode)

解题思路:题目要求给定一个字符串数组,需要选用字符串组成m个0和n个1,问最多的组成子集。那么可以选用一个背包,两个容量维度;一个容量是m,一个容量是n。
dp数组的含义:dp[i][j]表示装i个0和j个1最多有dp[i][j]个子集。
递推公式:对于每个字符串,也就是背包中的物品,可以选择装和不装,先计算出物品中0的个数x,和1的个数y;当不装该物品子集个数是:dp[i][j];当装该物品子集个数是:dp[i-x][j-y] + 1。然后求最大值即可。那么递推公式为:dp[i][j] = max(dp[i][j], dp[i-x][j-y] + 1)
遍历顺序:
先遍历物品,再遍历容量。容量需要倒序遍历

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<vector<int>>dp(m+1,vector<int>(n+1, 0));
        for(string e : strs)
        {
            int x = 0, y = 0;
            for(char ch : e)
            {
                if(ch == '0')
                    x++;
                else
                    y++;
            }
            for(int i = m; i >= x; i--)
            {
                for(int j = n; j >= y; j--)
                    dp[i][j] = max(dp[i][j], dp[i-x][j-y] + 1);
            }
        }
        return dp[m][n];
    }
};

完全背包

完全背包是对于一个物品可以多次使用,01背包是每个物品只能使用一次。所以完全背包在遍历容量采用正序遍历

零钱兑换II

题目链接:. - 力扣(LeetCode)

解题思路:该题求得也是方法数,和前面的目标和很像。这种题目可以抽象为:给定一个容量,去选择物品去装满背包,问有多少种选法。只不过在前面目标和题目中,每种物品只能使用一次,而该题可以多次使用同一种物品。两个题目也就在遍历背包容量时顺序不同。其他都是一样的。

注意该题先遍历物品再遍历背包,不可颠倒。因为求的是组合数。而先遍历背包再遍历物品求的是排列。
先遍历物品再遍历背包:
是在物品中按照顺序依次取出一个物品,那么后面取出的物品肯定不是前面的物品。
先遍历背包再遍历物品:对于背包的容量依次去考虑放入物品(需要从容量等于0开始)。每次考虑一个容量,然后再依次拿出物品,而对于每个背包容量可以组成的方法数,都是由前面的容量可以组成的方法数得到的。那么前面背包容量可以组成的方法数,可能已经包含了即将要取出的物品。
举个例子:当coins=[1,2,5];amount = 5。那么对于dp[0] = 1;
考虑dp[1]:只能放入元素1, 所以dp[1] = 1. 【1】
考虑dp[2]:可以放入元素1,这个时候需要加上背包容量为1时的方法数(dp[1]);也可以放入元素2,这个时候需要加上背包容量为0时的方法数(dp[0])。即dp[2] = 2。【1,1】;【2】
考虑dp[3]:可以放入元素1,这个时候需要加上背包容量为2时的方法数(dp[2]);也可以放入元素2,这个时候需要加上背包容量为1的方法数(dp[1])。即dp[3] = 3。【1,1,1】;【1,2】;【2,1】。这是排列数,并不是组合。

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int>dp(amount+1, 0);
        dp[0] = 1;
        for(int i = 0; i < coins.size(); i++)
        {
            for(int j = coins[i]; j <= amount; j++)
            {
                dp[j] += dp[j-coins[i]];
            }
        }
        return dp[amount];
    }
};

组合总和IV

题目链接:. - 力扣(LeetCode)

解题思路:这道题求的是排列数,不像上一题求的是组合数。所以先遍历背包再遍历物品

注意:
1、先遍历背包需要从容量为0开始遍历,那么在计算dp[j]的时候需要加上判断条件(j -nums[i] >=0)防止dp数组越界。
2、在计算dp数组值的时候需要判断值是否溢出,也需要加上判断条件(dp[j]  <= INT_MAX - dp[j-nums[i]]),记住不可这样使用(dp[j] + dp[j-nums[i]]  <= INT_MAX),这样也会造成溢出。
 

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<int>dp(target+1, 0);
        dp[0] = 1;
        for(int j = 0; j <= target; j++)
        {
            for(int i = 0; i < nums.size(); i++)
            {
                if(j-nums[i] >= 0 && dp[j]  <= INT_MAX - dp[j-nums[i]])
                    dp[j] += dp[j-nums[i]];
            }
        }
        return dp[target];
    }
};

零钱兑换

题目链接:. - 力扣(LeetCode)

解题思路:该题求的是组成amount的硬币的最少数量,不像零钱兑换II求的是组成amount的方法。

dp数组的含义:组成j的硬币最少个数为dp[j]。

递推公式:dp[j] = min(dp[j], dp[j-coins[i]] + 1)。没遍历到一个硬币可以选择选与不选,不选:dp[j];选:dp[j-coins[i]] + 1。注意:本题求的是硬币的个数,每选择一个就+1

遍历顺序:本题求的是最少的硬币数,而不是硬币的组成或者排列,所以遍历背后容量和物品的顺序可以颠倒。

初始化:根据dp数组的递推公式可得,不能初始化为0,否则会覆盖,因为求的是最小。所以初始化为INT_MAX,对于dp[0]初始化为0,假如也初始化为INT_MAX,那么就不会给dp赋值了。再说题目的测试用例也是dp[0] = 0。

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int n = coins.size();
        vector<int>dp(amount+1, INT_MAX); // 表示凑成j的最少硬币数有dp[j]个
        dp[0] = 0;
        // 先遍历物品再遍历背包容量
        // for(int i = 0; i < n; i++)
        // {
        //     for(int j = coins[i]; j <= amount; j++)
        //     {
        //         if(dp[j - coins[i]] != INT_MAX)
        //             dp[j] = min(dp[j], dp[j-coins[i]] + 1);
        //     }
        // }
        
        //先遍历背包容量再遍历物品
        for(int j = 0; j <= amount; j++)
        {
            for(int i = 0; i < n; i++)
            {
                if(j >= coins[i] && dp[j-coins[i]] != INT_MAX )  // j >= coins[i]要在前面,否则后面的j-coins[i]会造成越界
                    dp[j] = min(dp[j], dp[j-coins[i]] + 1);
            }
        }
        return dp[amount] == INT_MAX ? -1 : dp[amount];
    }
};

完全平方数

题目链接:https://leetcode.cn/problems/perfect-squares/

解题思路:本题可以抽象为,给定一个容量为n的背包,去装[1,n]的完全平方数,问可以装满的最少完全平方数是多少。这样看来就和上一题是一样的了。

class Solution {
public:
    int numSquares(int n) {
        vector<int>dp(n+1, INT_MAX);
        dp[0] = 0;
        for(int i = 1; i*i <= n; i++) // i*i不能超过n
        {
            for(int j = i*i; j <= n; j++)
            {  
                dp[j] = min(dp[j], dp[j-i*i] + 1);
            }
        }
        return dp[n];
    }
};

单词拆分

题目链接:. - 力扣(LeetCode)

解题思路:本题可以转换为:字符串s为背包,字典中的字符串为物品。s的长度为背包的容量。

dp数组的含义:容量为j的背包是否可以被字典中的子串配备。

递推公式:每次需要根据容量去s中去截取子串,然后拿着这个子串去字典中匹配。

遍历顺序:先遍历背包再遍历物品,和顺序是有关联的。

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
       // unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
        vector<bool> dp(s.size()+1, false); // 需要给dp[0]一个状态,所以容量为s.size()
        dp[0] = true;
        // 
        for(int j = 1; j <= s.size(); j++) // 遍历背包容量
        {
            for(int i = 0; i < j; i++) // 遍历物品,在s上截取j-i的子串,去判断是否在字典中
            {
                string word = s.substr(i, j - i);
                //if(wordSet.find(word) != wordSet.end() && dp[i]) // 如果在字典中能够找到s的子串,并且这个子串之前的子串能够在字典中匹配(dp[i] == true)
                if(find(wordDict.begin(), wordDict.end(), word) != wordDict.end() && dp[i])
                    dp[j] = true;
            }
        }
        return dp[s.size()];
    }
};

其他

打家劫舍

题目链接:. - 力扣(LeetCode)

题目分析:题目要求相邻的两家不能偷窃,需要计算可以偷的最大金额。
确定dp数组的含义:dp[i]表示在[1,i]房间能偷的最大金额。
递推公式:题目要求不能偷相邻的两家,那么dp[i] = max(dp[i-1], dp[i-2] + nums[i])。dp[i-1]:表示不偷第i家;dp[i-2] + nums[i]:表示偷了第i家。
初始化:由递推公式可知,dp[i]是由第i家之前两家的情况而确定的,所以dp[0] = nums[0]; dp[1] = max(dp[0], nums[1])。当然还得判断一下是否只有一家。

class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        if(n == 1)
        {
            return nums[0];
        }
        int dp[101];
        dp[0] = nums[0];
        dp[1] = max(nums[0], nums[1]);
        for(int i = 2; i < n; i++)
        {
            dp[i] = max(dp[i-1], dp[i-2] + nums[i]); 
            // 选i这个位置前一个位置的值,不选前一个的值,那么就符合没有相邻的情况,就可以加上nums[i]
        }
        return dp[n - 1];
    }
};

空间优化

class Solution {
public:
    int rob(vector<int>& nums) {
        if(nums.size() == 1) return nums[0]; 
        // 优化空间
        int res1 = nums[0];
        int res2 = max(nums[1], res1);
        for(int i = 2; i < nums.size(); i++)
        {
            int temp = res2;
            res2 = max(res2, res1 + nums[i]);
            res1 = temp;
        }
        return res2;
    }
};

买卖股票的最佳时机

题目链接:. - 力扣(LeetCode)

贪心

解题思路:本题暴力很容易想到,但是会超时。我们不如把思路逆转过来,不去找哪一天买入,而是以卖出的日子作为基点考虑,遍历一遍数组,以每一天作为卖出出来计算最大值即可。那么买入肯定是要在卖出前的最小值。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int min_price = prices[0];
        int res = 0;
        for(int price : prices)
        {
            res = max(res, price - min_price);
            min_price = min(min_price, price);
        }
        return res;
    }
};

 动规

解题思路:可以把每一天的

  • 17
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值