算法训练Day2 | LeetCode209 长度最小的子数组、LeetCode59 螺旋矩阵 II、区间和、开发商购买土地

83 阅读10分钟

209 长度最小的子数组

1.题目描述

给定一个含有 n 个正整数的数组和一个正整数 target。找出该数组中满足其总和大于等于 target 的长度最小的子数组[numsl, numsl+1, ..., numsr-1, numsr],并返回其长度。如果不存在符合条件的子数组,返回 0 。

示例 1:

输入: target = 7, nums = [2,3,1,2,4,3]
输出: 2
解释: 子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

输入: target = 4, nums = [1,4,4]
输出: 1

示例 3:

输入: target = 11, nums = [1,1,1,1,1,1,1,1]
输出: 0

提示:

  • 1 <= target <= 10^9
  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^4

2.算法分析

从直觉上说,这道题可以用时间复杂度为O(n2)O(n^2)的暴力算法解决,即用两层for循环,外层循环用于遍历数组中的元素,内层循环用于从某个元素开始进行累计求和,寻找满足条件的子数组。暴力算法的代码实现为:

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int minLen = 0;
        for(int i = 0; i < nums.length; i++) {
            int sum = 0;
            int j = i;
            for(; j < nums.length; j++) {
                sum += nums[j];
                if(sum >= target) {
                    break;
                }
            }
            if(sum >= target) {
                if(minLen == 0 || (minLen > 0 && (j - i + 1) < minLen)) {
                    minLen = j - i + 1;
                }
            }
        }
        return minLen;
    }
}

但是很遗憾,由于数组长度nn最大为10510^5,因此使用暴力算法会出现超时。要降低时间复杂度,可以考虑使用双指针法的变种——滑动窗口法。滑动窗口法本质上还是用两个指针在一层for循环内同时完成暴力算法中两层for循环的操作,在这里就是同时完成数组元素遍历和累计求和:

  • end指针表示窗口/子数组的终点;
  • start指针表示窗口/子数组的起点。

在滑动窗口法中,首先不断向后移动end指针,在这个过程中同时完成数组元素遍历和累计求和操作,当窗口内的元素和大于等于目标值时,就将start当前位置的元素值从累计和中减去,并将start向后移,缩小窗口大小,以寻找最小窗口(因为题目要求寻找最小子数组)。注意到和暴力算法不同的是,此处不需要重新遍历一次窗口,只需要从累计和中减去start所处位置的元素值即可计算出新窗口内的元素和,这也是滑动窗口法降低时间复杂度的奥秘所在。

3.解题代码

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int start = 0;
        int sum = 0;
        int result = 0;
        for(int end = 0; end < nums.length; end++) {
            sum += nums[end];
            // 由于是寻找最小子数组长度,所以此处用while而不是if
            while(sum >= target) {
                int subLen = end - start + 1;
                if(result == 0) {
                    result = subLen;
                }
                else {
                    if(subLen < result) {
                        result = subLen;
                    }
                }
                sum -= nums[start];
                start++;
            }
        }
        return result;
    }
}

59 螺旋矩阵 II

1.题目描述

给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。

示例 1:

输入: n = 3
输出: [[1,2,3],[8,9,4],[7,6,5]]

示例 2:

输入: n = 1
输出: [[1]]

提示:

  • 1 <= n <= 20

2.算法分析

这是一道模拟题,没有什么特别的算法,主要是模拟螺旋的过程,关键在于:

  1. 如何用代码表示螺旋的过程;
  2. 如何处理边界条件。

第一,因为目标矩阵是一个n x n的方阵,因此每一次旋转都固定处理四条边且各条边的长度相同。又因为每一次旋转后,矩阵待填充的行数会减少2,待填充的列数也会减少2,因此每一次旋转都会使得边的长度减2。并且不难看出,每次旋转的起始位置都在矩阵的主对角线上:第一次旋转的起始位置是(0,0),第二次是(1,1),第三次是(2,2),以此类推。

第二,由于矩阵的四个顶点是被两条边所共享的,因此需要设计好各个顶点是被哪条边处理。我建议每条边都处理其起始位置,将终止位置交由下一条边处理,因为上一条边的终止位置就是下一条边的起始位置。这种对于边的表示,就类似于一种左闭右开区间。在这种表示下,每一条边要处理的元素个数就是边的长度减1。

最后需要注意一种特殊情况,当n为偶数时,经过若干次旋转,恰好可以填充完整个矩阵,而当n为奇数时,最后会剩下矩阵的中心位置,此时边长为1,也无所谓旋转,直接将值填入该位置即可。

基于上述分析,就可以写出模拟螺旋的代码了。

3.解题代码

class Solution {
    public int[][] generateMatrix(int n) {
        int[][] matrix = new int[n][n];
        int val = 1;
        int count = 0;// 记录旋转的次数,用于表示旋转的起始位置
        // 初始边长为n,每一次旋转后边长减2
        for(int len = n; len > 0; len -= 2) {
            int i = count;// 旋转起始位置的行索引
            int j = count;// 旋转起始位置的列索引
            if(len == 1) {// 处理n为奇数时最后剩下的中心位置
                matrix[i][j] = val;
            }
            else {
                // 第一条边
                for(; j < count + len - 1; j++) {
                    matrix[i][j] = val;
                    val++;
                }
                // 第二条边
                for(; i < count + len - 1; i++) {
                    matrix[i][j] = val;
                    val++;
                }
                // 第三条边
                // 循环开始时,j为count + len - 1,又因为要处理len - 1个元素,
                // 所以循环条件是j > count,下同
                for(; j > count; j--) {
                    matrix[i][j] = val;
                    val++;
                }
                // 第四条边
                for(; i > count; i--) {
                    matrix[i][j] = val;
                    val++;
                }
            }
            count++;
        }
        return matrix;
    }
}

区间和

1.题目描述

给定一个整数数组Array,请计算该数组在每个指定区间内元素的总和。

输入描述

第一行输入为整数数组Array的长度n,接下来n行,每行一个整数,表示数组的元素。随后的输入为需要计算总和的区间下标a,b(b >= a),直至文件结束。

输出描述

输出每个指定区间内元素的总和。

输入示例

5
1
2
3
4
5
0 1
1 3

输出示例

3
9

提示信息

0 < n <= 100000

题目链接:58.区间和|代码随想录

2.算法分析

对于区间求和,当然可以用循环进行累加求和,即每读入一个区间(a,b),就在(a,b)上做循环累加。显然,这种暴力算法的时间复杂度为O(n2)O(n^2),用于解此题会出现超时的情况。

通过分析不难看出,暴力算法之所以效率低下,是因为进行了多次重复的累计求和。如果能用一个数组rangeSum保存从数组开始到某个元素为止的累加,就可以直接计算出区间和。例如,rangeSum[b]表示从0到b的元素的累加,rangeSum[a]表示从0到a的元素的累加,那么用rangeSum[b] - rangeSum[a - 1](要注意处理a为0的情形)就得到了(a,b)上的区间和。这种方法被称为前缀和,若采用前缀和,则求区间和的时间复杂度降为O(1)O(1),整体的时间复杂度降为O(n)O(n)

3.解题代码

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int[] nums = new int[n];
        int[] rangeSum = new int[n];// 前缀和数组
        int sum = 0;
        for(int i = 0; i < n; i++) {
            int val = scanner.nextInt();
            sum += val;// 读取元素的同时累加计算前缀和
            nums[i] = val;
            rangeSum[i] = sum;
        }
        while(scanner.hasNextInt()) {
            int a = scanner.nextInt();
            int b = scanner.nextInt();
            if(a > 0) {
                System.out.println(rangeSum[b] - rangeSum[a - 1]);
            }
            else { // 注意处理a为0的特殊情形
                System.out.println(rangeSum[b]);
            }
        }
        scanner.close();
    }
}

开发商购买土地

1.题目描述

在一个城市区域内,被划分成了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.如果将区域按照如下方式划分:

1 2 | 3  
2 1 | 3  
1 2 | 3 

两个子区域内土地总价值之间的最小差距可以达到0。

2.数据范围

1 <= n, m <= 100;
n和m不同时为 1。

题目链接:44.开发商购买土地|代码随想录

2.算法分析

由于矩阵只能按行或者列分为两部分,因此实际上我们要处理的是一维数组而非二维矩阵。以按行划分为例,用一个数组来保存各个行上元素的和,那么题目就转化为了将该数组划分为两个子区间,并使得这两个子区间上的元素和的差值最小,故本题实际上是一种区间和问题的变体。要计算两个子区间上元素和的差值,就可以借助前缀和。若A公司的区域范围是行0到行i,B公司的区域范围是行i+1到行n-1,那么A公司的土地价值总和就是行和数组区间(0,i)上的元素和,即行和前缀和数组的元素i的值,类似的,B公司的土地价值总和就是行和数组区间(i+1, n-1)上的元素和,即行和前缀和数组的元素n-1减去元素i(按行计算的总土地价值减去A公司的土地价值总和)。

综上所述,算法分为两步:

第一步,计算矩阵的行和前缀和数组和列和前缀和数组;

第二步,计算按行划分的最小价值差和计算按列划分的最小价值差,二者进行比较得到整体的最小价值差。

第一步的时间复杂度是O(nm)O(n * m),第二步的时间复杂度是O(n+m)O(n+m),总体时间复杂度为O(nm)O(n * m)

3.解题代码

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int m = scanner.nextInt();
        int[][] land = new int[n][m];
        int[] rowRangeSum = new int[n];// 行和前缀和数组
        int[] colRangeSum = new int[m];// 列和前缀和数组
        // 读取元素并计算行和前缀和
        for(int i = 0; i < n; i++) {
            int r_sum = 0;// 行和
            for(int j = 0; j < m; j++) {
                int val = scanner.nextInt();
                land[i][j] = val;
                r_sum += val;
            }
            if(i == 0 ) {
                rowRangeSum[i] = r_sum;
            }
            else {
                rowRangeSum[i] = rowRangeSum[i - 1] + r_sum;// 行和前缀和
            }
        }
        // 计算列和前缀和
        for(int j = 0; j < m; j++) {
            int c_sum = 0;// 列和
            for(int i = 0; i < n; i++) {
                c_sum += land[i][j];
            }
            if(j == 0) {
                colRangeSum[j] = c_sum;
            }
            else {
                colRangeSum[j] = colRangeSum[j - 1] + c_sum;// 列和前缀和
            }
        }
        // 计算按行划分的最小价值差
        int min_row_diff = Integer.MAX_VALUE;
        for(int i = 0; i < n - 1; i++) {
            int val_a = rowRangeSum[i];
            int val_b = rowRangeSum[n - 1] - val_a;
            int diff = Math.abs(val_b - val_a);
            if(diff < min_row_diff) {
                min_row_diff = diff;
            }
        }
        // 计算按列划分的最小价值差
        int min_col_diff = Integer.MAX_VALUE;
        for(int j = 0; j < m - 1; j++) {
            int val_a = colRangeSum[j];
            int val_b = colRangeSum[m - 1] - val_a;
            int diff = Math.abs(val_b - val_a);
            if(diff < min_col_diff) {
                min_col_diff = diff;
            }
        }
        // 计算整体的最小价值差
        int min_val_diff = Math.min(min_row_diff, min_col_diff);
        System.out.println(min_val_diff);
        scanner.close();
    }
}