LeetCode高频题刷题笔记(十四)动态规划

基础知识

动态规划,英文:Dynamic Programming,简称DP。
动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。


题目

1. 爬楼梯( LeetCode 70

难度: 简单
题目表述:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
代码(C++):

class Solution {
public:
    int climbStairs(int n) {
        int p = 0, q = 1, r = 0;
        for (int i = 1; i <= n; i++) {
            r = p + q;
            p = q;
            q = r;
        }
        return r;
    }
    // 完全背包求排列数
    // 一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。本题 m = 2
    int climbStairs(int n) {
        vector<int> dp(n + 1);
        dp[0] = 1;
        for (int j = 1; j <= n; j++) {
            for (int i = 1; i <= 2; i++) { 
                if (j >= i) dp[j] += dp[j - i];
            }
        }
        return dp[n];
    }
};

题解:
1.斐波那契数列的第n项 : f(x)=f(x−1)+f(x−2)
2.一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。完全背包求排列数问题,本题 m = 2。


2. 使用最小花费爬楼梯( LeetCode 746

难度: 简单
题目表述:
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。
代码(C++):

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        vector<int> dp(cost.size() + 1);
        dp[0] = 0;
        dp[1] = 0;
        for (int i = 2; i <= cost.size(); i++) {
            dp[i] = min(dp[i - 2] + cost[i - 2], dp[i - 1] + cost[i - 1]);
        }
        return dp[cost.size()];
    }
};

题解:


3. 不同路径( LeetCode 62

难度: 中等
题目表述:
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?
代码(C++):

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> dp(m, vector<int>(n));
        for (int i = 0; i < m; i++) {
            dp[i][0] = 1;
        }
        for (int j = 0; j < n; j++) {
            dp[0][j] = 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];
    }
};

题解:
动态规划转移方程:f(i,j)=f(i−1,j)+f(i,j−1)


4. 不同路径 II( LeetCode 63

难度: 中等
题目表述:
网格中的障碍物和空位置分别用 1 和 0 来表示。
代码(C++):

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

题解:


5. 整数拆分( LeetCode 343

难度: 中等
题目表述:
给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。
代码(C++):

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++) {
                dp[i] = max({dp[i], j * (i - j), j * dp[i - j]});
            }
        }
        return dp[n];
    }
};

题解:
j * (i - j) 是单纯的把整数拆分为两个数相乘,而 j * dp[i - j] 是拆分成三个以及三个以上的个数相乘。


6. 不同的二叉搜索树( LeetCode 96

难度: 中等
题目表述:
给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
代码(C++):

class Solution {
public:
    int numTrees(int n) {
        vector<int> dp(n + 1, 0);
        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];
    }
};

题解:
dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]


7. 分割等和子集( LeetCode 416

难度: 中等
题目表述:
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
代码(C++):

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for (int i = 0; i < nums.size(); i++) {
            sum += nums[i];
        }
        if (sum % 2) return false;
        int m = nums.size();
        int target = sum / 2;
        vector<int> dp(target + 1, 0);
        for (int i = 0; i < m; i++) {
            for (int j = target; j >= nums[i]; j--) {
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }
        return dp[target] == target;
    }
};

题解:
背包容量为target, dp[target]就是装满 背包之后的重量,所以 当 dp[target] == target 的时候,背包就装满了。
问题转化为在 sum / 2 容量的背包中最多可以装多少。dp[j] 表示: 容量为j的背包,所背的物品价值最大可以为dp[j]。


8. 最后一块石头的重量 II( LeetCode 1049

难度: 中等
题目表述:
每一回合,从中选出任意两块石头,然后将它们一起粉碎。最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
代码(C++):

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int n = stones.size();
        int sum = 0;
        for (const auto &s: stones) {
            sum += s;
        }
        int target = sum / 2;
        vector<int> dp(target + 1, 0);
        for (int i = 0; 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];
    }
};

题解:
尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小。
计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的。
上一题相当于是求背包是否正好装满,而本题是求背包最多能装多少。
问题转化为在 sum / 2 容量的背包中最多可以装多少。dp[j]表示容量为j的背包,最多可以背最大重量为dp[j]。


9. 目标和( LeetCode 494

难度: 中等
题目表述:
返回可以向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数构造一个 表达式 、运算结果等于 target 的不同 表达式 的数目。
代码(C++):

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

题解:
left - right = target,left + right = sum => right = sum - left,则,
left - (sum - left) = target => left = (target + sum)/2 。
问题转化为装满容量为left的背包有几种方法。dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法


10. 一和零( LeetCode 474

难度: 中等
题目表述:
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
代码(C++):

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 str : strs) {
            int zeroNum = 0, oneNum = 0; 
            for (char c : str) {
                if (c == '0') zeroNum++;
                else oneNum++;
            }
            for (int i = m; i >= zeroNum; i--) {
                for (int j = n; j >= oneNum; j--) {
                    dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
                }
            }
        }
        return dp[m][n];
    }
};

题解:
m 和 n相当于是一个背包,两个维度的背包
dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。


11. 零钱兑换 II( LeetCode 518

难度: 中等
题目表述:
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
代码(C++):

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];
    }
};

题解: 本质是求完全背包 组合数 (先遍历物品,再遍历背包)


12.组合总和 Ⅳ( LeetCode 377

难度: 中等
题目表述:
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。顺序不同的序列被视作不同的组合。
代码(C++):

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] && dp[j] < INT_MAX - dp[j - nums[i]]) dp[j] += dp[j - nums[i]];
            }
        }
        return dp[target];
    }
};

题解: 本质是求完全背包 排列数 (先遍历背包,再遍历物品)


13. 零钱兑换( LeetCode 322

难度: 中等
题目表述:
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数
代码(C++):

class Solution {
public:
	//动态规划
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount + 1, amount + 1);
        dp[0] = 0;
        for (int i = 0; i < coins.size(); i++) {
            for (int j = coins[i]; j <= amount; j++) {//遍历背包【正序】
                dp[j] = min(dp[j], dp[j - coins[i]] + 1);
            }
        }
        return dp[amount] == amount + 1 ? -1 : dp[amount];
    }
};

题解: 本质是求完全背包 组合数 / 排列数(for循环嵌套顺序无所谓)
要求最少硬币数量,硬币是组合数还是排列数都无所谓!所以两个for循环先后顺序怎样都可以!


14.完全平方数( LeetCode 279

难度: 中等
题目表述:
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
代码(C++):

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++) {// 遍历物品
            for (int j = i * i; j <= n; j++) {// 遍历背包
                dp[j] = min(dp[j], dp[j - i * i] + 1);
            }
        }
        return dp[n];
    }
    int numSquares(int n) {
        vector<int> dp(n + 1, INT_MAX);
        dp[0] = 0;
        for (int i = 0; i <= n; i++) { // 遍历背包
            for (int j = 1; j * j <= i; j++) { // 遍历物品
                dp[i] = min(dp[i - j * j] + 1, dp[i]);
            }
        }
        return dp[n];
    }
};

题解: 本质是求完全背包 组合数 / 排列数(for循环嵌套顺序无所谓)
dp[j]:和为j的完全平方数的最少数量为dp[j]


15.单词拆分( LeetCode 139

难度: 中等
题目表述:
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
代码(C++):

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        vector<bool> dp(s.size() + 1, false);
        dp[0] = true;
        for (int i = 1; i <= s.size(); i++) { 	// 遍历背包
            for (string word : wordDict) {   	// 遍历物品
                int len = word.size();
                if (i >= len && dp[i - len] && word == s.substr(i - len, len)) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.size()];
    }
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
        vector<bool> dp(s.size() + 1, false);
        dp[0] = true;
        for (int i = 1; i <= s.size(); i++) {   // 遍历背包
            for (int j = 0; j < i; j++) {       // 遍历物品
                string word = s.substr(j, i - j); //substr(起始位置,截取的个数)
                if (wordSet.find(word) != wordSet.end() && dp[j]) {
                    dp[i] = true;
                }
            }
        }
        return dp[s.size()];
    }
};

题解: 本质是求完全背包 排列数 (先遍历背包,再遍历物品)
强调物品之间顺序,一定是 先遍历 背包,再遍历物品。


16. 打家劫舍( LeetCode 198

难度: 中等
题目表述:
不能连续偷两间相邻的房屋,能够偷窃到的最高金额。
代码(C++):

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

题解: 当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了。
dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i],i间有偷或不偷两种情况


17. 打家劫舍 II( LeetCode 213

难度: 中等
题目表述:
所有的房屋都 围成一圈,不能连续偷两间相邻的房屋,能够偷窃到的最高金额。
代码(C++):

class Solution {
public:
    int robRange(vector<int>& nums, int left, int right) {
        if (left == right) return nums[left];
        vector<int> dp(nums.size());
        dp[left] = nums[left];
        dp[left + 1] = max(nums[left], nums[left + 1]);
        for (int i = left + 2; i <= right; i++) {
            dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
        }
        return dp[right];
    }
    int rob(vector<int>& nums) {
        if (nums.size() == 0) return 0;
        if (nums.size() == 1) return nums[0];
        return max(robRange(nums, 0, nums.size() - 2), robRange(nums, 1, nums.size() - 1));
    }
};

题解: max(考虑包含首元素,不包含尾元素, 考虑包含尾元素,不包含首元素)


18. ⭐打家劫舍 III( LeetCode 337

难度: 中等
题目表述:
房屋的排列类似于一棵二叉树,不能连续偷两间相邻的房屋,能够偷窃到的最高金额。
代码(C++):

class Solution {
public:
	// 递归
	unordered_map<TreeNode*, int> umap;
    int rob(TreeNode* root) {
        if (!root) return 0;
        if (!root->left && !root->right) return root->val;
        if (umap[root]) return umap[root];
        int val1 = rob(root->left) + rob(root->right); // 不偷父节点
        int val2 = root->val; //偷父节点
        if (root->left) {
            val2 += rob(root->left->left);
            val2 += rob(root->left->right);
        }
        if (root->right) {
            val2 += rob(root->right->left);
            val2 += rob(root->right->right);
        }
        umap[root] = max(val1, val2);
        return umap[root];
    }
    // 动态规划:长度为2的数组,0:不偷,1:偷
    vector<int> robTree(TreeNode* root) {
        if (!root) return {0, 0};
        vector<int> left = robTree(root->left);
        vector<int> right = robTree(root->right);
        int val1 = max(left[0], left[1]) + max(right[0], right[1]); // 不偷父节点
        int val2 = root->val + left[0] + right[0]; // 偷父节点
        return {val1, val2};
    }
    int rob(TreeNode* root) {
        vector<int> res = robTree(root);
        return max(res[0], res[1]);
    }
};

题解: 树形DP
下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。


19. 买卖股票的最佳时机 ( LeetCode 121

难度: 简单
题目表述:
只能买卖一次
代码(C++):

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        if (n == 0) {
            return 0;
        }
        vector<vector<int>> dp(n, vector<int>(2));
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for (int i = 1; i < n; i++) {
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = max(dp[i - 1][1], -prices[i]);
        }
        return dp[n - 1][0];    
    }
    // 空间复杂度降为O(1)
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        if (n == 0) {
            return 0;
        }
        int dp0 = 0, dp1 = -prices[0];
        for (int i = 1; i < n; i++) {
            dp0 = max(dp0, dp1 + prices[i]);
            dp1 = max(dp1, -prices[i]);
        }
        return dp0;    
    }
};

题解: 股票全程只能买卖一次,那么推导第i天持有股票时用到的dp[i-1][0]一定就是0。
k = 1 维度k可省略
T[i][1][0] = max(T[i - 1][1][0], T[i - 1][1][1] + prices[i])
T[i][1][1] = max(T[i - 1][1][1], T[i - 1][0][0] - prices[i]) = max(T[i - 1][1][1], -prices[i])
T[i][0][0] = 0


20. 买卖股票的最佳时机 II( LeetCode 122

难度: 中等
题目表述:
可以买卖多次
代码(C++):

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        if (n == 0) return 0;
        vector<vector<int>> dp(n, vector<int>(2));
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for (int i = 1; i < n; i++) {
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        }
        return dp[n - 1][0];
    }
   	// 空间复杂度降为O(1)
   	int maxProfit(vector<int>& prices) {
        int n = prices.size();
        if (n == 0) return 0;
        int dp0 = 0, dp1 = -prices[0];
        for (int i = 1; i < n; i++) {
            int newdp0 = max(dp0, dp1 + prices[i]);
            dp1 = max(dp1, dp0 - prices[i]);
            dp0 = newdp0;
        }
        return dp0;
    }
};

题解: 一只股票可以买卖多次,那么推导第i天持有股票时用到的dp[i-1][0]就是之前买卖过的利润。
k = +∞ k 和 k - 1 可以看成是相同的,维度k可省略
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k - 1][0] - prices[i]) = max(T[i - 1][k][1], T[i - 1][k][0] - prices[i])
T[i - 1][k - 1][0] = T[i - 1][k][0]


21. 买卖股票的最佳时机 III( LeetCode 123 )⭐

难度: 困难
题目表述:
最多买卖两次
代码(C++):

class Solution {
public:
	int maxProfit(vector<int>& prices) {
        vector<vector<int>> dp1(prices.size(), vector<int>(2, 0));
        vector<vector<int>> dp2(prices.size(), vector<int>(2, 0));
        dp1[0][0] = 0;
        dp1[0][1] = -prices[0];
        dp2[0][0] = 0;
        dp2[0][1] = -prices[0];
        for (int i = 1; i < prices.size(); i++) {
            dp1[i][0] = max(dp1[i - 1][0], dp1[i - 1][1] + prices[i]);
            dp1[i][1] = max(dp1[i - 1][1], -prices[i]);
            dp2[i][0] = max(dp2[i - 1][0], dp2[i - 1][1] + prices[i]);
            dp2[i][1] = max(dp2[i - 1][1], dp1[i - 1][0] - prices[i]);
        }
        return dp2[prices.size() - 1][0];
    }
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        int profitOne0 = 0, profitOne1 = -prices[0], profitTwo0 = 0, profitTwo1 = -prices[0];
        for (int i = 1; i < n; i++) {
        	// 更新用到上一行结果,故 按照 k 的反序更新 2->1
        	profitTwo0 = max(profitTwo0, profitTwo1 + prices[i]);
            profitTwo1 = max(profitTwo1, profitOne0 - prices[i]);
            profitOne0 = max(profitOne0, profitOne1 + prices[i]);
            profitOne1 = max(profitOne1, -prices[i]);
        }
        return profitTwo0;
    }
};

题解:
T[i][2][0] = max(T[i - 1][2][0], T[i - 1][2][1] + prices[i])
T[i][2][1] = max(T[i - 1][2][1], T[i - 1][1][0] - prices[i])
T[i][1][0] = max(T[i - 1][1][0], T[i - 1][1][1] + prices[i])
T[i][1][1] = max(T[i - 1][1][1], T[i - 1][0][0] - prices[i]) = max(T[i - 1][1][1], -prices[i])
注意到第 i 天的最大收益只和第 i - 1 天的最大收益相关,空间复杂度可以降到 O(1),但是要注意更新的先后顺序,优先更新会用到其他尚未更新的变量的变量。


22. 买卖股票的最佳时机 IV( LeetCode 188

难度: 困难
题目表述:
最多买卖k次
代码(C++):

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int n = prices.size();
        if (k >= n / 2) return maxProfit(prices);
        vector<vector<vector<int>>> dp(n, vector<vector<int>>(k + 1, vector<int>(2)));
        for (int j = 0; j < k + 1; j++) {
            dp[0][j][0] = 0;
            dp[0][j][1] = -prices[0];
        }
        for (int i = 1; i < n; i++) {
            for (int j = 1; j < k + 1; j++) {
                dp[i][j][0] = max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
                dp[i][j][1] = max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
            }
        }
        return dp[n - 1][k][0];
    }
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> dp(n, vector<int>(2));
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for (int i = 1; i < n; i++) {
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        }
        return dp[n - 1][0];
    }
};
// 空间复杂度降到 O(k)
class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int n = prices.size();
        if (k >= n / 2) return maxProfit(prices);
        vector<vector<int>> dp(k + 1, vector<int>(2));
        for (int j = 1; j < k + 1; j++) {
            dp[j][0] = 0;
            dp[j][1] = -prices[0];
        }
        for (int i = 1; i < n; i++) {
            for (int j = k; j > 0; j--) {
            	// 注意内层更新用到上一行结果,故反序遍历
                dp[j][0] = max(dp[j][0], dp[j][1] + prices[i]);
                dp[j][1] = max(dp[j][1], dp[j - 1][0] - prices[i]);
            }
        }
        return dp[k][0];
    }
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        int dp0 = 0, dp1 = -prices[0];
        for (int i = 1; i < n; i++) {
        	// 注意更新均要用到上一行,因此需要暂存更新后的结果,待全部变量完成更新后再更新进维护变量
            int newdp0 = max(dp0, dp1 + prices[i]);
            int newdp1 = max(dp1, dp0 - prices[i]);
            dp0 = newdp0;
            dp1 = newdp1;
        }
        return dp0;
    }
};

题解:
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k - 1][0] - prices[i])
本题是最通用的情况,如果 k 超过一个临界值,最大收益就不再取决于允许的最大交易次数,而是取决于股票价格数组的长度,因此可以进行优化。一个有收益的交易至少需要两天(在前一天买入,在后一天卖出,前提是买入价格低于卖出价格)。如果股票价格数组的长度为 n,则有收益的交易的数量最多为 n / 2(整数除法)。因此 k 的临界值是 n / 2。如果给定的 k 不小于临界值,即 k >= n / 2,则可以将 k 扩展为正无穷,此时问题等价于题 6 。


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

难度: 中等
题目表述:
买卖多次,卖出有一天冷冻期
代码(C++):

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> dp(n, vector<int>(2));
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for (int i = 1; i < n; i++) {
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]); 
            dp[i][1] = max(dp[i - 1][1], (i >= 2 ? dp[i - 2][0] : 0) - prices[i]); 
        }
        return dp[n - 1][0];
    }
    // 空间复杂度降到O(1)
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        int preProit0 = 0, profit0 = 0, profit1 = -prices[0];
        for (int i = 1; i < n; i++) {
            int newprofit0 = max(profit0, profit1 + prices[i]); 
            profit1 = max(profit1, preProit0 - prices[i]); 
            preProit0 = profit0;
            profit0 = newprofit0;
        }
        return profit0;
    }
};

题解:
k 为正无穷但有冷冻期,意味着在 i 天买入的时候应该使用 i - 2 天的卖出([0])收益
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 2][k][0] - prices[i])


24. 买卖股票的最佳时机含手续费( LeetCode 714

难度: 中等
题目表述:
买卖多次,每次有手续费
代码(C++):

class Solution {
public:
	// 买入时扣手续费
	int maxProfit(vector<int>& prices, int fee) {
        int n = prices.size();
        int dp0 = 0, dp1 = -prices[0] - fee;
        for (int i = 1; i < n; i++) {
            int tmp0 = max(dp0, dp1 + prices[i]);
            dp1 = max(dp1, dp0 - prices[i] - fee);
            dp0 = tmp0;
        }
        return dp0;
    }
    // 卖出时扣手续费
    int maxProfit(vector<int>& prices, int fee) {
        int n = prices.size();
        int dp0 = 0, dp1 = -prices[0];
        for (int i = 1; i < n; i++) {
            int tmp0 = max(dp0, dp1 + prices[i] - fee);
            int dp1 = max(dp1, dp0 - prices[i]);
            dp0 = tmp0;
        }
        return dp0;
    }
};

题解:
k 为正无穷但有手续费
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k][0] - prices[i] - fee)


25. 最长递增子序列( LeetCode 300

难度: 中等
题目表述:
索引不连续的递增
代码(C++):

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int ans = 1, n = nums.size();
        vector<int> dp(n, 1);
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) dp[i] = max(dp[i], dp[j] + 1);
            }
            ans = max(ans, dp[i]);
        }
        return ans;
    }
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size(), ans = 0, len = 1;
        vector<int> d(n + 1, 0);
        d[len] = nums[0];
        for (int i = 1; i < n; i++) {
            if (nums[i] > d[len]) {
                len++;
                d[len] = nums[i];
            } else {
                int l = 1, r = len;
                while (l < r) {
                    int mid = (l + r) / 2;
                    if (d[mid] >= nums[i]) {
                        r = mid;
                    } else {
                        l = mid + 1;
                    }
                }
                d[l] = nums[i];
            }
        }
        return len;
    }
};

题解: 以nums[i]结尾
dp[i]表示以nums[i]结尾的最长递增子序列的长度


26. 最长连续递增序列( LeetCode 674

难度: 简单
题目表述:
索引连续的递增
代码(C++):

class Solution {
public:
    int findLengthOfLCIS(vector<int>& nums) {
        vector<int> dp(nums.size(), 1);
        int ans = 1;
        for (int i = 1; i < nums.size(); i++) {
            if (nums[i] > nums[i - 1]) 
                dp[i] = dp[i - 1] + 1;
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};

题解: 以nums[i]结尾
dp[i]表示以nums[i]结尾的连续递增的子序列长度为dp[i]


27. 最长重复子数组( LeetCode 718

难度: 中等
题目表述:
索引连续的公共
代码(C++):

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size(), n = nums2.size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        int ans = 0;
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (nums1[i - 1] == nums2[j - 1])
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                ans = max(ans, dp[i][j]);
            }
        }
        return ans;
    }
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size(), n = nums2.size();
        vector<int> dp(n + 1, 0);
        int ans = 0;
        for (int i = 1; i <= m; i++) {
            for (int j = n; j >= 1; j--) {
                if (nums1[i - 1] == nums2[j - 1])
                    dp[j] = dp[j - 1] + 1;
                else // 注意这里不相等的时候要有赋0的操作
                    dp[j] = 0;
                ans = max(ans, dp[j]);
            }
        }
        return ans;
    }
};

题解: 以nums[i]结尾
dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组(连续子序列)长度为dp[i][j]。


28. 最长公共子序列( LeetCode1143

难度: 中等
题目表述:
索引不连续的公共
代码(C++):

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

题解: 以 i - 1结尾
dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列长度为dp[i][j]
无需知道前一个的公共的位置,只用根据当前的字符判重就可以推导当前dp状态,故不需要像题25一样通过遍历来找到前一个递增尾部数字,来和当前进行比较,推导出当前是否需要+1。


29. 不相交的线( LeetCode1035

难度: 中等
题目表述:
现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足满足:
nums1[i] == nums2[j],且绘制的直线不与任何其他连线(非水平线)相交。
索引不连续的公共
代码(C++):

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

题解: 以 i - 1结尾
本质是求最长公共子序列


30. 最大子数组和( LeetCode53

难度: 中等
题目表述:
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。
代码(C++):

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        vector<int> dp(nums.size(), 0);
        dp[0] = nums[0];
        int ans = dp[0];
        for (int i = 1; i < nums.size(); i++) {
            dp[i] = max(nums[i], dp[i - 1] + nums[i]);
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};

题解: 以nums[i]结尾
dp[i]:包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]。


31. 判断子序列( LeetCode392

难度: 简单
题目表述:
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
代码(C++):

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

题解: 以 i - 1结尾
dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]。
本题 如果删元素一定是字符串t,而 题28 最长公共子序列 是两个字符串都可以删元素。


32. 不同的子序列( LeetCode115

难度: 困难
题目表述:
给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数。
代码(C++):

class Solution {
public:
    int numDistinct(string s, string t) {
        int m = s.size(), n = t.size();
        vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1));
        for (int i = 0; i <= m ; i++) dp[i][0] = 1;
        for (int i = 1; i <= m ; i++) {
            for (int j = 1; j <= n; j++) {
                if (s[i - 1] == t[j - 1]) 
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];//s串用最后一位匹配 + 不用最后一位。
                else dp[i][j] = dp[i - 1][j];
            }
        }
        return dp[m][n];
    }
};

题解: 以 i - 1结尾
dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。


33. 两个字符串的删除操作( LeetCode583

难度: 中等
题目表述:
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
代码(C++):

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

题解: 以 i - 1结尾
dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。


34. 编辑距离( LeetCode 72

难度: 困难
题目表述:
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。你可以对一个单词进行如下三种操作:插入、删除、替换
代码(C++):

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

题解: 以 i - 1结尾
dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。


35. 回文子串( LeetCode647

难度: 中等
题目表述:
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
代码(C++):

class Solution {
public:
    int countSubstrings(string s) {
        int n = s.size();
        vector<vector<bool>> dp(n, vector<bool>(n, false));
        int cnt = 0;
        for (int i = n - 1; i >= 0; i--) {
            for (int j = i; j < n; j++) {
                if (s[i] == s[j]) {
                    if (j - i <= 1) {
                        dp[i][j] = true;
                        cnt++;
                    } else if (dp[i + 1][j - 1]) {
                        dp[i][j] = true;
                        cnt++;
                    }
                }
            }
        }
        return cnt;
    }
};

题解: 索引连续的回文
dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。


36. 最长回文子序列( LeetCode516

难度: 中等
题目表述:
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
代码(C++):

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n = s.size();
        vector<vector<int>> dp(s.size(), vector<int>(n, 0));
        for (int i = n - 1; i >= 0; i--) {
        	dp[i][i] = 1;
            for (int j = i + 1; j < n; j++) {
                if (s[i] == s[j]) 
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                else
                    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
            }
        }
        return dp[0][n - 1];
    }
};

题解: 索引不连续的回文
dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]


小结

解题步骤

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
    组合类问题仅仅是求个数的话,就可以用dp,递推公式类似:dp[j] += dp[j - nums[i]]
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

背包问题

动态规划 是解决「0−1 背包问题」和「完全背包问题」的标准做法。
0-1背包: 如果限定每件物品最多只能选取 1 次 (即0 或 1 次),则问题称为 0-1背包问题。
完全背包: 如果每件物品最多可以选取无限次,则问题称为 完全背包问题。
多重背包: 是0-1背包的一种,第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。在01背包里面在加一个for循环遍历一个每种物品的数量。

记第 i 件物品的重量 (weight) 为 wi ,价值 (value) 为 vi​。dp[i][j] 表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值。
「0-1背包状态转移方程」:dp[i][j] = max/min{ dp[i−1][j], dp[i−1][j−wi​ ]+vi } , 0 ≤ wi ≤ j
「完全背包状态转移方程」:dp[i][j] = max/min{ dp[i−1][j], dp[i][j−wi ]+vi }, 0 ≤ wi <=j
差别(以红色标注)在于第二项中的状态转移是来自上一行还是本行。

如果 i 的最优解只和 i - 1 的最优解相关,可去掉 [i] 来降低空间复杂度,使二维降为一维,但需要注意:「完全背包问题」内层循环正序,而「0-1 背包问题」中内层循环反序。因为正序决定了计算dp[j] 的 dp[j−wi ]来自本行,而反序决定dp[j−wi ]来自上一行。


1. 0-1背包

纯 0 - 1 背包 是求 给定背包容量 装满背包 的最大价值是多少。
416. 分割等和子集 是求 给定背包容量,能不能装满这个背包。
1049. 最后一块石头的重量 II 是求 给定背包容量,尽可能装,最多能装多少
494. 目标和 是求 给定背包容量,装满背包有多少种方法。
474. 一和零 是求 给定背包容量,装满背包最多有多少个物品。
在求装满背包有几种方案的时候,认清遍历顺序是非常关键的。内层反序!!!


2. 完全背包

内层正序!!!
此外,还有一个关键点在于for循环嵌套的顺序:
组合数(无序):外层遍历物品,内层遍历背包。
排列数(有序):外层遍历背包,内层遍历物品。
求组合数:518.零钱兑换II
求排列数:377. 组合总和 Ⅳ、70. 爬楼梯进阶版(完全背包)、139.单词拆分
求最小数(顺序无所谓):322. 零钱兑换、279.完全平方数


股票问题

持有股票的数量是隐藏的关键因素,该因素影响第 i 天可以进行的操作,进而影响最大收益。因此对 T[i][k] 的定义需要分成两项:

  • T[i][k][0] 表示在第 i 天结束时,最多进行 k 次交易且在进行操作后持有 0 份股票的情况下可以获得的最大收益;
  • T[i][k][1] 表示在第 i 天结束时,最多进行 k 次交易且在进行操作后持有 1 份股票的情况下可以获得的最大收益。

基准情况:
T[-1][k][0] = 0, T[-1][k][1] = -prices[0]
T[i][0][0] = 0, T[i][0][1] = -prices[0]
状态转移方程:
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k - 1][0] - prices[i])
注:第一天对应 i = 0;因为每次交易包含两次成对的操作,买入和卖出,只有买入操作会改变允许的最大交易次数。
最终答案是 T[n - 1][k][0],因为结束时持有 0 份股票的收益一定大于持有 1 份股票的收益。

同背包问题类似,如果 i 的最优解只和 i - 1 的最优解相关,可去掉 [i] 来降低空间复杂度,但需要注意:内层循环反序 / 优先更新会用到其他尚未更新的变量的变量。 因为计算新变量要用到的变量均是来自上一行。


子序列问题

1. 索引不连续的( 递增 / 公共 )子序列: 最长递增子序列、最长公共子序列、不相交的线
2. 索引连续的( 递增 / 公共 )子序列: 最长连续递增序列、最长重复子序列、最大子数组和
dp定义:数组以nums[i]结尾(含nums[i]),字符串以 i - 1 结尾(不一定含s[i - 1])
若是 索引连续 / 递增的前后依赖关系,则dp需要以nums[i]结尾

编辑距离: 判断子序列、不同的子序列、两个字符串的删除操作、编辑距离
字符串以 i - 1 结尾(不一定含s[i - 1])

回文: 从左下角开始遍历
1. 索引连续的回文:
回文子串:[i,j]闭区间是否是回文 bool
2. 索引不连续的回文:
最长回文子序列: [i,j]范围内最长回文长度


参考链接

玩转 LeetCode 高频 100 题
https://leetcode.cn/problems/coin-change/solution/by-flix-su7s/
https://leetcode.cn/circle/article/qiAgHn/
LeetCode 刷题攻略

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. Two Sum 2. Add Two Numbers 3. Longest Substring Without Repeating Characters 4. Median of Two Sorted Arrays 5. Longest Palindromic Substring 6. ZigZag Conversion 7. Reverse Integer 8. String to Integer (atoi) 9. Palindrome Number 10. Regular Expression Matching 11. Container With Most Water 12. Integer to Roman 13. Roman to Integer 14. Longest Common Prefix 15. 3Sum 16. 3Sum Closest 17. Letter Combinations of a Phone Number 18. 4Sum 19. Remove Nth Node From End of List 20. Valid Parentheses 21. Merge Two Sorted Lists 22. Generate Parentheses 23. Swap Nodes in Pairs 24. Reverse Nodes in k-Group 25. Remove Duplicates from Sorted Array 26. Remove Element 27. Implement strStr() 28. Divide Two Integers 29. Substring with Concatenation of All Words 30. Next Permutation 31. Longest Valid Parentheses 32. Search in Rotated Sorted Array 33. Search for a Range 34. Find First and Last Position of Element in Sorted Array 35. Valid Sudoku 36. Sudoku Solver 37. Count and Say 38. Combination Sum 39. Combination Sum II 40. First Missing Positive 41. Trapping Rain Water 42. Jump Game 43. Merge Intervals 44. Insert Interval 45. Unique Paths 46. Minimum Path Sum 47. Climbing Stairs 48. Permutations 49. Permutations II 50. Rotate Image 51. Group Anagrams 52. Pow(x, n) 53. Maximum Subarray 54. Spiral Matrix 55. Jump Game II 56. Merge k Sorted Lists 57. Insertion Sort List 58. Sort List 59. Largest Rectangle in Histogram 60. Valid Number 61. Word Search 62. Minimum Window Substring 63. Unique Binary Search Trees 64. Unique Binary Search Trees II 65. Interleaving String 66. Maximum Product Subarray 67. Binary Tree Inorder Traversal 68. Binary Tree Preorder Traversal 69. Binary Tree Postorder Traversal 70. Flatten Binary Tree to Linked List 71. Construct Binary Tree from Preorder and Inorder Traversal 72. Construct Binary Tree from Inorder and Postorder Traversal 73. Binary Tree Level Order Traversal 74. Binary Tree Zigzag Level Order Traversal 75. Convert Sorted Array to Binary Search Tree 76. Convert Sorted List to Binary Search Tree 77. Recover Binary Search Tree 78. Sum Root to Leaf Numbers 79. Path Sum 80. Path Sum II 81. Binary Tree Maximum Path Sum 82. Populating Next Right Pointers in Each Node 83. Populating Next Right Pointers in Each Node II 84. Reverse Linked List 85. Reverse Linked List II 86. Partition List 87. Rotate List 88. Remove Duplicates from Sorted List 89. Remove Duplicates from Sorted List II 90. Intersection of Two Linked Lists 91. Linked List Cycle 92. Linked List Cycle II 93. Reorder List 94. Binary Tree Upside Down 95. Binary Tree Right Side View 96. Palindrome Linked List 97. Convert Binary Search Tree to Sorted Doubly Linked List 98. Lowest Common Ancestor of a Binary Tree 99. Lowest Common Ancestor of a Binary Search Tree 100. Binary Tree Level Order Traversal II

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值