代码随想录day2 | LeetCode209.长度最小的子数组、LeetCode59.螺旋矩阵II、KamaCoder 58. 区间和(第九期模拟笔试)、KamaCoder 44. 开发商购买土地(第五期模拟笔试)
(上传图片各种方法都尝试了一遍,最后终于上传成功了)
看题解部分我给了原题解链接(别问我的笔记看题解部分为啥和原题解大篇幅相同,问就是懒得画图,懒得组织语言,而且题解每一句话说的都很好,我只修改了一小部分)
最后一题周末更新
长度最小的子数组
题目链接:LeetCode209.长度最小的子数组
自己敲
没有什么思路,采用暴力解法
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int len=nums.size();
bool if_large=0;//默认找不到满足条件的子数组,即数组中全部元素之和小于target
for(int i=0;i<nums.size();i++){
int sum=0;
for(int j=i;j<nums.size();j++){
sum+=nums[j];
if(sum>=target){
if(j-i+1<len) len=j-i+1;
if_large=1;
break;
}
}
}
if(if_large==0){
return 0;
}
return len;
}
};
看题解
暴力解法
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX; // 最终的结果
int sum = 0; // 子序列的数值之和
int subLength = 0; // 子序列的长度
for (int i = 0; i < nums.size(); i++) { // 设置子序列起点为i
sum = 0;
for (int j = i; j < nums.size(); j++) { // 设置子序列终止位置为j
sum += nums[j];
if (sum >= s) { // 一旦发现子序列和超过了s,更新result
subLength = j - i + 1; // 取子序列的长度
result = result < subLength ? result : subLength;
break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就break
}
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
虽然思路一样,但是这个代码明显更深(更难读 -_- )
result = result < subLength ? result : subLength;
说人话就是,result一开始设置成一个最大值(int result = INT32_MAX;
),然后每次要把result更新成一个更小的
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
我是通过一个标记来实现不同情况不同返回值的,但是完全可以在做标记的地方直接返回,代码更简洁
滑动窗口
滑动窗口也可以理解为双指针法的一种!只不过这种解法更像是一个窗口的移动,所以叫做滑动窗口更适合一些。
这是数组操作中一个重要的方法。
所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
在暴力解法中,是一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环,完成了一个不断搜索区间的过程。
那么滑动窗口如何用一个for循环来完成这个操作呢?
首先要思考如果用一个for循环,那么这个for循环的循环变量i应该表示 滑动窗口的起始位置,还是终止位置。
如果只用一个for循环来表示 滑动窗口的起始位置,那么如何遍历剩下的终止位置?
此时难免再次陷入暴力解法的怪圈。(要从起始位置开始遍历确定终止位置)
所以只用一个for循环,那么这个循环的索引,一定是表示 滑动窗口的终止位置。
在本题中实现滑动窗口,主要确定如下三点:
- 窗口内是什么?
- 如何移动窗口的起始位置?
- 如何移动窗口的结束位置?
窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。
窗口的起始位置如何移动:如果当前窗口的值大于等于s了,窗口就要向前移动了(这个窗口已经满足要求了,再看看下一个窗口是否满足要求)。
//滑动窗口起始位置移动一次
if(sum >= s) {
subLength = (j - i + 1); // 取子序列的长度
result = result < subLength ? result : subLength;//result取值为本循环所求子序列长度和当前result中更小的那个
sum -= nums[i];
i++;
}
起始位置要移动多次,所以把if改成while
理解:while其实就是多个if(当需要多次判断时,if升级成while)
窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX;//一个最大值
int sum = 0; // 滑动窗口数值之和
int i = 0; // 滑动窗口起始位置
int subLength = 0; // 滑动窗口的长度
for (int j = 0; j < nums.size(); j++) {
sum += nums[j];
// 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件
while (sum >= s) {
subLength = (j - i + 1); // 取子序列的长度
result = result < subLength ? result : subLength;
sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置)
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
-
为什么滑动窗口时间复杂度为O(n)?
不要以为for里放一个while就以为是O(n^2)
主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是O(n)。
总结
- 滑动窗口精髓:在找到满足条件的窗口后,开始移动起始位置来缩小窗口,再移动终止位置使滑动窗口整体向后移动,找下一个满足条件的滑动窗口
螺旋矩阵Ⅱ
题目链接:LeetCode59.螺旋矩阵II
自己敲
关于我写的半成品代码
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
int i=0;j=0,k=0;
while(k<=n*n){
for(;j<n-1;j++){
generateMatrix[i][j]=k++;
}
for(;i<n-1;i++){
generateMatrix[i][j]=k++;
}
for(;j>0;j--){
generateMatrix[i][j]=k++;
}
for(;i>0;i--){
generateMatrix[i][j]=k++;
}
i--;
}
}
};
想按照螺旋顺序填入元素但短时间内没想通每一个边的起始结束位置怎么确定
看题解
这道题目可以说在面试中出现频率较高的题目,本题并不涉及到什么算法,就是模拟过程,但却十分考察对代码的掌控能力
之前讲二分法的时候提到循环不变量原则,求解本题依然是要坚持循环不变量原则。
模拟顺时针画矩阵的过程:
- 填充上行从左到右
- 填充右列从上到下
- 填充下行从右到左
- 填充左列从下到上
由外向内一圈一圈这么画下去。
可以发现这里的边界条件非常多,在一个循环中,如此多的边界条件,如果不按照固定规则来遍历,那就是一进循环深似海,从此offer是路人。
这里一圈下来,我们要画每四条边,这四条边怎么画,每画一条边都要坚持一致的左闭右开,或者左开右闭的原则,这样这一圈才能按照统一的规则画下来。
那么我按照左闭右开的原则,来画一圈,大家看一下:
这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。
这也是坚持了每条边左闭右开的原则。
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
int count = 1; // 用来给矩阵中每一个空格赋值
int offset = 1; // 需要控制每一条边遍历的长度,每次循环右边界收缩一位
int i,j;
while (loop --) {
i = startx;
j = starty;
// 下面开始的四个for就是模拟转了一圈
// 模拟填充上行从左到右(左闭右开)
for (j; j < n - offset; j++) {
res[i][j] = count++;
}
// 模拟填充右列从上到下(左闭右开)
for (i; i < n - offset; i++) {
res[i][j] = count++;
}
// 模拟填充下行从右到左(左闭右开)
for (; j > starty; j--) {
res[i][j] = count++;
}
// 模拟填充左列从下到上(左闭右开)
for (; i > startx; i--) {
res[i][j] = count++;
}
// 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
startx++;
starty++;
// offset 控制每一圈里每一条边遍历的长度
offset += 1;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
if (n % 2) {
res[mid][mid] = count;
}
return res;
}
};
总结
其实个人感觉坚持循环不变量原则 左闭右开并非此题难点
难点在于思考过程中陷入循环,困扰于每轮循环起始结束位置
- 每轮循环起始位置单独设置变量(由于旋转时既有x轴移动又有y轴移动,所以设置两个起始位置)
- 每轮循环结束位置用一个变量来表示(这里无论怎么移动,采用一个统一的变量表示,因为每轮循环,每转一圈,结束位置都会以固定规律内缩)
- while循环判断条件采用转圈次数,每一圈确定新的起始和结束位置,这样即可把大问题化小(考虑把类似操作循环化,进而解决问题)
- 通过
n/2
确定转圈次数,还可以分出基偶,进一步简化拿到问题时的思维发散程度(为了更好解决问题,尽量减少对旁支末节的思考,抓问题主干)(不代表不思考旁支末节,只是不要让对旁支末节的思考影响主干思路)
一进循环深似海,从此offer是路人🌚🌚🌚
区间和
题目链接:KamaCoder 58. 区间和(第九期模拟笔试)
自己敲
#include<iostream>
using namespace std;
#include<vector>
int main(){
int n;
cin>>n;
vector<int> array;
for(int i=0;i<n;i++){
int sum;//临时变量,存每轮要赋的值
cin>>sum;
array.push_back(sum);
//由于开始没有初始化array,array为空,所以不能通过下标赋值cin>>array[i]
}
int a,b;//区间[a,b]
vector<int> result;
while(cin>>a>>b){
int sum=0;//区间内元素和
for(int i=a;i<=b;i++){
sum+=array[i];
}
result.push_back(sum);
}
for(int i=0;i<result.size();i++){
cout<<result[i]<<endl;
}
return 0;
}
这个程序无法主动结束,没有退出while循环,结果正确但超时
主要问题出在,题中要求 随后的输入为需要计算总和的区间,直至文件结束。
直至文件结束怎么表示?
看题解
暴力解法:给一个区间,然后把这个区间的和都累加一遍
题解说本题时间卡的很严,专门针对这种暴力做法-_-
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, a, b;
cin >> n;
vector<int> vec(n);
for (int i = 0; i < n; i++) cin >> vec[i];
while (cin >> a >> b) {
int sum = 0;
// 累加区间 a 到 b 的和
for (int i = a; i <= b; i++) sum += vec[i];
cout << sum << endl;
}
}
这种暴力做法会超时,但并未提及while (cin >> a >> b)
导致程序未结束问题
上述代码本地运行结果如下
可见程序并没有结束
先不管这个
数组 上常用的解题技巧:前缀和
前缀和 在涉及计算区间和的问题时非常有用!
例如,我们要统计 vec[i] 这个数组上的区间和。
我们先做累加,即 p[i] 表示 下标 0 到 i 的 vec[i] 累加 之和。
如图:
如果,我们想统计,在vec数组上 下标 2 到下标 5 之间的累加和,那是不是就用 p[5] - p[1] 就可以了。
为什么呢?
p[1] = vec[0] + vec[1];
p[5] = vec[0] + vec[1] + vec[2] + vec[3] + vec[4] + vec[5];
p[5] - p[1] = vec[2] + vec[3] + vec[4] + vec[5];
这不就是我们要求的 下标 2 到下标 5 之间的累加和吗。
如图所示:
p[5] - p[1]
就是 红色部分的区间和。
而 p 数组是我们之前就计算好的累加和,所以后面每次求区间和的之后 我们只需要 O(1) 的操作。
特别注意: 在使用前缀和求解的时候,要特别注意 求解区间。
如上图,如果我们要求 区间下标 [2, 5] 的区间和,那么应该是 p[5] - p[1]
,而不是 p[5] - p[2]
。
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, a, b;
cin >> n;
vector<int> vec(n);
vector<int> p(n);
int presum = 0;
for (int i = 0; i < n; i++) {
cin >> vec[i];
presum += vec[i];
p[i] = presum;
}
while (cin >> a >> b) {
int sum;
if (a == 0) sum = p[b];
else sum = p[b] - p[a - 1];
cout << sum << endl;
}
}
C++ 代码 面对大量数据 读取 输出操作,最好用scanf
和 printf
,耗时会小很多:
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, a, b;
cin >> n;
vector<int> vec(n);
vector<int> p(n);
int presum = 0;
for (int i = 0; i < n; i++) {
scanf("%d", &vec[i]);
presum += vec[i];
p[i] = presum;
}
while (~scanf("%d%d", &a, &b)) {
int sum;
if (a == 0) sum = p[b];
else sum = p[b] - p[a - 1];
printf("%d\n", sum);
}
}
总结
- 计算区间问题时采用前缀和思想(有一点像KMP的next数组,多一个辅助数组)
- 以上所有做法程序无法主动结束(ctrl+z+回车自己结束) 当
cin
遇到文件结束符(windows中为:ctrl +Z , Unix 中为:ctrl +D),或无效输入才能使cin
状态无效。
开发商购买土地
题目链接:KamaCoder 44. 开发商购买土地(第五期模拟笔试)
(未完成,本周末更新)
自己敲
思路:垂直切割方向降维,计算前缀和,进而根据数学关系得到结果
优化思路:可以把输入(通过取模运算),降维(输入利用取模运算用一层for实现,无需降维),计算前缀 每个步骤都只用一个for循环实现
没在规定时间敲完,半成品代码如下(样例过了就算对🤓)
#include<iostream>
using namespace std;
#include<vector>
int main(){
int n,m;
cin>>n>>m;
int i,j;
vector<vector<int>> ground(n,vector<int>(m,0));
for(i=0;i<n;i++){
for(j=0;j<m;j++){
cin>>ground[i][j];
}
}
vector<vector<int>> g(ground);
int result1;
//纵向切
//降维
for(j=0;j<m;j++){
int sum=0;
for(i=0;i<n;i++){
sum+=g[i][j];
}
g[0][j]=sum;
}
// for(j=0;j<m;j++){
// cout<<g[0][j]<<" ";
// }
//计算前缀和
int pre=0;
vector<int> p(n+m);
for(j=0;j<m;j++){
pre+=g[0][j];
p[j]=pre;
//cout<<p[j]<<" "<<endl;
}
//切割
for(j=0;j<m;j++){
if(p[j]<=p[m-1]/2&&p[j+1]>p[m-1]/2){//
result1=p[m-1]-2*p[j];
//cout<<p[m-1]<<" "<<p[j]<<" ";
break;
}
}
//cout<<result1<<endl;
g=ground;
int result2;
//横向切
//降维
for(i=0;i<n;i++){
int sum=0;
for(j=0;j<m;j++){
sum+=g[i][j];
}
g[i][0]=sum;
}
//计算前缀和
pre=0;
for(i=0;i<n;i++){
pre+=g[i][0];
p[i]=pre;
//cout<<p[j]<<" "<<endl;
}
//切割
for(i=0;i<n;i++){
if(p[i]<=p[n-1]/2&&p[i+1]>p[n-1]/2){
result2=p[n-1]-2*p[i];
break;
}
}
int res=result1<=result2?result1:result2;
cout<<res<<endl;
return 0;
}