算法题目概览
76.最小覆盖子串
由于长度最小的子数组这道题中的滑动窗口对于我来说有点难以理解,之前没有接触过滑动窗口题目,我就先找了一下一个更简单的题目,名字叫做:最小覆盖子串。感觉学习了这个对于滑动窗口有了一个理解。
题目链接:76. 最小覆盖子串 - 力扣(LeetCode)
视频讲解:两分钟搞懂滑动窗口算法,致敬3b1b_哔哩哔哩_bilibili
这个视频讲解强烈推荐观看!下面的评论区非常精彩,总结的非常到位。
209.长度最小的子数组
题目建议: 本题关键在于理解滑动窗口,这个滑动窗口看文字讲解 还挺难理解的,建议大家先看视频讲解。 拓展题目可以先不做。
题目链接:209. 长度最小的子数组 - 力扣(LeetCode)
文章讲解:代码随想录
视频讲解:拿下滑动窗口! | LeetCode 209 长度最小的子数组_哔哩哔哩_bilibili
这道题的思想和上面最小覆盖子串思考方式一致,都是使用滑动窗口,由于今天时间比较紧,所以这道题我没有仔细看,呜呜呜。
59.螺旋矩阵II
题目建议: 本题关键还是在转圈的逻辑,在二分搜索中提到的区间定义,在这里又用上了。
题目链接:59. 螺旋矩阵 II - 力扣(LeetCode)
文章讲解:代码随想录
视频讲解:一入循环深似海 | LeetCode:59.螺旋矩阵II_哔哩哔哩_bilibili
这道题对于初学者,推荐使用逐行填充的方式来实现,很锻炼编码的规范
区间和
前缀和是一种思维巧妙很实用 而且 很有容易理解的一种算法思想,大家可以体会一下
文章讲解:代码随想录
最小覆盖子串(滑动窗口)
滑动窗口代码
#include <iostream>
#include <string>
#include <map>
#include <climits>
using namespace std;
bool test_cover(map<int, int>& map_str2, map<int, int>& map_str_tem) {
for (auto it = map_str2.begin(); it != map_str2.end(); it++) {
if (map_str_tem[it->first] < it->second) { // 检查 map_str_tem 中对于 it->first 对应的值是否大于等于 it->second
return false;
}
}
return true;
}
int main() {
string str1, str2;
cin >> str1 >> str2;
map<int, int> map_str2;
map<int, int> map_str_tem;//存储当前窗口中的字符情况
for (int i = 0; i < str2.size(); i++) {
map_str2[str2[i]]++;
}
int str_left = 0, str_right = 0; // 初始化滑动窗口左右边界
int res_left = 0, res_right = -1; // 初始化结果窗口,-1 表示初始无有效结果
int minLen = INT_MAX; // 初始化最小长度为无穷大
for (str_right = 0; str_right < str1.size(); str_right++) { // 遍历 str1,str_right 作为滑动窗口右边界
map_str_tem[str1[str_right]]++; // 滑动窗口右边界字符计数增加
if (test_cover(map_str2, map_str_tem)) { // 检查是否覆盖
bool cover_flag = true;
while (cover_flag) {
if (str_right - str_left + 1 < minLen) { // 窗口长度比较,注意这里 + 1
minLen = str_right - str_left + 1;
res_right = str_right;
res_left = str_left;
}
map_str_tem[str1[str_left]]--; // 尝试缩小窗口左边界
str_left++;
if (!test_cover(map_str2, map_str_tem)) { // 缩小后是否仍然覆盖
cover_flag = false;
}
}
}
}
if (res_right != -1) { // 检查是否找到了有效子串 (修改为与 -1 比较)
for (int i = res_left; i <= res_right; i++) {
cout << str1[i];
}
}
cout << endl;
return 0;
}
滑动窗口核心思路
关于滑动窗口这个问题,核心的思路为:
1. 当不满足条件时,拓展右边界,直到满足条件得到一个解,然后开始缩短左边界。(缩短左边界的过程就是对这个解进行优化,因为我们的目标是使得子串尽可能长度减小,缩短过程是对这个解的优化过程)
2.持续缩短左边界直到滑动窗口中子串不满足条件,然后立即停止缩短左边界(此时滑动窗口中的子串是不满足要求的,同时注意滑动窗口是左闭右闭的),开始拓展右边界,重复第一步的操作。
OK想必你就会开始怀疑这样子进行操作是否会漏掉一些解?这个算法为什么会有效?
要想搞明白为什么这样子的话,我认为核心就是理解一个东西:
滑动窗口左边界之外的无需回溯。
你将你认为所有想要进行左回溯的点都尝试回溯的时候,你想一下就会发现根本没有必要进行回溯。你必须来仔细想一下,把不需要进行回溯这个点想清楚之后,你就理解了这个算法!对了不需要回溯的一个很重要的核心原因就是这里要求的是连续的子串
如果想不明白建议观看视频:
两分钟搞懂滑动窗口算法,致敬3b1b_哔哩哔哩_bilibili
浅浅总结一下什么情况下适合使用滑动窗口
就是你最终求解的串或者数组必须是连续的才行,不需要对于窗口左边的部分进行回溯。
不需要回溯和求解为连续这两个点真的密切相关,大胆可以认为一下就是因为求解的东西是连续的(连续子序列)所以才不需要回溯,所以才可以使用滑动窗口这个方法!!!
之前我看到求解这种很多连续子序列的题目的时候,看到就可能想用到动态规划,现在看来,有些可以用滑动窗口来解决!!!
螺旋矩阵(大模拟)
题目链接:59. 螺旋矩阵 II - 力扣(LeetCode)
方向向量方式(了解)
感觉代码随想录给出的这个题的题解不是很好,有点过于麻烦难以理解,我一开始看这道题的时候也是一头雾水,后面在力扣上找到一篇写的很好的题解,很值得借鉴。首先表明一下源代码作者:
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0));
int dx = 0, dy = 1;
int x = 0, y = 0;
for (int i = 1; i <= n * n; ++i) {
res[x][y] = i;
if (res[(x + dx) % n][(y + dy) % n] != 0) {
int tmp = dy;
dy = -dx;
dx = tmp;
}
x += dx;
y += dy;
}
return res;
}
};
这个算法非常巧妙,它使用了一种基于方向变化的方法来生成螺旋矩阵,相较于传统的边界控制方法,它更加简洁和优雅。
算法的核心思想:
-
方向控制:
- 使用
dx
和dy
变量来控制填充矩阵的方向。 - 初始方向为向右(
dx = 0
,dy = 1
)。 - 当遇到已填充的单元格时,改变填充方向。
- 使用
-
方向变化:
- 方向变化通过交换
dx
和dy
,并取反dx
来实现。 - 这种方式实现了顺时针方向的旋转。
- 方向变化通过交换
-
循环填充:
- 使用一个循环,从 1 到
n * n
依次填充矩阵。 - 每次填充后,根据当前方向更新
x
和y
坐标。 - 使用取模运算符
% n
来实现循环填充,避免数组越界。
- 使用一个循环,从 1 到
这个方向控制非常巧妙!!!(0,1)——>(1,0)——>(0,-1)——>(-1,0),总结即为: ( dx,dy)->(dy,- dx )这个顺时针旋转的方向向量可以自行在纸上推演一下。
这里的用%进行数组边界的控制的方式真的学到啦,用%来防止数组越界这个小trick在二维的数组中常见一些,一维一般不用。
回过头来想一下这个代码确实简洁高效,但是对于初学者来说可能不太能想到,这种方式大概了解一下就行,下面介绍一种传统的边界控制法来进行逐行填充,这种方式需要重点掌握!!!。
逐行填充方式(重点掌握)
#include <iostream>
#include <vector>
using namespace std;
vector<vector<int>> generateMatrix(int n) {
// 初始化一个 n x n 的矩阵,所有元素初始化为 0
vector<vector<int>> matrix(n, vector<int>(n, 0));
// 定义四个边界
int top = 0, bottom = n - 1, left = 0, right = n - 1;
// 定义填充的数字
int num = 1;
// 循环填充矩阵
while (num <= n * n) {
// 从左到右填充上边界
for (int i = left; i <= right && num <= n * n; ++i) {
matrix[top][i] = num++;
}
top++; // 上边界下移
// 从上到下填充右边界
for (int i = top; i <= bottom && num <= n * n; ++i) {
matrix[i][right] = num++;
}
right--; // 右边界左移
// 从右到左填充下边界
for (int i = right; i >= left && num <= n * n; --i) {
matrix[bottom][i] = num++;
}
bottom--; // 下边界上移
// 从下到上填充左边界
for (int i = bottom; i >= top && num <= n * n; --i) {
matrix[i][left] = num++;
}
left++; // 左边界右移
}
return matrix;
}
// 主函数,用于测试
int main() {
int n = 3;
vector<vector<int>> result = generateMatrix(n);
// 打印结果矩阵
for (const auto& row : result) {
for (int val : row) {
cout << val << " ";
}
cout << endl;
}
return 0;
}
这个思路真的非常清晰,将螺旋矩阵填充一圈巧妙地转换成了四次单独的for循环填充,并且使用了四个变量top ,right,buttom,left来逐渐记录缩小待填充螺旋矩阵的范围,so cool!一定要好好理解,掌握这种方式!!!
螺旋矩阵(求指定位置的元素值)
一个n行n列的螺旋矩阵可由如下方法生成:
从矩阵的左上角(第1行第1列)出发,初始时向右移动;如果前方是未曾经过的格子,则继续前进,否则右转;重复上述操作直至经过矩阵中所有格子。根据经过顺序,在格子中依次填入1,2,3,...,n2,便构成了一个螺旋矩阵。
下图是一个n=4时的螺旋矩阵:
输入
输入共一行,包含三个整数 n,i,j,每两个整数之间用一个空格隔开,分别表示矩阵大小、待求的数所在的行号和列号。
1≤n≤30000,1≤i≤n,1≤j≤n
输出
输出共一行,包含一个整数,表示相应矩阵中第i行第j列的数。
这题看到之后最朴素的思路就是,将螺旋矩阵打印,然后直接用O(1)的复杂度求出 (x,y)的值。结果TLE,因为我们看到n的范围30000,输出一个螺旋矩阵,需要内存n^2,时间n^2,所以无论从内存上还是时间上,这都不是一个好的办法,所以我们需要换一种思路。