石器时代 —— Leetcode刷题日记 (二 算法思维)

算法是寻找思维定式的思维

前篇 - 数据结构

文章目录

2 算法思维

数学技巧

脑筋急转弯 - 简单的几个博奕类问题

  • Nim游戏,你和你的朋友面前有一堆石子,你们轮流拿,一次至少拿一颗,最多拿三颗,谁拿走最后一颗石子谁获胜。 return n%4!=0;
  • 石头游戏,一行石头,只能选左选右,谁先手谁赢,因为选择奇数位或偶数位中和较大的序列就能赢!
  • 电灯开关问题,return sqrt(n). 因为1、奇数次才能亮;2、因数都是成对存在除非因子相同;3、总共有 16 盏灯,我们求 16 的平方根,等于 4,这就说明最后会有 4 盏灯亮着,它们分别是第 11=1 盏、第 22=4 盏、第 33=9 盏和第 44=16 盏

204 素数(质数)筛选

TLE暴力搜索

class Solution {
public:
    int countPrimes(int n) {
        int res = 0;
        for (int i = 2; i < n; ++i) {
            bool flag = false;
            for (int j = 2; j * j <= i; ++j) {
                if (i % j == 0)
                    flag = true;
            }
            if (!flag) res++;
        }
        return res;
    }
};

高效算法 Sieve of Eratosthenes:

class Solution {
public:
    int countPrimes(int n) {
        if (n < 2) return 0;
        int cnt = 0;
        vector<bool> nums(n,true); 
        nums[0] = false; nums[1] = false;
        for (int i = 2; i * i < n; ++i) {
            if (nums[i]) {
                for (int j = i * i; j < n; j += i)
                    nums[j] = false;
            }
        }
        for (auto n : nums) cnt += n;
        return cnt;
    }
};

372 高效的模幂运算

a^b 对 1337 取模,a 是一个正整数,b 是一个非常大的正整数且会以数组形式给出。
模和取余的区别:链接
(1)在这里插入图片描述
(2) (a * b) % k = (a % k)(b % k) % k

class Solution {
public:
    int N = 1337;
    int superPow(int a, vector<int>& b) {
        if (b.empty()) return 1;
        if (b.size() == 1) return epow(a,b[0]) % N;
        int t = b.back(); b.pop_back();
        return ((epow(a,t) % N) * (epow(superPow(a,b), 10) % N)) % N;
    }
    int epow(int a, int b) {
        int res = 1;
        a %= N;
        while (b--) {
            res *= a;
            res %= N;
        }
        return res;
    }
};

(3) 高效求幂
在这里插入图片描述

int fastPow(int a, int b) {
    if (b == 0) return 1;
    a %= N;
    if (b%2==1) return (a*fastPow(a,b-1))%N;
    else {
        int x = fastPow(a,b/2);
        return (x * x) % N;
    }
}

洗牌算法(随机乱置算法)

链接

  • 什么是真的乱
    • 产生的结果必须有 n! 种可能,否则就是错误的 (全排列)
    • 正确性衡量标准是:对于每种可能的结果出现的概率必须相等,也就是说要足够随机。
  • 如何达到
  • 验证:蒙特卡洛或者每位置上出现数字的频率

常见位运算

  • 利用或操作 | 和空格将英文字符转换为小写
  • 利用与操作 & 和下划线将英文字符转换为大写
  • 利用异或操作 ^ 和空格进行英文字符大小写互换
  • 判断两个数是否异号:bool f = ((x ^ y) < 0);
  • 交换两个数:a^=b; b^=a; a^=b
  • 加1:-~n
  • 减1:~-n
  • 消除数字 n 的二进制表示中的最后一个1:n&(n-1)
  • 判断是不是2的幂:n&(n-1) == 0,作用是将n的二进制表示中的最低位为1的改为0!
  • 得到n最低位的1的位置:n&(-n)

下面2个题同属一类题,有排序/异或/映射 三大思路!

PS: 268 寻找缺失元素

W1 排序比较
class Solution {
public:
    int missingNumber(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        for (int i = 0; i < nums.size(); ++i) {
            if (i != nums[i]) return i;
        }
        return nums.size();
    }
};
W2 哈希表

使用unordered_set储存后,在0~n搜索比较!

W3 位运算

在这里插入图片描述
n^0 = n; n^n = 0;

int missingNumber(int[] nums) {
    int n = nums.length;
    int res = 0;
    // 先和新补的索引异或一下
    res ^= n;
    // 和其他的元素、索引做异或
    for (int i = 0; i < n; i++)
        res ^= i ^ nums[i];
    return res;
}
W4 等差数列

sum(0,1,…n) - sum(nums)

public int missingNumber(int[] nums) {
    int n = nums.length;
    int res = 0;
    // 新补的索引
    res += n - 0;
    // 剩下索引和元素的差加起来
    for (int i = 0; i < n; i++) 
        res += i - nums[i];
    return res;
}

PS: 645 同时找到缺少或重复的元素

W1 哈希表
class Solution {
public:
    vector<int> findErrorNums(vector<int>& nums) {
        unordered_map<int,int> m;
        int a = 0, b = 0;
        for (int i = 0; i < nums.size(); ++i) m[nums[i]]++;
        for (int i = 1; i <= nums.size(); ++i) {
            if (!m.count(i)) a = i;
            if (m[i] > 1) b = i;
        }
        return {b,a};
    }
};
W2 映射

在这里插入图片描述

class Solution {
public:
    vector<int> findErrorNums(vector<int>& nums) {
        int n = nums.size();
        int dup = -1, mis = -1;
        for (auto& n : nums) { // 引用
            int idx = abs(n) - 1;
            if (nums[idx] < 0) dup = abs(n);
            else nums[idx] *= -1;
        }
        for (int i = 0; i < n; ++i) 
            if (nums[i] > 0) mis = i+1;
        return {dup,mis};
    }
};

反直觉的概率问题

男孩女孩问题

一家两个小孩,其中一个是男孩,问另一个是男孩的概率?考虑年龄差异带来的不同,哥哥妹妹和姐姐弟弟不同,就是1/3

生日悖论

一屋子人,多少人情况下,存在相同生日的人的情况概率为0.5?求其反问题,每个人都是唯一生日的概率,365/365 * 364/365 * … 大概23个之后是0.497,即存在的概率是0.503!即要考虑整体

三门问题

三道门,两个门之后是养,一个门之后是跑车。你先选一个门,之后主持人将剩下门中是羊的门打开,问你换不换门?不换就是1/3,换就是2/3,所以从概率上来说就是要换!
你选的那道门有车概率为1/3,而剩下的是2/3,所以换其实就是在使用主持人帮你“浓缩”过的“一个门”
在这里插入图片描述

累积数组(前缀和)

L560 和为K的子数组

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
    // 连续子数组之和跟其出现次数之间的映射
        unordered_map<int,int> m{{0,1}}; 
        int sum = 0;
        int res = 0;
        for (auto& n : nums) {
            sum += n;
            res+=m[sum-k];
            m[sum]++;
        }
        return res;
    }
};

无限序列中随机抽取元素

蓄水塘抽烟问题 Reservoir Sampling,定义:链接
蓄水池抽样是一系列的随机算法,其目的在于从包含 nn 个项目的集合 SS 中选取 kk 个样本,其中 nn 为一很大或未知的数量,尤其适用于不能把所有 nn 个项目都存放到内存的情况。
随机是均匀随机(uniform random)
==> 当你遇到第 i 个元素时,应该有 1/i 的概率选择该元素,1 - 1/i 的概率保持原有的选择。
证明
对于第 i 个元素,它被选择的概率就是:
第 i 个元素被选择的概率是 1/i,第 i+1 次不被替换的概率是 1 - 1/(i+1),以此类推,相乘就是第 i 个元素最终被选中的概率,就是 1/n
在这里插入图片描述
同理,如果要随机选择 k 个数,只要在第 i 个元素处以 k/i 的概率选择该元素,以 1 - k/i 的概率保持原有选择即可。
在这里插入图片描述
流程

  • 首先构建一个可容纳 k个元素的数组,将序列的前 k 个元素放入数组中。
  • 然后对于第 j (j>k) 个元素开始,以 k / j 的概率来决定该元素是否被替换到数组中(数组中的k个元素被替换的概率是相同的)。 当遍历完所有元素之后,数组中剩下的元素即为所需采取的样本。
/* 返回链表中 k 个随机节点的值 */
int[] getRandom(ListNode head, int k) {
    Random r = new Random();
    int[] res = new int[k];
    ListNode p = head;

    // 前 k 个元素先默认选上
    for (int j = 0; j < k && p != null; j++) {
        res[j] = p.val;
        p = p.next;
    }

    int i = k;
    // while 循环遍历链表
    while (p != null) {
        // 生成一个 [0, i) 之间的整数
        int j = r.nextInt(++i);
        // 这个整数小于 k 的概率就是 k/i
        if (j < k) {
            res[j] = p.val;
        }
        p = p.next;
    }
    return res;
}

382 链表随机节点

如果知道链表的长度,那么直接取0~len的随机数就行!
但是链表可能很长,我们没法提前知道长度,所以要使用上述的理论!

class Solution {
private:
    ListNode *head;
public:
    /** @param head The linked list's head.
        Note that the head is guaranteed to be not null, so it contains at least one node. */
    Solution(ListNode* head) {
        this->head = head;
    }
    /** Returns a random node's value. */
    int getRandom() {
        int res = head->val, i = 2;
        ListNode *cur = head->next;
        while (cur) {
            int j = rand() % i;
            if (j == 0) res = cur->val; // 概率 1/i
            i++;
            cur = cur->next;
        }
        return res;
    }
};

398 随机数索引

int[] nums = new int[] {1,2,3,3,3};
Solution solution = new Solution(nums);
// pick(3) 应该返回索引 2,3 或者 4。每个索引的返回概率应该相等。
solution.pick(3);
// pick(1) 应该返回 0。因为只有nums[0]等于1。
solution.pick(1);
class Solution {
private:
    vector<int> nums;
public:
    Solution(vector<int>& nums) : nums(nums) {}
    int pick(int target) {
        int cnt = 0, res = -1;
        for (int i = 0; i < nums.size(); ++i) {
            if (nums[i] != target) continue;
            cnt++;
            if (rand()%cnt==0) res = i;
        }
        return res;
    }
}

加权随机抽样问题要怎么解决呢?

排序

  • 7个排序算法 GitHub
  • 分配式排序略
  • 交换类
    在这里插入图片描述
    在这里插入图片描述
void bubbleSort(vector<int>& nums) { 
    // O(n^2) O(n) O(n^2) 稳定 noAC
    int n = nums.size();
    bool loop = true;
    for (int i = 0; i < n - 1 && loop; i++) {
        for (int j = n - 1, loop = false; j > i; j--) {
            if (nums[j] < nums[j-1]) 
                swap(nums[j], nums[j-1]);
                loop = true;
        }
    }
}
void quickSort(vector<int>& nums, int left, int right) {
       // O(nlogn)	O(nlogn) O(n^2) 不稳定 AC
       if (left >= right) return;
       int pivot = nums[left];
       int c1 = left, c2 = right;
       while (c1 < c2) {
           while (c1 < c2 && nums[c2] > pivot) c2--;
           if (c1 < c2) nums[c1++] = nums[c2];
           while (c1 < c2 && nums[c1] < pivot) c1++;
           if (c1 < c2) nums[c2--] = nums[c1];
       }
       nums[c1] = pivot;
       quickSort(nums, left, c1 - 1);
       quickSort(nums, c1 + 1, right);
}
  • 选择类
    在这里插入图片描述
    在这里插入图片描述
void selectSort(vector<int>& nums) {
    // O(n^2) O(n^2) O(n^2) 不稳定 noAC
    int n = nums.size();
    for (int i = 0, j, min; i < n - 1; i++) {
        for (j = i + 1, min = i; j < n; j++) 
            if (nums[j] < nums[min]) min = j;
        swap(nums[min], nums[i]);
    }
}
void heapBuild(vector<int>& nums, int root, int end) {
    int left = 2 * root + 1; // 0为根节点
    if (left >= end) return;
    int right = left + 1;
    int maxIdx = ((right < end) && (nums[right] > nums[left]))?right:left;
    if (nums[maxIdx] > nums[root]) {
        swap(nums[root], nums[maxIdx]);
        heapBuild(nums, maxIdx, end);
    }
}
void heapSort(vector<int>& nums) {
    // O(nlogn)	O(nlogn) O(nlogn) 不稳定 AC
    int n = nums.size();
    for (int i = n / 2 - 1; i >= 0; i--)
        heapBuild(nums, i, n);
    for (int i = n - 1; i > 0; i--) {
        swap(nums[0], nums[i]);
        heapBuild(nums, 0, i);
    }
}
  • 插入类
    在这里插入图片描述
    在这里插入图片描述
void insertSort(vector<int>& nums) {
    // O(n^2) O(n) O(n^2) 稳定 noAC
    int n = nums.size();
    if (n < 2) return;
    for (int i = 1; i < n; i++)
        for (int j = i - 1; j >= 0 && nums[j] > nums[j+1]; j--)
            swap(nums[j], nums[j + 1]);
}
void shellSort(vector<int>& nums) {
    // 	O(n^1.25) 不稳定 noAC
    int n = nums.size();
    if (n < 2) return;
    for (int gap = n / 2; gap > 0; gap--) 
        for (int i = gap; i < n; i++) 
            for (int j = i - gap; j >= 0 && nums[j] > nums[j + gap]; j -= gap)
                swap(nums[j], nums[j + gap]);
}
  • 归并
    在这里插入图片描述
void merge(vector<int>& nums, int a, int b, int c) {
    vector<int> nums2 {nums.begin()+b+1, nums.begin()+c+1};
    int x = b, y = nums2.size() - 1, z = c;
    while (x >= a && y >= 0) {
        if (nums2[y] > nums[x]) nums[z--] = nums2[y--];
        else nums[z--] = nums[x--];
    }
    while (y >= 0) nums[z--] = nums2[y--];
}
// 递归
void mergeSort(vector<int>& nums, int left, int right) {
    // O(nlogn) O(nlogn) O(nlogn) 稳定 AC
    if (left >= right) return;
    int mid = left + (right - left) / 2;
    mergeSort(nums, left, mid);
    mergeSort(nums, mid + 1, right);
    merge(nums, left, mid, right);
}
// 迭代
void mergeSort(vector<int>& nums) {
    int sz = 1, left, mid, right;
    int N = nums.size();
    while (sz < N) {
        left = 0; // 先sz = 1排一波 sz = 2 排一波 ...
        while (left + sz < N) {
            mid = left + sz - 1;
            right = mid + sz;
            if (right >= N) right = N - 1;
            merge(nums, left, mid, right);
            left = right + 1;
        }
        sz *= 2;
    }
}
  • 分配类
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

分治

归并排序
XX

动态规划

动规是运筹学的一种最优化方法,用于计算最值!

  • 特点:这类问题存在「重叠子问题」,导致暴力枚举效率低下!
    具备「最优子结构」正确的「状态转移方程」 注意动态规划有两种形式 (数组和迭代)

  • 思路/流程: 明确「状态」 > 定义 dp 数组/函数的含义 > 明确「选择/择优」> 明确 base case

    • 确定dp数组(dp table)以及下标的含义
    • 确定递推公式
    • dp数组如何初始化
    • 确定遍历顺序
    • 举例推导dp数组
  • 递归树
    在这里插入图片描述
    递归算法的时间复杂度怎么计算?子问题个数乘以解决一个子问题需要的时间。图中复杂度O(2^n).
    爆炸!如f(18)计算了两次!即重复子问题! 纯粹迭代极其暴力,所以有两种解决方法:
    1 带备忘录的递归 相当于递归树「剪枝」O(n) 自底向上 0 - n
    2 递归数组从低到高 O(n) 注意解决数组初始条件问题 递归数组多一维或for里面加if 自顶向下 n - 0

  • 递归数组的空间优化(减少数组维度)

  • 暴力解代表找到了 状态转移方程

  • 最优子结构 上述斐波那契数列没有体现出来,因为不是求最值!min() max() 之类出现在转移方程

  • DP数组的遍历方向:
    正向 + 反向 + 斜向 ( 如按字符串长度小到大遍历 ),原则如下:
    1、遍历的过程中,所需的状态必须是已经计算出来的。且一次循环不能覆盖!
    2、遍历的终点必须是存储结果的那个位置。

  • 例子:
    回文序列个数:
    dp[i][j] 需要从 dp[i+1][j], dp[i][j-1], dp[i+1][j-1] 转移而来,想要求的最终答案是 dp[0][n-1]
    在这里插入图片描述
    在这里插入图片描述

322 凑零钱

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int res = 0;
        vector<int> dp(amount+1, amount+1);
        dp[0] = 0;
        for (auto& n : coins) {
            for (int i = n; i < amount + 1; ++i) {
                dp[i] = min(dp[i-n]+1, dp[i]);
            }
        }
        return (dp[amount] == amount+1) ? -1 : dp[amount];
    }
};

300. 最长上升子序列

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

W1 动态规划

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

W2 二分查找

纸牌接龙游戏玩法
在这里插入图片描述
规则:只能把点数小的牌压到点数比它大的牌上。如果当前牌点数较大没有可以放置的堆,则新建一个堆,把这张牌放进去。如果当前牌有多个堆可供选择,则选择最左边的堆放置。
所以可以使用二分查找法搜索栈顶(2,4,7,8,Q),设置数组表示各个堆的栈顶元素!O(nlgn)!
查找左边界

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        if (n < 2) return n;
        vector<int> tops;
        for (int i = 0; i < n; ++i) {
            int t = nums[i];
            int left = 0, right = tops.size();
            while (left < right) {
                int mid = left + (right - left) / 2;
                if (tops[mid] == t) right = mid;
                else if (tops[mid] > t) right = mid;
                else left = mid + 1;
            }
            if (left == tops.size()) tops.push_back(t);
            else tops[left] = t;
        }
        return tops.size();
    }
};

354 信封嵌套问题

  • 求嵌套最多的信封的个数!
    可以看成最长上升子序列的进阶题!
class Solution {
public:
    int maxEnvelopes(vector<vector<int>>& envelopes) {
        vector<int> dp(envelopes.size(),1);
        sort(envelopes.begin(),envelopes.end(),
            [](const vector<int>&a, const vector<int>&b){return a[0]<b[0];});
        for (int i = 0; i < envelopes.size(); ++i) {
            for (int j = i; j >= 0; --j) {
                if (envelopes[i][0] > envelopes[j][0] &&
                    envelopes[i][1] > envelopes[j][1]) {
                    dp[i] = max(dp[i], dp[j]+1);
                }
            }
        }
        int M = 0;
        for (auto& x : dp) M = max(M, x);
        return M;
    }
};

这种偏序问题,如果按照动态规划或者二分查找来做会导致复杂度指数增加!
这种偏序问题,就可以使用数据结构,如树状数组解决!
树状数组XX

673 最长递增子序列的个数

  • 给定一个未经过排序的数组,找到最长子序列有几个?
  • 两个动态规划的交叠
class Solution {
public:
    int findNumberOfLIS(vector<int>& nums) {
        int n = nums.size();
        if (n < 2) return n;
        vector<int> len(n,1), cnt(n,1);
        int mx = 0, res = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < i; ++j) {
                if (nums[i] <= nums[j]) continue;
                if (len[i] == len[j]+1) cnt[i] += cnt[j];
                else if (len[i] < len[j]+1) {
                    len[i] = len[j] + 1;
                    cnt[i] = cnt[j];
                }
            }
            if (len[i] == mx) res += cnt[i];
            else if (len[i] > mx){
                res = cnt[i];
                mx = len[i];
            }
        }
        return res;
    }
};

1143 最大公共子序列数

解决两个字符串的动态规划问题,一般都是用两个指针 i,j 分别指向两个字符串的最后,然后一步步往前走,缩小问题的规模。
找两个字符串间共有的字符串(不一定相邻)个数

W1 递归+记忆数组

注意下面的代码,dfs函数参数中string替换string&会导致TLE,因为临时变量赋值的耗时??

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

W2 动态规划

dp[i][j] 表示字符串1的 0>>i 和字符串2的 0>>j 间的最长公共子序列长度!

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

总结:子序列问题!子序列问题本身就相对子串、子数组更困难一些,因为前者是不连续的序列,而后两者是连续的。
这类问题的框架有两种:
(1)一维数组

int n = array.length;
int[] dp = new int[n];
for (int i = 1; i < n; i++) {
    for (int j = 0; j < i; j++) {
        dp[i] = 最值(dp[i], dp[j] + ...)
    }
}

在子数组 array[0…i] 中,我们要求的子序列(最长递增子序列)的长度是 dp[i]。
(2)二维数组

int n = arr.length;
int[][] dp = new dp[n][n];
for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        if (arr[i] == arr[j]) 
            dp[i][j] = dp[i][j] + ...
        else
            dp[i][j] = 最值(...)
    }
}

(2.1)涉及两个字符串/数组时(比如最长公共子序列),dp 数组的含义如下:
子数组 arr1[0…i] 和子数组 arr2[0…j] 中,我们要求的子序列(最长公共子序列)长度为 dp[i][j]。
(2.2)只涉及一个字符串/数组时(比如本文要讲的最长回文子序列),dp 数组的含义如下:
在子数组 array[i…j] 中,我们要求的子序列(最长回文子序列)的长度为 dp[i][j]。

最长公共子序列打印

  • 使用两个DP数组
class Solution {
private:
    string res, str1, str2;
    void Printf(vector<vector<int>>& dp, vector<vector<int>>& fa, int i, int j) {
        int N1 = dp.size(), N2 = dp[0].size();
        if (i < 0 || i >= N1 || j < 0 || j >= N2) return;
        if (fa[i][j] == 0) {
            Printf(dp, fa, i - 1, j - 1);
            res += str1[i];
        }
        else if (fa[i][j] == 1) {
            Printf(dp, fa, i - 1, j);
            // cout << str1[i - 1];
        } else {
            Printf(dp, fa, i, j-1);
            // cout << str2[j - 1];
        }
    }
public:
    string LCS(string& s1, string& s2) {
        str1 = s1, str2 = s2;
        // Handler
        int N1 = str1.size(), N2 = str2.size();
        vector<vector<int>> dp(N1+1, vector<int>(N2+1, 0));
        vector<vector<int>> fa(N1+1, vector<int>(N2+1, 0));
        for (int i = 1; i <= N1; i++) {
            for (int j = 1; j <= N2; j++) {
                if (str1[i-1] == str2[j-1]) 
                	dp[i][j] = dp[i-1][j-1] + 1;
                else {
                    if (dp[i-1][j] > dp[i][j-1]) {
                        dp[i][j] = dp[i-1][j];
                        fa[i][j] = 1;
                    } else {
                        dp[i][j] = dp[i][j-1];
                        fa[i][j] = -1;
                    }
                }
            }
        }
        Printf(dp, fa, N1, N2);
        return res;
    }
};

5. 最长回文子串

可能会想,将反转字符串和字符串间找最大公共子串,但是这种思路不对!
所以要用双指针法!

516. 最长回文子序列

在子串 s[i…j] 中,最长回文子序列的长度为 dp[i][j]

  • 这里的重点在于根据递归公式得知的遍历顺序:可知 i i i需要从 i + 1 i+1 i+1 j j j需要 j − 1 j-1 j1得到,所以 i i i方向得从大到小进行遍历, j j j方向得从小到大进行遍历
class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n = s.size();
        vector<vector<int>> dp(n, vector<int>(n, 0));
        for (int i = n - 1; i >= 0; i--) {
            dp[i][i] = 1;
            for (int j = i + 1; j < n; j++) {
                if (s[i] == s[j]) dp[i][j] = dp[i+1][j-1] + 2;
                else dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
            }
        }
        return dp[0][n-1];
    }
};

72 编辑距离

编辑距离的刷题流程

  • 39判断子序列:只判断 删除情况下的子序列匹配
  • 115不同子序列:统计删除情况下的子序列匹配个数
  • 583两个字符串的删除操作:两个字符串都可以删除了
  • 72编辑距离:考虑两个字符中一个字符可以(删除、插入和替换),由于字符串删除等于另一个字符串的插入,所以72题等价于583加上替换的操作

题意

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符

dp[i][j]: 返回 s1[0…i] 和 s2[0…j] 的最小编辑距离

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

将动态规划转换为递归+备忘录

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

博弈问题

nim游戏+石头游戏+电灯开关
博弈问题要假设你和你的对手一样都很聪明!
博弈问题是典型的动态规划问题 dp[i,j].who
修改过的石子游戏,不限堆数,计算先手后手的石子差值!见 添加链接描述

高楼扔鸡蛋

K个鸡蛋,N层楼,每个鸡蛋在高于某层楼扔下后会摔碎,鸡蛋摔碎就不能再用!
计算至少扔几次才能找到这个楼层!
如果鸡蛋数无限,那答案就可以用二分查找思想得到答案 lgN向上取整!
但是这里鸡蛋是有限的,一个朴素的思想是:
— 第一个鸡蛋可以尽可能大间隔的试,后序的就可以在间隔内试出!
K = 2的算法思想 解释见 Youtube链接

动态数组:dp[k][n] 拥有k个鸡蛋要求检测n层楼 的最少操作数!
转移公式:
d p ( K , N ) = min ⁡ 0 < = i < = N { max ⁡ { d p ( K − 1 , i − 1 ) , d p ( K , N − i ) } + 1 } d p(K, N)=\min _{0<=i<=N}\{\max \{d p(K-1, i-1), d p(K, N-i)\}+1\} dp(K,N)=0<=i<=Nmin{max{dp(K1,i1),dp(K,Ni)}+1}
框架:

def dp(K, N):
    for 1 <= i <= N:
        # 最坏情况下的最少扔鸡蛋次数
        res = min(res, 
                  max( 
                        dp(K - 1, i - 1), # 碎
                        dp(K, N - i)      # 没碎
                     ) + 1 # 在第 i 楼扔了一次
                 )
    return res

朴素代码:TLE!O(K*N^2)

class Solution {
public:
    int superEggDrop(int K, int N) {
        vector<vector<int>> dp(K+1,vector<int>(N+1));
        for (int i = 0; i <= N; ++i) dp[1][i] = i;
        for (int i = 2; i <= K; ++i) {
            for (int j = 1; j <= N; ++j) {
                dp[i][j] = j; // 线性搜索(最多情况)
                for (int k = 1; k < j; ++k) {
                    dp[i][j] = min(dp[i][j], 1+max(dp[i-1][k-1], dp[i][j-k]));
                }
            }
        }
        return dp.back().back();
    }
};

在这里插入图片描述
优化技巧1: 可以用二分查找替换最内循环!找到交点, O(K*NlgN) 可过OJ!

class Solution {
public:
    int superEggDrop(int K, int N) {
        vector<vector<int>> dp(K+1,vector<int>(N+1));
        for (int i = 0; i <= N; ++i) dp[1][i] = i;
        for (int i = 2; i <= K; ++i) {
            for (int j = 1; j <= N; ++j) {
                dp[i][j] = j; // 线性搜索(最多情况)
                // 二分查找交点
                int left = 1, right = j;
                while (left <= right) {
                    int mid = left + (right - left) / 2;
                    int broken = dp[i][mid-1];
                    int noBroken = dp[i-1][j-mid];
                    if (broken > noBroken) {
                        right = mid - 1;
                        dp[i][j] = min(dp[i][j], broken+1);
                    }
                    else {
                        left = mid + 1;
                        dp[i][j] = min(dp[i][j], noBroken+1);
                    }
                }
            }
        }
        return dp.back().back();
    }
};

优化技巧2
(1)重新定义状态转移
确定当前的鸡蛋个数和最多允许的扔鸡蛋次数,就知道能够确定 F 的最高楼层数
dp[1][7] = 7,给你一个鸡蛋允许扔7次,只能确定小于等于7层的楼

  • K 个 鸡 蛋 , 可 以 扔 T 次 ! K个鸡蛋,可以扔T次! KT
  • 如果鸡蛋没有碎,那么对应的是 f ( K , T − 1 ) f(K, T - 1) f(K,T1),也就是说在这一层的上方可以有 f ( K , T − 1 ) f(K, T - 1) f(K,T1)层;
  • 如果鸡蛋碎了,那么对应的是 f ( K − 1 , T − 1 ) f(K - 1, T - 1) f(K1,T1),也就是说在这一层的下方可以有 f ( K − 1 , T − 1 ) f(K - 1, T - 1) f(K1,T1)层。
  • 因此我们就可以写出状态转移方程:
    f ( K , T ) = 1 + f ( K − 1 , T − 1 ) + f ( K − 1 , T ) f(K,T)=1+f(K−1,T−1)+f(K−1,T) f(K,T)=1+f(K1,T1)+f(K1,T)
    边界条件为:当 T ≥ 1 T \geq 1 T1 的时候 f ( T , 1 ) = T f(T, 1) = T f(T,1)=T,当 K ≥ 1 K \geq 1 K1时, f ( K , 1 ) = 1 f(K,1) = 1 f(K,1)=1
int superEggDrop(int K, int N) {
    int m = 0;
    while (dp[K][m] < N) {
        m++;
        // 状态转移方程
    }
    return m;
}

代码瞬间简化!

class Solution {
public:
    int superEggDrop(int K, int N) {
        vector<vector<int>> dp(K+1,vector<int>(N+1,0));
        int res = 0;
        while (dp[K][res] < N) {
            res++;
            for (int k = 1; k <= K; ++k) {
                dp[k][res] = dp[k][res-1] + dp[k-1][res-1] + 1;
            }
        }
        return res;
    }
};

(2)优化空间复杂度:
看这个简洁度!

class Solution {
public:
    int superEggDrop(int K, int N) {
        vector<int> dp(K+1,0);
        int res = 0;
        while (dp[K] < N) {
            res++;
            for (int k = K; k > 0; --k) {
                dp[k] = dp[k] + dp[k-1] + 1;
            }
        }
        return res;
    }
};

背包问题

01背包问题

for 状态1 in 状态1的所有取值:
    for 状态2 in 状态2的所有取值:
        for ...
            dp[状态1][状态2][...] = 择优(选择1,选择2...)

dp[i][w] 表示:对于前 i 个物品,当前背包的容量为 w 时,这种情况下可以装下的最大价值是

for i in [1..N]:
    for w in [1..W]:
        dp[i][w] = max(
            dp[i-1][w],
            dp[i-1][w - wt[i-1]] + val[i-1]
        )
return dp[N][W]

空间优化:

for i in [1..N]:
    for w in [W..1]:
        dp[w] = max(dp[w], dp[w - wt[i-1]] + val[i-1])
return dp[W]

416. 分割等和子集
01背包,判断半和是否能被装满!
dp[i]表示重量为i的能否装满!
注意:第二个for循环从大值开始,否则循环内的会被覆盖!

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

474. 一和零

  • 你的任务是使用给定的 m 个 0 和 n 个 1 ,找到能拼出存在于数组中的字符串的最大数量。每个 0 和 1 至多被使用一次。
class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
        for (auto& s : strs) {
            int zeros = 0, ones = 0;
            for (auto& c : s) {zeros+=(c=='0'); ones+=(c=='1');}
            for (int i = m; i >= zeros; i--) {
                for (int j = n; j >= ones; j--) {
                    dp[i][j] = max(dp[i][j], dp[i-zeros][j-ones]+1);
                }
            }
        }
        return dp[m][n];
    }
};

1049. 最后一块石头的重量 II
在这里插入图片描述

  • 将数分为两堆,使其中一堆最为接近sum / 2, 从而转化为0-1背包问题
class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int sum = accumulate(stones.begin(), stones.end(), 0);
        vector<bool> dp(sum / 2 + 1, false); dp[0] = true;
        for (auto& s : stones) {
            for (int i = sum / 2; i >= s; i--) {
                dp[i] = dp[i] || dp[i - s];
            }
        }
        int x = 0;
        for (x = dp.size() - 1; x >= 0; --x) {
            if (dp[x]) break;
        }
        return sum - x * 2;
    }
};
class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int sum = accumulate(stones.begin(),stones.end(),0);
        int cap = sum / 2;
        vector<int> dp(cap+1, 0);
        for (auto& s : stones) {
            for (int i = cap; i >= s; --i) {
                dp[i] = max(dp[i],dp[i-s]+s);
            }
        }
        return sum - 2 * dp.back();
    }
};

完全背包问题

518. 零钱兑换 II

  • 给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount+1, 0); dp[0] = 1;
        for (auto& c : coins) {
            for (int i = c; i <= amount; ++i) {
                dp[i] += dp[i - c];
            } 
        }
        return dp.back();
    }
};

有限背包问题

XXX

正则表达式问题

leetcode 第 10 题:'.'匹配任何字符,’*‘匹配前面1~任意字符!
字符匹配递归框架:

def isMatch(text, pattern) -> bool:
    if pattern is empty: return (text is empty?)
    first_match = (text not empty) and pattern[0] == text[0]
    return first_match and isMatch(text[1:], pattern[1:])

(1)处理 " . “:判断加一个字符匹配就好了 pattern[0] in [text[0], ‘.’]
(2)处理” * ":不知道重复多少次,暴力递归 / 动态规划!

W1 暴力递归

竟然能过OJ!

我发现OJ的耗时电脑配置有关

class Solution {
public:
    bool isMatch(string s, string p) {
        if (p.empty()) return s.empty();
        bool m = (!s.empty()) && (p[0] == s[0] || p[0] == '.');
        if (p.size() >= 2 && p[1] == '*') // 处理*字符 重复0次||重复1次
            return isMatch(s,p.substr(2)) || (m && isMatch(s.substr(1),p));
        else return m && isMatch(s.substr(1), p.substr(1));
    }
};

W2 递归+备忘录

注意map<pair<int,int>,int>有效,而unordered_map不含pair的散列函数,所以要自己定义!
时间加快几十倍!!

class Solution {
private:
    // A hash function used to hash a pair of any kind 
    struct hash_pair { 
        template <class T1, class T2> 
        size_t operator()(const pair<T1, T2>& p) const
        { 
            auto hash1 = hash<T1>{}(p.first); 
            auto hash2 = hash<T2>{}(p.second); 
            return hash1 ^ hash2; 
        } 
    }; 
public:
    bool isMatch(string s, string p) {
        unordered_map<pair<int,int>, bool, hash_pair> memo;
        return dfsSearch(s,0,p,0,memo);
    }
    bool dfsSearch(string& s, int i, string& p, int j, 
        unordered_map<pair<int,int>, bool, hash_pair>& memo) {
        if (memo.count({i,j})) 
            return memo[{i,j}];
        if (j == p.size()) return i == s.size();
        bool firstMatch = i < s.size() && (s[i] == p[j] || p[j] == '.');
        bool secondMatch = false;
        if (p.size() > j + 1 && p[j+1] == '*')
            secondMatch = dfsSearch(s,i,p,j+2,memo) || 
                (firstMatch && dfsSearch(s,i+1,p,j,memo));
        else secondMatch = firstMatch && dfsSearch(s,i+1,p,j+1,memo);
        return memo[{i,j}] = secondMatch;
    }
};

W3 DP数组

dp[i][j] 表示 s[0,i) 和 p[0,j) 是否 match

 class Solution {
public:
    bool isMatch(string s, string p) {
        int n = s.size(), m = p.size();
        vector<vector<bool>> dp(n+1,vector<bool>(m+1, false));
        dp[0][0] = true;
        for (int i = 0; i <= n; ++i) {
            for (int j = 1; j <= m; ++j) {
                if (j > 1 && p[j - 1] == '*') {
                    dp[i][j] = dp[i][j-2] || // 重复0次
                        (i > 0 && (s[i-1] == p[j-2] || p[j-2] == '.') && dp[i-1][j]); // 重复1次
                } else {
                    dp[i][j] = i > 0 && dp[i-1][j-1] && 
                        (s[i-1] == p[j-1] || p[j-1] == '.'); // 不含*
                }
            }
        }
        return dp.back().back();
    }
};

四键键盘 (会员题)

含有四个键的键盘:A键,全选键,复制键,粘贴键
现允许按N次键,求最多能得到多少A
方法1:O(N^3)
动态规划数组:dp[n][a_num][copy] 分别表示剩余次数/a个数/缓冲区内a个数
该方法复杂度高!

	dp(n - 1, a_num + 1, copy),    # A
	解释:按下 A 键,屏幕上加一个字符
	同时消耗 1 个操作数
	dp(n - 1, a_num + copy, copy), # C-V
	解释:按下 C-V 粘贴,剪切板中的字符加入屏幕
	同时消耗 1 个操作数
	dp(n - 2, a_num, a_num)        # C-A C-C
	解释:全选和复制必然是联合使用的,
	剪切板中 A 的数量变为屏幕上 A 的数量
	同时消耗 2 个操作数

方法2:O(N^2)
最优策略只会有两种类型(1)全按A(2)A … … A 全选 复制 粘贴 … …

	public int maxA(int N) {
    int[] dp = new int[N + 1];
    dp[0] = 0;
    for (int i = 1; i <= N; i++) {
        // 按 A 键
        dp[i] = dp[i - 1] + 1;
        for (int j = 2; j < i; j++) {
            // 全选 & 复制 dp[j-2],连续粘贴 i - j 次
            // 屏幕上共 dp[j - 2] * (i - j + 1) 个 A
            dp[i] = Math.max(dp[i], dp[j - 2] * (i - j + 1));
        }
    }
    // N 次按键之后最多有几个 A?
    return dp[N];
}

字符串匹配KMP算法

在字符串S中找到字符串p的第一次出现的位置,若不存在则返回-1

W1 暴力匹配

在这里插入图片描述

class Solution {
public:
    int strStr(string haystack, string needle) {
        if (needle.empty()) return 0;
        if (haystack.size() < needle.size()) return -1;
        for (int i = 0; i <= haystack.size() - needle.size(); ++i) {
            int j = 0;
            for (; j < needle.size(); ++j) {
                if (haystack[i+j] != needle[j]) break;
            }
            if (j == needle.size()) return i;
        }
        return -1;
    }
};

W2 KMP算法

在这里插入图片描述

  • 暴力匹配每次匹配左指针还要回退,KMP算法不用回退!
  • KMP 算法永不回退 txt 的指针 i,不走回头路(不会重复扫描 txt),而是借助 dp 数组中储存的信息把 pat 移到正确的位置继续匹配,时间复杂度只需 O(N),用空间换时间,所以我认为它是一种动态规划算法。
  • 上图中的这个j 不要理解为索引,它的含义更准确地说应该是状态(state)
  • 思路:用p构造KMP,用KMP去匹配上s
  • 例如,ABABC字符匹配,建造KMP本质就是状态机建立,可以用动态规划实现。
  • 明确两个状态:当前状态和遇到的字符
dp[j][c] = next
0 <= j < M,代表当前的状态
0 <= c < 256,代表遇到的字符(ASCII 码)
0 <= next <= M,代表下一个状态

(1)构建状态转移图
找到最佳回退位置(shadow)!和必要的前进!
在这里插入图片描述

int X # 影子状态: 最近的回退状态
for 0 <= j < M:
    for 0 <= c < 256:
        if c == pat[j]:
            # 状态推进
            dp[j][c] = j + 1
        else: 
            # 状态重启
            # 委托 X 计算重启位置
            dp[j][c] = dp[X][c]

shadow 一定在 j 的后面,影子状态!
(2)搜索

全部代码如下:

class Solution {
public:
    int strStr(string haystack, string needle) {
        if (needle.empty()) return 0;
        vector<vector<int>> dp(needle.size(), vector<int>(256, 0));
        KMPCreator(needle, dp);
        int stat = 0;
        for (int i = 0; i < haystack.size(); ++i) {
            stat = dp[stat][haystack[i]];
            if (stat == needle.size()) return i - stat + 1;
        }
        return -1;
    }
private:
    void KMPCreator(string& needle, vector<vector<int>>& dp) {
        int shadow = 0;
        dp[0][needle[0]] = 1;
        for (int i = 1; i < needle.size(); ++i) {
            for (int c = 0; c < 256; c++)
                dp[i][c] = dp[shadow][c];
            dp[i][needle[i]] = i + 1;
            shadow = dp[shadow][needle[i]];
        }
    }
};

546. 移除盒子

[1, 3, 2, 2, 2, 3, 4, 3, 1]
----> [1, 3, 3, 4, 3, 1] (33=9 分)
----> [1, 3, 3, 3, 1] (1
1=1 分)
----> [1, 1] (33=9 分)
----> [] (2
2=4 分)

三维动态规划

dp[l][r][k]表示 l~r范围的操作的最大值,k表示l前和nums[l]相同的数字数目
在这里插入图片描述

递归法
class Solution {
public:
    int removeBoxes(vector<int>& boxes) {
        int dp[100][100][100] = {0};
        return dfs(boxes, 0, boxes.size() - 1, 0, dp);
    }
    int dfs(vector<int>& boxes, int start, int end, int k, int dp[100][100][100]) {
        if (start > end) return 0;
        if (dp[start][end][k] > 0) return dp[start][end][k];
        // #1 直接处理start
        for (int i = start; i < end; i++) { 
            // 这段for循环是为了提高效率 不用也行
            if (boxes[i + 1] != boxes[start]) break;
            k++;
            start++;
        }
        int res = (k + 1) * (k + 1) + dfs(boxes, start + 1, end, 0, dp);
        // #2 联合start和区间内元素处理
        for (int i = start + 1; i <= end; i++) {
            if (boxes[i] != boxes[start]) continue;
            res = max(res, 
                dfs(boxes, start + 1, i - 1, 0, dp) + 
                dfs(boxes, i, end, k + 1, dp));
        }
        return dp[start][end][k] = res;
    }
};
动态规划

这里的动规遍历方向和边界条件都不容易想到!

class Solution {
public:
    int removeBoxes(vector<int>& boxes) {
        int len = boxes.size();
        if (len < 2) return len;
        int dp[102][102][102] = {0};
        for (int subLen = 1; subLen <= len; subLen++) { // 区间长度
            for (int end = 1; end < len; end++) { // 右边界位置
                int start = end - subLen + 1; // 计算出左边界位置
                for (int k = 0; k <= start; k++) {
                    int res = dp[start+1][end][0] + (k + 1) * (k + 1);
                    for (int m = start + 1; m <= end; m++) {
                        if (boxes[m] != boxes[start]) continue;
                        res = max(res, dp[start+1][m-1][0] + dp[m][end][k+1]);
                    }
                    dp[start][end][k] = res;
                }
            }
        }
        return dp[0][len-1][0];
    }
};

股票买卖问题集合

  • 六道股票题的动态规划算法框架
    动态规划数组:dp[i][j][k],第一个是天数,第二个是 最多交易数,第三个是当前的持有状态 (0/1)
  • 状态转移图:
    在这里插入图片描述
  • base case:
    dp[-1][k][0] = dp[i][0][0] = 0
    dp[-1][k][1] = dp[i][0][1] = -infinity
  • 状态转移方程:
    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可以和上面的sell的k互换

121 只能买卖一次: k = 1, dp[i-1][0][0] = 0

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (prices.empty()) return 0;
        int n = prices.size();
        int dp1, dp2;
        for (int i = 0; i < n; ++i) {
            if (i == 0) {
                dp1 = 0;
                dp2 = -prices[0];
                continue;
            }
            dp1 = max(dp1, dp2+prices[i]);
            dp2 = max(dp2,-prices[i]);
        }
        return dp1;
    }
};

122 不限买卖次数: k=k-1, dp[i-1][k-1][0]=dp[i-1][k][0]

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (prices.empty()) return 0;
        int n = prices.size();
        int dp0 = 0, dp1 = 0;
        for (int i = 0; i < n; i++) {
            if (i == 0) {
                dp0 = 0, dp1 = -prices[0];
                continue;
            }
            dp0 = max(dp0, dp1+prices[i]);
            dp1 = max(dp1, dp0-prices[i]);
        }
        return dp0;
    }
};

123 限制买卖2次: k=2

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (prices.empty()) return 0;
        int n = prices.size();
        int dp00 = 0, dp01 = -prices[0];
        int dp10 = 0, dp11 = -prices[0];
        for (int i = 1; i < n; ++i) {
            dp10 = max(dp10, dp11+prices[i]);
            dp11 = max(dp11, dp00-prices[i]);
            dp00 = max(dp00, dp01+prices[i]);
            dp01 = max(dp01, -prices[i]);
        }
        return dp10;
    }
};

188 限制买卖k次

容易出现个delaysignal错误

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        if (prices.empty()) return 0;
        int n = prices.size();
        if (k > n / 2) return maxProfit_inf(prices);

        int dp[k+1][2];
        dp[0][0] = 0;
        dp[0][1] = -prices[0];

        for (int i = 0; i < n; ++i) {
            for (int j = 1; j < k+1; ++j) {
                if (i == 0) {
                    dp[j][0] = 0;
                    dp[j][1] = -prices[i];
                    continue;
                }
                dp[j][0] = max(dp[j][0],dp[j][1]+prices[i]);
                dp[j][1] = max(dp[j][1],dp[j-1][0]-prices[i]);
            }
        }
        return dp[k][0];
    }

private:
    int maxProfit_inf(vector<int>& prices) {
        if (prices.empty()) return 0;
        int n = prices.size();
        int dp0 = 0, dp1 = 0;
        for (int i = 0; i < n; i++) {
            if (i == 0) {
                dp0 = 0, dp1 = -prices[0];
                continue;
            }
            dp0 = max(dp0, dp1+prices[i]);
            dp1 = max(dp1, dp0-prices[i]);
        }
        return dp0;
    }
};

309 不限买卖次数 含有冷冻期(卖后一天不能买)

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])
解释:第 i 天选择 buy 的时候,要从 i-2 的状态转移,而不是 i-1 。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (prices.empty()) return 0;
        int n = prices.size();
        int dp0 = 0, dp1 = 0; 
        int pre = 0; // i-2
        for (int i = 0; i < n; i++) {
            if (i == 0) {
                dp0 = 0, dp1 = -prices[0];
                continue;
            }
            int tmp = dp0;
            dp0 = max(dp0, dp1+prices[i]);
            dp1 = max(dp1, pre-prices[i]);
            pre = tmp;
        }
        return dp0;
    }
};

714 不限买卖次数 交易需要手续费

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)
解释:相当于买入股票的价格升高了。
在第一个式子里减也是一样的,相当于卖出股票的价格减小了。

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        if (prices.empty()) return 0;
        int n = prices.size();
        int dp0 = 0, dp1 = 0;
        for (int i = 0; i < n; i++) {
            if (i == 0) {
                dp0 = 0, dp1 = -prices[0]-fee;
                continue;
            }
            dp0 = max(dp0, dp1+prices[i]);
            dp1 = max(dp1, dp0-prices[i]-fee);
        }
        return dp0;
    }
};

打家劫舍集合

198 & 213 & 337

198 一维数组 不能抢劫相邻房子

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

213 198+头尾相连

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

337 二叉树 不能相邻

下面的方法TLE:

class Solution {
public:
    int rob(TreeNode* root) {
        if (!root) return 0;
        int s0 = root->val;
        int s1 = rob(root->left) + rob(root->right);
        if (root->left)
            s0 += (rob(root->left->left) + rob(root->left->right));
        if (root->right)
            s0 += (rob(root->right->left) + rob(root->right->right));
        return max(s0, s1);
    }
};

增加记忆数组,以减少递归分支:

class Solution {
public:
    int rob(TreeNode* root) {
        unordered_map<TreeNode*, int> memo;
        return dfs(root, memo);
    }
    int dfs(TreeNode* root, unordered_map<TreeNode*, int>& memo) {
        if (!root) return 0;
        if (memo.count(root)) return memo[root];
        int s1 = 0;
        int s0 = dfs(root->left, memo)+dfs(root->right, memo);
        if (root->left)
            s1 += (dfs(root->left->left, memo) + dfs(root->left->right, memo));
        if (root->right)
            s1 += (dfs(root->right->left, memo) + dfs(root->right->right, memo));
        int val = max(root->val+s1, s0);
        memo[root] = val;
        return val;
    }
};

奇思妙想:
方法1 res[0] 表示不包含当前节点值的最大值,res[1] 表示包含当前值的最大值

class Solution {
public:
    int rob(TreeNode* root) {
        vector<int> res = dfs(root);
        return max(res[0], res[1]);
    }
    vector<int> dfs(TreeNode *root) {
        if (!root) return vector<int>(2, 0);
        vector<int> left = dfs(root->left);
        vector<int> right = dfs(root->right);
        vector<int> res(2, 0);
        res[0] = max(left[0], left[1]) + max(right[0], right[1]);
        res[1] = left[0] + right[0] + root->val;
        return res;
    }
};

方法2 里面的两个参数l和r表示分别从左子结点和右子结点开始 rob,分别能获得的最大钱数。

class Solution {
public:
    int rob(TreeNode* root) {
        int l = 0, r = 0;
        return helper(root, l, r);
    }
    int helper(TreeNode* node, int& l, int& r) {
        if (!node) return 0;
        int ll = 0, lr = 0, rl = 0, rr = 0;
        l = helper(node->left, ll, lr);
        r = helper(node->right, rl, rr);
        return max(node->val + ll + lr + rl + rr, l + r);
    }
};

数位DP

  • 数位就是 个十百等 …
  • 解决的问题:求出在给定区间 [ A , B ] [A,B] [A,B]内,符合条件 f ( i ) f(i) f(i)的数 i i i的个数
  • 条件 f ( i ) f(i) f(i) 一般与数的大小无关,而与数的组成有关
    由于数是按位dp,数的大小对复杂度的影响很小

贪心算法思想

属于动态规划的特例,需要满足更多条件!但是速度更快!
比如说一个算法问题使用【暴力解法】需要指数级时间,如果能使用动态规划【消除重叠子问题】,就可以降到多项式级别的时间,如果满足【贪心选择性质】,那么可以进一步降低时间复杂度,达到线性级别的。
贪心选择性质:我们不需要【递归地】计算出所有选择的具体结果然后比较求最值,而只需要做出那个最有【潜力】,看起来最优的选择即可。即妄图用局部最优的累计看为全局最优!
动态规划 = 最优子结构 + 重叠子问题
贪心算法 = 最优子结构 + 贪心选择

  • 动态规划希望复用子问题的解,最好被反复依赖。其本质还是穷举,所以当前并不知道哪个子问题的解会构成最终最优解。但知道这个子问题可能会被反复计算,所以把结果缓存起来。整个过程是【树状的搜索过程】。
  • 贪心希望每次都能排除一堆子问题。它不需要复用子问题的解,当前最优解从子问题最优解即可得出。整个过程是【线性的推导过程】。

不相交区间

L435. 无重叠区间

class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if (intervals.size() < 2) return 0;
        sort(intervals.begin(),intervals.end(),
            [](const vector<int>& a, const vector<int>& b) {return a[1] < b[1];});
        int res = 0, endx = intervals[0][1];
        for (int i = 1; i < intervals.size(); ++i) {
            if (intervals[i][0] < endx) res++;
            else endx = intervals[i][1];
        }
        return res;
    }
};

452. 用最少数量的箭引爆气球

有几个重叠一起的区间?

class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        if (points.size() < 2) return points.size();
        sort(points.begin(),points.end(),
            [](const vector<int>& a, const vector<int>& b) {return a[1] < b[1];});
        int res = 1, endx = points[0][1];
        for (int i = 1; i < points.size(); ++i) {
            if (points[i][0] > endx) {
                res++;
                endx = points[i][1];
            }
        }
        return res;
    }
};

跳跃问题

55 跳跃问题I
  • 判断你是否能够到达最后一个位置
W1 动规
class Solution {
public:
    bool canJump(vector<int>& nums) {
        vector<int> dp(nums.size(),0); // 剩余跳力
        for (int i = 1; i < nums.size(); ++i) {
            dp[i] = max(dp[i-1],nums[i-1])-1;
            if (dp[i] < 0) return false;
        }
        return true;
    }
};
W2 贪婪

看最远跳多远!

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int S = 0;
        for (int i = 0; i < nums.size() - 1; ++i) {
            S = max(S, i + nums[i]);
            if (S <= i) return false;
        }
        return (S >= nums.size() - 1);
    }
};
45 跳跃游戏II
  • 一定能到最后,求最少的步骤!
    递归TLE
class Solution {
public:
    int jump(vector<int>& nums) {
        int n = nums.size();
        vector<int> memo(n, n);
        return recur(nums, memo, 0);
    }
    int recur(vector<int>& nums, vector<int>& memo, int pos) {
        int n = nums.size();
        if (pos >= n - 1) return 0;
        if (memo[pos] != n) return memo[pos];
        int steps = nums[pos];
        for (int i = 1; i <= steps; ++i) {
            int subP = recur(nums, memo, pos + i);
            memo[pos] = min(memo[pos], subP+1);
        }
        return memo[pos];
    }
};

动态规划,仍然TLE!
d p i = min ⁡ j < i { d p j } + 1 d p_{i}=\min _{j<i}\left\{d p_{j}\right\}+1 dpi=j<imin{dpj}+1

class Solution {
public:
    int jump(vector<int>& nums) {
        if (nums.size() < 2) return 0;
        int n = nums.size();
        vector<int> dp(n, n);
        dp[n-1] = 0;
        for (int i = n - 2; i >= 0; i--) {
            int X = nums[i];
            int E = (i + X > n - 1) ? n : i + X + 1;
            int m = *min_element(dp.begin()+i+1,dp.begin()+E);
            dp[i] = min(dp[i],m+1);
        }
        return dp[0];
    }
};

使用单调队列优化!TODO!
如果动态规划都超时,说明该问题存在贪心选择性质无疑了。
贪婪
pre: 上一个范围,cur:当前范围

class Solution {
public:
    int jump(vector<int>& nums) {
        int n = nums.size();
        int pre = 0, cur = 0, res = 0;
        for (int i = 0; i < n - 1; ++i) {
            cur = max(cur, i+nums[i]);
            if (pre == i) {
                pre = cur;
                res++;
                if (cur >= n - 1) break;
            }
        }
        return res;
    }
};

递归

递归的江湖地位:

  • 递归是一种算法思维,分治和动态规划是递归基础上解决问题的具体方法,贪心算法是动规的子集,需要问题满足额外条件!
  • 分治算法:> 归并排序 分解 -> 解决 -> 合并

递归两个特征:结束条件和自我调用
递归解释性强,解决某些问题是高效的,但是某些问题会低效,因为需要额外的堆栈空间
写递归的技巧:不要试图跳进细节,因为大脑不适合压栈!

汉诺塔问题

  • 在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时受到以下限制:
    • 每次只能移动一个盘子;
    • 盘子只能从柱子顶端滑出移到下一根柱子;
    • 盘子只能叠在比它大的盘子上
  • 思路
    • 把n-1个盘子由A 移到 B
    • 把第n个盘子由 A移到 C
    • 把n-1个盘子由B 移到 C;
      在这里插入图片描述
class Solution {
public:
    void hanota(vector<int>& A, vector<int>& B, vector<int>& C) {
        int num = A.size();
        backtrack(num,A,B,C);
    }
    void backtrack(int num, vector<int>& A, vector<int>& B, vector<int>& C) {
        if (num == 1) {
            C.push_back(A.back());
            A.pop_back();
            return;
        }
        backtrack(num-1, A, C, B);
        C.push_back(A.back());
        A.pop_back();
        backtrack(num-1, B, A, C);
    }
};

969 烧饼排序

  • 假设盘子上有 n 块面积大小不一的烧饼,你如何用一把锅铲进行若干次翻转,让这些烧饼的大小有序(小的在上,大的在下)?
    在这里插入图片描述
  • 煎饼反转 pancakeSort 类似汉诺塔,但是没要求不能在小圆盘上放大圆盘。
-  找到 n 个饼中最大的那个。
-  把这个最大的饼移到最底下:将0~最大值反转;再反转全部!
-  递归调用 pancakeSort(A, n - 1)
- base case:n == 1 时,排序 1 个饼时不需要翻转。
class Solution {
public:
    vector<int> pancakeSort(vector<int>& A) {
        vector<int> res;
        recursive(A,res);
        return res;
    }
    void recursive(vector<int>& A, vector<int>& res) {
        if (A.size() < 2) return;
        auto p = max_element(A.begin(),A.end());
        int pos = distance(A.begin(),p);
        if (p != --A.end()) {
            res.push_back(pos+1);
            reverse(A.begin(),p+1);
            res.push_back(A.size());
            reverse(A.begin(),A.end());
        }
        A.pop_back();
        recursive(A, res);
    }
};

上述不一定得到最优结果!leetcode能过!

Floodfill算法

二维矩阵中的搜索问题,都逃不出这个算法框架

L733 图像填充

输入:
image = [[1,1,1],[1,1,0],[1,0,1]]
sr = 1, sc = 1, newColor = 2
输出: [[2,2,2],[2,2,0],[2,0,1]]
解析:
在图像的正中间,(坐标(sr,sc)=(1,1)),
在路径上所有符合条件的像素点的颜色都被更改成2。
四连通渲染!

class Solution {
public:
    vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int newColor) {
        int X = image[sr][sc];
        if (X == newColor) return image; // 这句需要有,否则重复着色会导致无穷递归!
        recursive(image,sr,sc,X,newColor);
        return image;
    }
    void recursive(vector<vector<int>>& image, int sr, int sc, int X, int newColor) {
        if (sr < 0 || sr >= image.size()) return;
        if (sc < 0 || sc >= image[0].size()) return;
        if (image[sr][sc] != X) return; // 1碰到原图中不连通的2碰到已着色的
        image[sr][sc] = newColor;
        recursive(image,sr+1,sc,X,newColor);
        recursive(image,sr-1,sc,X,newColor);
        recursive(image,sr,sc+1,X,newColor);
        recursive(image,sr,sc-1,X,newColor);
    }
};

或者用回溯思想+访问数组

void fill(int[][] image, int x, int y,
        int origColor, int newColor) {
    // 出界:超出数组边界
    if (!inArea(image, x, y)) return;
    // 碰壁:遇到其他颜色,超出 origColor 区域
    if (image[x][y] != origColor) return;
    // 已探索过的 origColor 区域
    if (image[x][y] == -1) return;
    image[x][y] = -1;
    fill(image, x, y + 1, origColor, newColor);
    fill(image, x, y - 1, origColor, newColor);
    fill(image, x - 1, y, origColor, newColor);
    fill(image, x + 1, y, origColor, newColor);
    // unchoose:将标记替换为 newColor
    image[x][y] = newColor;
}

拓展要求找到边界,而不是区域填充怎么搞?
这种漫水法有很多拓展,如边框着色:

1034 边框着色

  • 连通分量的边界是指连通分量中的所有与不在分量中的正方形相邻(四个方向上)的所有正方形,或者在网格的边界上(第一行/列或最后一行/列)的所有正方形
  • 即类似魔棒功能!
  • 细节问题:
    1 必须借助 visited 来记录已探索的坐标,而无法使用回溯算法;
    2 开头几个 if 顺序不可打乱。
class Solution {
public:
    vector<vector<int>> colorBorder(vector<vector<int>>& grid, int r0, int c0, int color) {
        vector<vector<bool>> visited(grid.size(),vector<bool>(grid[0].size(),false));
        backtrack(grid,visited,r0,c0,grid[r0][c0],color);
        return grid;
    }
    int backtrack(vector<vector<int>>& grid, vector<vector<bool>>& visited,
                    int r0, int c0, int orign, int color) {
        if (r0 < 0 || r0 >= grid.size()) return 0; // 边界
        if (c0 < 0 || c0 >= grid[0].size()) return 0;
        if (visited[r0][c0]) return 1; // 已放问
        if (grid[r0][c0] != orign) return 0; // 不连通
        visited[r0][c0] = true;
        int con = backtrack(grid,visited,r0-1,c0,orign,color) +
                    backtrack(grid,visited,r0+1,c0,orign,color) +
                    backtrack(grid,visited,r0,c0-1,orign,color) + 
                    backtrack(grid,visited,r0,c0+1,orign,color);
        if (con < 4) grid[r0][c0] = color;
        return 1;
    }
};

### 695 岛屿的最大面积;200 岛屿数量
岛屿问题不能用漫水法,因为漫水法不适合有多个连通域的问题!只会找到局部解!

695 岛屿最大面积

  • 一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
  • 但是可以每个非0点使用漫水法,最后求最值!
  • 注意cnt要递归更新,不能返回,返回记录不了该联通的面积!
  • PS:访问数组可以用grid[row][col]*=-1替代,小于0表示已经访问!
class Solution {
public:
    int maxAreaOfIsland(vector<vector<int>>& grid) {
        int maxArea = 0;
        vector<vector<bool>> visited(grid.size(),vector<bool>(grid[0].size(),false));
        for (int i = 0; i < grid.size(); ++i) {
            for (int j = 0; j < grid[0].size(); ++j) {
                if (!grid[i][j]) continue;
                int t = 0;
                recur(grid, visited, i, j, t);
                maxArea = max(t, maxArea);
            }
        }
        return maxArea;
    }
    void recur(vector<vector<int>>& grid, 
            vector<vector<bool>>& visited, int row, int col, int& cnt) {
        if (row < 0 || row >= grid.size()) return;
        if (col < 0 || col >= grid[0].size()) return;
        if (visited[row][col]) return;
        if (!grid[row][col]) return;
        visited[row][col] = true;
        cnt++;
        recur(grid, visited, row-1, col, cnt);
        recur(grid, visited, row+1, col, cnt);
        recur(grid, visited, row, col-1, cnt);
        recur(grid, visited, row, col+1, cnt);
    }
};

200 岛屿个数

  • 给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
class Solution {
public:
    int numIslands(vector<vector<char>>& grid) {
        if (grid.empty() || grid[0].empty()) return 0;
        int m = grid.size(), n = grid[0].size(), res = 0;
        vector<vector<bool>> visited(m, vector<bool>(n));
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == '0' || visited[i][j]) continue;
                helper(grid, visited, i, j);
                ++res;
            }
        }
        return res;
    }
    void helper(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
        if (x < 0 || x >= grid.size() || y < 0 || y >= grid[0].size() || grid[x][y] == '0' || visited[x][y]) return;
        visited[x][y] = true;
        helper(grid, visited, x - 1, y);
        helper(grid, visited, x + 1, y);
        helper(grid, visited, x, y - 1);
        helper(grid, visited, x, y + 1);
    }
};

529 扫雷游戏

  • ‘M’ 代表一个未挖出的地雷,
  • ‘E’ 代表一个未挖出的空方块,
  • ‘B’ 代表没有相邻(上,下,左,右,和所有4个对角线)地雷的已挖出的空白方块,
  • 数字(‘1’ 到 ‘8’)表示有多少地雷与这块已挖出的方块相邻,
  • ‘X’ 则表示一个已挖出的地雷。
    在这里插入图片描述
class Solution {
public:
    vector<vector<char>> updateBoard(vector<vector<char>>& board, vector<int>& click) {
        // vector<vector<bool>> visited(board.size(),vector<bool>(board[0].size(),false));
        recur(board,click[0],click[1]);
        return board;
    }
    void recur(vector<vector<char>>& board, int row, int col) {
        vector<int> a {-1,-1,-1,0,0,1,1,1};
        vector<int> b {-1,0,1,-1,1,-1,0,1};
        int h = board.size(), w = board[0].size();
        if (row < 0 || row >= h || col < 0 || col >= w) return;
        // if (visited[row][col]) return;
        if (board[row][col] != 'M' && board[row][col] != 'E') return;
        if (board[row][col] == 'M') {
            board[row][col] = 'X';
            return;
        }
        // visited[row][col] = true;
        int cnt = 0;
        for (int i = 0; i < 8; ++i) {
            int y = row + a[i], x = col + b[i];
            if (y < 0 || y >= h || x < 0 || x >= w) continue;
            if (board[y][x] == 'M' || board[y][x] == 'X') cnt++;
        }
        if (cnt > 0) board[row][col] = cnt + '0'; // 搜索到数字之后就不再搜索
        else if (cnt == 0) { // 搜索到'B'继续搜索
            board[row][col] = 'B';
            for (int i = 0; i < 8; ++i) {
                recur(board, row+a[i], col+b[i]);
            }
        }
    }
};

回溯

回溯方法和动态规划起源相同!本质都是 决策树遍历问题
1 动态规划是具有重复子问题,所以可以使用DP数组和备忘录简化算法!
2 回溯算法不具有重复性,所以只能用剪枝优化,复杂度也就相当的高!
回溯考虑三点:
1、路径:也就是已经做出的选择
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return
    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

n叉树遍历框架:

void traverse(TreeNode root) {
    for (TreeNode child : root.childern)
        // 前序遍历需要的操作
        traverse(child);
        // 后序遍历需要的操作
}

括号问题

一类是前文写过的括号的合法性判断 ,一类是合法括号的生成。
L20 Valid Parentheses
L22 Generate Parentheses
L32 Longest Valid Parentheses
L301 Remove Invalid Parentheses

括号合法性一般需要“”(详见栈部分);
括号的生成,一般都要利用回溯递归的思想。

合法性:

  • 一个「合法」括号组合的左括号数量一定等于右括号数量,这个很好理解。
  • 对于一个「合法」的括号字符串组合 p,必然对于任何 0 <= i < len 都有:子串 p[0…i] 中左括号的数量都大于或等于右括号的数量
class Solution {
public:
    vector<string> generateParenthesis(int n) {
        vector<string> res;
        string track = "";
        backtrack(res,track,n,n);
        return res;
    }
    void backtrack(vector<string>& res, string& track, 
    				int n1, int n2) {
        if (n1 > n2) return; // 不合法
        if (n1 < 0 || n2 < 0) return;
        if (n1 == 0 && n2 == 0) {
            res.push_back(track);
            return;
        }
        // 2 choice
        track.push_back('(');
        backtrack(res,track,n1-1,n2);
        track.pop_back();
        track.push_back(')');
        backtrack(res,track,n1,n2-1);
        track.pop_back();
    }
};

子集问题

L 78 不重复集合的所有子集

W1 递归数学归纳法

O(N*2^N) 迭代2^N,res.push_back(x); O(N)

subset([1,2])[ [],[1],[2],[1,2] ]
subset([1,2,3]) = subset([1,2]) + 3
= [3],[1,3],[2,3],[1,2,3]
class Solution {
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        if (nums.empty()) return {{}};
        int n = nums.back();
        nums.pop_back();
        vector<vector<int>> res = subsets(nums);
        int N = res.size(); // res循环中会变长 所有长度要先提出来
        for (int i = 0; i < N; ++i) {
            res.push_back(res[i]);
            res.back().push_back(n);
        }
        return res;
    }
};
W2 回溯

在这里插入图片描述

class Solution {
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        vector<vector<int>> res;
        vector<int> track;
        backtrack(res, nums, track, 0);
        return res;
    }
    void backtrack(vector<vector<int>>& res,vector<int>& nums,vector<int>& track,int pos) {
        res.push_back(track);
        for (int i = pos; i < nums.size(); ++i) {
            track.push_back(nums[i]);
            backtrack(res,nums,track,i+1);
            track.pop_back();
        }
    }
};

L 90 含重复集合的所有子集

w1 递归数学归纳
  • 不同之处:排序将重复元素攒一起;对于相邻相近的元素,只在特殊范围类加
  • 例子:如[1,2,2],已有[],[1],[2],[1,2],还要加上最后一个元素2,则只对[2]和[1,2]后面加上2
class Solution {
private:
    int preSz;
public:
    Solution() {preSz = -1;}
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        return subsetsWithDup2(nums);
    }
    vector<vector<int>> subsetsWithDup2(vector<int>& nums) {
        if (nums.empty()) return {{}};
        int n = nums.back();
        nums.pop_back();
        int m = nums.empty() ? INT_MAX : nums.back(); // 标记#1
        auto res = subsetsWithDup2(nums);
        int sz = res.size();
        int start = 0; // 标记#2
        if (preSz > 0 && n == m)
            start = sz - preSz;
        for (int i = start; i < sz; i++) {
            res.push_back(res[i]);
            res.back().push_back(n);
        }
        preSz = sz - start; // 标记#3
        return res;
    }
};
w2 回溯

不同1 目的在于将重复元素聚在一起
不同2 目的在于减少重复选择,跳过X
在这里插入图片描述

class Solution {
public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        vector<vector<int>> res;
        vector<int> track;
        sort(nums.begin(), nums.end()); // 不同1
        backtrack(res, nums, track, 0);
        return res;
    }
    void backtrack(vector<vector<int>>& res,vector<int>& nums,vector<int>& track,int pos) {
        res.push_back(track);
        for (int i = pos; i < nums.size(); ++i) {
            track.push_back(nums[i]);
            backtrack(res,nums,track,i+1);
            track.pop_back();
            while (i+1 < nums.size()&&nums[i]==nums[i+1]) i++; // 不同2
        }
    }
};

L698 判断能否将集合划分为k个相等子集

  • 记忆化搜索
  • 位置,子集和,组数 递归
  • 可行条件为 当 k = 0,即装好了 k 组
class Solution {
public:
    bool canPartitionKSubsets(vector<int>& nums, int k) {
        int sum = 0, maxNum = 0;
        for (auto& n : nums) {
            sum += n;
            if (n > maxNum)
                maxNum = n;
        }
        if (sum % k || maxNum > sum / k) return false;
        vector<bool> vis(nums.size(), false);
        return backtrack(nums, vis, 0, 0, k, sum / k);
    }
    bool backtrack(vector<int>& nums, vector<bool>& vis,
                   int pos, int sum, int k, int tar) 
    {
        if (k == 0) return true;
        if (sum == tar)
            return backtrack(nums, vis, 0, 0, k-1, tar);
        for (int i = pos; i < nums.size(); i++) {
            if (!vis[i] && sum + nums[i] <= tar) {
                vis[i] = true;
                if (backtrack(nums, vis, i + 1, sum + nums[i], k, tar))
                    return true;
                vis[i] = false;
            }
        }
        return false;
    }
};

组合问题

L 77 组合

k 限制了树的高度,n 限制了树的宽度

输入: n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]
class Solution {
public:
    vector<vector<int>> combine(int n, int k) {
        vector<vector<int>> res;
        vector<int> track;
        vector<bool> visited(n,false);
        backtrack(res,visited,track,n,k);
        return res;
    }
    void backtrack(vector<vector<int>>& res, vector<bool>& visited, 
        vector<int>& track, int n, int k) {
        if (track.size() == k) {
            res.push_back(track);
            return;
        }
        for (int i = 1; i <= n; ++i) {
            if (visited[i-1]) continue; // 剪枝1
            if (!track.empty()&&track.back()>i) // 剪枝2
                continue;
            visited[i-1] = true;
            track.push_back(i);
            backtrack(res,visited,track,n,k);
            track.pop_back();
            visited[i-1] = false;
        }
    }
};

L 39 组合总和

输入: candidates = [2,3,6,7], target = 7,
所求解集为:
[
  [7],
  [2,2,3]
]
class Solution {
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<vector<int>> res;
        vector<int> track;
        backtrack(res,track,candidates,0,target);
        return res;
    }
    void backtrack(vector<vector<int>>& res, vector<int>& track, 
        vector<int>& candidates, int pos, int target) {
        if (target < 0) return;
        if (target == 0) {res.push_back(track); return;}
        for (int i = pos; i < candidates.size(); ++i) {
            track.push_back(candidates[i]);
            backtrack(res,track,candidates,i,target-candidates[i]);
            track.pop_back();
        }
    }
};

全排列问题

46 不重复序列

【1,2,3】
在这里插入图片描述

W1 交换
class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int>> res;
        permuteSearch(nums, 0, res);
        return res;
    }
    void permuteSearch(vector<int>& nums, int pos, vector<vector<int>>& res) {
        if (pos >= nums.size()) res.push_back(nums);
        for (int i = pos; i < nums.size(); ++i) { 
        // i不能等于pos+1 否则就不能进push_back!
            swap(nums[pos], nums[i]);
            permuteSearch(nums, pos+1, res);
            swap(nums[pos], nums[i]);
        }
    }
};
W2 枚举创建法
class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int>> res;
        vector<int> tmp;
        vector<bool> visited(nums.size(), false);
        permuteSearch(nums, tmp, res, visited);
        return res;
    }
    void permuteSearch(vector<int>& nums, vector<int>& tmp, 
                    vector<vector<int>>& res, vector<bool>& visited) {
        if (tmp.size() == nums.size()) {res.push_back(tmp);return;}
        for (int i = 0; i < nums.size(); ++i) {
            if (visited[i]) continue;
            visited[i] = true;
            tmp.push_back(nums[i]);
            permuteSearch(nums, tmp, res, visited);
            tmp.pop_back();
            visited[i] = false;            
        }
    }
};

47 重复序列

在这里插入图片描述

class Solution {
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<vector<int>> res;
        vector<int> track;
        vector<bool> visited(nums.size(),false);
        sort(nums.begin(),nums.end()); // 不同1
        backtrack(res,track,nums,visited);
        return res;
    }
    void backtrack(vector<vector<int>>& res, vector<int>& track, 
        vector<int>& nums, vector<bool>& visited) {
        if (track.size() == nums.size()) {
            res.push_back(track);
            return;
        }
        for (int i = 0; i < nums.size(); ++i) {
            if (visited[i]) continue;
            // 下面!visited[i-1]其实表示的是i-1和i在不同分支上
            // 那么i的所属分支就可以跳过 而i-1所属分支必然已经处理过
            if (i>0&&nums[i-1]==nums[i]&&!visited[i-1]) continue; // 不同2
            visited[i] = true;
            track.push_back(nums[i]);
            backtrack(res,track,nums,visited);
            track.pop_back();
            visited[i] = false;
        }
    }
};

推荐上面的代码,剪枝是简洁有效的!

class Solution {
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        set<vector<int>> res; // 不同 换成set 好像这个方法剪枝很复杂
        permute(nums, 0, res);
        return vector<vector<int>> (res.begin(), res.end());
    }
    void permute(vector<int>& nums, int start, set<vector<int>>& res) {
        if (start >= nums.size()) res.insert(nums);
        for (int i = start; i < nums.size(); ++i) {
            swap(nums[i], nums[start]);
            permute(nums, start + 1, res);
            swap(nums[i], nums[start]);
        }
    }
};

N皇后问题

一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。
PS: 皇后攻击方式是米字型直线攻击

class Solution {
public:
    vector<vector<string>> solveNQueens(int n) {
        vector<vector<string>> res;
        vector<string> board(n, string(n, '.'));
        search(board, res, 0);
        return res;
    }
private:
    void search(vector<string>& board, vector<vector<string>>& res, int row) {
        if (row == board.size()) {
            res.push_back(board); // 这里
            return;
        }
        for (int col = 0; col < board.size(); ++col) {
            if (!isValid(board,row,col)) continue;
            board[row][col] = 'Q';
            search(board,res,row+1); // 这里
            board[row][col] = '.';
        }
    }
    bool isValid(vector<string>& board, int row, int col) {
        int n = board.size();
        for (int i = row; i >= 0; i--) { // 上方
            if (board[i][col] == 'Q') return false;
        }
        for (int i = row - 1, j = col - 1; 
                i >= 0 && j >= 0; --i, --j) {
            if (board[i][j] == 'Q') return false;
        }
        for (int i = row - 1, j = col + 1;
                i >= 0 && j < n; --i, ++j) {
            if (board[i][j] == 'Q') return false;
        }
        return true;
    }
};

如果找到一个解就行,那就在中止条件里返回true,递归时检测true,就直接renturn true。
即修改代码中标记的两个标记”return false“

37 数独问题 <= O(9^M)

数独规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
PS:空白格用 ‘.’ 表示

class Solution {
public:
    void solveSudoku(vector<vector<char>>& board) {
        backtrack(board, 0, 0);
    }
    bool backtrack(vector<vector<char>>& board, int row, int col) {
        if (col == 9) // 下一行
            return backtrack(board, row+1, 0);
        if (row == 9) return true; // 找到一个解
        if (board[row][col] != '.') // 空格处
            return backtrack(board, row, col+1);
        for (char i = '1'; i <= '9'; i++) {
            if (!isValid(board, row, col, i)) continue;
            board[row][col] = i;
            if (backtrack(board, row, col + 1)) return true;
            board[row][col] = '.';
        }
        return false;
    }
    bool isValid(vector<vector<char>>& board, 
        int row, int col, char c) {
        for (int i = 0; i < 9; ++i) {
            if (board[row][i] == c) return false;
            if (board[i][col] == c) return false;
            int row0 = (row/3)*3 + i/3;
            int col0 = (col/3)*3 + i%3;
            if (row0 < 0 || row0 > 8) continue;
            if (col0 < 0 || col0 > 8) continue;
            if (board[row0][col0] == c) return false;
        }
        return true;
    }
};

分支定界

  • 围绕搜索树进行,但和回溯不同的是,分支定界不是找所有解,而是要找一个最优解,大概是要保存当前最优解用于比较,最后返回一个最优解!
  • 回溯以 DFS 搜索解空间树 ; 分支定界以 BFS 或 最小耗费优先方法 搜索解空间树
  • 回溯每次扩展一个孩子节点,分支定界一生成所有孩子节点

双指针技巧

  • 分为快慢指针和双指针两种类型,前者用于链表的遍历问题,后者用于数组和字符串的遍历

快慢指针


单链表中的环 头X - 环起点Y - 快慢指针相遇点Z
在这里插入图片描述

  1. 环的长度是多少?
    方法1 第一次相遇后,让slow,fast继续走,记录到下次相遇时循环了几次。 因为多走一圈
    方法2 第一次相遇时slow走过的距离:a+b,fast走过的距离:a+b+c+b结合速度关系得到2(a+b) = a+b+c+b,可以得到a=c,即二者第一次相遇循环的次数就等于环的长度。
  2. 如何找到环中第一个节点(即Linked List Cycle II)?
    a=c知道,第一次相遇之后,从Z和X的中点(这是有向图)即为环的第一个节点。
  3. 如何将有环的链表变成单链表(解除环)?
    将第一和环节点切断即可
  4. 如何判断两个单链表是否有交点?如何找到第一个相交的节点?
    判断两个链表是否有环:
    ①一个有环一个没环肯定不相交;
    ②都没有环尾部是否相等;
    ③都有环判断其中一个Z点是否在另一个链表上!

环检测

1 处理链表环!是否有环 / 环起始点 /
2 寻找链表的中点
3 返回链表的倒数第 k 个节点:

class Solution {
public:
    int kthToLast(ListNode* head, int k) {
        ListNode *fast, *slow;
        fast = head; slow = head;
        while (k--) fast = fast->next;
        while (fast) {
            slow = slow->next;
            fast = fast->next;
        }
        return slow->val;
    }
};

26 删除重复元素

在这里插入图片描述

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        if (nums.size() < 2) return nums.size();
        int N = nums.size();
        int slow = 0, fast = 0;
        while (fast < N) {
            if (nums[slow] == nums[fast]) fast++;
            else nums[++slow] = nums[fast++];
        }
        return slow + 1;
    }
};

左右指针

左右指针在数组中实际是指两个索引值

TwoSum系列

  • L1 哈希表
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int,int> m;
        for (int i = 0; i < nums.size(); ++i) {
            if (m.count(target - nums[i]))
                return {m[target - nums[i]],i};
            m[nums[i]] = i;
        }
        return {-1,-1};
    }
};
  • L167 有序的数组下可以使用二分查找
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        for (int i = 0; i < nums.size() - 1; ++i) {
            int X = target - nums[i];
            int left = i+1, right = nums.size() - 1;
            while (left <= right) {
                int mid = left + (right - left) / 2;
                if (nums[mid] == X) return {i+1,mid+1};
                else if (nums[mid] > X) right = mid - 1;
                else left = mid + 1;
            }
        } 
        return {-1,-1};
    }
};

或者使用双指针

class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        int low = 0, high = numbers.size() - 1;
        while (low < high) {
            int sum = numbers[low] + numbers[high];
            if (sum == target)
                return {low + 1, high + 1};
            else if (sum < target)
                ++low;
            else
                --high;
        }
        return {-1, -1};
    }
};

2 反转数组

class Solution {
public:
    void reverseString(vector<char>& s) {
        int i = 0, j = s.size() - 1;
        while (i < j) {
            swap(s[i],  s[j]);
            i++, j--;
        }
    }
};

L43 字符串乘法

  • num1 和 num2 可以非常长,所以不可以把他们直接转成整型然后运算,唯一的思路就是模仿我们手算乘法。
  • 乘法进位 + 错位相加 + 加法进位
  • 采用双指针,并使用vector保存中间乘积结果
  • 指针 i 和 j 乘积的个位和十位分别放在 vector[i+j] 和 vector[i+j+1] 处
    在这里插入图片描述
    具体代码如下:
class Solution {
public:
    string multiply(string num1, string num2) {
        int m = num1.size(), n = num2.size();
        vector<int> res(m+n,0);
        string ss;
        for (int i = n - 1; i >= 0; --i) {
            int n2 = num2[i] - '0';
            for (int j = m - 1; j >= 0; --j) {
                int n1 = num1[j] - '0';
                int p = m+n-2-(i+j), q = m+n-2-(i+j)+1;
                int S = n1 * n2 + res[p];
                res[p] = S % 10;
                res[q] += S / 10;
            }
        }
        while (!res.empty() && !res.back()) 
            res.pop_back();
        while (!res.empty()) {
            ss += (res.back()+'0');
            res.pop_back();
        }
        return ss.empty() ? "0" : ss;
    }
};

5 最长回文子串

class Solution {
public:
    string longestPalindrome(string s) {
        string res = "";
        for (int i = 0; i < s.size(); ++i) {
            string res1 = PalinSub(s, i, i);
            string res2 = PalinSub(s, i, i + 1);
            res = (res1.size() > res.size())? res1 : res;
            res = (res2.size() > res.size())? res2 : res;
        }
        return res;
    }
    string PalinSub(string& s, int p, int q) {
        while (p >= 0 && q < s.size() && s[p] == s[q]) {
            p--; q++;
        }
        return s.substr(p+1,q-p-1);
    }
};

可以用动态规划,dp[i,j]表示i~j之间的子串是否是回文子串!

二分查找框架

科普:
KMP算法,字符串匹配算法
二分查找框架!

int binarySearch(int[] nums, int targyiet) {
    int left = 0, right = 标记1;
    while(标记2) {
        int mid = left + (right - left) / 2; // (right+left)/2 可能导致溢出!
        if (nums[mid] == target) {
            标记3
        } else if (nums[mid] < target) {
            left = 标记4
        } else if (nums[mid] > target) {
            right = 标记5
        }
    }
    return 标记6;
}

查找一个数:
上述6个标记。具体可以解决查找一个数,查找左边界,右边界
区间思想:表现在标记2,当 left<=right 表示搜索区间为 [left,right]
当left<right时,标记处就要打个补丁 return nums[left] == target ? left : -1;
因为 [left,right) 缺少对 [left,left] 的查找!
标记4和标记5 是不是 = mid - 1 还是 mid ; = mid + 1还是mid!
这里由标记2决定!当标记2是left<=right ⇒ [left,right] ⇒ mid已经被查而且是闭区间搜索,所以都是±1

int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length - 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 left_bound(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0;
    int right = nums.length; // 注意
    while (left < right) { // 注意
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            right = mid;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid; // 注意
        }
    }
    return left;
}

不用返回-1,该函数返回的是小于target有几个数!
return left 和 right 都行,因为left=right

查找右边界
该函数返回的是大于target有几个数!

int right_bound(int[] nums, int target) {
    if (nums.length == 0) return -1;
    int left = 0, right = nums.length;
    while (left < right) {
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            left = mid + 1; // 注意
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid;
        }
    }
    return left - 1; // 注意
}

1、分析二分查找代码时,不要出现 else,全部展开成 else if 方便理解。
2、注意「搜索区间」和 while 的终止条件,如果存在漏掉的元素,记得在最后检查。
3、如需定义左闭右开的「搜索区间」搜索左右边界,只要在 nums[mid] == target 时做修改即可,搜索右侧时需要减一。
4、如果将「搜索区间」全都统一成两端都闭,好记,只要稍改 nums[mid] == target 条件处的代码和返回的逻辑即可

34 在排序数组中查找元素的第一个和最后一个位置

就是equal_range函数

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        vector<int> res;
        res.push_back(searchLeft(nums,target));
        res.push_back(searchRight(nums,target));
        return res;
    }
private:
    int searchLeft(vector<int>& nums, int target) {
        int left = 0, right = nums.size();
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                right = mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid;
            }
        }
        if (right < 0 || right >= nums.size()) return -1;
        return (nums[right]==target) ? right : -1;
    }
    int searchRight(vector<int>& nums, int target) {
        int left = 0, right = nums.size();
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                left = mid + 1;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid;
            }
        }
        if ((left-1) < 0 || (left-1) >= nums.size()) return -1;
        return (nums[left-1]==target) ? left-1 : -1;
    }
};
二分查找使用实例
875 KOKO吃香蕉

输入: piles = [3,6,7,11], H = 8
输出: 4
输入: piles = [30,11,23,4,20], H = 5
输出: 30
查找左边界

class Solution {
public:
    int minEatingSpeed(vector<int>& piles, int H) {
        int maxe = *max_element(piles.begin(),piles.end());
        if (H <= piles.size()) return maxe;
        int left = 1, right = maxe + 1, mid = 0;
        while (left < right) { // 左边界
            mid = left + (right - left) / 2;
            int tc = timeConsum(piles, mid);
            if (tc == H) right = mid;
            else if (tc > H) left = mid + 1;
            else right = mid;
        }
        return left;
    }
    int timeConsum(vector<int>& piles, int s) {
        int res = 0;
        for (auto& p : piles) {
            res += (p / s);
            res += ((p % s) > 0);
        }
        return res;
    }
};
1011. 在 D 天内送达包裹的能力

输入:weights = [1,2,3,4,5,6,7,8,9,10], D = 5
输出:15
解释:
船舶最低载重 15 就能够在 5 天内送达所有包裹,如下所示:
第 1 天:1, 2, 3, 4, 5
第 2 天:6, 7
第 3 天:8
第 4 天:9
第 5 天:10
请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) 是不允许的。

class Solution {
public:
    int shipWithinDays(vector<int>& weights, int D) {
        int left = *max_element(weights.begin(),weights.end());
        int right = accumulate(weights.begin(),weights.end(),0)+1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            int d = daysConsum(weights, mid);
            if (d == D) right = mid;
            else if (d > D) left = mid + 1;
            else right = mid;
        }
        return left;
    }
    int daysConsum(vector<int>& weights, int cap) {
        int res = 1, c = 0;
        for (auto& w : weights) {
            c += w;
            if (c > cap) {
                res++;
                c = w;
            }
        }
        return res;
    }
};
L392. 判断子序列

如何判定字符串 s 是否是字符串 t 的子序列(可以假定 s 长度比较小,且 t 的长度非常大)

W1 穷举
class Solution {
public:
    bool isSubsequence(string s, string t) {
        int i = 0, j = 0;
        while (i < s.size() && j < t.size()) {
            if (s[i] == t[j]) i++;
            j++;
        }
        return i == s.size();
    }
};
W2 哈希表+二分查找

下述算法适合场景:
如果给你一系列字符串 s1,s2,… 和字符串 t,你需要判定每个串 s 是否是 t 的子序列(可以假定 s 较短,t 很长)。
思路:
s = “a b c”
t = “c a c b h b c”
在这里插入图片描述
需要 j 线性前进扫描字符 “c”,但借助 index 中记录的信息,可以二分搜索 index[c] 中比 j 大的那个索引

对于多个字符串 s,可以把预处理部分(哈希表)抽出来。

class Solution {
public:
    bool isSubsequence(string s, string t) {
        unordered_map<char,vector<int>> m;
        for (int i = 0; i < t.size(); ++i) 
            m[t[i]].push_back(i);
        int bigger = 0;
        for (int i = 0; i < s.size(); ++i) {
            if (!m.count(s[i])) return false;
            int pos = left_bound(m[s[i]], bigger);
            if (pos == m[s[i]].size()) return false;
            bigger = m[s[i]][pos] + 1;
        }
        return true;
    }
    int left_bound(vector<int>& v, int target) {
        int left = 0, right = v.size();
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (v[mid] == target) right = mid;
            else if (v[mid] > target) right = mid;
            else left = mid + 1;
        }
        return left;
    }
};

3 滑动窗口技巧

维护一个窗口,不断滑动,更新答案
框架:

int left = 0, right = 0;
while (right < s.size()) {
    // 增大窗口
    window.add(s[right]);
    right++;
    while (window needs shrink) {
        // 缩小窗口
        window.remove(s[left]);
        left++;
    }
}

如何向窗口中添加新元素,如何缩小窗口,在窗口滑动的哪个阶段更新结果。

最小覆盖字串

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

class Solution {
public:
    string minWindow(string s, string t) {
        int left = 0, right = 0;
        unordered_map<char,int> count_t, count_s;
        int cnt = 0; // 个数相同的字符种类数
        int len = INT_MAX, start = 0; // 子串信息
        for (auto& c : t) count_t[c]++;
        while (right < s.size()) { // 窗口放
            // 更新数据
            char d = s[right];
            if (count_t.count(d)) {
                count_s[d]++;
                if (count_s[d] == count_t[d]) {
                    cnt++;
                }
            }
            right++;
            while (cnt == count_t.size()) { // 窗口缩
                // 更新数据
                char d = s[left];
                if (right - left < len) {
                    len = right - left;
                    start = left;
                }
                if (count_t.count(d)) {
                    if (count_s[d] == count_t[d]) cnt--;
                    count_s[d]--;
                }
                left++;
            }
        }
        return (len == INT_MAX) ? "" : s.substr(start, len);
    }
};
字符串排列 567

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

class Solution {
public:
    bool checkInclusion(string s1, string s2) {
        int left = 0, right = 0, cnt = 0;
        unordered_map<char,int> count_s1, count_s2;
        for (auto& s : s1) count_s1[s]++;
        while (right < s2.size()) {
            char c = s2[right];
            if (count_s1.count(c)) {
                count_s2[c]++;
                if (count_s1[c] == count_s2[c]) 
                    cnt++;
            }
            right++;
            while (right - left >= s1.size()) {
                if (cnt == count_s1.size()) return true;
                char c = s2[left];
                if (count_s1.count(c)) {
                    if (count_s1[c] == count_s2[c]) cnt--;
                    count_s2[c]--;
                }
                left++;
            }
        }
        return false;
    }
};

下面这种写法看起来比较简易,但是不是作者强调的框架!

class Solution {
public:
    bool checkInclusion(string s1, string s2) {
        if (s2.size() < s1.size()) return false;
        vector<int> cnt1(26,0),cnt2(26,0);
        for (int i = 0; i < s1.size(); ++i) {
            cnt1[s1[i]-'a']++;
            cnt2[s2[i]-'a']++;
        }
        if (cnt1 == cnt2) return true;
        for (int i = s1.size(); i < s2.size(); ++i) {
            cnt2[s2[i]-'a']++;
            cnt2[s2[i-s1.size()]-'a']--;
            if (cnt1 == cnt2) return true;
        }
        return false;
    }
};
438. 找到字符串中所有字母异位词

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

输出:
[0, 6]

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

之前刷题日记(一)的解法不是这里作者强调的框架!这里按照作者强调的框架,重写一遍!

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> res;
        int left = 0, right = 0, cnt = 0;
        unordered_map<char,int> cnt_s, cnt_p;
        for (auto& c : p) cnt_p[c]++;
        while (right < s.size()) {
            char c = s[right];
            if (cnt_p.count(c)) {
                cnt_s[c]++;
                if (cnt_s[c] == cnt_p[c]) cnt++;
            }
            right++;
            while (right - left >= p.size()) {
                char c = s[left];
                if (cnt == cnt_p.size()) res.push_back(left);
                if (cnt_p.count(c)) {
                    if (cnt_s[c] == cnt_p[c]) cnt--;
                    cnt_s[c]--;
                }
                left++;
            }
        }
        return res;
    }
};
3 无重复字符的最长子串

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

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int left = 0, right = 0, n = INT_MIN;
        unordered_map<char,int> m;
        while (right < s.size()) {
            char c = s[right]; m[c]++;
            right++;
            while (m[c] > 1) {
                char d = s[left];
                if (m.count(d)) m[d]--;
                left++;
            }
            n = max(n, right - left);
        }
        return (n == INT_MIN) ? 0 : n;
    }
};
1004 最大连续1的个数 III

给定一个由若干 0 和 1 组成的数组 A,我们最多可以将 K 个值从 0 变成 1
返回仅包含 1 的最长(连续)子数组的长度。
输入:A = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出:6
解释:
[1,1,1,0,0,1,1,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 6

  • 只需要统计零的个数,和K比较!
class Solution {
public:
    int longestOnes(vector<int>& A, int K) {
        int left = 0, right = 0;
        int cnt = 0, res = 0;
        while (right < A.size()) {
            cnt += (A[right] == 0);
            right++;
            while (cnt > K) {
                cnt -= (A[left] == 0);
                left++;
            }
            res = max(res, right - left);
        }
        return res;
    }
};

总结上述的双指针滑动窗思想,你需要考虑:
1 窗口右边界右移时更新什么变量
2 什么时候右移左边界
3 什么时候保存需要返回的数据

L42 接雨水问题

在这里插入图片描述

water[i] = ...
min(max(height[0..i]),max(height[i..end]))-height[i]
  # 左边最高的柱子       # 右边最高的柱子
遍历法

W! 暴力
TLE

class Solution {
public:
    int trap(vector<int>& height) {
        int a = height.size(), res = 0;
        for (int i = 0; i < height.size(); ++i) {
            int h = height[i];
            int l_max = 0, r_max = 0;
            for (int j = 0; j <= i; ++j)
                l_max = max(l_max, height[j]);
            for (int j = i; j < height.size(); ++j) 
                r_max = max(r_max, height[j]);
            res += (min(l_max,r_max) - h);
        }
        return res;
    }
};

W2 带备忘录

class Solution {
public:
    int trap(vector<int>& height) {
        if (height.size() < 3) return 0;
        int a = height.size(), res = 0;
        vector<int> Left(a,0), Right(a,0);
        Left[0] = height[0];
        Right[a-1] = height[a-1];
        for (int i = 1; i < a; ++i)
            Left[i] = max(Left[i-1], height[i]);
        for (int i = a-2; i >= 0; --i)
            Right[i] = max(Right[i+1], height[i]);
        for (int i = 0; i < a; ++i)
            res += (min(Left[i],Right[i])-height[i]);
        return res;
    }
};
双指针
class Solution {
public:
    int trap(vector<int>& height) {
        if (height.size() < 3) return 0;
        int N = height.size(), res = 0;
        int left = 0, right = N - 1;
        int l_max = height[0], r_max = height[N-1];
        while (left <= right) {
            l_max = max(l_max, height[left]);
            r_max = max(r_max, height[right]);
            if (l_max < r_max) {
                res += (l_max - height[left]);
                left++;
            } else {
                res += (r_max - height[right]);
                right--;
            }
        }
        return res;
    }
};
单调栈
class Solution {
public:
    int trap(vector<int>& height) {
        if (height.size() < 3) return 0;
        stack<int> st;
        int N = height.size(), i = 0, res = 0;
        while (i < N) {
            if (st.empty() || height[i] <= height[st.top()]) st.push(i++);
            else {
                int t = st.top(); st.pop();
                if (st.empty()) continue;
                res += (min(height[st.top()],height[i])-height[t])
                            *(i-st.top()-1);
            }
        }
        return res;
    }
};

宽度优先搜索 BFS

DFS就是回溯框架
抽象成图+利用队列
BFS 相对 DFS 的最主要的区别是:
— BFS 找到的路径一定是最短的,但代价就是空间复杂度比DFS大很多
大致框架如下:

// 计算从起点 start 到终点 target 的最近距离
int BFS(Node start, Node target) {
    Queue<Node> q; // 核心数据结构
    Set<Node> visited; // 避免走回头路
    q.offer(start); // 将起点加入队列
    visited.add(start);
    int step = 0; // 记录扩散的步数
    while (q not empty) {
        int sz = q.size();
        /* 将当前队列中的所有节点向四周扩散 */
        for (int i = 0; i < sz; i++) {
            Node cur = q.poll();
            /* 划重点:这里判断是否到达终点 */
            if (cur is target)
                return step;
            /* 将 cur 的相邻节点加入队列 */
            for (Node x : cur.adj())
                if (x not in visited) {
                    q.offer(x);
                    visited.add(x);
                }
        }
        /* 划重点:更新步数在这里 */
        step++;
    }
}

111. 二叉树的最小深度

class Solution {
public:
    int minDepth(TreeNode* root) {
        if (!root) return 0;
        int depth = 1;
        queue<TreeNode*> q;
        q.push(root);
        while (!q.empty()) {
            int sz = q.size();
            for (int i = 0; i < sz; ++i) {
                TreeNode* t = q.front(); q.pop();
                if (!t->left && !t->right) return depth;
                if (t->left) q.push(t->left);
                if (t->right) q.push(t->right);
            }
            depth++;
        }
        return depth;
    }
};

752. 打开转盘锁

输入:deadends = [“0201”,“0101”,“0102”,“1212”,“2002”], target = “0202”
输出:6
解释:
可能的移动序列为 “0000” -> “1000” -> “1100” -> “1200” -> “1201” -> “1202” -> “0202”。
注意 “0000” -> “0001” -> “0002” -> “0102” -> “0202” 这样的序列是不能解锁的,
因为当拨动到 “0102” 时这个锁就会被锁定。

class Solution {
public:
    int openLock(vector<string>& deadends, string target) {
        int move = 0;
        queue<string> q;
        unordered_set<string> st;
        unordered_set<string> visited;
        for (auto& dd : deadends) st.emplace(dd);        
        q.push("0000");
        while (!q.empty()) {
            int sz = q.size();
            for (int i = 0; i < sz; ++i) {
                string t = q.front(); q.pop();
                if (t == target) return move;
                if (st.count(t)) continue;
                for (int i = 0; i < 4; ++i) {
                    string ut = upRotate(t, i);
                    string dt = dwRotate(t, i);
                    if (!visited.count(ut)) {
                        q.push(ut); visited.emplace(ut);
                    }
                    if (!visited.count(dt)) {
                        q.push(dt); visited.emplace(dt);
                    }
                }
            }
            move++;
        }
        return -1;
    }
private:
    string upRotate(string s, int i) {
        if (s[i] == '9') s[i] = '0';
        else s[i] += 1;
        return s;
    }
    string dwRotate(string s, int i) {
        if (s[i] == '0') s[i] = '9';
        else s[i] -= 1;
        return s;
    }
};

技巧:双向BFS

在这里插入图片描述
1 双向 BFS 其实只遍历了半棵树就出现了交集,也就是找到了最短距离。
2 大O法角度,双向的复杂度没有减少,因为大O是算的最坏情况
3 不过,双向 BFS 也有局限,因为你必须知道终点在哪里。

class Solution {
public:
    int openLock(vector<string>& deadends, string target) {
        int move = 0;
        unordered_set<string> layer,layer0; // 不再使用queue搜索
        unordered_set<string> deads;
        unordered_set<string> visited;
        for (auto& dd : deadends) deads.emplace(dd);        
        layer.emplace("0000");
        layer0.emplace(target);
        while (!layer.empty() && !layer0.empty()) {
            unordered_set<string> tmp;
            for (auto i = layer.begin(); i != layer.end(); ++i) {
                string t = *i;
                if (layer0.count(t)) return move;
                if (deads.count(t)) continue;
                visited.emplace(t);
                for (int i = 0; i < 4; ++i) {
                    string ut = upRotate(t, i);
                    string dt = dwRotate(t, i);
                    if (!visited.count(ut)) tmp.emplace(ut);
                    if (!visited.count(dt)) tmp.emplace(dt);
                }
            }
            move++;
            layer = layer0; // 交换 扩散
            layer0 = tmp;
        }
        return -1;
    }
private:
    string upRotate(string s, int i) {
        if (s[i] == '9') s[i] = '0';
        else s[i] += 1;
        return s;
    }
    string dwRotate(string s, int i) {
        if (s[i] == '0') s[i] = '9';
        else s[i] -= 1;
        return s;
    }
};

PS:还有个技巧,每次都取层上节点个数小的layer进行遍历!因为可以认为个数少的邻接点少,进而复杂度减缓增加!

if (layer0.size() < layer.size()) swap(layer,layer0);

状态图解法

KMP字符匹配算法

表示数值的字符串

使用有限状态机做 … if-else之流的太蛋疼了
先设定开始状态(未处理时)为0
再确定有几种状态:

1点状态类似于" . "
2正负号类似于" + "
3整数类似于"132"
4浮点数类似于"3.2"
5科学计数类似于"5e3"
6数加e状态类似于"5e"
7数加e加正负状态类似于"5e+"
8额外状态以便处理尾空格情况
转移图(无8)如下:
在这里插入图片描述
class Solution {
public:
    bool isNumber(string s) {
        // 状态x操作
        // 状态:0起始状态 1点 2正负 3整数 4浮点数 5科学计数 6数+"e" 7数+”正负"+"e" 
        // 中间状态1/2/6/7
        // 完美状态3/4/5
        // 操作:0数 1空 2点 3正负 4e
        // 添一个空状态8 以处理尾部空格
        s += ' '; // 添上一个尾部空格
        vector<vector<int>> State {
            {3, 0, 1, 2,-1},
            {4,-1,-1,-1,-1},
            {3,-1, 1,-1,-1},
            {3, 8, 4,-1, 6},
            {4, 8,-1,-1, 6},
            {5, 8,-1,-1,-1},
            {5,-1,-1, 7,-1},
            {5,-1,-1,-1,-1},
            {-1,8,-1,-1,-1}
        };
        vector<int> legalState {3,4,5,8};
        int sta = 0;
        for (auto& c : s) {
            if (c >= '0' && c <= '9') sta = State[sta][0];
            else if (c == ' ') sta = State[sta][1];
            else if (c == '.') sta = State[sta][2];
            else if (c == '+' || c == '-') sta = State[sta][3];
            else if (c == 'e') sta = State[sta][4];
            else sta = -1;
            if (sta == -1) return false;
        }
        for (auto& ls : legalState)
            if (sta == ls) return true;
        return false;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值