今天的内容是209.长度最小的子数组(滑动窗口)、59.螺旋矩阵II(循环不变量)、区间和(前缀和)、开发商购买土地,拓展题周末解决。
一、长度最小的子数组
题目:给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
示例:输入:s = 7, nums = [2,3,1,2,4,3] 输出:2(子数组 [4,3] )
分析:①暴力解法:两个for循环,然后不断寻找符合条件的子序列,时间复杂度是O(n^2)。
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;
}
};
②滑动窗口:实际上,很多题目需要动笔画画图,有时候多比划几下思路就有了。题目中要求的是“连续”子数组,那么画出一个方框,就往数组里套。所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。动图就显得很明白,引用一下:
这个滑动窗口就是满足其和 ≥ s 的长度最小的连续子数组。如果当前滑动窗口的值大于等于s了,滑动窗口起始位置就要向前移动了(也就是该缩小了)。如果小于s,那么滑动窗口终止位置就要向前移动,以保证滑动窗口内元素之和大于等于s。终止条件则显然是终止位置到达数组末尾。这里对终止条件的思考在二叉树部分屡见不鲜,不过二叉树那边是递归,这里是循环(笑)。
因此可以写出如下代码(用while循环比for循环容易理解):
#include <iostream>
#include <vector>
class Solution
{
public:
int length(std::vector<int>& nums, int s)
{
int i = 0, j = 0, sum = 0, result = INT_MAX;
while(j < nums.size())
{
sum += nums[j];
j++;
while(sum >= s) //用if则会造成一击脱离的情况,例如[1,1,1,1,1,100]
{
result = min(result, j - i + 1);
sum -= nums[i];
i++;
}
}
return result == INT_MAX ? 0 : result;
}
};
int main()
{
int n, s;
while (std::cin >> n) { // 读取数组长度
std::vector<int> nums(n);
for (int i = 0; i < n; ++i) { // 读取数组元素
std::cin >> nums[i];
}
std::cin >> s; // 读取目标值
Solution S;
std::cout << "长度为: " << S.length(nums, s) << std::endl; // 输出结果
}
return 0;
}
滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)暴力解法降为O(n)。
二、59螺旋矩阵II
题目:给定一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
示例:输入: 3 输出: [ [ 1, 2, 3 ], [ 8, 9, 4 ], [ 7, 6, 5 ] ]
这题上来就被吓懵了,后来听讲解发现没有机制,单纯的数值,十分考察对代码的掌控能力。我自觉理解能力不强口齿不清,这里不多写,附上讲解的链接:BV1SL4y1N7mV。
三、区间和
题目:给定一个整数数组 Array,请计算该数组在每个指定区间内元素的总和。
输入:第一行输入为整数数组 Array 的长度 n,接下来 n 行,每行一个整数,表示数组的元素。随后的输入为需要计算总和的区间,直至文件结束。如下:
5
1
2
3
4
5
0 1
1 3
输出:每个指定区间内元素的总和。
3
9
分析:本题最直观的想法是给一个区间,然后把这个区间的和都累加一遍,是一道简单不能再简单的题目。
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, a, b; //a和b为区间左右端点,n为数组元素数量
cin >> n;
vector<int> vec(n);
for (int i = 0; i < n; i++)
{
cin >> vec[i];
}
while (cin >> a >> b) //这里是指输入两个数,第一个为a,第二个为b
{
int sum = 0;
// 累加区间 a 到 b 的和
for (int i = a; i <= b; i++)
{
sum += vec[i];
}
cout << sum << endl;
}
}
这个解法的时间复杂度是 O(n * m) ,m 是查询的次数。非常有优化的空间。
接下来我们来引入前缀和,看看前缀和如何解决这个问题。前缀和的思想是重复利用计算过的子数组之和,从而降低区间查询需要累加计算的次数。
※前缀和 在涉及计算区间和的问题时非常有用!
用一张图来说明(此处计算从下标2☞5的区间和, p[i] 表示数组下标 0 到 i 的累加和。)
#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;
}
}
※特别注意: 在使用前缀和求解的时候,要特别注意求解区间。如果我们要求区间下标 [2, 5] 的区间和,那么应该是 p[5] - p[1],而不是 p[5] - p[2]。
四、开发商购买土地
题目:在一个城市区域内,被划分成了n * m个连续的区块,每个区块都拥有不同的权值,代表着其土地价值。目前,有两家开发公司,A 公司和 B 公司,希望购买这个城市区域的土地。现在,需要将这个城市区域的所有区块分配给 A 公司和 B 公司。然而,由于城市规划的限制,只允许将区域按横向或纵向划分成两个子区域,而且每个子区域都必须包含一个或多个区块。为了确保公平竞争,你需要找到一种分配方式,使得 A 公司和 B 公司各自的子区域内的土地总价值之差最小。注:区块不可再分。
示例:第一行输入两个正整数,代表 n 和 m。接下来的 n 行,每行输出 m 个正整数。请输出一个整数,代表两个子区域内土地总价值之间的最小差距。
【输入示例】3 3 1 2 3 2 1 3 1 2 3 【输出示例】0
【提示信息】如果将区域按照如下方式划分:1 2 | 3 2 1 | 3 1 2 | 3,那么两个子区域内土地总价值之间的最小差距可以达到 0。
这题第一遍时没有看懂,二刷才发现是一刀切,那么可以用上一个前缀和来解决。
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
int main () {
int n, m;
cin >> n >> m;
int sum = 0;
vector<vector<int>> vec(n, vector<int>(m, 0)) ;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> vec[i][j];
sum += vec[i][j];
}
}
int result = INT_MAX;
int count = 0; // 统计遍历过的行
for (int i = 0; i < n; i++) {
for (int j = 0 ; j < m; j++) {
count += vec[i][j];
// 遍历到行末尾时候开始统计
if (j == m - 1) result = min (result, abs(sum - count - count));
}
}
count = 0; // 统计遍历过的列
for (int j = 0; j < m; j++) {
for (int i = 0 ; i < n; i++) {
count += vec[i][j];
// 遍历到列末尾的时候开始统计
if (i == n - 1) result = min (result, abs(sum - count - count));
}
}
cout << result << endl;
}