玩转算法面试:(八)动态规划

玩转算法面试:(八)动态规划_第1张图片

动态规划将原问题拆解成若干子问题,同时保存子问题的答案,使得每个子问题只求解一次,最终获得原问题的答案。

1,斐波那契数列

(1)递归方法

int  fib(int n)
{
    if(n == 0)
        return 0;

    if(n == 1)
        return 1;

    return fib(n - 1) + fib(n - 2);
}

(2)记忆化搜索(自顶向下)

#include
#include
using namespace std;
vectormemo;

int fib(int n)
{
    if(n == 0)
        return 0;
    if(n == 1)
        return 1;
    if(memo[n] == -1)
        memo[n] = fib(n-1) + fib(n-2);
    return memo[n];
}

int main()
{
    int n = 20;
    memo = vector(n+1, -1);
    cout<

(3)动态规划(自底向上)

#include
#include
using namespace std;

int fib(int n)
{
    vector memo(n+1, -1);
    memo[0] = 0;
    memo[1] = 1;
    for(int i = 2; i <= n; i++)
        memo[i] = memo[i-1] + memo[i-2];
    return memo[n];
}

int main()
{
    int n = 20;
    cout<

70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:

输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.  1 阶 + 1 阶
2.  2 阶

示例 2:

输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1.  1 阶 + 1 阶 + 1 阶
2.  1 阶 + 2 阶
3.  2 阶 + 1 阶

(1)递归方法(超时)

class Solution {
private:
    int climbWays(int  n)
    {
        if(n == 1)
            return 1;
        if(n == 2)
            return 2;
        return climbWays(n-1) + climbWays(n-2);
    }
public:
    int climbStairs(int n) {
        return climbWays(n);
    }
};

(2)记忆化搜索

class Solution {
private:
    vector memo;
    int climbWays(int  n)
    {
        if(n == 0 || n == 1)
            return 1;
        if(memo[n] == -1)
            memo[n] = climbWays(n-1) + climbWays(n-2);
        return memo[n];
    }
public:
    int climbStairs(int n) {
        memo = vector(n+1, -1);   
        return climbWays(n);
    }
};

(3)动态规划

class Solution {
public:
    int climbStairs(int n) {
        vectormemo(n+1, -1); 
        memo[0] = 1;
        memo[1] = 1;
        for(int i = 2; i <= n; i++)
            memo[i] = memo[i-1] + memo[i-2];
        return memo[n];
    }
};

120. 三角形最小路径和

给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。

例如,给定三角形:

[
     [2],
    [3,4],
   [6,5,7],
  [4,1,8,3]
]

自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。

说明:

如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。

(1)使用动态规划,空间复杂度为O(n2)

class Solution {
public:
    int minimumTotal(vector>& triangle) {
        int n = triangle.size();
        vector >sum(n);
        for(int i = 0 ; i < n; i++)
            sum[i].resize(i+1);
        sum[0][0] = triangle[0][0];
        for(int i = 1; i < n; i++)
        {
            for(int j = 0; j <= i; j++)
            {
                if(j == 0)
                    sum[i][j] = sum[i-1][j] + triangle[i][j];
                else if(j == i)
                    sum[i][j] = sum[i-1][j-1] + triangle[i][j];
                else
                    sum[i][j] = min(sum[i-1][j-1], sum[i-1][j]) + triangle[i][j];
            }
        }
        int res = sum[n-1][0];
        for(int i = 1; i < n; i++)
            if(sum[n-1][i] < res)
                res = sum[n-1][i];
        return res;
    }
};

(2)空间复杂度为O(n)时

class Solution {
public:
    int minimumTotal(vector>& triangle) {
        int n = triangle.size();
        vectorsum(n);
        sum[0] = triangle[0][0];
        for(int i = 1; i < n; i++)
        {
            for(int j = i; j >= 0; j--)
            {
                if(j == 0)
                    sum[j] = sum[j] + triangle[i][j];
                else if(j == i)
                    sum[j] = sum[j-1] + triangle[i][j];
                else
                    sum[j] = min(sum[j-1], sum[j]) + triangle[i][j];
            }
        }
        int res = sum[0];
        for(int i = 1; i < n; i++)
            if(sum[i] < res)
                res = sum[i];
        return res;
    }
};

64. 最小路径和

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

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

示例:

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

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

343. 整数拆分

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。

示例 1:

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

示例 2:

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

说明: 你可以假设 n 不小于 2 且不大于 58。

(1)递归方法(超时)

class Solution {
public:
    int max3(int a, int b, int c)
    {
        return max(a, max(b, c));
    }
    //将n进行分割(至少两部分),可以获得的最大乘积
    int integerBreak(int n) {
        if(n == 1)
           return 1;
        int mul = 0;
        for(int i = 1; i < n; i++)
            mul = max3(mul, i*(n-i), i*integerBreak(n-i));
        return mul;
    }
};

(2)记忆化搜索

将中间的分割结果保存起来

class Solution {
private:
    vectormemo;
    int max3(int a, int b, int c)
    {
        return max(a, max(b, c));
    }
    //memo[i]表示将数字i分割(至少分割成两部分)后得到的最大乘积
    int breakInteger(int n)
    {
        if(n == 1)
           return 1;
        if(memo[n] != -1)
            return memo[n];
        int mul = 0;
        for(int i = 1; i < n; i++)
            mul = max3(mul, i*(n-i), i*breakInteger(n-i));
        memo[n] = mul;
        return memo[n];
    }
public:
    int integerBreak(int n) {
        memo = vector(n+1, -1);
        return breakInteger(n);
    }
};

(3)动态规划

class Solution {
public:
    int max3(int a, int b, int c)
    {
        return max(a, max(b, c));
    }
    int integerBreak(int n) {
        vectormemo(n+1, -1);
        memo[1] = 1;
        for(int i = 2; i <= n; i++)
        {
            for(int j = 1; j < i; j++)
                memo[i] = max3(memo[i], j*(i-j), j * memo[i-j]);
        }
        return memo[n];
    }
};

279. 完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

示例 1:

输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.

示例 2:

输入: n = 13
输出: 2
解释: 13 = 4 + 9.

class Solution {
public:
    int numSquares(int n) {
        if(n <= 0)
            return 0;
        vectornum(n+1, INT_MAX);
        num[0] = 0;
        num[1] = 1;
        for(int i = 2; i <= n; i++)
            for(int j = 1; j*j <= i; j++)
                num[i] = min(num[i], num[i-j*j]+1);
        return num[n];
    }
};

62. 不同路径

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

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

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

例如,上图是一个7 x 3 的网格。有多少可能的路径?

说明:m 和 n 的值均不超过 100。

示例 1:

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

示例 2:

输入: m = 7, n = 3
输出: 28

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

63. 不同路径 II

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

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

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

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

说明:m 和 n 的值均不超过 100。

示例 1:

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

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

198. 打家劫舍

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

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

示例 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)递归方法

class Solution {
private:
    //考虑打劫[index, nums.size())范围内的房子
    int maxValue(vector& nums, int index)
    {
        if(index >= nums.size())
            return 0;
        int sum = -1;
        for(int i = index; i < nums.size(); i++)
            sum = max(sum, nums[i] + maxValue(nums, i+2));
        return sum;
    }
public:
    int rob(vector& nums) {
        return maxValue(nums, 0);
    }
};

(2)记忆化搜索

class Solution {
private:
    vectormemo;
    //memo[index]表示打劫[index, nums.size())范围的房子所获得的最大收益
    int maxValue(vector& nums, int index)
    {
        if(index >= nums.size())
            return 0;
        if(memo[index] != -1)
            return memo[index];
        int sum = -1;
        for(int i = index; i < nums.size(); i++)
            sum = max(sum, nums[i] + maxValue(nums, i+2));
        memo[index] = sum;
        return sum;
    }
public:
    int rob(vector& nums) {
        int n = nums.size();
        memo = vector(n, -1);
        return maxValue(nums, 0);
    }
};

(3)动态规划

玩转算法面试:(八)动态规划_第2张图片

class Solution {
public:
    int rob(vector& nums) {
        int n = nums.size();
        if (n == 0) return 0;
        if (n == 1) return nums[0];
        if (n == 2) return max(nums[0], nums[1]);
        vectormemo(n, -1);
        memo[0] = nums[0];
        memo[1] = max(nums[0], nums[1]);
        for(int i = 2; i < n; i++)
        {
            memo[i] = max(memo[i-1], nums[i] + memo[i-2]);
        }
        return memo[n-1];
    }
};

213. 打家劫舍 II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

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

示例 1:

输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

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

解题思路:跟上一个题目198. 打家劫舍 其实是一样的,直接调用这个即可。唯一不同的就是这个是环状。解决办法就是[1,2,3,4,5] 把第一个数字POP掉求[2,3,4,5]的最大。然后求[1,2,3,4]的最大。最后求取两种的最大值。

(1)记忆化搜索

class Solution {
private:
    vectormemo;
    //memo[index]表示[0, index]范围内能抢劫到的最大值
    int maxValue(vector &nums, int index)
    {
        if(index < 0)
            return 0;
        if(memo[index] != -1)
            return memo[index];
        int sum = -1;
        for(int i = index; i >= 0;i--)
            sum = max(sum, nums[i] + maxValue(nums, i-2));
        memo[index] = sum;
        return sum;
    }
public:
    int rob(vector& nums) {
        int n = nums.size();
        if(n == 0) return 0;
        if(n == 1) return nums[0];
        if(n == 2) return max(nums[0], nums[1]);
        memo = vector(n-1, -1);
        int sum1 = maxValue(nums, n-2);
        
        memo = vector(n-1, -1);
        vectortemp(nums.begin()+1, nums.end());
        int sum2 = maxValue(temp, n-2);
        
        return max(sum1, sum2);
    }
};

(2)动态规划

class Solution {
private:
    int maxValue(vector &nums)
    {
        int n = nums.size();
        vectormemo(n, -1);
        memo[0] = nums[0];
        memo[1] = max(nums[0], nums[1]);
        for(int i = 2; i < n;i++)
            memo[i] = max(memo[i-1], nums[i]+memo[i-2]);
        return memo[n-1];
    }
public:
    int rob(vector& nums) {
        int n = nums.size();
        if(n == 0) return 0;
        if(n == 1) return nums[0];
        if(n == 2) return max(nums[0], nums[1]);
        vectortemp1(nums.begin(), nums.end()-1);
        int sum1 = maxValue(temp1);
        
        vectortemp2(nums.begin()+1, nums.end());
        int sum2 = maxValue(temp2);
        
        return max(sum1, sum2);
    }
};

121. 买卖股票的最佳时机

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。注意你不能在买入股票前卖出股票。

示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

示例 2:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

(1)动态规划

股票的最大利润依赖于卖出时间和之前的买入时间,时间复杂度O(n2)

class Solution {
public:
    int maxProfit(vector& prices) {
        int n = prices.size();
        if(n <= 1)
            return 0;
        int profit = 0;
        for(int i = 1; i < n; i++)
            for(int j = 0; j < i; j++)
            {
                if(prices[i] <= prices[j])
                    continue;
                else
                    profit = max(profit, prices[i]-prices[j]);
            }
        return profit;
    }
};

(2)用一个变量记录买入的最低价,后面的卖出价减去这个最低价就可以找到卖出股票的最大利润,时间复杂度O(n)

class Solution {
public:
    int maxProfit(vector& prices) {
        int n = prices.size();
        if(n <= 1)
            return 0;
        int buy = INT_MAX;
        int profit = 0;
        for(int i = 0; i < n; i++)
        {
            buy = min(buy, prices[i]);
            profit = max(profit, prices[i] - buy);
        }
        return profit;
    }
};

309. 最佳买卖股票时机含冷冻期

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

    你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
    卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

示例:

输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

动态规划:此题有两种状态

(1)第i个时刻卖出股票,profit[i] = max(profit[i], (prices[i] - prices[j]) +  profit[j-2]);

(2)第i个时刻什么也不做,profit[i] = max(profit[i], profit[i-1]);

class Solution {
public:
    int maxProfit(vector& prices) {
        int n = prices.size();
        if(n <= 1) return 0;
        if(n == 2) return prices[1] > prices[0] ? prices[1]-prices[0] : 0;
        vector profit(n, 0);
        profit[1] = prices[1] > prices[0] ? prices[1]-prices[0] : 0;
        for(int i = 2; i < n; i++)
        {
            for(int j = 0; j < i; j++)
            {
                if(prices[i] > prices[j])
                {
                    int temp = (prices[i] - prices[j]) + ((j - 2) >= 0 ? profit[j-2] : 0);
                    profit[i] = max(profit[i], temp);//卖出股票的最大收益
                }
            }
            profit[i] = max(profit[i], profit[i-1]);//不做任何交易
        }
        return profit[n-1];
    }
};

0-1背包问题

玩转算法面试:(八)动态规划_第3张图片

玩转算法面试:(八)动态规划_第4张图片

(1)递归方法

class Knapsack01
{
    private:
        //用[0,1,... ...,index]的物品来填充体积为c的背包的最大价值
        int bestValue(const vector &w, const vector &v, int index, int c)
        {
            if(index < 0 || c <= 0)
                return 0;
            
            int res = bestValue(w, v, index -1, c);
            if(c >= w[index])
                res = max(res, v[index] + bestValue(w, v, index -1, c - w[index]);
            return res;
        }
    public:
        int knapsack01(const vector &w, const vector &v, int C)
        {
            int n = w.size();
            return bestValue(w, v, n-1, C);
        }
};

(2)记忆搜索

class Knapsack02
{
    private:
        vector> memo;
        //用[0,1,... ...,index]的物品来填充体积为c的背包的最大价值
        int bestValue(const vector &w, const vector &v, int index, int c)
        {
            if(index < 0 || c <= 0)
                return 0;
            if(memo[index][c] != -1)
                return memo[index][c];
            int res = bestValue(w, v, index -1, c);
            if(c >= w[index])
                res = max(res, v[index] + bestValue(w, v, index -1, c - w[index]);
            memo[index][c] = res;
            return res;
        }
    public:
        int knapsack02(const vector &w, const vector &v, int C)
        {
            int n = w.size();
            memo = vector(n, vector(C+1, -1));
            return bestValue(w, v, n-1, C);
        }
};

(3)动态规划

时间复杂度O(n*C),空间复杂度O(n*C)

class Knapsack03
{
    
    public:
        int knapsack02(const vector &w, const vector &v, int C)
        {
            assert(w.size() == v.size());
            int n = w.size();
            if(n == 0)
                return 0;
            vectormemo(n, vector(C+1, -1));
            
            for(int j = 0; j <= C; j++)
                memo[0][j] = j >= w[0] ? v[0] : 0;
            
            for(int i = 1; i < n; i++)
                for(int j = 0; j <= C; j++)
                {
                    memo[i][j] = memo[i-1][j];
                    if(j >= w[i])
                        memo[i][j] = max(memo[i][j], v[i] + memo[i-1][j-w[i]]);
                }
      
            return memo[n-1][C];
        }
};

玩转算法面试:(八)动态规划_第5张图片

class Knapsack04
{
    
    public:
        int knapsack04(const vector &w, const vector &v, int C)
        {
            assert(w.size() == v.size());
            int n = w.size();
            if(n == 0)
                return 0;
            vectormemo(2, vector(C+1, -1));
            
            for(int j = 0; j <= C; j++)
                memo[0][j] = j >= w[0] ? v[0] : 0;
            
            for(int i = 1; i < n; i++)
                for(int j = 0; j <= C; j++)
                {
                    memo[i%2][j] = memo[(i-1)%2][j];
                    if(j >= w[i])
                        memo[i%2][j] = max(memo[i%2][j], v[i] + memo[(i-1)%2][j-w[i]]);
                }
      
            return memo[(n-1)%2][C];
        }
};
class Knapsack04
{
    
    public:
        int knapsack04(const vector &w, const vector &v, int C)
        {
            assert(w.size() == v.size());
            int n = w.size();
            if(n == 0)
                return 0;
            vectormemo(C+1, -1);
            
            for(int j = 0; j <= C; j++)
                memo[j] = j >= w[0] ? v[0] : 0;
            
            for(int i = 1; i < n; i++)
                for(int j = C; j >= 0; j--)
                {
                     memo[j] = max(memo[j], v[i] + memo[j-w[i]]);
                }
      
            return memo[C];
        }
};

玩转算法面试:(八)动态规划_第6张图片

416. 分割等和子集

玩转算法面试:(八)动态规划_第7张图片

(1)递归方法

class Solution {
private:
    //用[0,......,index]的数组填充容量为sum的背包
    bool tryPartition(vector &num, int index, int sum)
    {
        if(sum == 0)
            return true;
        if(index < 0 || sum < 0)
            return false;
        
        return tryPartition(num, index-1, sum) || tryPartition(num, index-1, sum-num[index]);
    }
public:
    bool canPartition(vector& nums) {
        int n = nums.size();
        int sum = 0;
        for(int i = 0; i < n;i++)
            sum = sum + nums[i];
        if(sum %2 == 1)
            return false;
        return tryPartition(nums, n-1, sum/2);
    }
};

(2)记忆化搜索

class Solution {
private:
    vector>memo;
    //memo[index][sum]用[0,......,index]的数组填充容量为sum的背包,是否可以填满
    //-1表示未填充,0表示填充不满,1表示可以填充
    bool tryPartition(vector &num, int index, int sum)
    {
        if(sum == 0)
            return true;
        if(index < 0 || sum < 0)
            return false;
        if(memo[index][sum] != -1)
            return memo[index][sum];
        memo[index][sum] = tryPartition(num, index-1, sum) || tryPartition(num, index-1, sum-num[index]);
        return memo[index][sum];
    }
public:
    bool canPartition(vector& nums) {
        int n = nums.size();
        int sum = 0;
        for(int i = 0; i < n;i++)
            sum = sum + nums[i];
        if(sum %2 == 1)
            return false;
        memo = vector>(n,vector(sum/2+1, -1));
        return tryPartition(nums, n-1, sum/2);
    }
};

(3)动态规划

class Solution {
public:
    bool canPartition(vector& nums) {
        int n = nums.size();
        int sum = 0;
        for(int i = 0; i < n;i++)
            sum = sum + nums[i];
        if(sum %2 == 1)
            return false;
        vector>memo(n,vector(sum/2+1, -1));
        
        for(int j = 0; j <= sum/2; j++)
            memo[0][j] = j == nums[0] ? 1 : 0;
        for(int i = 1; i < n; i++)
            for(int j = 0; j <= sum/2; j++)
            {
                memo[i][j] = memo[i-1][j];
                if(j >= nums[i])
                    memo[i][j] = memo[i][j] || memo[i-1][j-nums[i]];
            }
        return memo[n-1][sum/2];
    }
};
class Solution {
public:
    bool canPartition(vector& nums) {
        int n = nums.size();
        int sum = 0;
        for(int i = 0; i < n;i++)
            sum = sum + nums[i];
        if(sum %2 == 1)
            return false;
        int C = sum/2;
        vectormemo(C+1, -1);
        
        for(int j = 0; j <= C; j++)
            memo[j] = (j == nums[0]) ? 1 : 0;
        for(int i = 1; i < n; i++)
            for(int j = C; j >=nums[i]; j--)
            {
                memo[j] = memo[j] || memo[j-nums[i]];
            }
        return memo[C];
    }
};

322. 零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

示例 1:

输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1

示例 2:

输入: coins = [2], amount = 3
输出: -1

动态规划sum[j]记录达到amount总金额最少的硬币数,状态转移方程sum[j] = min(sum[j], sum[j-coins[i]]+1);

sum[j]初始化为amount+1,因为总金额amount所兑换的硬币数不可能超过amount。

class Solution {
public:
    int coinChange(vector& coins, int amount) {
        int n = coins.size();
        if(n == 0)
            return 0;
        vectorsum(amount+1, amount+1);
        sum[0] = 0;
        
        for(int i = 0; i < n; i++)
        {
            for(int j = coins[i]; j <= amount; j++)
                sum[j] = min(sum[j], sum[j-coins[i]]+1);
        }
        if(sum[amount] == (amount+1))
            return -1;
        return sum[amount];
    }
};

377. 组合总和 Ⅳ

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

示例:

nums = [1, 2, 3]
target = 4

所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

请注意,顺序不同的序列被视作不同的组合。

因此输出为 7。

(1)递归(超时)

class Solution {
private:
    int helper(const vector &nums, int target)
    {
        if(target == 0)
            return 1;
        int res = 0;
        for(int i = 0; i < nums.size();i++)
        {
            if(target >= nums[i])
                res = res + helper(nums, target - nums[i]);
        }
        return res;
    }
public:
    int combinationSum4(vector& nums, int target) {
        int n = nums.size();
        if(n == 0)
            return 0;
        return helper(nums, target);
    }
};

(2)记忆化搜索

class Solution {
private:
    vectormemo;
    int helper(const vector &nums, int target)
    {
        if(target == 0)
            return 1;
        if(memo[target] != -1)
            return memo[target];
        int res = 0;
        for(int i = 0; i < nums.size();i++)
        {
            if(target >= nums[i])
                res = res + helper(nums, target - nums[i]);
        }
        memo[target] = res;
        return res;
    }
public:
    int combinationSum4(vector& nums, int target) {
        int n = nums.size();
        if(n == 0)
            return 0;
        memo = vector(target+1, -1);
        return helper(nums, target);
    }
};

(3)动态规划

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

139. 单词拆分

给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:

    拆分时可以重复使用字典中的单词。
    你可以假设字典中没有重复的单词。

示例 1:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。

示例 2:

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
     注意你可以重复使用字典中的单词。

示例 3:

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

(1)递归方法(超时)

class Solution {
private:
    bool helper(string &s, unordered_set &word)
    {
        if(s.length() == 0)
            return true;
        
        for(int i = 1; i <= s.length(); i++)
        {
            if(word.find(s.substr(0, i)) != word.end())
            {
                string second = s.substr(i);
                if(helper(second, word))
                    return true;
            } 
        }
        return false;
    }
public:
    bool wordBreak(string s, vector& wordDict) {
        int n = s.length();
        unordered_set word;
        for(int i = 0; i < wordDict.size(); i++)
            word.insert(wordDict[i]);
        
        return helper(s, word);
    }
};

(2)记忆化搜索

用memo记录不匹配的字符串,减少递归次数

class Solution {
public:
    unordered_set fail;
    bool wordBreak(string s, vector& wordDict) {
        unordered_set dict;
        for(int i = 0; i < wordDict.size(); i++)
            dict.insert(wordDict[i]);
        
        return helper(s, dict);
    }
    bool helper(string s, unordered_set & wordDict)
    {
        if(s.length() == 0)
            return true;
        for(int j = 1; j <= s.length();j++)
        {
            string first = s.substr(0, j);
            if(wordDict.find(first) != wordDict.end())
            {
                string second = s.substr(j);
                if(fail.find(second) == fail.end())
                {
                    if(helper(second, wordDict))
                        return true;
                    else
                        fail.insert(second);
                }
            }
        }
        return false;  
    }
};

(3)动态规划

class Solution {
public:
    bool wordBreak(string s, vector& wordDict) {
        int n = s.length();
        unordered_set word;
        for(int i = 0; i < wordDict.size(); i++)
            word.insert(wordDict[i]);
        vectormemo(n+1, false);
        memo[0] = true;
        
        for(int i = 1; i <= s.length(); i++)
            for(int j = 0; j < i; j++)
            {
                if(memo[j] && word.find(s.substr(j, i-j)) != word.end())
                {
                    memo[i] = true;
                    break;
                }
            }
        return memo[n];
    }
};

494. 目标和

给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例 1:

输入: nums: [1, 1, 1, 1, 1], S: 3
输出: 5
解释:

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

一共有5种方法让最终目标和为3。

解题思路,假设有两个集合P和N

                  sum(P) - sum(N) = target
sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N)
                       2 * sum(P) = target + sum(nums)

因此,原来的问题已转化为一个求子集的和问题: 找到nums的一个子集 P,使得sum(P) = (target + sum(nums)) / 2

这和416分割集合类似。

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

300. 最长上升子序列

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

说明:

    可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
    你算法的时间复杂度应该为 O(n2) 。

进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

玩转算法面试:(八)动态规划_第8张图片

(1)动态规划解法(时间复杂度o(n2))

class Solution {
public:
    int lengthOfLIS(vector& nums) {
        int n = nums.size();
        if(n == 0)
            return 0;
        
        vectormemo(n, 1);
        for(int i = 1; i < n; i++)
            for(int j = 0; j < i; j++)
            {
                if(nums[i] > nums[j])
                    memo[i] = max(memo[i], memo[j] + 1);
            }
        int res = 1;
        for(int i = 0; i < n; i++)
            res = max(res, memo[i]);
        return res;
    }
};

(2)使用贪心选择 + 二分查找算法

玩转算法面试:(八)动态规划_第9张图片

玩转算法面试:(八)动态规划_第10张图片

玩转算法面试:(八)动态规划_第11张图片

class Solution {
public:
    int binarySearch(const vector &memo, int target)
    {
        int l = 0;
        int r = memo.size();
        while(l < r)
        {
            int mid = l + (r-l)/2;
            if(memo[mid] == target)
                return mid;
            else if(memo[mid] < target)
                l = mid + 1;
            else
                r = mid;
        }
        
        return l;
    }
    int lengthOfLIS(vector& nums) {
        int n = nums.size();
        if(n == 0)
            return 0;
        
        vectormemo;
        for(int i = 0; i < n; i++)
        {
            int index = binarySearch(memo, nums[i]);
            if(index == memo.size())
                memo.push_back(nums[i]);
            else 
                memo[index] = nums[i];
        }
        
        return memo.size();
    }
};

376. 摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。

例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

示例 1:

输入: [1,7,4,9,2,5]
输出: 6
解释: 整个序列均为摆动序列。

示例 2:

输入: [1,17,5,10,13,15,10,5,16,8]
输出: 7
解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。

示例 3:

输入: [1,2,3,4,5,6,7,8,9]
输出: 2

进阶:
你能否用 O(n) 时间复杂度完成此题?

(1)动态规划时间复杂度O(n2),状态转移方程

当nums[i] > nums[j] && i > j,up[i] = down[j] + 1
当nums[i] < nums[j] && i > j,down[i] = up[j] + 1

class Solution {
public:
    int wiggleMaxLength(vector& nums) {
        int n = nums.size();
        if(n < 2)
            return n;
        
        vectorup(n, 1);
        vectordown(n, 1);
        for(int i = 1; i < nums.size(); i++)
            for(int j = 0; j < i; j++)
            {
                if(nums[i] > nums[j])
                    up[i] = max(up[i], down[j]+1);
                else if(nums[i] < nums[j])
                    down[i] = max(down[i], up[j]+1);
            }
        
        int res = 1;
        for(int i =0 ;i < n; i++)
        {
            res = max(res, up[i]);
            res = max(res, down[i]);
        }
        return res;
    }
};

(2)简化版,时间复杂度O(n)

class Solution {
public:
    int wiggleMaxLength(vector& nums) {
        int n = nums.size();
        if(n < 2)
            return n;
        
        int up = 1, down =1;
        for(int i = 1; i < nums.size(); i++)
        {
            if(nums[i] > nums[i-1])
                up = down +1;
            else if(nums[i] < nums[i-1])
                down = up +1;
        }
        
        return max(up, down);
    }
};

玩转算法面试:(八)动态规划_第12张图片

玩转算法面试:(八)动态规划_第13张图片

72. 编辑距离

给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

    插入一个字符
    删除一个字符
    替换一个字符

示例 1:

输入: word1 = "horse", word2 = "ros"
输出: 3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

示例 2:

输入: word1 = "intention", word2 = "execution"
输出: 5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')

解题思路:

我们使用一个二维vector数组来记录从word1到word2的修改步骤,memo[i][j]对应的是从word1[0...i]到word[0...j]需要修改的次数

状态转移方程

  • replace: memo[i - 1][j - 1] + 1 //保留从word1[0 ... i-1]转变到word2[0 ... j-1]的次数,再加一,加一指的是本次的修改
  • insert: memo[i][j - 1] + 1 // 保留从word1[0 ... i]转变到word2[0 ... j-1]的次数,加一
  • delete: memo[i - 1][j] + 1// 保留从word1[0 ... i-1]转变到word2[0 ... j]的次数,加一

边界值

 

dp[i][0] = i;
dp[0][j] = j;

对于替换替换还是删除,我们选其中最小的值

class Solution {
public:
    int minDistance(string word1, string word2) {
        int m = word1.size(), n = word2.size();
        
        vector>memo(m+1, vector(n+1, INT_MAX));
        for(int i = 0; i <= m; i++)
            memo[i][0] = i;
        for(int j = 0; j <= n; j++)
            memo[0][j] = j;
        
        for(int i = 1; i <= m; i++)
            for(int j = 1; j <= n; j++)
            {
                if(word1[i-1] == word2[j-1])
                    memo[i][j] = memo[i-1][j-1];
                else
                {
                    memo[i][j] = min(memo[i][j], 1+memo[i-1][j-1]);//替换
                    memo[i][j] = min(memo[i][j], 1+memo[i][j-1]);//删除或者插入
                    memo[i][j] = min(memo[i][j], 1+memo[i-1][j]);//删除或者插入
                }
            }
        return memo[m][n];
    }
};

5. 最长回文子串

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例 1:

输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。

示例 2:

输入: "cbbd"
输出: "bb"

class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.length();
        vector>memo(n, vector(n, false));
        
        int start = 0;
        int maxlength = 1;
        for(int i = 0; i < n; i++)
        {
            memo[i][i] = true;
            if(i < n-1 && s[i] == s[i+1])
            {
                memo[i][i+1] = true;
                start = i;
                maxlength = 2;
            }
        }
        
        for(int len = 3; len <= n; len++)
            for(int i = 0; i <= n - len; i++)
            {
                int j = i + len -1;
                if(s[i] == s[j] && memo[i+1][j-1])
                {
                    memo[i][j] = true;
                    start = i;
                    maxlength = len;
                }
            }
        return s.substr(start, maxlength);
    }
};

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(数据结构与算法)