0算法类型训练

文章目录

  • 算法训练
    • 训练路线
    • 数组
      • 1. 旋转图像矩阵
    • 链表
      • 1. 两数相加
      • 2. 删除链表的倒数第N个节点
      • 3. 合并两个有序链表
      • 4. 合并K个排序链表
      • 5. 链表存在环,找环的入口
      • 6. 链表排序
      • 7. 相交链表
      • 8. 反转链表
      • 9. 回文链表
      • 10. 奇偶数链表
    • 树与递归
      • 1. 二叉树中序遍历
      • 2. 不同的二叉搜索树数量
      • 3. 验证二叉搜索树
      • 4. 对称二叉树
      • 5. 二叉树的层序遍历
      • 6. 二叉树最大深度
      • 7. 从前序与中序遍历序列构造二叉树
      • 8. 二叉树展开为链表
      • 9. 二叉树中的最大路径和 (困难)
      • 10. 翻转二叉树
      • 11. 二叉树的最近公共祖先
    • 栈与队列
      • 1. 括号匹配
      • 2. 接雨水
      • 3. 柱状图中最大的矩形
      • 4. 最大矩形
      • 5. 最小栈
      • 7. 每日温度
      • 8. 字符串解码
    • 哈希表
      • 1. 字母异位词分组
      • 2. 前K个高频元素
      • 3. 找字符串中所有字母异位词
      • 4. 和为K的子数组
    • 滑动窗口
      • 1. 找字符串中所有字母异位词
      • 2. 最小覆盖子串
      • 3. 字符串排列
      • 4. 最长无重复子串
      • 5. 滑动窗口最大值
    • 双指针
      • 1. 有序数组的两数和 序号
      • 2. 两数平方和
      • 3. 反转字符串中的元音字符
      • 4. 验证回文字符串,可删除一个字符
      • 5. 归并两个有序数组
      • 6. 判断链表是否存在环
      • 7. 通过删除字母匹配字典里的最长单词
    • 二分查找
      • 模板
      • 1. 找到K个最接近的元素
    • DFS BFS 回溯
      • 1. 八皇后问题
      • 2. 数组全排列
    • 贪心
      • 1. 跳跃游戏
      • 2. 跳跃游戏II
    • 动态规划
      • 常见类型
      • 求最值型动态规划总结
      • 斐波那契数列
        • 1. 打家劫舍
        • 2. 打家劫舍 环形
        • 3. 打家劫舍 二叉树
      • 坐标型动态规划
        • 1. 矩阵最小路径和
        • 2. 矩阵不同路径
        • 3. 考虑障碍时的矩阵不同路径
      • 数组区间
        • 1. 数组区间和
        • 2. 数组中等差递增子区间的个数
      • 分割整数
        • 1. 分割整数的最大乘积
        • 2. 按平方数分割整数
        • 3. 分割整数解码为字母字符串
      • 最长递增/上升子序列
        • 1. 最长上升子序列
        • 2. 无重叠区间
        • 3. 最长数对链
        • 4. 最少数量的箭引爆气球
      • 最长公共子序列
        • 1. 最长公共子序列
        • 2. 最长公共子串
        • 3. 不相交的线
      • 0-1背包
        • 1. 分割等和子集
        • 2. 目标和,改变一组数的正负号使其和为S
        • 3. 字符0和1构成最多字符串的个数 二维背包
      • 完全背包
        • 4. 无限个数硬币兑换
        • 5. 零钱兑换II
        • 6. 字符串按单词列表拆分
        • 7. 组合总和IV
      • 背包问题总结
      • 股票交易
        • 1. 买卖股票最佳时机,一次买卖
        • 2. 买卖股票最佳时机,多次买卖
        • 3. 最佳买卖股票时机,多次买卖 含冷冻期
        • 4. 最佳买卖股票 多次买卖 含手续费
        • 5. 买卖股票最佳时机,k=2次买卖
        • 6. 买卖股票最佳时机 k
      • 字符串编辑
    • 数学
      • 1. 判断数字是否能被7整除

算法训练

训练路线

基础篇(30 天)

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

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

思想篇(30 天)

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

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

提高篇(31 天)

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

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

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

数组

1. 旋转图像矩阵

力扣48

给定一个 n × n 的二维矩阵表示一个图像。

将图像顺时针旋转 90 度。

说明:

你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。

示例 1:

给定 matrix =
[
[1,2,3],
[4,5,6],
[7,8,9]
],

原地旋转输入矩阵,使其变为:
[
[7,4,1],
[8,5,2],
[9,6,3]
]

class Solution {
public:
    //先对角线翻转,再中心对称左右翻转
    void rotate(vector<vector<int>>& matrix) {
        if(matrix.empty()) return;
        int row = matrix.size();
        int column = matrix.front().size();
        if(row!=column) return;
        //对角线翻转
        for(int i=0; i<row; ++i){
            for(int j=0; j<i; ++j){
                int tmp = matrix[i][j];
                matrix[i][j] = matrix[j][i];
                matrix[j][i] = tmp;
            }
        }
        //中心对称左右翻转
        for(int i=0; i<row; ++i){
            for(int j=0; j<column/2; ++j){
                int tmp = matrix[i][j];
                matrix[i][j] = matrix[i][column-j-1];
                matrix[i][column-j-1] = tmp;            
            }
        }
        return;
    }
};

链表

1. 两数相加

力扣2

给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。

如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。

您可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例:

输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807

思路:使用一个preHead, 注意进位 carry

    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode preHead(0);
        ListNode* pre = &preHead;
        int overflow = 0;
        while(l1!=NULL || l2!=NULL || overflow>0){
            int sum = overflow;
            if(l1!=NULL){
                sum+=l1->val;
                l1 = l1->next;
            }
            if(l2!=NULL){
                sum+=l2->val;
                l2 = l2->next;
            }
            overflow = sum / 10;
            sum = sum % 10;
            ListNode* cur = new ListNode(sum);
            pre->next = cur;
            pre = cur;
        }
        return preHead.next;
    }

2. 删除链表的倒数第N个节点

力扣19

给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。

示例:

给定一个链表: 1->2->3->4->5, 和 n = 2.

当删除了倒数第二个节点后,链表变为 1->2->3->5.
说明:

给定的 n 保证是有效的。

    ListNode* removeNthFromEnd(ListNode* head, int n) {
        if(head==NULL) return head;
        ListNode preHead(0);
        preHead.next = head;
        ListNode* fastP = &preHead;
        ListNode* slowP = &preHead;
        while(n--){
            fastP = fastP->next;
        }
        while(fastP->next!=NULL){
            fastP = fastP->next;
            slowP = slowP->next;
        }
        ListNode* toDelete = slowP->next;
        slowP->next = slowP->next->next;
        delete toDelete;
        return preHead.next;
    }

3. 合并两个有序链表

力扣21

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

思路 :

  • 递归
  • 循环
    //循环合并  
    //1.新建一个preHead,最后返回preHead->next。
    //2.维持一个pre的指针 和p1 p2 两个指针。共计三个,循环往下走
    //3.最后还未空的链表要整体连接
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        ListNode preHead(0);
        ListNode* preP = &preHead;
        while(l1!=nullptr && l2!=nullptr)
        {
            if(l1->val <= l2->val)
            {
                preP->next = l1;
                preP = l1;
                l1 = l1->next;
            }
            else
            {
                preP->next = l2;
                preP = l2;
                l2 = l2->next;
            }
        }
        preP->next = l1==nullptr ? l2 : l1;
        return preHead.next;
    }

    //递归合并
    ListNode* mergeTwoLists1(ListNode* l1, ListNode* l2) {
        if(l1==nullptr) return l2;
        if(l2==nullptr) return l1;
        ListNode* pHead = nullptr;
        if(l1->val <= l2->val)
        {
            pHead = l1;
            pHead->next = mergeTwoLists(l1->next, l2);
        }
        else
        {
            pHead = l2;
            pHead->next = mergeTwoLists(l1, l2->next);
        }
        return pHead;
    }

4. 合并K个排序链表

力扣23

合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。

示例:

输入:
[
1->4->5,
1->3->4,
2->6
]
输出: 1->1->2->3->4->4->5->6

思路:

  • 归并
  • 最小堆
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:

    class Node{
    public:
        ListNode* head;
        Node(ListNode* list) { head = list; }
        bool operator<(const Node& rhs) const { return head->val > rhs.head->val; }
    };

    //堆   priority_queue 默认是最大堆, 比较方法是 less。  如果是最小堆,比较方法需要是greater
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        ListNode preHead(0);
        ListNode* pre = &preHead;
        priority_queue<Node> mHeap;
        for(int i=0; i<lists.size(); ++i){
            if(lists[i]!=NULL) mHeap.push(Node(lists[i]));
        }
        while(!mHeap.empty()){
            Node tmp = mHeap.top();
            mHeap.pop();
            pre->next = tmp.head;
            pre = pre->next;
            if(tmp.head->next) mHeap.push(Node(tmp.head->next));
        }
        return preHead.next;
    }



    //归并
    ListNode* mergeKLists2(vector<ListNode*>& lists) {
        int k = lists.size();
        if(k==0) return NULL;
        if(k==1) return lists.front();
        vector<ListNode*> ans;
        for(int i=0; i<k; ){
            ListNode* l1 = i<k ? lists[i++] : NULL;
            ListNode* l2 = i<k ? lists[i++] : NULL;
            ans.push_back(mergeTwoLists(l1, l2));
        }
        return mergeKLists(ans);
    }

    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2){
        ListNode preHead(0);
        ListNode* pre = &preHead;
        while(list1!=NULL && list2!=NULL){
            if(list1->val <= list2->val){
                pre->next = list1;
                list1 = list1->next;
                pre = pre->next;
            }else{
                pre->next = list2;
                list2 = list2->next;
                pre = pre->next;
            }
        }
        pre->next = list1==NULL? list2 : list1;
        return preHead.next;
    }
};

5. 链表存在环,找环的入口

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

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

说明:不允许修改给定的链表。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        if(head==NULL) return NULL;
        ListNode* p1 = head;
        ListNode* p2 = head;
        while(p2!=NULL){
            p1 = p1->next;
            if(p2->next==NULL) return NULL;
            p2 = p2->next->next;
            if(p1==p2){ //has circle
                //find entrance
                ListNode* p = head;
                while(p != p1){
                    p = p->next;
                    p1 = p1->next;
                }
                return p;
            }
        }
        return NULL;
    }
};

6. 链表排序

力扣148

在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。

示例 1:

输入: 4->2->1->3
输出: 1->2->3->4
示例 2:

输入: -1->5->3->4->0
输出: -1->0->3->4->5

思路:

  • 归并排序
  • 断开链表
  • 快慢指针找中点
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* sortList(ListNode* head) {
        if(head==NULL || head->next==NULL) return head;
        //快慢指针找中点
        ListNode* mid = head;
        ListNode* fast = head;
        while(fast->next!=NULL && fast->next->next!=NULL){
            fast = fast->next->next;
            mid = mid->next;
        }
        ListNode* midNext = mid->next;
        mid->next = NULL;  //断开链表

        ListNode* h1 = sortList(head);
        ListNode* h2 = sortList(midNext);
        return mergeTwoList(h1, h2);
    }
    //合并两个链表
    ListNode* mergeTwoList(ListNode* l1, ListNode* l2){
        if(l1==NULL) return l2;
        if(l2==NULL) return l1;
        ListNode* pHead = NULL;
        if(l1->val < l2->val){
            pHead = l1;
            pHead->next = mergeTwoList(l1->next, l2);
        }else{
            pHead = l2;
            pHead->next = mergeTwoList(l1, l2->next);
        }
        return pHead;
    }
};

7. 相交链表

力扣160

intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/intersection-of-two-linked-lists
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
	//别人多么简洁的代码!!!!
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* pa = headA;
        ListNode* pb = headB;
        while(pa!=pb){  //如果没有相交,最后也都会到达末尾,相等同时返回NULL
            pa = pa==NULL? headB : pa->next;
            pb = pb==NULL? headA : pb->next;
        }
        return pa;
    }

//自己写的怎么这么长!!!
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if(headA==NULL || headB==NULL) return NULL;
        ListNode* pA = headA;
        ListNode* pB = headB;
        bool changeA = true;
        bool changeB = true;
        while(pA!=NULL && pB!=NULL){
            if(pA==pB) return pA;
            pA = pA->next;
            pB = pB->next;
            if(pA==NULL && changeA){
                pA = headB;
                changeA=false;
            }
            if(pB==NULL && changeB){
                pB = headA;
                changeB = false;
            }
        }
        return NULL;
    }
};

8. 反转链表

反转一个单链表。

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
  • 迭代
  • 递归
    ListNode* reverseList(ListNode* head) {
        if(head==NULL) return NULL;
        ListNode* pre = NULL;
        ListNode* cur = head;
        while(cur!=NULL){
            ListNode* next = cur->next;
            cur->next = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }


    ListNode* reverseList2(ListNode* head) {
        if(head==NULL || head->next==NULL) return head;
        ListNode* rHead = reverseList(head->next);
        head->next->next = head;
        head->next = NULL;
        return rHead;
    }

9. 回文链表

请判断一个链表是否为回文链表。

示例 1:

输入: 1->2
输出: false

示例 2:

输入: 1->2->2->1
输出: true

思路:

  • 存入数组,或使用栈
  • 对链表拆分 ,然后反向,然后比较
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    bool isPalindrome(ListNode* head) {
        if(head==NULL) return true;
        ListNode* mid = head;
        ListNode* fast = head;
        while(fast->next!=NULL && fast->next->next!=NULL){
            fast = fast->next->next;
            mid = mid->next;
        }
        ListNode* rightSides = mid->next; //1221偶数时,mid指向第一个2,  12321时,mid指向3 
        mid->next = NULL;
        ListNode* rHead = reverseList(rightSides);
        ListNode* rP = rHead;
        while(rP!=NULL && head!=NULL){
            if(rP->val != head->val) return false;
            rP = rP->next;
            head = head->next;
        }
        return true;
    }

    ListNode* reverseList(ListNode* head){
        if(head==NULL) return head;
        ListNode* pre = NULL;
        ListNode* cur = head;
        while(cur!=NULL){
            ListNode* next = cur->next;
            cur->next = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }

};

10. 奇偶数链表

力扣328

给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。

请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。

示例 1:

输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL

思路:和复制多路链表那道题的最后一步一样

  • 把一个链表拆分为两个
  • 串起来
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    //先拆分为两个链条,再串起来
    ListNode* oddEvenList(ListNode* head) {
        if(head==nullptr) return head;
        ListNode* odd = head;
        ListNode* even = head->next;
        ListNode* evenHead = even;
        while(even && even->next){
            odd->next = even->next;
            even->next = even->next->next;
            odd = odd->next;
            even = even->next;
        }
        odd->next = evenHead;
        return head;
    }
};

树与递归

1. 二叉树中序遍历

  • 迭代
  • 递归
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        if(root==NULL) return vector<int>{};
        vector<int> ans = inorderTraversal(root->left);
        ans.push_back(root->val);
        vector<int> rans = inorderTraversal(root->right);
        if(!rans.empty()) std::copy(rans.begin(), rans.end(), back_inserter(ans));
        return ans;
    }
};


/**
 * 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:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> ans;
        if(root==NULL) return ans;
        stack<pair<TreeNode*, bool>> mStack;
        mStack.push(make_pair(root, false));
        while(!mStack.empty()){
            TreeNode* cur = mStack.top().first;
            bool isVisit = mStack.top().second;
            mStack.pop();
            if(cur==NULL){
                continue;
            }
            else if(isVisit){
                ans.push_back(cur->val);
            }else{
                mStack.push(make_pair(cur->right, false));
                mStack.push(make_pair(cur, true));
                mStack.push(make_pair(cur->left, false));
            }
        }
        return ans;
    }
};

2. 不同的二叉搜索树数量

给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?

示例:

输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:

1 3 3 2 1
\ / / / \
3 2 1 1 3 2
/ / \
2 1 2 3

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/unique-binary-search-trees
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

class Solution {
public:
    //动态规划,左右子树每个子树的 个数与数量相关
    //3个数字时,
    // dp[3] = dp[0]*dp[2] + dp[1]*dp[1]  + dp[2]*dp[0]
    //dp[4] = dp[0]*dp[3] + dp[1]*dp[2] + dp[2]*dp[1] + dp[3]*dp[0]
    int numTrees(int n) {
        vector<int> dp(n+1, 0);
        dp[0] = 1;
        dp[1] = 1;
        for(int i=2; i<=n; ++i){
            for(int j=0; j<i; ++j){
                dp[i] += dp[j]*dp[i-j-1];
            }
        }
        return dp[n];
    }
};

3. 验证二叉搜索树

给定一个二叉树,判断其是否是一个有效的二叉搜索树。

假设一个二叉搜索树具有如下特征:

节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:

输入:
2
/
1 3
输出: true
示例 2:

输入:
5
/
1 4
/
3 6
输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
根节点的值为 5 ,但是其右子节点值为 4 。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/validate-binary-search-tree
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

/**
 * 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:

    //左右边界范围
    bool isValidBST(TreeNode* root) {
        long long leftBound = LLONG_MIN;
        long long rightBound = LLONG_MAX;
        return isValidBSTCore(root, leftBound, rightBound);
    }

    bool isValidBSTCore(TreeNode* root, long long leftBound, long long rightBound){
        if(root==NULL) return true;
        if(root->val <= leftBound || root->val >= rightBound){
            return false;
        }        
        return isValidBSTCore(root->left, leftBound, root->val) && isValidBSTCore(root->right, root->val, rightBound);
    }


    //中序遍历 是 上升数组
    bool isValidBST2(TreeNode* root) {
        if(root==NULL) return true;
        stack<pair<TreeNode*, bool>> mStack;
        mStack.push(make_pair(root, false));
        int preVal;
        bool preInit = false;
        while(!mStack.empty()){
            TreeNode* cur = mStack.top().first;
            bool isVisit = mStack.top().second;
            mStack.pop();
            if(cur==NULL){
                continue;
            }else if(isVisit){
                if(preInit){
                    if(cur->val>preVal){
                        preVal = cur->val;
                    }else{
                        return false;
                    }
                }else{
                    preVal = cur->val;
                    preInit = true;
                }
            }else{
                mStack.push(make_pair(cur->right, false));
                mStack.push(make_pair(cur, true));
                mStack.push(make_pair(cur->left, false));
            }
        }
        return true;
    }
};

4. 对称二叉树

/**
 * 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:    
    bool isSymmetric(TreeNode* root) {
        return isSymmetricCore(root, root);
    }

    //队列
    bool isSymmetricCore(TreeNode* root1, TreeNode* root2) {
        queue<TreeNode*> mQueue;
        mQueue.push(root1);
        mQueue.push(root2);
        while(!mQueue.empty()){
            TreeNode* n1 = mQueue.front();
            mQueue.pop();
            TreeNode* n2 = mQueue.front();
            mQueue.pop();
            if(n1==NULL && n2==NULL) continue;
            if(n1==NULL || n2==NULL) return false;
            if(n1->val != n2->val) return false;
            mQueue.push(n1->left);
            mQueue.push(n2->right);
            mQueue.push(n1->right);
            mQueue.push(n2->left);
        }
        return true;
    }
    //递归
    bool isSymmetricCore1(TreeNode* root1, TreeNode* root2){
        if(root1==NULL && root2==NULL) return true;
        if(root1!=NULL && root2!=NULL && root1->val==root2->val){
            return isSymmetricCore(root1->left, root2->right) && isSymmetricCore(root1->right, root2->left);
        }
        return false;
    }
};

5. 二叉树的层序遍历

力扣102

给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。

示例:
二叉树:[3,9,20,null,null,15,7],

3
/
9 20
/
15 7
返回其层次遍历结果:

[
[3],
[9,20],
[15,7]
]

思路:

  • 常规:BFS,使用queue队列
  • dfs 递归,前序遍历,每次到一个新的深度就创建一个vector,根据当前递归的深度,将当前元素放入对应深度的vector里。
/**
 * 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:
    //递归 dfs
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> ans;
        dfs(root, ans, 1);
        return ans;
    }

    void dfs(TreeNode* root, vector<vector<int>>& ans, int level){
        if(root==NULL) return;
        if(ans.size()<level) ans.push_back(vector<int>{});
        ans[level-1].push_back(root->val);
        dfs(root->left, ans, level+1);
        dfs(root->right, ans, level+1);
    }

    //队列
    vector<vector<int>> levelOrder2(TreeNode* root) {
        vector<vector<int>> ans;
        queue<TreeNode*> mQueue;
        mQueue.push(root);
        while(!mQueue.empty()){
            int levelSize = mQueue.size();
            vector<int> levelVals;
            for(int i=0; i<levelSize; ++i){
                TreeNode* cur = mQueue.front();
                mQueue.pop();
                if(cur==NULL) continue;
                else{
                    levelVals.push_back(cur->val);
                    mQueue.push(cur->left);
                    mQueue.push(cur->right);
                }
            }
            if(!levelVals.empty()) ans.push_back(levelVals);
        } 
        return ans;
    }
};

6. 二叉树最大深度

力扣104

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例:
给定二叉树 [3,9,20,null,null,15,7],

3
/
9 20
/
15 7
返回它的最大深度 3 。

    int maxDepth(TreeNode* root) {
        if(root==NULL) return 0;
        return max(maxDepth(root->left), maxDepth(root->right))+1;
    }

7. 从前序与中序遍历序列构造二叉树

根据一棵树的前序遍历与中序遍历构造二叉树。

注意:
你可以假设树中没有重复的元素。

例如,给出

前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:

3
/
9 20
/
15 7

思路:找 root,区分left 和 right

8. 二叉树展开为链表

给定一个二叉树,原地将它展开为一个单链表。

例如,给定二叉树

​ 1
/
2 5
/ \
3 4 6
将其展开为:

1

2

3

4

5

6

思路:

  • 原地展开,将右子树不断循环移动到左子树的最右节点上
  • 利用二叉树的前序遍历
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    //将右子树不断循环移动到左子树的最右节点上
    void flatten(TreeNode* root) {
        if(root==nullptr) return;
        while(root!=nullptr){
            if(root->left!=nullptr){
                TreeNode* leftSubTreeRightLeaf = root->left;
                while(leftSubTreeRightLeaf->right!=nullptr){
                    leftSubTreeRightLeaf = leftSubTreeRightLeaf->right; //找到左子树的最右节点
                }
                leftSubTreeRightLeaf->right = root->right; //将右子树移动到左子树的最右节点
                root->right = root->left;//左节点移为右节点
                root->left = nullptr; //左节点置空
            }
            root = root->right;
        }
        return;
    }
};

9. 二叉树中的最大路径和 (困难)

给定一个非空二叉树,返回其最大路径和。

本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。

示例 1:

输入: [1,2,3]

   1
  / \
 2   3

输出: 6
示例 2:

输入: [-10,9,20,null,null,15,7]

-10
/
9 20
/
15 7

输出: 42

思路:

  • 后序遍历递归,返回不同的路径和,使用全局变量记录最大值。 看注释
  • 当前节点的左右子节点 返给当前节点的信息应该是以 其左右子节点为结尾的 路径的最大和。
  • 在当前节点,要计算包含当前节点路径的最大和,即 max(左子最大和+当前, 右子最大和+当前, 当前, 左子+当前+右子) ->用于计算answer
  • 但是当前节点返回给上一节点的信息应该是以当前节点为结尾的最大路径和,即 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:
    int maxPathSum(TreeNode* root) {
        int ans = INT_MIN;
        dfs(root, ans);
        return ans;
    }

    // 函数返回 当前节点的单向路径和, ans存储全局最大路径和
    int dfs(TreeNode* root, int& ans){
        if(root==NULL) return 0;
        int leftPathSum = dfs(root->left, ans); //包含左子节点的最大路径和
        int rightPathSum = dfs(root->right, ans);//包含右子节点的最大路径和
        int curSum = max(max(leftPathSum, rightPathSum) + root->val, root->val); //包含当前节点的单向路径和 max(rootval+left/right, rootval)
        ans = max(ans, max(curSum, leftPathSum+rightPathSum+root->val));//全局最大 增加对 left+root+right 情况的判断
        return curSum;
    }
};

10. 翻转二叉树

翻转一棵二叉树。

示例:

输入:

4
/
2 7
/ \ /
1 3 6 9
输出:

4
/
7 2
/ \ /
9 6 3 1

    TreeNode* invertTree(TreeNode* root) {
        if(root==NULL) return root;
        TreeNode* tmp = root->left;
        root->left = root->right;
        root->right = tmp;
        invertTree(root->left);
        invertTree(root->right);
        return root;
    }

11. 二叉树的最近公共祖先

力扣236

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]

示例 1:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。

思路:

  • 后序遍历!!!

    • //后序遍历,返回当前节点是否包含其中一个目标节点
      
      //对于当前节点的左右子树
      // 1. 若左右子树均返回true,则当前节点为公共祖先
      // 2. 若有一个子树返回true,且当前节点等于另一个节点,则当前节点为公共祖先
      // 3. 否则,向上返回 leftHas || rightHas || rootp || rootq, 用于上层节点的判断
  • 思路2:dfs回溯找到包含两个目标节点的路径,寻找两个路径的最后一个公共节点

/**
 * 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:
    //直接通过返回TreeNode* 来判断
     TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root==NULL || root==p || root==q) return root;
        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);
        if(left && right) return root;
        if(root==p || root==q) return root;
        if(left) return left;
        if(right) return right;
        return NULL;
    }
    
    
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        TreeNode* ans = NULL;
        hasTarget(root, p, q, &ans);
        return ans;
    }
    //后序遍历,返回当前节点是否包含其中一个目标节点
    //对于当前节点的左右子树
    // 1. 若左右子树均返回true,则当前节点为公共祖先
    // 2. 若有一个子树返回true,且当前节点等于另一个节点,则当前节点为公共祖先
    // 3. 否则,向上返回 leftHas || rightHas || root==p || root==q, 用于上层节点的判断
    bool hasTarget(TreeNode* root, TreeNode* p, TreeNode* q, TreeNode** ans){
        if(root==NULL) return false;
        if(*ans!=NULL) return false; //提前剪枝
        bool leftHas = hasTarget(root->left, p, q, ans);
        bool rightHas = hasTarget(root->right, p, q, ans);
        if(leftHas && rightHas){
            *ans = root;
        }else if((leftHas || rightHas) && (root==p || root==q)){
            *ans = root;
        }
        return leftHas || rightHas || root==p || root==q;
    }

};

栈与队列

以下列出了单调栈的问题,供大家参考。

序号 题目 题解
1 42. 接雨水(困难) 暴力解法、优化、双指针、单调栈
2 739. 每日温度(中等) 暴力解法 + 单调栈
3 496. 下一个更大元素 I(简单) 暴力解法、单调栈
4 316. 去除重复字母(困难) 栈 + 哨兵技巧(Java、C++、Python)
5 901. 股票价格跨度(中等) 「力扣」第 901 题:股票价格跨度(单调栈)
6 402. 移掉K位数字
7 581. 最短无序连续子数组
这里感谢 @chwma 朋友提供资料。

作者:liweiwei1419
链接:https://leetcode-cn.com/problems/largest-rectangle-in-histogram/solution/bao-li-jie-fa-zhan-by-liweiwei1419/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

1. 括号匹配

力扣20

给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。

示例 1:

输入: “()”
输出: true

class Solution {
public:
    bool isValid(string s) {
        if(s.empty()) return true;
        map<char, char> hash;
        hash['{'] = '}';
        hash['('] = ')';
        hash['['] = ']';
        stack<char> mStack;
        for(int i=0; i<s.size(); ++i){
            char c = s[i];
            if(c=='[' || c=='{' || c=='('){
                mStack.push(c);
            }else{
                if(mStack.empty()) return false;
                char t = mStack.top();
                mStack.pop();
                if(hash[t]!=c) return false;
            }
        }
        return mStack.empty() ? true : false;
    }
};

2. 接雨水

力扣42

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 Marcos 贡献此图。

示例:

输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6

思路:

  • 暴力
  • 先遍历得到最大值,然后左右找,记录preMax即可
  • 两个数组分别存储当前节点左右最大的柱子,动态规划
  • 双指针
  • 单调下降栈,和下一题 柱状图中的最大矩形 问题相反,一个是单调下降栈,一个单调上升栈
class Solution {
public:
    int trap(vector<int>& height) {
        if(height.empty()) return 0;
        int maxHeightIndex = 0;
        for(int i=1; i<height.size(); ++i){
            if(height[i]>height[maxHeightIndex]){
                maxHeightIndex = i;
            }
        }
        int ans = 0;
        int preMax = 0;
        for(int i=0; i<maxHeightIndex; ++i){
            preMax = max(preMax, height[i]);
            ans += preMax - height[i];
        }
        preMax = 0;
        for(int j=height.size()-1; j>maxHeightIndex; --j){
            preMax = max(preMax, height[j]);
            ans += preMax - height[j];
        }
        return ans;
    }
    
    
    //单调下降栈
    int trap(vector<int>& height) {
        int ans = 0;
        if(height.empty()) return ans;
        int n = height.size();
        stack<int> mStack;
        for(int i=0; i<n; ++i){
            while(!mStack.empty() && height[mStack.top()]<height[i]){ //当前高度大于栈顶元素,形成凹槽
                int index = mStack.top();
                mStack.pop();
                if(!mStack.empty())
                    ans += (min(height[i], height[mStack.top()]) - height[index]) * (i-mStack.top()-1); //左右高度差减index高度*宽度
            }
            mStack.push(i);
        }
        return ans;
    }
    
    //双指针
    int trap(vector<int>& height) {
        int ans = 0;
        int n = height.size();
        if(n<=2) return ans;        
        int left = 0;
        int right = n-1;
        //只要 leftMax < rightMax, 则凹槽右边界不会超过rightMax
        int leftMax = height[0];
        int rightMax = height[n-1];
        while(left<=right){  //需要等于号,不然最后一个不会被计算到
            if(leftMax<rightMax){
                if(height[left]<leftMax){
                    ans += leftMax-height[left];
                }else{
                    leftMax = max(leftMax, height[left]);
                }
                ++left;
            }else{
                if(height[right]<rightMax){
                    ans += rightMax-height[right];
                }else{
                    rightMax = max(rightMax, height[right]);
                }
                --right;
            }
        }
        return ans;
    }
};

3. 柱状图中最大的矩形

力扣84

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]。

图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10 个单位。

示例:

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

  • 暴力解
  • 单调栈
  • 加哨兵的单调栈
class Solution {
public:
    //暴力解
    int largestRectangleArea(vector<int>& heights) {
        int ans = 0;
        if(heights.empty()) return ans;
        for(int i=0; i<heights.size(); ++i){
            int tmpMin = heights[i];
            for(int j=i; j>=0;--j){
                tmpMin = min(tmpMin, heights[j]);
                ans = max(ans, tmpMin*(i-j+1));
            }
        }
        return ans;
    }

        //  暴力法  推荐的方法,固定高度,找左右宽度
    int largestRectangleArea1(vector<int>& heights) {
        int ans = 0;
        if(heights.empty()) return ans;
        int n = heights.size();
        for(int i=0; i<n; ++i){
            int left = i;
            int right = i;
            while(left>=0 && heights[left]>=heights[i]) 
                --left;
            while(right<n && heights[right]>=heights[i]) 
                ++right;
            ans = max(ans, heights[i]*(right-left-1));
        }
        return ans;
    }
  
    //单调栈
    int largestRectangleArea2(vector<int>& heights) {
        int ans = 0;
        if(heights.empty()) return ans;
        int n = heights.size(); 
        stack<int> mStack;
        for(int i=0; i<n; ++i){
            while(!mStack.empty() && heights[mStack.top()] > heights[i]){
                int index = mStack.top();
                mStack.pop();
                int hei = heights[index];
                int width = i-(mStack.empty() ? -1 : mStack.top())-1;
                ans = max(ans, hei*width);
            }
            mStack.push(i);
        }
        //栈内剩余的元素
        while(!mStack.empty()){
            int index = mStack.top();
            mStack.pop();
            int hei = heights[index];
            int width = n - (mStack.empty() ? -1 : mStack.top()) -1;
            ans = max(ans, hei*width);
        }
        return ans;
    }
    
    //单调栈 + 哨兵
    int largestRectangleArea(vector<int>& heights) {
        int ans = 0;
        if(heights.empty()) return ans;
        int n = heights.size(); 
        vector<int> newHeights(n+2, -1);
        for(int i=1; i<n+1; ++i){
            newHeights[i] = heights[i-1];
        }
        stack<int> mStack;
        mStack.push(0);
        for(int i=1; i<n+2; ++i){
            while(newHeights[mStack.top()] > newHeights[i]){
                int index = mStack.top();
                mStack.pop();
                int hei = newHeights[index];
                int width = i-mStack.top()-1;
                ans = max(ans, hei*width);
            }
            mStack.push(i);
        }
        return ans;
    }
};

4. 最大矩形

力扣85

给定一个仅包含 0 和 1 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。

示例:

输入:
[
[“1”,“0”,“1”,“0”,“0”],
[“1”,“0”,“1”,“1”,“1”],
[“1”,“1”,“1”,“1”,“1”],
[“1”,“0”,“0”,“1”,“0”]
]
输出: 6

思路:

  • 转换为柱状图中的最大矩形问题
  • 动态规划,分别存储当前[i] [j] 最大高度,以及最大高度下,所能到达的最左边界和最右边界,然后不断更新
class Solution {
public:
    int maximalRectangle(vector<vector<char>>& matrix) {
        if(matrix.empty()) return 0;
        int row = matrix.size();
        int column = matrix.front().size();
        vector<int> heights(column+2, 0); //带哨兵
        int ans = 0;
        for(int i=0; i<row; ++i){
            for(int j=0; j<column; ++j){
                heights[j+1] = matrix[i][j]=='1' ? heights[j+1]+1 : 0;  //获取每一行时的 柱状图 heights
            }
            ans = max(ans, getMaxRectangleFromList(heights));
        }
        return ans;
    }

    //转换后的子问题
    int getMaxRectangleFromList(vector<int>& heights){
        if(heights.empty()) return 0;
        int ans = 0;
        stack<int> mStack; //存放index,单调上升栈
        mStack.push(0);
        for(int i=1; i<heights.size(); ++i){
            while(heights[mStack.top()]>heights[i]){
                int index = mStack.top();
                mStack.pop();
                int hei = heights[index];
                int width = i-mStack.top()-1;
                ans = max(ans, hei*width);
            }
            mStack.push(i);
        }
        return ans;
    }
};

//动态规划方法
class Solution {
public:
    int maximalRectangle(vector<vector<char>>& matrix) {
        if(matrix.empty()) return 0;
        int row = matrix.size();
        int column = matrix.front().size();
        vector<int> height(column, 0);
        vector<int> left(column, 0);  //存储的是 [i][j]位置,以height 为高度时,矩形所能到达的左边界的位置
        vector<int> right(column, column); //初始化为n, 默认能到达最右边
        int ans = 0;
        for(int i=0; i<row; ++i){
            for(int j=0; j<column; ++j){
                height[j] = matrix[i][j]=='1' ? height[j]+1 : 0;
            }
            int curZeroIndex = 0;
            for(int j=0; j<column; ++j){
                if(matrix[i][j]=='1'){
                    left[j] = max(left[j], curZeroIndex); //如果zeroindex比上一行的位置大,则这一行左边界要比上一行小
                }else{
                    curZeroIndex = j+1;
                    left[j] = 0; //因为height肯定为0,故此时要把该点的左边界释放为初始值,避免影响下一行的计算
                }
            }
            curZeroIndex = column-1;
            for(int j=column-1; j>=0; --j){
                if(matrix[i][j]=='1'){
                    right[j] = min(right[j], curZeroIndex);
                }else{
                    curZeroIndex = j-1;
                    right[j] = column;
                }
            }
            //计算max area
            for(int j=0; j<column; ++j){
                int area = height[j] * (right[j]-left[j]+1);
                ans = max(ans, area);
            }
        }
        return ans;
//方法四里的 left[j] = 0 和 right[j] = n 我觉得有点难懂。写一点我自己的理解:当matrix[i][j]是0的时候,height[j]是0,所以面积肯定是0了,left[j]和right[j]并不影响这个位置的面积的计算。但是如果我们不更新left[j]和right[j],到下一行时,代码还以为上一行这个位置的矩形最多只能在left[j]和right[j]之间,但实际上上一行这个位置并没有构成矩形。所以left[j] = 0 和 right[j] = n是为了在计算下一行时保证上一行无效的left[j]和right[j]被丢弃,即重置。
    }
};

5. 最小栈

class MinStack {
public:
    stack<int> mStack;
    stack<int> mMinStack;

    /** initialize your data structure here. */
    MinStack() {
        
    }
    
    void push(int x) {
        mStack.push(x);
        if(mMinStack.empty()){
            mMinStack.push(x);
        }else{
            if(x<mMinStack.top()){
                mMinStack.push(x);
            }else{
                mMinStack.push(mMinStack.top());
            }
        }
    }
    
    void pop() {
        if(mStack.empty()) return;
        mStack.pop();
        mMinStack.pop();
    }
    
    int top() {
        return mStack.top();
    }
    
    int getMin() {
        return mMinStack.top();
    }
};

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack* obj = new MinStack();
 * obj->push(x);
 * obj->pop();
 * int param_3 = obj->top();
 * int param_4 = obj->getMin();
 */

7. 每日温度

力扣739

请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。

例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。

提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。

思路:

  • 单调栈
class Solution {
public:
    //从后往前
    vector<int> dailyTemperatures(vector<int>& T) {
        stack<int> mStack;
        vector<int> ans(T.size(), 0);
        if(T.empty()) return ans;
        for(int i=T.size()-1; i>=0; --i){
            int temperature = T[i];
            while(!mStack.empty() && T[mStack.top()]<=temperature //单调上升栈
                mStack.pop();
            }
            if(mStack.empty()) ans[i] = 0;
            else ans[i] = mStack.top()-i;
            mStack.push(i);
        }
        return ans;
    }
    
    //从前往后
    vector<int> dailyTemperatures(vector<int>& T) {
        stack<int> mStack;
        vector<int> ans(T.size(), 0);
        if(T.empty()) return ans;
        for(int i=0; i<T.size(); ++i){
            int temperature = T[i];
            if(mStack.empty() || temperature<=T[mStack.top()]){ //单调下降栈
                mStack.push(i);
            }else{
                while(!mStack.empty() && T[mStack.top()]<temperature){
                    int preIndex = mStack.top();
                    mStack.pop();
                    ans[preIndex] = i-preIndex;
                }
                mStack.push(i);
            }
        }
        return ans;
    }    

};

8. 字符串解码

力扣394

给定一个经过编码的字符串,返回它解码后的字符串。

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。

你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。

此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。

示例 1:

输入:s = “3[a]2[bc]”
输出:“aaabcbc”
示例 2:

输入:s = “3[a2[c]]”
输出:“accaccacc”
示例 3:

输入:s = “2[abc]3[cd]ef”
输出:“abcabccdcdcdef”

class Solution {
public:
    string decodeString(string s) {
        if(s.empty()) return "";
        stack<int> stack_k;
        stack<string> stack_prefix;
        string ans = "";
        int k = 0;
        string prefix = "";
        for(int i=0; i<s.size(); ++i){
            if(s[i]>='0'&& s[i]<='9'){
                k = k*10+(s[i]-'0');
            }else if(s[i]=='['){
                stack_prefix.push(prefix);
                stack_k.push(k);
                k = 0;
                prefix = "";
            }else if(s[i]==']'){
                string tmp = "";
                int preK = stack_k.top();
                stack_k.pop();
                string prePrefix = stack_prefix.top();
                stack_prefix.pop();
                while(preK--) tmp += prefix;
                prefix = prePrefix + tmp;
            }
            else// if(s[i]>='a'&&s[i]<='z' ||){  //char
            {
                prefix += s[i];
            }
        }
        ans = prefix;
        return ans;
    }
};

哈希表

1. 字母异位词分组

力扣49

给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。

示例:

输入: [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”]
输出:
[
[“ate”,“eat”,“tea”],
[“nat”,“tan”],
[“bat”]
]

思路:

  • 找到hash表的 索引方式,将 str 排序sort 后可作为 key
  • 将str 转换为 以 字母个数为 表达形式的字符串 作为 key, 如 “#1#2#24#0#0…”(#共26个)
class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        vector<vector<string>> ans;
        if(strs.empty()) return ans;
        map<string, vector<string>> hash;
        for(auto str : strs){
            string tmp = str;
            sort(tmp.begin(), tmp.end());
            hash[tmp].push_back(str);
        }
        for(auto pair : hash){
            ans.push_back(pair.second);
        }
        return ans;
    }
}; 

2. 前K个高频元素

力扣347

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:

输入: nums = [1], k = 1
输出: [1]

思路:

  • 堆,最小堆
  • 堆顶是前k个最大里面 最小的那个,新来一个之后,和这个最小的比较,如果比它大,就弹出最近,插入较大。
//堆的代码
//默认是最大堆
priority_queue<int, vector<int>, less<int>> maxHeap;          //最大堆
priority_queue<int, vector<int>, std::greater<int>> minHeap; //最小堆

当Node为类时,
提供 operator< 重载后,如果正常提供小于的 比较,那就是最大堆,
    如果提供的 operator< 是相反的那就是最小堆。
priority_queue<Node> maxHeap;
priority_queue<Node> minHeap;


class Solution {
public:

    class Node{
    public:
        int num;
        int cnt;
        Node(int tnum, int tcnt):num(tnum),cnt(tcnt){}
        bool operator<(const Node& rhs) const { return cnt>rhs.cnt; } 
        //注意,如果return cnt>rhs.cnt; 是最小堆
        //如果return cnt
    };
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int, int> hash;
        vector<int> ans;
        for(int i=0; i<nums.size(); ++i){
            ++hash[nums[i]];
        }
        priority_queue<Node, vector<Node>> mHeap; //默认是最大堆,这里使用最小堆
        for(auto pair : hash){
            if(mHeap.size()==k){
                if(mHeap.top().cnt < pair.second){
                    mHeap.pop();
                    mHeap.push(Node(pair.first, pair.second));
                }
            }else{
                mHeap.push(Node(pair.first, pair.second));
            }
        }
        for(int i=0; i<k; ++i){
            ans.push_back(mHeap.top().num);
            mHeap.pop();
        }
        return ans;
    }
};

3. 找字符串中所有字母异位词

力扣438

给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。

字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。

说明:

字母异位词指字母相同,但排列不同的字符串。
不考虑答案输出的顺序。
示例 1:

输入:
s: “cbaebabacd” p: “abc”

输出:
[0, 6]

解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的字母异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的字母异位词。

思路:

  • 使用hash / vector 来保存字符串字母计数
    • 使用循环对比 判断 hash 是否相同
    • 或使用 diffNum 记录 不同的个数
  • 滑动窗口方法
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> thash(26, 0);
        for(int i=0; i<p.size(); ++i){
            ++thash[p[i]-'a'];
        }
        vector<int> ans;
        int k = p.size();
        if(k>s.size()) return ans;
        vector<int> hash(26, 0);
        int diffNum = 0;
        for(int i=0; i<s.size(); ++i){
            if(i<k){
                ++hash[s[i]-'a'];
                if(i==k-1){
                    for(int j=0; j<26; ++j){
                        if(hash[j]!=thash[j]) ++diffNum;
                    }
                    if(diffNum==0) ans.push_back(i-k+1);
                }
            }else{
                ++hash[s[i]-'a'];
                if(hash[s[i]-'a'] == thash[s[i]-'a']) --diffNum;
                else if(hash[s[i]-'a']-1==thash[s[i]-'a']) ++diffNum;
                --hash[s[i-k]-'a'];
                if(hash[s[i-k]-'a'] == thash[s[i-k]-'a']) --diffNum;
                else if(hash[s[i-k]-'a']+1==thash[s[i-k]-'a']) ++diffNum;
                if(diffNum==0) ans.push_back(i-k+1);
            }
        }
        return ans;
    }
};

4. 和为K的子数组

力扣560

给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。

示例 1 :

输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
说明 :

数组的长度为 [1, 20,000]。
数组中元素的范围是 [-1000, 1000] ,且整数 k 的范围是 [-1e7, 1e7]。

思路:

  • 使用hash来记录前缀和为某个数值的个数
class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        if(nums.empty()) return 0;
        unordered_map<int, int> hash; //使用hash 记录前缀和的个数
        int sum = 0;
        hash[sum]++; //设置初始 0 的个数为1
        int ans = 0;
        for(int i=0; i<nums.size(); ++i){
            sum += nums[i]; //当前总的和
            if(hash.count(sum-k)){
                ans += hash[sum-k]; //如果 有 前缀和 + k = 当前和 的话,就存在这样的 子数组
            }
            hash[sum]++;
        }
        return ans;
    }
};

滑动窗口

labuladong框架

1. 找字符串中所有字母异位词

力扣438

给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。

字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。

说明:

字母异位词指字母相同,但排列不同的字符串。
不考虑答案输出的顺序。
示例 1:

输入:
s: “cbaebabacd” p: “abc”

输出:
[0, 6]

解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的字母异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的字母异位词。

思路:

  • 使用hash / vector 来保存字符串字母计数
    • 使用循环对比 判断 hash 是否相同
    • 或使用 diffNum 记录 不同的个数
  • 滑动窗口方法
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> ans;
        if(s.size()<p.size()) return ans;
        unordered_map<char, int> window, need; //两个hash
        for(auto c : p) need[c]++; //先记录需要的
        int left=0, right = 0;//滑动窗口的两个指针  [) 前闭后开区间
        int valid = 0;//记录滑动窗口与 need 的匹配度,是否满足需求
        while(right<s.size()){
            //滑动窗口向右扩展,这部分和缩减部分基本镜像
            char cr = s[right];
            ++right;
            if(need.count(cr)){
                window[cr]++;
                if(window[cr]==need[cr]){
                    ++valid;
                }
            }
            //当滑动窗口满足时,进行缩减
            while(valid==need.size()){
                if(right-left==p.size()){
                    ans.push_back(left);  //满足条件时,记录
                }

                char cl = s[left];
                ++left;
                if(need.count(cl)){
                    if(window[cl]==need[cl]){
                        --valid;
                    }
                    window[cl]--;
                }                
            }
        }
        return ans;
    }
};

2. 最小覆盖子串

力扣76

给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字符的最小子串。

示例:

输入: S = “ADOBECODEBANC”, T = “ABC”
输出: “BANC”
说明:

如果 S 中不存这样的子串,则返回空字符串 “”。
如果 S 中存在这样的子串,我们保证它是唯一的答案。

思路:

  • 标准滑动窗口
    string minWindow(string s, string t) {
        if(s.empty() || s.size()<t.size()) return "";
        unordered_map<char, int> window, need;
        int left=0, right=0;
        int valid = 0; //是否满足条件的标尺
        int ansStart=0;
        int ansLen = INT_MAX;
        for(auto c:t) need[c]++;
        while(right<s.size()){
            //向右扩展
            char cr = s[right++];
            if(need.count(cr)){
                window[cr]++;
                if(window[cr] == need[cr]){
                    valid++;
                }
            }
            //满足条件时,左边收缩
            while(valid == need.size()){
                if(right-left<ansLen){
                    ansStart = left;
                    ansLen = right-left;
                }
                char cl = s[left++];
                if(need.count(cl)){
                    if(window[cl]==need[cl]){
                        valid--;
                    }
                    window[cl]--;
                }
            }
        }
        return ansLen==INT_MAX ? "" : s.substr(ansStart, ansLen);
    }

3. 字符串排列

力扣567

给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。

换句话说,第一个字符串的排列之一是第二个字符串的子串。

示例1:

输入: s1 = “ab” s2 = “eidbaooo”
输出: True
解释: s2 包含 s1 的排列之一 (“ba”).

示例2:

输入: s1= “ab” s2 = “eidboaoo”
输出: False

class Solution {
public:
    bool checkInclusion(string s1, string s2) {
        if(s2.empty() || s2.size()<s1.size()) return false;
        unordered_map<char, int> window, need;
        for(auto c:s1) need[c]++;
        int left=0, right=0;
        int valid = 0;
        while(right<s2.size()){
            char cr = s2[right++];
            if(need.count(cr)){
                window[cr]++;
                if(window[cr]==need[cr]){
                    ++valid;
                }
            }
            while(valid==need.size()){
                if((right-left)==s1.size()) return true;
                char cl = s2[left++];
                if(need.count(cl)){
                    if(window[cl]==need[cl]){
                        --valid;
                    }
                    window[cl]--;
                }
            }
        }
        return false;
    }
};

4. 最长无重复子串

力扣3

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:

输入: “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:

输入: “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。

class Solution {
public:
    //基础框架的 滑动窗口
    int lengthOfLongestSubstring(string s) {
        if(s.empty()) return 0;
        unordered_map<char, int> window;
        int left=0, right=0;
        int ans = 0;
        int duplicateNum = 0;
        while(right<s.size()){
            char cr = s[right++];
            window[cr]++;
            if(window[cr]>1){
                duplicateNum++;
            }
            while(duplicateNum){
                char cl = s[left++];
                if(window[cl]>1){
                    duplicateNum--;
                }
                window[cl]--;
            }
            ans = max(ans, right-left);
        }
        return ans;
    }
	
    //优化之后,取消 duplicateNum
        int lengthOfLongestSubstring(string s) {
        if(s.empty()) return 0;
        unordered_map<char, int> window;
        int left=0, right=0;
        int ans = 0;
        while(right<s.size()){
            char cr = s[right++];
            window[cr]++;
            while(window[cr]>1){ //当当前 right 所在字符个数大于1时,存在重复,缩减窗口
                char cl = s[left++];
                window[cl]--;
            }
            ans = max(ans, right-left);
        }
        return ans;
    }


5. 滑动窗口最大值

力扣239

给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

进阶:

你能在线性时间复杂度内解决此题吗?

示例:

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:

滑动窗口的位置 最大值


[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7

思路:

  • 利用deque存储 滑动窗口,当新进入一个元素,比之前的元素大时,就替换之前的元素,若比之前元素小,就不动
  • 进一步优化: 存储元素的index
    //存储元素
	vector<int> maxSlidingWindow1(vector<int>& nums, int k) {
        vector<int> ans;
        if(nums.empty() || k<=0) return ans;
        deque<int> window;
        for(int i=0; i<nums.size(); ++i){
            if(i>=k){
                window.pop_front();
            }
            int cnt = 1;
            while(!window.empty() && window.back()<nums[i]){
                window.pop_back(); ++cnt;
            }
            while(cnt--) window.push_back(nums[i]);
            if(i>=k-1){
                ans.push_back(window.front());
            }
        }        
        return ans;
    }
	
	//存储序号
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int> ans;
        if(nums.empty() || k<=0) return ans;
        deque<int> window;
        for(int i=0; i<nums.size(); ++i){
            if(i>=k && window.front()<=i-k ){
                window.pop_front();
            }
            while(!window.empty() && nums[window.back()]<nums[i]){
                window.pop_back();
            }
            window.push_back(i);
            if(i>=k-1){
                ans.push_back(nums[window.front()]);
            }
        }        
        return ans;
    }

双指针

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,使得 a*a + b*b = 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;
}

https://leetcode-cn.com/problems/binary-search/solution/704-er-fen-cha-zhao-cer-fen-mo-ban-by-ivan_allen/

1. 找到K个最接近的元素

力扣658

给定一个排序好的数组,两个整数 k 和 x,从数组中找到最靠近 x(两数之差最小)的 k 个数。返回的结果必须要是按升序排好的。如果有两个数与 x 的差值一样,优先选择数值较小的那个数。

示例 1:

输入: [1,2,3,4,5], k=4, x=3
输出: [1,2,3,4]

示例 2:

输入: [1,2,3,4,5], k=4, x=-1
输出: [1,2,3,4]

思路:

  • 先找到最近接的元素,然后向两边扩展
  • 双指针,从左右两边往里缩小
  • 二分查找左边界

https://leetcode-cn.com/problems/find-k-closest-elements/solution/pai-chu-fa-shuang-zhi-zhen-er-fen-fa-python-dai-ma/

https://leetcode-cn.com/problems/find-k-closest-elements/solution/658-zhao-dao-k-ge-zui-jie-jin-de-yuan-su-cer-fen-s/

from typing import List


class Solution:
    def findClosestElements(self, arr: List[int], k: int, x: int) -> List[int]:
        size = len(arr)
        left = 0
        right = size - k

        while left < right:
            # mid = left + (right - left) // 2
            mid = (left + right) >> 1
            # 尝试从长度为 k + 1 的连续子区间删除一个元素
            # 从而定位左区间端点的边界值
            if x - arr[mid] > arr[mid + k] - x:
                left = mid + 1
            else:
                right = mid
        return arr[left:left + k]

作者:liweiwei1419
链接:https://leetcode-cn.com/problems/find-k-closest-elements/solution/pai-chu-fa-shuang-zhi-zhen-er-fen-fa-python-dai-ma/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

DFS BFS 回溯

1. 八皇后问题

力扣51

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

上图为 8 皇后问题的一种解法。

给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。

每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

示例:

输入: 4
输出: [
[".Q…", // 解法 1
“…Q”,
“Q…”,
“…Q.”],

["…Q.", // 解法 2
“Q…”,
“…Q”,
“.Q…”]
]
解释: 4 皇后问题存在两个不同的解法。

提示:

皇后,是国际象棋中的棋子,意味着国王的妻子。皇后只做一件事,那就是“吃子”。当她遇见可以吃的棋子时,就迅速冲上去吃掉棋子。当然,她横、竖、斜都可走一到七步,可进可退。(引用自 百度百科 - 皇后 )

思路:

  • 回溯法
class Solution {
public:
    vector<vector<string>> solveNQueens(int n) {
        vector<vector<int>> ans;
        vector<int> path;
        backtrace(ans, path, 0, n);
        vector<vector<string>> strans;
        for(auto p : ans){
            vector<string> pstr;
            for(auto node : p){
                string tstr;
                for(int i=0; i<n; ++i){
                    tstr += (i==node? "Q":".");
                }
                pstr.push_back(tstr);
            }
            strans.push_back(pstr);
        }
        return strans;
    }

    void backtrace(vector<vector<int>>& ans, vector<int>& path, int level, int n){
        if(level==n){
            ans.push_back(path);
        }
        for(int i=0; i<n; ++i){
            path.push_back(i);//做出选择
            if(isvalid(path)){//如果选择可行,继续下一轮选择
                backtrace(ans, path, level+1, n);
            }
            path.pop_back();//回溯
        }
        return;
    }

    bool isvalid(vector<int>& path){
        int size = path.size();
        int t = path.back();
        int tIndex = size-1;
        //最后一个之前没有与它相同的id
        for(int i=0; i<size-1; ++i){
            if(path[i]==t) return false;
        }
        //看斜角上有没有其他皇后
        for(int i= size-2; i>=0; --i){
            if(abs(path[i]-t)==(tIndex-i)) return false;
        }
        return true;
    }

};

2. 数组全排列

力扣46

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

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

class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int>> ans;
        vector<int> path;
        if(nums.empty()) return ans;
        vector<bool> visit(nums.size(), false); //使用visit来记录是否已经访问过
        dfs(ans, path, nums, visit);
        return ans;
    }

    void dfs(vector<vector<int>>& ans, vector<int>& path, vector<int>& nums, vector<bool>& visit){
        if(path.size()==nums.size()){
            ans.push_back(path);
            return;
        }
        for(int i=0; i<nums.size(); ++i){
            if(visit[i]) continue;
            path.push_back(nums[i]);
            visit[i] = true;
            dfs(ans, path, nums, visit);
            path.pop_back();
            visit[i] = false;
        }
        return;
    }
};

贪心

1. 跳跃游戏

力扣55

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个位置。

示例 1:

输入: [2,3,1,1,4]
输出: true
解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。
示例 2:

输入: [3,2,1,0,4]
输出: false
解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。

class Solution {
public:
    bool canJump(vector<int>& nums) {
        if(nums.empty()) return false;
        int maxPos = 0;
        for(int i=0; i<nums.size(); ++i){
            if(i>maxPos) break;
            maxPos = max(maxPos, i+nums[i]);
            if(maxPos>=nums.size()-1) return true;
        }
        return maxPos>=nums.size()-1;
    }
};

2. 跳跃游戏II

力扣45

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

示例:

输入: [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

class Solution {
public:
    
    int jump(vector<int>& nums){
        if(nums.empty()) return false;
        int cnt = 0;
        int end = 0; //当前可起跳点的范围
        int maxPos = 0;//记录最远点
        for(int i=0; i<nums.size()-1; ++i){ //注意 不读取最后一个数字
            maxPos = max(maxPos, nums[i]+i);
            if(i==end){ //进入下一次起跳范围
                end = maxPos;//起跳范围是  前一个起跳点能达到的最远距离
                ++cnt; //起跳次数加1
            }
        }
        return cnt;
    }

    //通过,但是逻辑有点复杂
    int jump1(vector<int>& nums) {
        if(nums.empty()) return false;
        int cnt = 0;
        int index = 0;
        while(index<nums.size()-1){
            int dis = nums[index];
            int maxPos = 0;
            int tIndex = index;
            ++cnt;
            for(int i=dis; i>=1; --i){
                if((index+i)>=nums.size()-1) return cnt;
                int tmpPos = nums[index+i] + index + i;
                if(tmpPos>maxPos){
                    tIndex = index+i;
                    maxPos = tmpPos;
                }
                if(maxPos>=nums.size()-1) return ++cnt;
            }
            index = tIndex;
        }
        return cnt;
    }



    int ans = INT_MAX;
    //dfs  超时
    int jump2(vector<int>& nums) {
        if(nums.empty()) return false;
        dfs(nums, 0, 0);
        return ans;
    }

    void dfs(vector<int>& nums, int index, int cnt){
        if(cnt>ans) return;
        if(index>=nums.size()-1){
            ans = min(ans, cnt);
            return;
        }
        int dis = nums[index];
        for(int i=1; i<=dis; ++i){
            dfs(nums, index+i, cnt+1);
        }
        return;
    }
};

动态规划

常见类型

  • 坐标型动态规划, 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,一次都没买,不可能持有股票,故为 负无穷

使用压缩状态解法时,

一次买卖、多次买卖都只需要一个 dp0 和 dp1,分别初始化为0和 INT_MIN

K次买卖时,初始化两个数组 dp0[k+1] 和 dp1[k+1],元素都分别初始化为0和INT_MIN

循环时, 天数 就 从(i=0,i

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

字符串编辑

数学

1. 判断数字是否能被7整除

知乎解析

思路:

  • 21 能被7 整除 -》 数字N能不7整除 = 数字N去除个位数 并 减 个位数的2倍后 能被7整除
  • 1001能被7整除 -》数字N能被7整除= 数字N最末三位数字和 其余数字的差 能被 7 整除

根据21可以被7整除的原理:

设一个数字是六位数,其每位 数字为 ABCDEF,如785467,A=7, B=8 … F=7

ABCDEF 可以表示为 10*(ABCDE) + F

ABCDEF = 10*(ABCDE) + F = ( 20*(ABCDE) + 2F ) / 2 = ( 21*(ABCDE) + 2F - ABCDE ) / 2

ABCDEF 能被7整除,意味着 ( 21*(ABCDE) + 2F - ABCDE ) 能被7整除,因为21*(ABCDE) 肯定能被7整除,所以,只要 ABCDE-2F 能被7整除,那么原数字 ABCDEF就可以被7整除。

根据1001可以被7整除(也可以被13整除)的原理:

ABCDEF 可以表示为 1000*(ABC) + DEF

ABCDEF = 1000*(ABC) + DEF = 1001*(ABC) + DEF -ABC

ABCDEF 能被7整除,意味着 ( 1001*(ABC) + DEF -ABC ) 能被7整除,因为1001*(ABC)肯定能被7整除,所以,只要 DEF -ABC 能被7整除,那么原数字 ABCDEF就可以被7整除。

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