算法是寻找思维定式的思维
文章目录
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 j−1得到,所以 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(K−1,i−1),dp(K,N−i)}+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次! K个鸡蛋,可以扔T次!
- 如果鸡蛋没有碎,那么对应的是 f ( K , T − 1 ) f(K, T - 1) f(K,T−1),也就是说在这一层的上方可以有 f ( K , T − 1 ) f(K, T - 1) f(K,T−1)层;
- 如果鸡蛋碎了,那么对应的是 f ( K − 1 , T − 1 ) f(K - 1, T - 1) f(K−1,T−1),也就是说在这一层的下方可以有 f ( K − 1 , T − 1 ) f(K - 1, T - 1) f(K−1,T−1)层。
- 因此我们就可以写出状态转移方程:
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(K−1,T−1)+f(K−1,T)
边界条件为:当 T ≥ 1 T \geq 1 T≥1 的时候 f ( T , 1 ) = T f(T, 1) = T f(T,1)=T,当 K ≥ 1 K \geq 1 K≥1时, 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] (11=1 分)
----> [1, 1] (33=9 分)
----> [] (22=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第一次相遇后,让slow,fast继续走,记录到下次相遇时循环了几次。
因为多走一圈
方法2 第一次相遇时slow走过的距离:a+b
,fast走过的距离:a+b+c+b
结合速度关系得到2(a+b) = a+b+c+b
,可以得到a=c
,即二者第一次相遇循环的次数就等于环的长度。 - 如何找到环中第一个节点(即Linked List Cycle II)?
由a=c
知道,第一次相遇之后,从Z和X的中点(这是有向图)即为环的第一个节点。 - 如何将有环的链表变成单链表(解除环)?
将第一和环节点切断即可 - 如何判断两个单链表是否有交点?如何找到第一个相交的节点?
判断两个链表是否有环:
①一个有环一个没环肯定不相交;
②都没有环尾部是否相等;
③都有环判断其中一个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;
}
};