Leetcode算法类型训练

算法训练

基础篇(30 天)

基础永远是最重要的,先把最最基础的这些搞熟,磨刀不误砍柴工。

  • 数组,队列,栈
  • 链表
  • 树与递归
  • 哈希表
  • 双指针

思想篇(30 天)

这些思想是投资回报率极高的,强烈推荐每一个小的专题花一定的时间掌握。

  • 二分
  • 滑动窗口
  • 搜索(BFS,DFS,回溯)
  • 动态规划

提高篇(31 天)

这部分收益没那么明显,并且往往需要一定的技术积累。出现的频率相对而言比较低。但是有的题目需要你使用这些技巧。又或者可以使用这些技巧可以实现**「降维打击」**。

  • 贪心
  • 分治
  • 位运算
  • KMP & RK https://www.zhihu.com/question/21923021
  • 并查集
  • 前缀树
  • 线段树

作者:lucifer
链接:https://www.zhihu.com/question/321738058/answer/1279464192
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

双指针

1. 有序数组的两数和 序号

力扣167

题目: 给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。

输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。

思路:双指针 或 二分法

双指针:首末指针,如果和大于target,尾指针左移,如果小于,首指针右移。

二分:固定一个元素,二分查找另外一个元素。

class Solution {
public:
    vector<int> twoSum2(vector<int>& numbers, int target) {
        if(numbers.size()<=1) return vector<int>{};

        int left = 0, right = numbers.size()-1;

        while(left<right){
            int tmp = numbers[left]+numbers[right];
            if(tmp<target){
                ++left;
            }else if(tmp>target){
                --right;
            }else if (tmp==target){
                return vector<int>{left+1, right+1};
            }
        }
        return vector<int>{};
    }
};

2. 两数平方和

力扣633

题目: 给定一个非负整数 c ,你要判断是否存在两个整数 ab,使得 a2 + b2 = c。

示例1:

输入: 5
输出: True
解释: 1 * 1 + 2 * 2 = 5

思路:和力扣167类似,双指针,不过右指针初始化为 sqrt©,提前剪枝.

class Solution {
public:
    bool judgeSquareSum(int c) {
        long right = int(sqrt(c*1.0));
        long left = 0;
        while(left<=right){
            long tmp = left*left+right*right;
            if(tmp<c){
                left++;
            }else if(tmp>c){
                right--;
            }else if(tmp==c){
                return true;
            }
        }
        return false;
    }
};

3. 反转字符串中的元音字符

力扣345

编写一个函数,以字符串作为输入,反转该字符串中的元音字母。

输入: “leetcode”
输出: “leotcede”

初始化一个包含所有元音的set,使用前后双指针定位元音,然后交换。

class Solution {
public:
    string reverseVowels(string s) {
        if(s.empty()) return s;
        int left = 0, right = s.size()-1;
        set<char> vowels{'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'};
        while(left<right){
            while(left<right && vowels.find(s[left])==vowels.end()) ++left;
            while(left<right && vowels.find(s[right])==vowels.end()) --right;
            if(left>=right) break;
            char tmp = s[left];
            s[left++] = s[right];
            s[right--] = tmp;
        }
        return s;
    }
};

4. 验证回文字符串,可删除一个字符

力扣680

题目:给定一个非空字符串 s最多删除一个字符。判断是否能成为回文字符串。

输入: “abca”
输出: True
解释: 你可以删除c字符。

思路:双指针判断前后字符串是否相等。

针对删除一个字符,可通过递归进行判断,是删除左边的一个,还是右边的一个。

return validPalindromeCore(s, left+1, right) || validPalindromeCore(s, left, right-1);

class Solution {
public:
    bool validPalindrome(string s) {
        if(s.empty()) return true;
        int left = 0, right = s.size()-1;
        while(left<=right){
            if(s[left]!=s[right]){
                return validPalindromeCore(s, left+1, right) || validPalindromeCore(s, left, right-1);
            }
            ++left; --right;
        }
        return true;
    }

    bool validPalindromeCore(string s, int left, int right){
        while(left<=right){
            if(s[left++]!=s[right--]){
                return false;
            }
        }
        return true;
    }
};

5. 归并两个有序数组

力扣88

题目:给你两个有序整数数组 nums1nums2,请你将 nums2 合并到 nums1 中*,*使 nums1 成为一个有序数组。

说明:

  • 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。
  • 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。

示例:

输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3

输出: [1,2,2,3,5,6]

思路:倒序合并。初始化指针为nums1 和 nums2 包含数据部分的尾部,放置指针为 所需空间的尾部。 倒序插入。

class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        int p1 = m-1;
        int p2 = n-1;
        int p = m+n-1;
        while(p1>=0 && p2>=0){
            if(nums1[p1]>nums2[p2]){
                nums1[p--] = nums1[p1--];
            }else{
                nums1[p--] = nums2[p2--];
            }
        }
        while(p1>=0){
            nums1[p--] = nums1[p1--];
        }
        while(p2>=0){
            nums1[p--] = nums2[p2--];
        }
        return;
    }
};

6. 判断链表是否存在环

力扣141

题目:给定一个链表,判断链表中是否有环。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

示例 1:

输入:head = [3,2,0,-4], pos = 1 (-4->2)
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

思路:快慢两个指针,一个指针一次走一格,一个一次走两格。当两者 相遇时,就说明存在环。当快指针走到空时,说明不存在环。

环是否存在:快慢指针,一个走1,一个走2,两者相遇,存在环。

环的大小:相遇点继续往下走,记录长度,再次相遇时走过的长度就是环的大小。

环的入口点:假设环之前的路径为m, 环大小为 c , 相遇点距入口点距离为k。

则: 慢指针距离: ls = m+K

​ 快指针距离:fs =2*ls = ls + nc

因此: ls = nc m+k=nc m = nc-k = (n-1)c + c-k

c-k就是从相遇点继续走到入口点的距离。

因此,安排两个指针,一个从相遇点,一个从链表起始点,都开始走,每次走1,相遇点就是环的入口点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    bool hasCycle(ListNode *head) {
        if(head==NULL) return false;
        ListNode* pFast =  head->next;
        ListNode* pSlow = head;
        while(pFast!=NULL){
            if(pSlow==pFast) return true;
            pSlow = pSlow->next;
            if(pFast->next==NULL) return false;
            pFast = pFast->next->next;
        }
        return false;
    }
};

7. 通过删除字母匹配字典里的最长单词

力扣524

题目:给定一个字符串和一个字符串字典,找到字典里面最长的字符串,该字符串可以通过删除给定字符串的某些字符来得到。如果答案不止一个,返回长度最长且字典顺序最小的字符串。如果答案不存在,则返回空字符串。

输入:
s = “abpcplea”, d = [“ale”,“apple”,“monkey”,“plea”]

输出:
“apple”

思路:很简单的题目,不要想得太复杂。

判断函数:判断一个子串是否可以通过删除给定字符串的某些字符来得到。字符串和子串各一个指针,字符相同,都加1,不相同,字符串加1,子串不变,最后看子串能够都走完。

主函数:根据当前匹配到的子串,剔除一些不需要检查的。

class Solution {
public:
    string findLongestWord(string s, vector<string>& d) {
        string ans = "";
        for(auto seg : d){
            if(seg.size()<ans.size() || (seg.size()==ans.size() && seg>ans)) continue;
            if(isContain(s, seg)){
                    ans = seg;
            }
        }
        return ans;
    }

    bool isContain(string s, string segment){
        if(s.empty()) return false;
        if(segment.empty()) return true;
        int p=0;
        for(int i=0; i<s.size(); ++i){
            if(s[i]==segment[p]){
                ++p;
            }
            if(p>=segment.size()) return true;
        }
        return false;
    }
};

二分查找

//normal
int find(vector<int> nums, int target){
    if(nums.empty()) return -1;
    int left = 0;
    int right = nums.size()-1;
    while(left<=right){
        int mid = left+(right-left)/2;
        if(nums[mid]==target){
            return mid;
        }else if(nums[mid]<target){
            left = mid+1;
        }else if(nums[mid]>target){
            right = mid-1;
        }
    }
    return -1;
}

int findLeftBound(vector<int> nums, int target){
    if(nums.empty()) return -1;
    int left = 0;
    int right = nums.size()-1;
    while(left<=right){
        int mid = left+(right-left)/2;
        if(nums[mid]==target){
             right = mid-1; //固定左边界,右边界左移1
        }else if(nums[mid]<target){
            left = mid+1;
        }else if(nums[mid]>target){
            right = mid-1;
        }
    }
    return (left>=nums.size() || nums[left]!=target) ? -1 : left;    
}

int findRightBound(vector<int> nums, int target){
    if(nums.empty()) return -1;
    int left = 0;
    int right = nums.size()-1;
    while(left<=right){
        int mid = left+(right-left)/2;
        if(nums[mid]==target){
             left = mid+1; //固定右边界,左边界左移1
        }else if(nums[mid]<target){
            left = mid+1;
        }else if(nums[mid]>target){
            right = mid-1;
        }
    }
    return (right<0 || nums[right]!=target) ? -1 : right;
}

动态规划

常见类型

  • 坐标型动态规划, 20%,重点
  • 序列性动态规划,20%,重点
  • 划分型动态规划,20%,重点
  • 区间型动态规划,15%,重点
  • 背包型动态规划,10%
  • 最长序列性动态规划,5%
  • 博弈性动态规划,5%
  • 综合型动态规划,5%

动态规划时间空间优化

动态规划打印路径

求最值型动态规划总结

四个组成部分:

  1. 确定状态
    • 研究最优策略的最后一步,(硬币问题中,最优策略中使用的最后一枚硬币)
    • 化成子问题,(最少的硬币拼出更少的面值 27-ak)
  2. 转移方程
    • f[x] = min(f[x-2]+1, f[x-5]+1, f[x-7]+1)
    • 根据子问题定义直接得到
  3. 初始条件和边界情况
    • f[0] = 0
    • 如果不满足条件,拼不出,则f[Y] = INT_MAX
  4. 计算顺序
    • 0 , 1, 2 ,……
    • 消除冗余,加速计算

斐波那契数列

1. 打家劫舍

力扣198

题目:你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

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

思路:dp[i]代表偷前 i 间屋子获得的收益。当前屋子可以偷、可以不偷。故:dp 方程 dp[i] = max(dp[i-2]+nums[i], dp[i-1])

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

2. 打家劫舍 环形

力扣213

与上一题的区别是:地方所有的房屋都**围成一圈,**这意味着第一个房屋和最后一个房屋是紧挨着的。

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

思路:把首末相邻的房屋拆开,复用上一题的函数,变成两种上一题的情况,运算两次求最值,max(robRange(nums, 0, nums.size()-2), robRange(nums, 1, nums.size()-1));

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

    int robRange(vector<int>& nums, int left, int right) {
        if(nums.empty() || left>right) return 0;
        vector<int> dptable(right-left+2, 0);
        dptable[0] = 0;
        dptable[1] = nums[left];
        for(int i=left+1; i<=right; ++i){
            dptable[i+1-left] = max(nums[i]+dptable[i-1-left], dptable[i-left]); 
        }
        return dptable[right-left+1];
    }
};

3. 打家劫舍 二叉树

力扣337

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。计算在不触动警报的情况下,小偷一晚能够盗取的最高金额

输入: [3,2,3,null,3,null,1]
3
/
2 3
\ \
3 1
输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.

思路1:递归:当前节点最高 = max(本节点+四个孙子节点, 两个儿子节点)

思路2:上述的递归中,子问题太多,需要记录,使用hash备忘录记录每个节点的最大值。

思路3:改变问题定义,每次返回当前节点偷和不偷的最大值。

当前节点偷 = 当前节点价值+孩子节点不偷的价值

当前节点不偷 = max(左儿子偷,左儿子不偷) + max(右儿子偷,有儿子不偷)

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:

    //方法3:方法1中,定义地问题需要递归调用6次(四个孙子节点+2个儿子节点)
    //为了减少调用,现在重新定义问题
    //每次每个节点返回两个值,一个是当前节点偷时的最大收益,一个是当前节点不偷的最大收益
    //1. 当前节点不偷的最大收益 = max(左儿子偷,左儿子不偷) + max(右儿子偷,右儿子不偷)
    //2. 当前节点偷的最大收益 = 当前节点 + 两个儿子都不偷的最大收益
    int rob(TreeNode* root){
        pair<int, int> earn = robNode(root);
        return max(earn.first, earn.second);
    }
    pair<int, int> robNode(TreeNode* root){
        pair<int, int> earn = {0,0};
        if(root==nullptr) return earn;
        int robCurEarn = 0;
        int robChildrenEarn = 0;
        pair<int, int> leftEarn = robNode(root->left);
        pair<int, int> rightEarn = robNode(root->right);

        robCurEarn = root->val + leftEarn.second + rightEarn.second;
        robChildrenEarn = max(leftEarn.first, leftEarn.second) + max(rightEarn.first, rightEarn.second);

        earn.first = robCurEarn;
        earn.second = robChildrenEarn;
        return earn;
    }

    //方法2:用hash来记录节点的 max money
    int rob2(TreeNode* root) {
        map<TreeNode*, int> hash;
        return robCore(root, hash);
    }

    int robCore(TreeNode* root, map<TreeNode*, int>& hash){
        if(root==nullptr) return 0;
        if(hash.find(root)!=hash.end())
            return hash[root];
        int robCurEarn = 0;
        int robChildrenEarn = 0;
        robCurEarn += root->val;
        if(root->left!=nullptr){
            robCurEarn += (robCore(root->left->left, hash) + robCore(root->left->right, hash));
            robChildrenEarn += robCore(root->left, hash);
        }
        if(root->right!=nullptr){
            robCurEarn += (robCore(root->right->left, hash) + robCore(root->right->right, hash));
            robChildrenEarn += robCore(root->right, hash);
        }
        hash[root] = max(robCurEarn, robChildrenEarn);
        return max(robCurEarn, robChildrenEarn);
    }


    //方法1:动态规划,超时
    //爷爷节点偷的话,儿子就不能偷,只能偷孙子
    //故 max((本节点+四个孙子节点), 两个儿子节点),超时.....
    int rob1(TreeNode* root) {
        if(root==nullptr) return 0;
        int robCurEarn = 0;
        int robChildrenEarn = 0;
        robCurEarn += root->val;
        if(root->left!=nullptr){
            robCurEarn += (rob(root->left->left) + rob(root->left->right));
            robChildrenEarn += rob(root->left);
        }
        if(root->right!=nullptr){
            robCurEarn += (rob(root->right->left) + rob(root->right->right));
            robChildrenEarn += rob(root->right);
        }
        return max(robCurEarn, robChildrenEarn);
    }
};

坐标型动态规划

1. 矩阵最小路径和

力扣64

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

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

思路:

dp[i][j] = min(dp[i-1][j]+grid[i][j], dp[i][j-1]+grid[i][j])

dptable可以进一步简化,只用一行 dp[column]来存储

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

2. 矩阵不同路径

力扣62

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

思路:

dp[i][j] = dp[i-1][j]+dp[i][j-1]

下面的代码中,将二维的dp table 优化为了一维

    int uniquePaths(int m, int n) {
        if(m<=0 || n<=0) return 0;
        vector<int>dp(n);
        for(int i=0; i<m; ++i){
            for(int j=0; j<n; ++j){
                if(i==0) dp[j]=1;
                else if(j==0) dp[j]=dp[j];
                else dp[j] = dp[j-1]+dp[j]; 
            }
        }  
        return dp[n-1];
    }

3. 考虑障碍时的矩阵不同路径

力扣63

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

与上道题相比,增加了网格中的障碍。

增加下面的代码:如果有障碍,清0。if(obstacleGrid[i][j]==1) dp[j]=0;

其他不变化。

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

数组区间

1. 数组区间和

力扣303

给定一个整数数组 nums,求出数组从索引 ij (ij) 范围内元素的总和,包含 i, j 两点。

sumrange(i, j) = sum(0, j)-sum(0,i-1)

使用数组预存 前缀和

class NumArray {
public:
    vector<int> dp;

    NumArray(vector<int>& nums) {
        dp.push_back(0);
        for(int i=0; i<nums.size(); ++i){
            dp.push_back(nums[i] + dp.back());
        }
    }
    
    int sumRange(int i, int j) {
        return dp[j+1]-dp[i];
    }
};

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray* obj = new NumArray(nums);
 * int param_1 = obj->sumRange(i,j);
 */

2. 数组中等差递增子区间的个数

力扣413

数组 A 包含 N 个数,且索引从0开始。数组 A 的一个子数组划分为数组 (P, Q),P 与 Q 是整数且满足 0<=P

A = [1, 2, 3, 4]

返回: 3, A 中有三个子等差数组: [1, 2, 3], [2, 3, 4] 以及自身 [1, 2, 3, 4]

思路1:循环遍历找出所有的等差区间的范围,对每个范围采用公式计算等差数列个数: n个元素的等差数列,其等差子数列的个数为 (1+n-2)*(n-2)/2

思路2:动态规划,dp[i]记录以当前为结尾的等差数组的长度(元素个数-2)。另外用一个 ans 累加所有的 dp[i]。

class Solution {
public:
    //思路2
    int numberOfArithmeticSlices(vector<int>& A) {
        if(A.size()<3) return 0;
        int n = A.size();
        int dpi = 0; //dptable 存储以i结尾的 等差数列长度
        int ans = 0; //记录等差数列总个数
        for(int i=2; i<n; ++i){
            if(A[i]-A[i-1] == A[i-1]-A[i-2]){
                dpi = dpi+1;
            }else{
                dpi = 0;
            }
            ans += dpi;
        }
        return ans;
    }
    
    //思路1
        int numberOfArithmeticSlices(vector<int>& A) {
        if(A.size()<3) return 0;
        int p1 = 0, p2 = 1;
        vector<pair<int, int>> ranges;
        while(p2<A.size()){
            int gap = A[p2]-A[p1];
            while(p2<A.size() && (A[p2]-A[p2-1]==gap)) ++p2;
            if(p2-p1>=3){
                ranges.push_back(make_pair(p1,p2-1));
            }
            p1 = p2-1;
        }

        int ans = 0;
        for(int i=0; i<ranges.size(); ++i){
            pair<int,int> range = ranges[i];
            int t = range.second-range.first+1;
            ans += (1+t-2)*(t-2)/2;
        }
        return ans;
    }
};

分割整数

1. 分割整数的最大乘积

力扣343

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

和剑指offer割绳子是一样的。

动态规划或贪心(尽量剪3)

输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
class Solution {
public:
    int integerBreak(int n) {
        vector<int> dp(n+1, 1);
        for(int i=2; i<=n; ++i){
            for(int j=1; j<=i/2; ++j){
                dp[i] = max(dp[i], max(j, dp[j])*max(i-j, dp[i-j]));
            }
        }
        return dp[n];
    }
};

2. 按平方数分割整数

类似 硬币组合金额

力扣279

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

示例:

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

思路:

  1. 动态规划,和硬币组合类似
  2. BFS,树状结构,分析剩余的数是不是完全平方数,返回树的深度
class Solution {
public:

//BFS方法,看剩余的数里是不是 完全平方数
    int numSquares(int n){
        int m = sqrt(n);
        queue<int> mQueue;
        mQueue.push(n);
        int level = 1;
        while(!mQueue.empty()){
            int size = mQueue.size();
            set<int> numbers;
            for(int i=0; i<size; ++i){
                int cur = mQueue.front();
                mQueue.pop();
                for(int j=1; j<=m; ++j){
                    if(cur<j*j) break;
                    if(cur==j*j) return level;
                    numbers.insert(cur-j*j);
                    //mQueue.push(cur-j*j);
                }
            }
            for(auto num : numbers){
                mQueue.push(num);
            }

            ++level;
        }
        return level;
    }

// 动态规划,类似硬币问题
    int numSquares2(int n) {
        int m = sqrt(n);
        vector<int> dp(n+1, INT_MAX);
        dp[0] = 0;
        dp[1] = 1;
        for(int i=2; i<=n; ++i){
            for(int j=1; j<=m; ++j){
                int remain = i-j*j;
                if(remain<0) continue;
                dp[i] = min(dp[i], 1+dp[remain]);
            }
        }
        return dp[n];
    }
};

3. 分割整数解码为字母字符串

力扣91

类似 剑指offer46 把数字翻译成字符串

一条包含字母 A-Z 的消息通过以下方式进行了编码:

‘A’ -> 1
‘B’ -> 2

‘Z’ -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。

输入: "226"
输出: 3
解释: 它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6)

思路:根据当前字母和前一字母能否组成一个字母,实现转移方程 dp[i] = (num%10>0 ? dp[i-1] : 0) + ((num<=26 && num>=10)? dp[i-2] : 0);

class Solution {
public:
        int numDecodings(string s) {
        if(s.empty()) return 0;
        vector<int> dp(s.size()+1, 0);
        dp[0] = 1;
        dp[1] = (s[0]-'0')>0 ? 1:0;
        for(int i=2; i<=s.size(); ++i){
            int num = (s[i-2]-'0')*10+(s[i-1]-'0');
            dp[i] = (num%10>0 ? dp[i-1] : 0) + ((num<=26 && num>=10)? dp[i-2] : 0);
        }
        return dp[s.size()];
    }
};

最长递增/上升子序列

https://lucifer.ren/blog/2020/06/20/LIS/

1. 最长上升子序列

力扣300

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

示例:

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

思路:

  • dp[i] 以i为结尾的(一定包含nums[i])的子序列
  • dp[i] = max(dp[k1], dp[k2] …) 其中nums[k]
  • 最长子序列并不出现在 dp[-1],而是 max(dp[i]) !!

思路2:

维护一个上升数组,每次采用二分查找法更新里面的值

如例子中,不同时刻数组的值为 :

  • 10
  • 9
  • 2
  • 2 5
  • 2 3
  • 2 3 7
  • 2 3 7 101
  • 2 3 7 18,二分查找比18小的,然后将其后面的替换
class Solution {
public:
    //dp[i] 以i为结尾的(一定包含nums[i])的子序列
    //dp[i] = max(dp[k1], dp[k2] ....) 其中nums[k]
    //最长子序列并不出现在 dp[-1],而是 max(dp[i])!!
    int lengthOfLIS(vector<int>& nums) {
        if(nums.empty()) return 0;
        int n = nums.size();
        vector<int> dp(n, 1);
        int ans = 1;
        for(int i=1; i<n; ++i){
            for(int j=i-1; j>=0; --j){
                if(nums[j]>=nums[i]) continue;
                dp[i] = max(dp[i], dp[j]+1);
            }
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};

2. 无重叠区间

力扣435

给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

注意:

可以认为区间的终点总是大于它的起点。
区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。

输入: [ [1,2], [2,3], [3,4], [1,3] ]

输出: 1

解释: 移除 [1,3] 后,剩下的区间没有重叠。

思路:

  • 需要移除的最小区间的数量 = 最大上升子区间
  • 先对区间进行排序,根据first 排序
  • 类似上一题
class Solution {
public:
    //需要先对 intervals 进行排序
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if(intervals.empty()) return 0;
        //最长上升区间数量, n-该数量就是需要移除的区间
        sort(intervals.begin(), intervals.end());
        
        int n = intervals.size();
        vector<int> dp(n, 1);
        dp[0] = 1;
        int longestRiseLength = 1;
        for(int i=1; i<n; ++i){
            for(int j=i-1; j>=0; --j){
                if(intervals[i][0]>=intervals[j][1]){
                    dp[i] = max(dp[i], dp[j]+1);
                }
            }
            longestRiseLength = max(longestRiseLength, dp[i]);
        }
        return n-longestRiseLength;
    }
};

3. 最长数对链

力扣646

给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。

现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。

给定一个对数集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。

示例 :

输入: [[1,2], [2,3], [3,4]]
输出: 2
解释: 最长的数对链是 [1,2] -> [3,4]

思路:和上题目一样。判断条件稍微有些不同

class Solution {
public:
    //sort 后 找最长上升子序列
    int findLongestChain(vector<vector<int>>& pairs) {
        if(pairs.empty()) return 0;
        sort(pairs.begin(), pairs.end());
        int n = pairs.size();
        vector<int> dp(n, 1);
        int ans = 1;
        for(int i=1; i<n; ++i){
            for(int j=i-1; j>=0; --j){
                if(pairs[i][0]>pairs[j][1]){
                    dp[i] = max(dp[i], dp[j]+1);
                }
            }
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};

4. 最少数量的箭引爆气球

力扣452

在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以y坐标并不重要,因此只要知道开始和结束的x坐标就足够了。开始坐标总是小于结束坐标。平面内最多存在104个气球。

一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。

思路:和最长数对链一样,动态规划,但是对于该题会超时。

思路2:贪心解法:根据区间是否变化,xStart是否超出preEnd来 判断是否需要用xEnd来更新preEnd,并对箭的数量+1。

class Solution {
public:
    static bool compare(vector<int>& lhs, vector<int>& rhs){
        return lhs[1]<rhs[1];
    }
//动态规划
    int findMinArrowShots2(vector<vector<int>>& points) {
        if(points.empty()) return 0;
        sort(points.begin(), points.end(), compare);
        int n = points.size();
        vector<int> dp(n, 1);
        for(int i=1; i<n; ++i){
            for(int j=i-1; j>=0; --j){
                if(points[i][0]>points[j][1]){
                    dp[i] = max(dp[i], dp[j]+1);
                }
            }
        }
        return dp[n-1];
    }

//贪心
    int findMinArrowShots(vector<vector<int>>& points){
        if(points.empty()) return 0;
        sort(points.begin(), points.end(), compare);
        int preEnd = points.front()[1];
        int n = points.size();
        int ans = 1;
        for(int i=1; i<n; ++i){
            int xstart = points[i][0];
            int xend = points[i][1];
            if(xstart>preEnd){
                preEnd = xend;
                ++ans;
            }
        }
        return ans;
    }

};

最长公共子序列

https://lucifer.ren/blog/2020/07/01/LCS/

1. 最长公共子序列

力扣1143

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace",它的长度为 3。

思路:

dp[i][j]表示以 字符串1的i 和 字符串2的 j 结尾的最长公共子序列
dp[i][j] = s[i]==s[j] ? dp[i-1][j-1]+1 : max(dp[i-1][j], dp[i][j-1])
class Solution {
public:

    //dp[i][j]是以 text[i] text[j]为结尾的字符串的最大公共子序列
    // s[i]==s[j] then dp[i][j] = dp[i-1][j-1]+1;
    //s[i]!=s[j] then dp[i][j] = max(dp[i-1][j], dp[i][j-1])
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size();
        int n = text2.size();
        if(m==0 || n==0) return 0;
        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-1][j], dp[i][j-1]);
                }
            }
        }
        return dp[m][n];
    }
};

2. 最长公共子串

力扣718

给两个整数数组 AB ,返回两个数组中公共的、长度最长的子数组的长度。

输入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出:3
解释:
长度最长的公共子数组是 [3, 2, 1]

动态规划解法: //子数组是连续的!! 因此只需要在 A[i]==A[j] 时, dp[i][j] = dp[i-1][j-1]+1

滑动窗口解法:https://leetcode-cn.com/problems/maximum-length-of-repeated-subarray/solution/javadong-tai-gui-hua-jie-jue-qi-shi-jiu-shi-zui-ch/

class Solution {
public:

    //子数组是连续的!! 因此只需要在  A[i]==A[j] 时, dp[i][j] = dp[i-1][j-1]+1
    int findLength(vector<int>& A, vector<int>& B) {
        int m = A.size();
        int n = B.size();
        if(m==0  || n==0) return 0;
        int ans = 0;
        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(A[i-1]==B[j-1]){
                    dp[i][j] = dp[i-1][j-1]+1;
                    ans = max(ans, dp[i][j]);
                }
            }
        }
        return ans;
    }
};

3. 不相交的线

力扣1035

题目:我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。

现在,我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。

以这种方法绘制线条,并返回我们可以绘制的最大连线数。

输入:A = [1,4,2], B = [1,2,4]
输出:2
解释:
我们可以画出两条不交叉的线,如上图所示。
我们无法画出第三条不相交的直线,因为从 A[1]=4 到 B[2]=4 的直线将与从 A[2]=2 到 B[1]=2 的直线相交。

最长公共子序列

class Solution {
public:
    int maxUncrossedLines(vector<int>& A, vector<int>& B) {
        //最长公共子序列,不要求连续
        // A[i]==B[j]时, dp[i][j] = dp[i-1][j-1]+1
        //     !=         dp[i][j] = max(dp[i-1][j], dp[i][j-1])
        int m = A.size(); 
        int n = B.size();
        if(m==0 || n==0) return 0;
        vector<int> dpPre(n+1, 0); //用两行代替 矩阵
        vector<int> dp(n+1, 0);
        for(int i=1; i<=m; ++i){
            for(int j=1; j<=n; ++j){
                if(A[i-1]==B[j-1]){
                    dp[j] = dpPre[j-1] + 1;
                }else{
                    dp[j] = max(dp[j-1], dpPre[j]);
                }
            }
            dpPre = dp;
        }
        return dp[n];
    }
};

0-1背包

背包问题分析求解

有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。

定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论:

  • 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]
  • 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v

第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为:

dp[i][j] = max(dp[i-1][j], dp[i-1][j-w]+v)

完全背包由于可以使用多次物品。

所以状态转移方程为:

dp[i][j] = max(dp[i-1][j], dp[i][j-w]+v)

注意max()中第二项 添加该物品到背包时 的区别,01背包是dp[i-1] ,因为只能用一次,完全背包时dp[i],物品可以用多次!!

空间优化:

可用一维数组代替二维,因为 j 依赖于 j-w。 故 使用一维数组时,对于 j 倒序计算。

1. 分割等和子集

力扣416

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

注意:

每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:

输入: [1, 5, 11, 5]

输出: true

解释: 数组可以分割成 [1, 5, 5] 和 [11].

**思路:**转换为 sum/2 的0-1背包问题

bool类型 dp 数组 可进一步优化,压缩为一维, 从后向前填写

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        if(nums.empty()) return false;
        //容量为 sum(nums)/2 的背包问题
        int sum = 0;
        for(auto n : nums) sum+=n;
        if(sum%2!=0) return false;
        int capacity = sum/2;
        int n = nums.size();
        //bool dp[i][j] 前 i个number,总体积为j 能否正好装满
        vector<vector<bool>> dp(n, vector<bool>(capacity+1, false));
        for(int i=0; i<n; ++i){
            dp[i][0] = true;
            for(int j=capacity; j>=nums[i]; --j){
                if(i==0) dp[i][j] = dp[i][j-nums[i]];
                else if(i>0) dp[i][j] = (dp[i-1][j] || dp[i-1][j-nums[i]]);
            }
        }
        return dp[n-1][capacity];
    }
};

2. 目标和,改变一组数的正负号使其和为S

力扣494

题解:转换成0-1背包

还有dfs解法,二叉树路径和,研究一下

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        //dp[i][j]  前i 个数 目标为 j时 的方法数
        // 当前 k=num[i] 选 +或-,  dp[i][j] =  dp[i-1][j-k] + dp[i-1][j+k] ,方法数之和
        //边界情况 
        if(nums.empty()) return 0;
        int n = nums.size();
        int sum = 0;
        for(auto ni:nums) sum += ni;
        if(S>sum || S<(-1*sum)) return 0;
        vector<vector<int>> dp(n, vector<int>(2*sum+1, 0));
        dp[0][nums[0]+sum] += 1;
        dp[0][sum-nums[0]] += 1;
        for(int i=1; i<n; ++i){
            for(int j=0; j<=2*sum; ++j){
                //int realj = j-sum;
                int k = nums[i];
                if(j-k>=0) dp[i][j] += dp[i-1][j-k];
                if(j+k<=2*sum) dp[i][j]+= dp[i-1][j+k];
            }
        }
        return dp[n-1][S+sum];
    }
};

3. 字符0和1构成最多字符串的个数 二维背包

力扣474

在计算机界中,我们总是追求用有限的资源获取最大的收益。

现在,假设你分别支配着 m 个 0 和 n 个 1。另外,还有一个仅包含 0 和 1 字符串的数组。

你的任务是使用给定的 m 个 0 和 n 个 1 ,找到能拼出存在于数组中的字符串的最大数量。每个 0 和 1 至多被使用一次。

输入: Array = {“10”, “0001”, “111001”, “1”, “0”}, m = 5, n = 3
输出: 4

解释: 总共 4 个字符串可以通过 5 个 0 和 3 个 1 拼出,即 “10”,“0001”,“1”,“0” 。

思路:二维背包问题

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        //背包问题, 背包容量有两个,1和0的个数,求最多装几个string
        //dp[k][i][j]  当前k个string下,容量满足i, j 时 最多的 str
        //k放入与否  dp[k][i][j] = max(dp[k-1][i-ki][j-kj]+1, dp[k-1][i][j])
        
        //需要用优化后的 01背包解法

        if(strs.empty() || (m<=0 && n<=0)) return 0;
        int k = strs.size();
        vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
        for(auto str : strs){
            int km = 0, kn = 0;
            for(auto c : str){
                if(c=='0') ++km; //the size of 0
                else ++kn;
            }
            for(int i=m; i>=km; --i){
                for(int j=n; j>=kn; --j){
                    dp[i][j] = max(dp[i-km][j-kn]+1, dp[i][j]);
                }
            }
        }
        return dp[m][n];
    }
};

完全背包

4. 无限个数硬币兑换

力扣322、

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

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

因为硬币可以重复使用,因此这是一个完全背包问题。完全背包只需要将 0-1 背包的逆序遍历 dp 数组改为正序遍历即可??。

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        //硬币数量无限,所以不是01背包
        //dp[i] 是 面额为i时,最小硬币组合数
        // dp[i] = min(dp[i-c1], dp[i-c2], ... )+1;
        if(coins.empty() || amount<0) return -1;
        vector<int> dp(amount+1, INT_MAX);
        dp[0] = 0;
        for(int i=1; i<=amount; ++i){
            for(auto coin : coins){
                if(i-coin>=0 && dp[i-coin]<INT_MAX) 
                    dp[i] = min(dp[i], dp[i-coin]+1);
            }
        }
        return dp[amount]==INT_MAX ? -1 : dp[amount];
    }
};

5. 零钱兑换II

力扣518

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无输入: amount = 5, coins = [1, 2, 5]

输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

  • target(钱数amount)放在内层循环,得到的是去重后的组合数。
  • 完全背包,所以是前序计算
class Solution {
public:
    int change(int amount, vector<int>& coins) {
        //dp[i][j] using  0-i coin for j amount, the combine methods
        //dp[i][j] = dp[i-1][j] + dp[i][j-coin[i]] 
        //dp[i-1][j] 不用当前coin,可凑成的方式,注意这里是 i-1
        //dp[i][j-coin[i]],用当前coin,可凑成的方式,注意这里是 i
        //i,j can be optimized as one linear arr
        //与0-1背包问题的区别在于,这里优化后,是从前往后计算,
        //而0-1背包因为其转移方程是dp[i][j] = max(dp[i-1][j], dp[i-1][j-w]+v),所以优化后是从后往前计算
        
        //if(amount==0 || coins.empty()) return 0;
        vector<int> dp(amount+1, 0);
        dp[0] = 1;
        for(auto coin : coins){
            for(int j=1; j<=amount; ++j){
                if(j-coin>=0){
                    dp[j] = dp[j] + dp[j-coin];
                }
            }
        }
        return dp[amount];
    }
};

6. 字符串按单词列表拆分

力扣139

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

说明:

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

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

完全背包,可以使用多个物品,最后正好组合成一个单词。

dp[i][j] 使用前i个物品,容量为j时,是否能正好组成
for 0 : n:
	for word in words:
		
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        int n = s.size();
        vector<bool> dp(n+1, false);
        dp[0] = true;
        for(int i=1; i<=n; ++i){
            for(auto& word : wordDict){
                if(dp[i-1]){ //前 i-1 个字符在dic 中
                    if(i-1+word.size()<=n && s.substr(i-1, word.size())==word){
                        dp[i-1+word.size()] = true;   //(i, i+word.size()) 在字典中
                    }
                }
            }
        }
        return dp[n];
    }
};

7. 组合总和IV

力扣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。

完全背包的 组合问题, 不同序列被视作不同的组合,该题和 力扣518 (5.零钱兑换II) 可以一起分析。力扣518是不同序列算一个组合。

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

背包问题总结

为什么考虑顺序需要把循环层次对调啊

@浪随风起 target 放在外层循环,得到的是所有种组合可能。如果target 在内层循环,得到的是去重后的结果。比如target=4,nums[1,2,3],不去重的话,1,2,1和2,1,1算两种结果,但是去重的话,只能算一种。

@浪随风起 target 放在外层循环的话,是一个target的值对应nums所有的值,说的简单点就是这个target的值由nums中的某些组成,所以是有可能重复的

股票交易

labuladong套路题解

对于力扣平台上的股票类型的题目:

  1. 买卖股票的最佳时机

  2. 买卖股票的最佳时机 II

  3. 买卖股票的最佳时机 III

  4. 买卖股票的最佳时机 IV

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

  6. 买卖股票的最佳时机含手续费

剑指 Offer 63. 股票的最大利润

一种常用的方法是将「买入」和「卖出」分开进行考虑:「买入」为负收益,而「卖出」为正收益。在初入股市时,你只有「买入」的权利,只能获得负收益。而当你「买入」之后,你就有了「卖出」的权利,可以获得正收益。显然,我们需要尽可能地降低负收益而提高正收益,因此我们的目标总是将收益值最大化。因此,我们可以使用动态规划的方法,维护在股市中每一天结束后可以获得的「累计最大收益」,并以此进行状态转移,得到最终的答案。

股票问题的总结

总的来说,股票问题里面有三个变量或状态:

  1. 时间或天数 i
  2. 购买次数 k
  3. 当天结束后是否持有股票,用 1表示持有,0表示未持有

因此可以用一个三维数组来表示该状态下的最大收益 即 dp[i][k][1/0]

dp[i][k][0]表示 第i天结束后,已经买了K次,当前未持有股票, dp元素值为当前的最大收益

dp[i][k][1]表示 第i天结束后,已经买了K次,当前持有股票, 当前的最大收益

因此,可写出如下的状态转移方程:

 dp[i][k][0]=max(dp[i-1][k][0], dp[i-1][k][1]+prices[i])
 未持有      =max(昨天未持有,      昨天持有,今天卖了)
 
 dp[i][k][1]=max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i])  //买的时候,将k+1
 持有        =max(昨天持有,      昨天未持有,今天买了) 

初始化边界:

dp[-1][k][0] = 0;
dp[-1][k][1] = INT_MIN; //股票没开市,不可能持有股票,故为 负无穷

dp[i][0][0] = 0;
dp[i][0][1] = INT_MIN; //k为0,一次都没买,不可能持有股票,故为 负无穷

1. 买卖股票最佳时机,一次买卖

力扣121

k=1

 dp[i][k][0]=max(dp[i-1][k][0], dp[i-1][k][1]+prices[i]) 
 dp[i][k][1]=max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i])//买的时候,将k+1
//动态规划方法
class Solution {
public:
    //初始转移方程
    //dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1]+prices[i]);
    //dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i]);
    //将 k=1 代入
    //dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1]+prices[i]);
    //dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0]-prices[i]);   
    //将初始条件 dp[i-1][0][0] = 0; 代入
    //dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i]);
    //dp[i][1] = max(dp[i-1][1], -prices[i]);      
    //推导dp[0][0] = max(0, INT_MIN+price[i]) = 0;
    //    dp[0][1] = max(INT_MIN, -prices[i]) = -prices[i];
    int maxProfit(vector<int>& prices) {
        if(prices.empty()) return 0;
        int dp_hold = -prices[0];
        int dp_none = 0;
        for(int i=1; i<prices.size(); ++i){
            dp_none = max(dp_none, dp_hold+prices[i]);
            dp_hold = max(dp_hold, -prices[i]);
        }
        return max(dp_hold, dp_none);
    }
};
class Solution {
public:
    //一次遍历, 不断更新最小值,并更新在当前天卖出的最大利润
    int maxProfit(vector<int>& prices) {
        if(prices.empty()) return 0;
        int minPrice=prices[0];
        int ans=0;
        for(int i=1; i<prices.size(); ++i){
            minPrice = min(minPrice, prices[i]);
            ans = max(ans, prices[i]-minPrice);
        }
        return ans;
    }
};

2. 买卖股票最佳时机,多次买卖

力扣122

    //dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1]+prices[i]);
    //dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i]);
    // k = INT_MAX, can be ignored
    int maxProfit(vector<int>& prices){
        if(prices.empty()) return 0;
        int dp_1 = INT_MIN, dp_0 = 0;  //i=-1时的 收益
        for(int i=0; i<prices.size(); ++i){
            int dp_pre_1 = dp_1;
            dp_1 = max(dp_1, dp_0-prices[i]);
            dp_0 = max(dp_0, dp_pre_1+prices[i]);
        }
        return dp_0;
    }
    //下面的方案是买卖股票多次的方案

    //dp[i] 第i天结束时的收益
    //dp_hold[i] 今天结束时持有股票,  买入
    //dp_none[i] 今天结束时没有股票, 卖出
    int maxProfit(vector<int>& prices) {
        if(prices.empty()) return 0;
        int n = prices.size();
        vector<int> dp_hold(n, 0);
        vector<int> dp_none(n, 0);
        dp_hold[0] = -prices[0];
        for(int i=1; i<n; ++i){
            dp_hold[i] = max(dp_hold[i-1], dp_none[i-1]-prices[i]);
            dp_none[i] = max(dp_none[i-1], dp_hold[i-1]+prices[i]);
        }
        return max(dp_hold[n-1], dp_none[n-1]);
    }

还有一个贪心解法,在股价上涨的每天都买卖

    //贪心解法  在股价上涨的每天都买卖
    int maxProfit(vector<int>& prices){
        int ans = 0;
        if(prices.empty()) return ans;
        for(int i=1; i<prices.size(); ++i){
            int tprofit = prices[i]-prices[i-1];
            if(tprofit>0) ans += tprofit;
        }
        return ans;
    }

3. 最佳买卖股票时机,多次买卖 含冷冻期

力扣309


    //dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1]+prices[i]);
    //dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i]);
    //包含冷冻期
    //dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1]+prices[i]);  //卖
    //dp[i][k][1] = max(dp[i-1][k][1], dp[i-2][k-1][0]-prices[i]); //买
    int maxProfit(vector<int>& prices) {
        if(prices.empty()) return 0;
        int n = prices.size();
        int dp_1 = INT_MIN;
        int dp_0 = 0;
        int dp_0_pre2 = 0;
        for(int i=0; i<n; ++i){
            int dp_0_pre = dp_0; //i-1
            dp_0 = max(dp_0, dp_1+prices[i]);
            dp_1 = max(dp_1, dp_0_pre2-prices[i]);
            dp_0_pre2 = dp_0_pre;// i-2 in next loop
        } 
        return dp_0;
    }



class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //dp[i] 代表每天结束时的最大收益
        //因为有不同的状态,所以dp[i]分三种情况
        //dp_hold[i] 表示今天结束时持有股票, 不操作或买入
        //dp_sell[i] 表示今天结束时不持有股票,卖出股票,当天结束时不持有,下一天会冻结
        //dp_none[i] 表示今天结束时不持有股票,当天不操作,所以下一天不会冻结
        if(prices.empty()) return 0;
        int n = prices.size();
        vector<int> dp_hold(n, 0);
        vector<int> dp_sell(n, 0);
        vector<int> dp_none(n, 0);
        dp_hold[0] = -prices[0]; //初始化dp_hold 为当天买入
        for(int i=1; i<n; ++i){
            dp_hold[i] = max(dp_hold[i-1], dp_none[i-1]-prices[i]);
            dp_sell[i] = dp_hold[i-1]+prices[i];
            dp_none[i] = max(dp_none[i-1], dp_sell[i-1]);
        }
        return max(dp_hold[n-1], max(dp_sell[n-1], dp_none[n-1]));
    }
};

4. 最佳买卖股票 多次买卖 含手续费

力扣714

    //dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1]+prices[i]);
    //dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i]);
    // k = INT_MAX, can be ignored
    int maxProfit(vector<int>& prices, int fee){
        if(prices.empty()) return 0;
        int dp_1 = INT_MIN, dp_0 = 0;  //i=-1时的 收益
        for(int i=0; i<prices.size(); ++i){
            int dp_pre_1 = dp_1;
            dp_1 = max(dp_1, dp_0-prices[i]-fee);
            dp_0 = max(dp_0, dp_pre_1+prices[i]);
        }
        return dp_0;
    }


class Solution {
public:
    //下面的方案是买卖股票多次的方案

    //dp[i] 第i天结束时的收益
    //dp_hold[i] 今天结束时持有股票,  买入
    //dp_none[i] 今天结束时没有股票, 卖出
    int maxProfit(vector<int>& prices, int fee) {
        if(prices.empty()) return 0;
        int n = prices.size();
        vector<int> dp_hold(n, 0);
        vector<int> dp_none(n, 0);
        dp_hold[0] = -prices[0]-fee;
        for(int i=1; i<n; ++i){
            dp_hold[i] = max(dp_hold[i-1], dp_none[i-1]-prices[i]-fee);
            dp_none[i] = max(dp_none[i-1], dp_hold[i-1]+prices[i]);
        }
        return max(dp_hold[n-1], dp_none[n-1]);
    }
};

5. 买卖股票最佳时机,k=2次买卖

力扣123

class Solution {
public:
    //dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1]+prices[i]);
    //dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i]);
    //k==2
    int maxProfit(vector<int>& prices) {
        if(prices.empty()) return 0;
        int n = prices.size();
        int K = 2;
        vector<int> dp_1(K+1, INT_MIN); //初始化所有 -1天的 dp_1 为 INT_MIN
        vector<int> dp_0(K+1, 0);       //初始化所有 -1天的 dp_0 为 0
        for(int i=0; i<n; ++i){
            for(int k=1; k<=K; ++k){
                //dp_0[k] = max(dp_0[k], dp_1[k]+prices[i]);
                dp_1[k] = max(dp_1[k], dp_0[k-1]-prices[i]);     
                dp_0[k] = max(dp_0[k], dp_1[k]+prices[i]);                                           
            }
        }
        return dp_0[K];
    }
};

6. 买卖股票最佳时机 k

力扣188

class Solution {
public:
    //dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1]+prices[i]);
    //dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0]-prices[i]);

    int maxProfit(int k, vector<int>& prices) {
        if(prices.empty() || k==0) return 0;
        int n = prices.size();
        if(k>n/2){ //无限次买卖
            int dp_0 = 0, dp_1 = INT_MIN;
            for(int i=0; i<n; ++i){
                int dp_pre_0 = dp_0;
                dp_0 = max(dp_0, dp_1+prices[i]);
                dp_1 = max(dp_1, dp_pre_0-prices[i]);
            }
            return dp_0;
        }

        vector<int> dpk_1(k+1, INT_MIN);
        vector<int> dpk_0(k+1, 0);
        for(int i=0; i<n; ++i){
            for(int ik=1; ik<=k; ++ik){
                dpk_1[ik] = max(dpk_1[ik], dpk_0[ik-1]-prices[i]);
                dpk_0[ik] = max(dpk_0[ik], dpk_1[ik]+prices[i]);
            }
        }
        return dpk_0[k];
    }
};

字符串编辑

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