巧妙解决问题专项

294 阅读5分钟

最长连续序列

题目

image.png

版本1 正确

    public int longestConsecutive(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }

        // 一个未排序的数组, 寻找最长连续序列
        // 注意要求序列是连续的, 即[1, 2, 3, 4]这种而[1, 3, 4]这种就不行
        // 并且构成序列的元素 不要求维持在原数组中的相对顺序 即原来是[4, 1, 2, 3]也可以得到长度为4的[1, 2, 3, 4]

        // 我们可以对nums中每一个元素nums[i]作为起点, 遍历数组, 看以nums[i]为最小值, 能构成的最长连续序列为多长
        // 不断更新最大值即可
        // 但是采用遍历的方式, 每一个元素都要遍历一遍数组, 复杂度是O(n^2)

        // 优化1
        // 有没有方法不用遍历, 就可以知道递增的元素是否存在呢? 就是用哈希set, 记录下每个元素是否出现过即可
        // 我们只需要知道递增的元素是否存在, 它在数组的哪个位置我们并不关心

        // 优化2
        // 我们是不是需要对数组中的所有nums[i]都计算一遍长度呢, 其实并不用, 例如一个元素是3, 另一个元素是2
        // 那么我们只需要计算2作为起点的长度即可

        HashSet<Integer> hashSet = new HashSet<>();
        for (int i = 0; i < nums.length; i ++) {
            hashSet.add(nums[i]);
        }

        int max = Integer.MIN_VALUE;
        for (int i = 0; i < nums.length; i ++) {
            if (hashSet.contains(nums[i] - 1)) {
                // 优化2
                continue;
            }

            // 计算当前元素作为起点的长度
            int temp = nums[i] + 1;
            int tempCount = 1;
            while (hashSet.contains(temp)) {
                tempCount ++;
                temp ++;
            }

            max = Math.max(max, tempCount);
        }

        return max;
    }

正确的原因

(1) 先分析暴力解法

(2) 注意优化1和优化2对算法做出了哪些提升

黑白方格画

题目

image.png

版本1 正确

    public int paintingPlan(int n, int k) {

        // 在一个n * n 的方格面板内, 画任意行, 任意列, 一共有多少种画法
        // 每一种画法都符合如下规则 选择i行 j列涂上黑色, 此时的 i * n + j * (n - i) = k
        // 这里假设先画所有行, 那么再画列的时候, 每一列只能贡献(n - i)个黑色格子
        // 同时题目要求有一个格子不同, 就认为方案不同, 因此(i, j)是(3, 5)和(5, 3)是两种方案
        // 我需要枚举出所有可能的(i, j), 然后计算出所有的组合数即可

        // 特殊情况
        if (k == 0) {
            // 一个都不需要涂
            return 1;
        }
        if (k < n) {
            // 一行都画不满, 不可能存在
            return 0;
        }

        if (k == n) {
            // 只需涂满一行
            if (k == 1) {
                return 1;
            } else {
                return 2 * n;
            }
        }

        if (k == n * n) {
            // 涂满所有格子
            return 1;
        }

        // 普通情况, 得到所有可能的(i, j)组合
        List<int []> temp = new ArrayList<>();
        for (int i = 0; i <= n; i ++) {
            for (int j = 0; j <= n; j ++) {
                if ((i * n + j * (n - i)) == k) {
                    temp.add(new int[]{i, j});
                }
            }
        }

        // 对于每一种组合, 计算可能的答案数
        int ans = 0;
        for (int i = 0; i < temp.size(); i ++) {
            int [] oneKind = temp.get(i);
            int row = oneKind[0];
            int col = oneKind[1];

            // 从n行中选择row行有多少种可能 即C n 取 row
            int rowComb = comb(n, row);
            // 从n列中选择col列有多少种可能
            int colComb = comb(n, col);

            ans += rowComb * colComb;
        }


        return ans;
    }

    // 计算组合数, 利用递归计算, 同时用map减少重复计算
    Map<int [], Integer> map = new HashMap<>();
    public int comb(int n, int m) {
        // 从n个数字, 挑选出m个, 有多少种组合数目 传统的计算公式是Cn取m = n! / (m! * (n - m)!)
        // 这里因为存在缩小问题规模的问题, 因此n 和 m的数目都无法保证, 需要base case

        if (n < m) {
            // 这种情况没有组合数
            return 0;
        }

        if (m == 0) {
            // 取0个数字是一种情况
            return 1;
        }

        // 优化1 例如C5取3 等于C5取2, 并且C5取2计算量小一些
        // 因此转化成C5取2计算
        if (m > n / 2) {
            return comb(n, n - m);
        }

        // 优化2 递归计算, 降低问题规模, comb(n,m)= comb(n-1,m-1)+ comb(n-1,m)
        // 解释思想,从n个球中取出m个球可以分成两种情况相加,从n个球中取出一个球,如果它属于m,还需要从n-1中取出m-1个球;如果它不属于m,则需要从n-1中取出m个球

        // 防止重复计算
        int [] key = new int[]{n, m};
        if (map.containsKey(key)) {
            return map.get(key);
        }
        // 从递归式子可以得出每次递归要么n < m, 要么n == m, 因此不能存在n = 0, m  != 0的情况
        // base case那里就不用管n < 1的情况
        int ans = comb(n - 1, m - 1) + comb(n - 1, m);
        map.put(key, ans);
        return ans;
    }

正确的原因

(1) 注意特殊的n和k

(2) 找到所有的可能数, 即不同的(i, j)

(3) 对于每种可能, 计算不同的组合数, 计算组合数的时候, 如何效率更高

在0到n-1中缺失的数字

题目

image.png

如果按照题目中认为数组元素是有序的

即当n=4的时候 可能是[0, 1, 2, 4] 可能是[0, 2, 3, 4]等 需要注意, 当n=4的时候, 多出来的那个元素一定是4, 不可能是别的数字, 不可能是[0, 1, 2, 10]这种

版本1 正确

    public int missingNumber(int[] nums) {
        
        int left = 0;
        int right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == mid) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        // 此时一定满足nums[left - 1] == left - 1
        return left;


    }

正确的原因

(1) 利用二分查找, 如果nums[mid] == mid, 就说明左侧的数组一定是正常的, 缺失的元素一定在右边, 如果nums[mid] != mid, 就说明缺失的元素在左侧数组. 然后跟二分一样, 缩小范围, 最后指针对应的索引就是缺失的元素

(2) 空间复杂度 o(logN) 时间复杂度o(1)

如果任务数组中的元素是无序的

当n = 4的时候, 给出的nums可能是[1, 2, 0, 4]或者[4, 1, 2, 3]等, 数组中任何元素都是无序的

版本1 求和法

    public static int missingNumber(int[] nums) {
        int length = nums.length;
        int sum = (0 + length) * (length + 1) / 2;
        for (int i = 0; i < length; i++)
            sum -= nums[i];
        return sum;
    }

正确的原因

(1) 遍历一遍数组, 将数组所有元素求和得到sum, 然后0到n本身的和可以由等差数列计算出来, 和为B

例如[0, 2, 3, 4] 计算出来sum = 2 + 3 + 4 = 9, 然后B是[0, 1, 2, 3, 4]的和, 即B = 1 + 2 + 3 + 4 = 10.

那么缺失的元素就是B - sum = 1

(2) 空间复杂度 o(n) 时间复杂度o(1)

版本2 数组下标法

    public int missingNumber(int[] nums) {

        int curIndex = 0;
        // 可能存在给出的nums是[0, 1, 2] 然后此时缺的元素就是3
        int ans = nums.length;
        while (curIndex < nums.length) {
            while (nums[curIndex] == curIndex || nums[curIndex] == nums.length) {
                // 如果当前元素就是nums.length, 需要记录一下位置
                if (nums[curIndex] == nums.length) {
                    ans = curIndex;
                }
                
                curIndex ++;
                // 当当前指针到达数组末尾的时候, 就可以返回结果了
                if (curIndex == nums.length) {
                    return ans;
                }

            }

            // 将nums[curIndex]和 nums[nums[curIndex]] 交换一次元素
            int now = nums[curIndex];
            int temp = nums[now];
            // 如果目标位置的元素是nums.length, 需要记录下位置
            if (temp == nums.length) {
                ans = curIndex;
            }
            nums[curIndex] = temp;
            nums[now] = now;
        }

        return ans;

    }

正确的原因

(1) 因为nums中的数字是连续的, 是0到n-1的, 只会有一个数字不是, 因此如果将数组中的任何一个元素nums[i], 移动到i位置, 那么只有一个元素不在该位置上(此时n所在的位置就是缺失的元素) 或者 所有元素都在对应位置上(那么n就是缺失的元素).

例如[0, 4, 1, 2], 第一次交换完后是[0, 1, 4, 2], 第二次交换完后是[0, 1, 2, 4], 此时4所在的数组索引, 就是缺失的元素, 即返回3.

(2) 需要注意ans初始化的问题, int ans = nums.length;; 因为有可能给出的元素就是0到n-1的, 此时缺的其实是n.

(3) 一旦n元素所在的位置发生变动, 都要记录