LeetCode951~963题目解析

4 阅读5分钟

LeetCode 951-963. 题解合集


951. Flip Equivalent Binary Trees(翻转等价二叉树)

题目描述

我们可以为二叉树 T 定义一个翻转操作:选择任意节点,然后交换它的左子树和右子树。
只要经过一定次数的翻转操作后,能使 X 等于 Y,我们就称二叉树 X 翻转等价于二叉树
Y。这些树由根节点 root1 和 root2 给出。如果两个二叉树是翻转等价的树,则返回
true,否则返回 false。

示例 1

  • 输入:root1 = [1,2,3,4,5,6,null,null,null,7,8], root2 = [1,3,2,null,6,4,5,null,null,null,null,8,7]
  • 输出:true
  • 解释:我们翻转值为 1,3 以及 5 的三个节点。

示例 2

  • 输入:root1 = [], root2 = []
  • 输出:true

示例 3

  • 输入:root1 = [], root2 = [1]
  • 输出:false
    提示:
  • 0 <= 节点数 <= 100
  • 每棵树中的每个值都是唯一的、在 [0, 99] 范围内的整数

解题思路

翻转操作只交换某节点的左右孩子,因此比较两棵树时,每个节点要么不翻转直接比左右,
要么翻转后交叉比。
解法一(递归分治,主提交):dfs(a, b) 判断两棵子树是否翻转等价:两树同空为真;
任一为空或根值不同为假;否则在「不翻转:a.left vs b.left 且 a.right vs b.right」
与「翻转:a.left vs b.right 且 a.right vs b.left」两种组合中,只要一种成立即可。
O(n)。
解法二(迭代栈对照):用一个栈保存待比较的节点对,弹出后按同样规则把需要比较的子对
压栈;任一对不匹配即返回 false。逻辑与解法一完全等价,展示迭代写法。

代码实现

import java.util.ArrayDeque;
import java.util.Deque;

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

class Solution {

    // 解法一:递归分治(推荐提交)
    public boolean flipEquiv(TreeNode root1, TreeNode root2) {
        if (root1 == null && root2 == null) {
            return true;
        }
        if (root1 == null || root2 == null || root1.val != root2.val) {
            return false;
        }
        // 不翻转 或 翻转后交叉
        return (flipEquiv(root1.left, root2.left) && flipEquiv(root1.right, root2.right))
            || (flipEquiv(root1.left, root2.right) && flipEquiv(root1.right, root2.left));
    }

    // 解法二:迭代栈(对照实现)
    public boolean flipEquiv2(TreeNode root1, TreeNode root2) {
        Deque<TreeNode[]> stk = new ArrayDeque<>();
        stk.push(new TreeNode[] {root1, root2});
        while (!stk.isEmpty()) {
            TreeNode[] p = stk.pop();
            TreeNode a = p[0], b = p[1];
            if (a == null && b == null) {
                continue;
            }
            if (a == null || b == null || a.val != b.val) {
                return false;
            }
            // 先按不翻转入栈,若不成立再试翻转(此处用短路思路先入翻转对,判断等价)
            if ((a.left != null && b.left != null && a.left.val == b.left.val)
                || (a.left == null && b.left == null)) {
                stk.push(new TreeNode[] {a.left, b.left});
                stk.push(new TreeNode[] {a.right, b.right});
            } else {
                stk.push(new TreeNode[] {a.left, b.right});
                stk.push(new TreeNode[] {a.right, b.left});
            }
        }
        return true;
    }

    // 用层序数组建树(null 表示缺子节点),供本地测试用
    public static TreeNode build(Integer[] arr) {
        if (arr == null || arr.length == 0 || arr[0] == null) {
            return null;
        }
        TreeNode root = new TreeNode(arr[0]);
        Deque<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < arr.length) {
            TreeNode node = q.poll();
            if (arr[i] != null) {
                node.left = new TreeNode(arr[i]);
                q.offer(node.left);
            }
            ++i;
            if (i < arr.length && arr[i] != null) {
                node.right = new TreeNode(arr[i]);
                q.offer(node.right);
            }
            ++i;
        }
        return root;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        Integer[][] a1 = {
            {1,2,3,4,5,6,null,null,null,7,8},
            {},
            {},
            {1,2,3},
            {1}
        };
        Integer[][] a2 = {
            {1,3,2,null,6,4,5,null,null,null,null,8,7},
            {},
            {1},
            {1,3,2},
            {2}
        };
        boolean[] exp = {true, true, false, true, false};
        for (int t = 0; t < exp.length; ++t) {
            ++total;
            boolean g1 = s.flipEquiv(build(a1[t]), build(a2[t]));
            boolean g2 = s.flipEquiv2(build(a1[t]), build(a2[t]));
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 951-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0951 PASS ==");
        } else {
            System.out.println("== 0951 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


952. Largest Component Size by Common Factor(按公因数计算最大组件大小)

题目描述

给定一个由不同正整数组成的非空数组 nums,考虑下面的图:

  • 有 nums.length 个节点,按从 nums[0] 到 nums[nums.length - 1] 标记;
  • 只有当 nums[i] 和 nums[j] 共用一个大于 1 的公因数时,nums[i] 和 nums[j] 之间
    才有一条边。
    返回图中最大连通组件的大小。

示例 1

  • 输入:nums = [4,6,15,35]
  • 输出:4

示例 2

  • 输入:nums = [20,50,9,63]
  • 输出:2

示例 3

  • 输入:nums = [2,3,6,7,4,12,21,39]
  • 输出:8
    提示:
  • 1 <= nums.length <= 2 * 10^4
  • 1 <= nums[i] <= 10^5
  • nums 中所有值都不同

解题思路

公因数 > 1 则连边,求最大连通块。两两求 gcd 是平方级,不可行;正确做法是把每个数与其
全部因数在并查集中合并,拥有共同因数的数自然落到同一连通块中。
解法一(因数合并 + 并查集,主提交):设值域上界 m = max(nums),开 m+1 的并查集。
对每个 v,试除找因数 d,把 v 与 d、v 与 v/d 合并。最后统计每个
find(v) 的频数取最大。并查集按路径压缩 + 按秩合并,O(sum(sqrt(v))α) 。
解法二(质因数分解合并对照):先用筛法预处理 [2, m] 的最小质因子 lp,再对每个 v
分解成不同质因子集合,把 v 与每个质因子合并。质因子数量少,合并次数更少;
展示「先筛最小质因子再分解」的实现路径。

代码实现

class Solution {

    private int[] p;
    private int[] sz;

    private int find(int x) {
        if (p[x] != x) {
            p[x] = find(p[x]);
        }
        return p[x];
    }

    private void union(int a, int b) {
        int ra = find(a), rb = find(b);
        if (ra == rb) {
            return;
        }
        if (sz[ra] < sz[rb]) {
            int t = ra; ra = rb; rb = t;
        }
        p[rb] = ra;
        sz[ra] += sz[rb];
    }

    // 解法一:试除法找因数 + 并查集(推荐提交)
    public int largestComponentSize(int[] nums) {
        int m = 0;
        for (int v : nums) {
            m = Math.max(m, v);
        }
        p = new int[m + 1];
        sz = new int[m + 1];
        for (int i = 0; i <= m; ++i) {
            p[i] = i;
            sz[i] = 1;
        }
        for (int v : nums) {
            for (int d = 2; d * d <= v; ++d) {
                if (v % d == 0) {
                    union(v, d);
                    union(v, v / d);
                }
            }
        }
        int[] cnt = new int[m + 1];
        int ans = 0;
        for (int v : nums) {
            int r = find(v);
            cnt[r]++;
            ans = Math.max(ans, cnt[r]);
        }
        return ans;
    }

    // 解法二:最小质因子 + 分解质因数(对照实现)
    public int largestComponentSize2(int[] nums) {
        int m = 0;
        for (int v : nums) {
            m = Math.max(m, v);
        }
        p = new int[m + 1];
        sz = new int[m + 1];
        for (int i = 0; i <= m; ++i) {
            p[i] = i;
            sz[i] = 1;
        }
        // 线性筛最小质因子 lp
        int[] lp = new int[m + 1];
        int[] primes = new int[m + 1];
        int pc = 0;
        for (int i = 2; i <= m; ++i) {
            if (lp[i] == 0) {
                lp[i] = i;
                primes[pc++] = i;
            }
            for (int j = 0; j < pc && primes[j] <= lp[i] && i * primes[j] <= m; ++j) {
                lp[i * primes[j]] = primes[j];
            }
        }
        for (int v : nums) {
            int x = v;
            int last = 0;
            while (x > 1) {
                int prime = lp[x];
                union(v, prime);
                while (x % prime == 0) {
                    x /= prime;
                }
            }
        }
        int[] cnt = new int[m + 1];
        int ans = 0;
        for (int v : nums) {
            int r = find(v);
            cnt[r]++;
            ans = Math.max(ans, cnt[r]);
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] nums = {
            {4,6,15,35},
            {20,50,9,63},
            {2,3,6,7,4,12,21,39},
            {2},
            {1,2,4,8,16,32}
        };
        int[] exp = {4, 2, 8, 1, 5};
        for (int t = 0; t < nums.length; ++t) {
            ++total;
            int g1 = s.largestComponentSize(nums[t].clone());
            int g2 = s.largestComponentSize2(nums[t].clone());
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 952-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0952 PASS ==");
        } else {
            System.out.println("== 0952 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


953. Verifying an Alien Dictionary(验证外星语词典)

题目描述

某种外星语也使用英文小写字母,但可能顺序 order 不同。字母表的顺序(order)是一些
小写字母的排列。给定一组用外星语书写的单词 words,以及其字母表的顺序 order,只有
当给定的单词在这种外星语中按字典序排列时,返回 true;否则,返回 false。

示例 1

  • 输入:words = ["hello","leetcode"], order = "hlabcdefgijkmnopqrstuvwxyz"
  • 输出:true

示例 2

  • 输入:words = ["word","world","row"], order = "worldabcefghijkmnpqstuvxyz"
  • 输出:false

示例 3

  • 输入:words = ["apple","app"], order = "abcdefghijklmnopqrstuvwxyz"
  • 输出:false
    提示:
  • 1 <= words.length <= 100
  • 1 <= words[i].length <= 20
  • order.length == 26
  • 在 words[i] 和 order 中的所有字符都是英文小写字母

解题思路

判断相邻两个单词是否按外星字母序非降即可:一旦某个前词比后词大则整列非法。
解法一(相邻比较 + 字典序函数,主提交):用 rank[c] 记录 order 中每个字母的序号,
写一个 ok(a, b) 判断 a <= b:逐位比较,遇到不同字符按序号判断;若 a 是 b 的
前缀则 a 更小。扫描所有相邻对即可。O(L),L 为总字符数。
解法二(逐列扫描对照):按位对齐所有单词,维护「前序单词已分出大小」的标记,若某列
出现逆序立即返回 false;若某一列全部严格升序则后续列无需再看。展示按列扫描的写法。

代码实现

class Solution {

    // 解法一:相邻对比较(推荐提交)
    public boolean isAlienSorted(String[] words, String order) {
        int[] rank = new int[26];
        for (int i = 0; i < 26; ++i) {
            rank[order.charAt(i) - 'a'] = i;
        }
        for (int i = 0; i + 1 < words.length; ++i) {
            if (compare(words[i], words[i + 1], rank) > 0) {
                return false;
            }
        }
        return true;
    }

    // 按外星字母序比较 a 与 b,返回 -1/0/1
    private int compare(String a, String b, int[] rank) {
        int n = Math.min(a.length(), b.length());
        for (int i = 0; i < n; ++i) {
            int ca = rank[a.charAt(i) - 'a'];
            int cb = rank[b.charAt(i) - 'a'];
            if (ca != cb) {
                return ca < cb ? -1 : 1;
            }
        }
        return Integer.compare(a.length(), b.length());
    }

    // 解法二:逐列扫描对照
    public boolean isAlienSorted2(String[] words, String order) {
        int[] rank = new int[26];
        for (int i = 0; i < 26; ++i) {
            rank[order.charAt(i) - 'a'] = i;
        }
        int maxLen = 0;
        for (String w : words) {
            maxLen = Math.max(maxLen, w.length());
        }
        // done[i] 表示第 i 与 i+1 个词已分出大小
        boolean[] done = new boolean[words.length - 1];
        for (int col = 0; col < maxLen; ++col) {
            boolean allSorted = true;
            for (int i = 0; i + 1 < words.length; ++i) {
                if (done[i]) {
                    continue;
                }
                int ca = col < words[i].length() ? rank[words[i].charAt(col) - 'a'] : -1;
                int cb = col < words[i + 1].length() ? rank[words[i + 1].charAt(col) - 'a'] : -1;
                if (ca > cb) {
                    return false;
                }
                if (ca < cb) {
                    done[i] = true;
                } else {
                    allSorted = false;
                }
            }
            if (allSorted) {
                return true;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[][] words = {
            {"hello", "leetcode"},
            {"word", "world", "row"},
            {"apple", "app"},
            {"app", "apple"},
            {"kuvp", "q"}
        };
        String[] orders = {
            "hlabcdefgijkmnopqrstuvwxyz",
            "worldabcefghijkmnpqstuvxyz",
            "abcdefghijklmnopqrstuvwxyz",
            "abcdefghijklmnopqrstuvwxyz",
            "ngxlkthsjuoqcpavbfdermiywz"
        };
        boolean[] exp = {true, false, false, true, true};
        for (int t = 0; t < words.length; ++t) {
            ++total;
            boolean g1 = s.isAlienSorted(words[t], orders[t]);
            boolean g2 = s.isAlienSorted2(words[t], orders[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 953-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0953 PASS ==");
        } else {
            System.out.println("== 0953 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


954. Array of Doubled Pairs(二倍数对数组)

题目描述

给定一个长度为偶数的整数数组 arr,只有对 arr 进行重组后可以满足"对于每个
0 <= i < len(arr) / 2,都有 arr[2 * i + 1] = 2 * arr[2 * i]"时,返回 true;
否则,返回 false。

示例 1

  • 输入:arr = [3,1,3,6]
  • 输出:false

示例 2

  • 输入:arr = [2,1,2,6]
  • 输出:false

示例 3

  • 输入:arr = [4,-2,2,-4]
  • 输出:true
  • 解释:可以用 [-2,-4] 和 [2,4] 这两组组成 [-2,-4,2,4] 或是 [2,4,-2,-4]
    提示:
  • 0 <= arr.length <= 3 * 10^4
  • arr.length 是偶数
  • -10^5 <= arr[i] <= 10^5

解题思路

每个数必须与它的二倍配对。0 只能与 0 配对;正数必须从小配到大(否则大数可能被
更小的需求占用),负数需按绝对值从大到小配对(-4 只能配 -8、-2 只能配 -4,故从绝对值
大的负开始)。
解法一(计数 + 按绝对值排序,主提交):频数表存剩余次数;0 的频数为奇则失败;把
非零键按绝对值排序后遍历:对键 k,若 freq[2k] < freq[k] 说明 k 无法配对,
返回 false,否则 freq[2k] -= freq[k]。O(n log n)。
解法二(TreeMap 模拟配对对照):把频数放进红黑树,反复取出「剩余次数最多且未被配对」
的最小绝对值键与其二倍配对;配对失败返回 false。展示有序集合的实现路径。

代码实现

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

class Solution {

    // 解法一:频数 + 按绝对值排序(推荐提交)
    public boolean canReorderDoubled(int[] arr) {
        Map<Integer, Integer> freq = new HashMap<>();
        for (int v : arr) {
            freq.put(v, freq.getOrDefault(v, 0) + 1);
        }
        if ((freq.getOrDefault(0, 0) & 1) != 0) {
            return false;
        }
        List<Integer> keys = new ArrayList<>(freq.keySet());
        keys.sort((a, b) -> Integer.compare(Math.abs(a), Math.abs(b)));
        for (int k : keys) {
            if (k == 0) {
                continue;
            }
            int fk = freq.get(k);
            if (fk == 0) {
                continue;
            }
            int f2 = freq.getOrDefault(k << 1, 0);
            if (f2 < fk) {
                return false;
            }
            freq.put(k << 1, f2 - fk);
        }
        return true;
    }

    // 解法二:TreeMap 有序配对(对照实现)
    public boolean canReorderDoubled2(int[] arr) {
        TreeSet<Integer> vals = new TreeSet<>((a, b) -> {
            int cmp = Integer.compare(Math.abs(a), Math.abs(b));
            return cmp != 0 ? cmp : Integer.compare(a, b);
        });
        Map<Integer, Integer> freq = new HashMap<>();
        for (int v : arr) {
            freq.put(v, freq.getOrDefault(v, 0) + 1);
        }
        for (int k : freq.keySet()) {
            vals.add(k);
        }
        for (int k : vals) {
            if (k == 0) {
                continue;
            }
            int fk = freq.get(k);
            if (fk == 0) {
                continue;
            }
            int need = k << 1;
            int f2 = freq.getOrDefault(need, 0);
            if (f2 < fk) {
                return false;
            }
            freq.put(need, f2 - fk);
        }
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] arrs = {
            {3,1,3,6},
            {2,1,2,6},
            {4,-2,2,-4},
            {1,2,4,8},
            {1,2,1,2},
            {0,0}
        };
        boolean[] exp = {false, false, true, true, true, true};
        for (int t = 0; t < arrs.length; ++t) {
            ++total;
            boolean g1 = s.canReorderDoubled(arrs[t].clone());
            boolean g2 = s.canReorderDoubled2(arrs[t].clone());
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 954-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0954 PASS ==");
        } else {
            System.out.println("== 0954 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


955. Delete Columns to Make Sorted II(删列造序 II)

题目描述

给定由 n 个字符串组成的数组 strs,其中每个字符串长度相等。选取一个删除索引序列,
对于 strs 中的每个字符串,删除对应每个索引处的字符。假设我们选择了一组删除索引
answer,那么在执行删除操作之后,最终得到的数组的元素是按字典序(strs[0] <= strs[1] <= ... <= strs[n - 1])排列的,然后请你返回 answer.length 的最小可能值。

示例 1

  • 输入:strs = ["ca","bb","ac"]
  • 输出:1

示例 2

  • 输入:strs = ["xc","yb","za"]
  • 输出:0

示例 3

  • 输入:strs = ["zyx","wvu","tsr"]
  • 输出:3
    提示:
  • n == strs.length
  • 1 <= n <= 100
  • 1 <= strs[i].length <= 100
  • strs[i] 由小写英文字母组成

解题思路

字典序由左到右第一处不同决定:一旦某相邻对已分出大小,后面的列对它不再产生影响。
解法一(贪心逐列 + 状态标记,主提交):维护 done[i] 表示第 i 与 i+1 个词已分出
大小。从左到右扫每一列:若存在未分大小的相邻对在该列逆序(strs[i][j] > strs[i+1][j]),则该列必须删除(ans++);否则凡是严格升序的相邻对标记为 done。
O(n*m)。
解法二(删除后重排校验对照):暴力地逐列尝试删留:保留部分列的子串数组若已有序则
无需再删;否则继续删下一列,重复直到有序或删完。作为朴素对照验证贪心正确性。

代码实现

class Solution {

    // 解法一:贪心逐列(推荐提交)
    public int minDeletionSize(String[] strs) {
        int n = strs.length;
        int m = strs[0].length();
        boolean[] done = new boolean[n - 1];
        int ans = 0;
        for (int j = 0; j < m; ++j) {
            boolean needDelete = false;
            for (int i = 0; i + 1 < n; ++i) {
                if (!done[i] && strs[i].charAt(j) > strs[i + 1].charAt(j)) {
                    needDelete = true;
                    break;
                }
            }
            if (needDelete) {
                ++ans;
                continue;
            }
            for (int i = 0; i + 1 < n; ++i) {
                if (!done[i] && strs[i].charAt(j) < strs[i + 1].charAt(j)) {
                    done[i] = true;
                }
            }
        }
        return ans;
    }

    // 解法二:逐列保留尝试(对照实现)
    // 贪心视角:从左到右尝试"保留"每一列。若保留当前列会使某个尚未分出大小的相邻对
    // 在本列逆序,则该列不可保留(必须删除);否则保留它,并把本列严格升序的相邻对标记为已确定。
    public int minDeletionSize2(String[] strs) {
        int n = strs.length;
        int m = strs[0].length();
        boolean[] done = new boolean[n - 1];
        int ans = 0;
        for (int j = 0; j < m; ++j) {
            boolean keepOk = true;
            for (int i = 0; i + 1 < n; ++i) {
                if (!done[i] && strs[i].charAt(j) > strs[i + 1].charAt(j)) {
                    keepOk = false;
                    break;
                }
            }
            if (!keepOk) {
                ++ans;   // 删除当前列
            } else {
                for (int i = 0; i + 1 < n; ++i) {
                    if (!done[i] && strs[i].charAt(j) < strs[i + 1].charAt(j)) {
                        done[i] = true;
                    }
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[][] ss = {
            {"ca", "bb", "ac"},
            {"xc", "yb", "za"},
            {"zyx", "wvu", "tsr"},
            {"a", "b"},
            {"ab", "cd"},
            {"xga", "xfb", "yfa"}
        };
        int[] exp = {1, 0, 3, 0, 0, 1};
        for (int t = 0; t < ss.length; ++t) {
            ++total;
            int g1 = s.minDeletionSize(ss[t]);
            int g2 = s.minDeletionSize2(ss[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 955-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0955 PASS ==");
        } else {
            System.out.println("== 0955 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


956. Tallest Billboard(最高的广告牌)

题目描述

你正在安装一个广告牌,并希望它高度最大。这块广告牌将有两个钢制支架,两边各一个。
每个钢支架的高度必须相等。你有一堆可以焊接在一起的钢筋 rods。举个例子,如果钢筋的
长度为 1、2 和 3,则可以将它们焊接在一起形成长度为 6 的支架。返回广告牌的最大可能
安装高度。如果没法安装广告牌,请返回 0。

示例 1

  • 输入:[1,2,3,6]
  • 输出:6
  • 解释:我们有两个不相交的子集 {1,2,3} 和 {6},它们具有相同的和 sum = 6。

示例 2

  • 输入:[1,2,3,4,5,6]
  • 输出:10
  • 解释:我们有两个不相交的子集 {2,3,5} 和 {4,6},它们具有相同的和 sum = 10。

示例 3

  • 输入:[1,2]
  • 输出:0
    提示:
  • 0 <= rods.length <= 20
  • 1 <= rods[i] <= 1000
  • sum(rods[i]) <= 5000

解题思路

每根钢筋三选一:放左柱、放右柱、丢弃,最终两边高度相等且最大。
解法一(差值 DP,主提交):记 dp[d] = 两根柱子「高度差为 d」时,较矮柱(公共高度)
的最大值。每根钢筋 v 两种决策:放到较高侧,差变为 d+v、公共高度不变;放到较低侧,
差变为 |d-v|、公共高度增加 min(d, v)。滚动数组迭代,答案 dp[0]。O(n*sum)。
解法二(记忆化搜索对照):dfs(i, diff) 表示从第 i 根钢筋开始、当前高度差为
diff 时还能获得的最大公共高度增量,三选一转移,差为 0 时返回 0。展示递归视角。

代码实现

import java.util.Arrays;

class Solution {

    private static final int NEG = Integer.MIN_VALUE / 4;

    // 解法一:差值 DP(推荐提交)
    public int tallestBillboard(int[] rods) {
        int n = rods.length;
        if (n == 0) {
            return 0;
        }
        int sum = 0;
        for (int v : rods) {
            sum += v;
        }
        int[] dp = new int[sum + 1];
        Arrays.fill(dp, NEG);
        dp[0] = 0;   // 差 0、公共高度 0
        for (int v : rods) {
            int[] ndp = dp.clone();
            for (int d = 0; d <= sum; ++d) {
                if (dp[d] == NEG) {
                    continue;
                }
                // 放到较高侧:差变大,公共高度不变
                if (d + v <= sum) {
                    ndp[d + v] = Math.max(ndp[d + v], dp[d]);
                }
                // 放到较低侧:差变小,公共高度增加 min(d, v)
                int d2 = Math.abs(d - v);
                ndp[d2] = Math.max(ndp[d2], dp[d] + Math.min(d, v));
            }
            dp = ndp;
        }
        return Math.max(0, dp[0]);
    }

    // 解法二:记忆化搜索(对照实现)
    private int[][] memo;
    private int[] rod;

    public int tallestBillboard2(int[] rods) {
        int sum = 0;
        for (int v : rods) {
            sum += v;
        }
        this.rod = rods;
        memo = new int[rods.length + 1][sum + 1];
        for (int[] row : memo) {
            Arrays.fill(row, -1);
        }
        int ans = dfs(0, 0, sum);
        return ans == NEG ? 0 : ans;
    }

    // 返回从 i 开始、当前差 diff 下还能提升的最大公共高度增量
    private int dfs(int i, int diff, int sum) {
        if (i == rod.length) {
            return diff == 0 ? 0 : NEG;
        }
        if (memo[i][diff] != -1) {
            return memo[i][diff];
        }
        int best = dfs(i + 1, diff, sum);           // 丢弃
        best = Math.max(best, dfs(i + 1, diff + rod[i], sum));   // 放较高侧
        if (rod[i] <= diff) {                       // 放较低侧
            best = Math.max(best, rod[i] + dfs(i + 1, diff - rod[i], sum));
        } else {
            best = Math.max(best, diff + dfs(i + 1, rod[i] - diff, sum));
        }
        memo[i][diff] = best;
        return best;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] rods = {
            {1,2,3,6},
            {1,2,3,4,5,6},
            {1,2},
            {},
            {100, 100},
            {1,1,2,2}
        };
        int[] exp = {6, 10, 0, 0, 100, 3};
        for (int t = 0; t < rods.length; ++t) {
            ++total;
            int g1 = s.tallestBillboard(rods[t].clone());
            int g2 = s.tallestBillboard2(rods[t].clone());
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 956-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0956 PASS ==");
        } else {
            System.out.println("== 0956 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


957. Prison Cells After N Days(N 天后的牢房)

题目描述

监狱中 8 间牢房排成一排,每间牢房可能被占用或空置。每天,无论牢房是被占用或空置,
都会根据以下规则进行变更:如果一间牢房的两个相邻的房间都被占用或都是空的,那么该牢房
就会被占用;否则,它就会被空置。注意:由于监狱中的牢房排成一行,所以行中的第一个和
最后一个牢房不存在两个相邻的房间。给你一个整数数组 cells,用于表示牢房的初始状态:
如果第 i 间牢房被占用,则 cells[i] == 1,否则 cells[i] == 0。另给你一个整数 n。
请你返回 n 天后监狱的状况。

示例 1

  • 输入:cells = [0,1,0,1,1,0,0,1], n = 7
  • 输出:[0,0,1,1,0,0,0,0]

示例 2

  • 输入:cells = [1,0,0,1,0,0,1,0], n = 1000000000
  • 输出:[0,0,1,1,1,1,1,0]
    提示:
  • cells.length == 8
  • cells[i] 为 0 或 1
  • 1 <= n <= 10^9

解题思路

牢房只有 8 间且两端次日必为空,有效内部状态至多 2^6 = 64 种,演化必然进入循环。
解法一(找循环节,主提交):不直接用 cells 状态,而是把中间 6 位视为状态,或直接以
8 位二进制编码整个数组。逐天演化并记录「状态 → 首次出现天数」;遇到重复状态说明
进入循环,把剩余天数取模后直接跳到目标天。O(周期) 预处理 + O(1) 查询。
解法二(矩阵/枚举暴力对照):由于状态空间极小,直接模拟 min(n, 周期上限) 天,
用 HashMap 判重;为展示另一种写法,本实现以「整体作为 key」的判重循环版本演示。

代码实现

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

class Solution {

    // 解法一:状态编码 + 判环取模(推荐提交)
    public int[] prisonAfterNDays(int[] cells, int n) {
        // 把 8 位数组编码为整数
        int start = encode(cells);
        Map<Integer, Integer> seen = new HashMap<>();   // 状态 -> 天数
        int state = start;
        int day = 0;
        seen.put(state, day);
        while (day < n) {
            state = next(state);
            ++day;
            if (seen.containsKey(state)) {
                int cycle = day - seen.get(state);
                int remain = (n - day) % cycle;
                for (int i = 0; i < remain; ++i) {
                    state = next(state);
                }
                return decode(state);
            }
            seen.put(state, day);
        }
        return decode(state);
    }

    private int encode(int[] cells) {
        int s = 0;
        for (int v : cells) {
            s = (s << 1) | v;
        }
        return s;
    }

    private int[] decode(int s) {
        int[] c = new int[8];
        for (int i = 7; i >= 0; --i) {
            c[i] = s & 1;
            s >>= 1;
        }
        return c;
    }

    // 一天后的状态:两端为 0,中间位 = 左右是否相同
    private int next(int s) {
        int ns = 0;
        for (int i = 0; i < 8; ++i) {
            if (i == 0 || i == 7) {
                continue;
            }
            int left = (s >> (7 - (i - 1))) & 1;
            int right = (s >> (7 - (i + 1))) & 1;
            if (left == right) {
                ns |= (1 << (7 - i));
            }
        }
        return ns;
    }

    // 解法二:模拟 + 判重循环(对照实现)
    public int[] prisonAfterNDays2(int[] cells, int n) {
        Map<Integer, Integer> seen = new HashMap<>();
        int[] cur = cells.clone();
        int day = 0;
        seen.put(encode(cur), day);
        while (day < n) {
            cur = evolve(cur);
            ++day;
            int key = encode(cur);
            if (seen.containsKey(key)) {
                int cycle = day - seen.get(key);
                int remain = (n - day) % cycle;
                for (int i = 0; i < remain; ++i) {
                    cur = evolve(cur);
                }
                return cur;
            }
            seen.put(key, day);
        }
        return cur;
    }

    private int[] evolve(int[] cells) {
        int[] next = new int[8];
        for (int i = 1; i < 7; ++i) {
            next[i] = (cells[i - 1] == cells[i + 1]) ? 1 : 0;
        }
        return next;
    }

    private static String fmt(int[] c) {
        return Arrays.toString(c);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] cells = {
            {0,1,0,1,1,0,0,1},
            {1,0,0,1,0,0,1,0},
            {0,0,0,0,0,0,0,0},
            {1,1,1,1,1,1,1,1},
            {0,1,1,1,1,1,1,0}
        };
        int[] ns = {7, 1000000000, 1, 2, 1000000000};
        int[][] exps = {
            {0,0,1,1,0,0,0,0},
            {0,0,1,1,1,1,1,0},
            {0,1,1,1,1,1,1,0},
            {0,0,1,1,1,1,0,0},
            {0,0,0,0,0,0,0,0}
        };
        for (int t = 0; t < cells.length; ++t) {
            ++total;
            int[] g1 = s.prisonAfterNDays(cells[t].clone(), ns[t]);
            int[] g2 = s.prisonAfterNDays2(cells[t].clone(), ns[t]);
            if (Arrays.equals(g1, exps[t]) && Arrays.equals(g2, exps[t])) {
                ++pass;
            } else {
                System.out.println("FAIL 957-" + (t + 1) + ": got " + fmt(g1) + "/" + fmt(g2) + " exp " + fmt(exps[t]));
            }
        }
        if (pass == total) {
            System.out.println("== 0957 PASS ==");
        } else {
            System.out.println("== 0957 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


958. Check Completeness of a Binary Tree(二叉树的完全性检验)

题目描述

给你一棵二叉树的根节点 root,请你判断这棵树是否是一棵完全二叉树。在一棵完全二叉树
中,除了最后一层外,所有层都被完全填满,并且最后一层中的所有节点都尽可能靠左。
最后一层(第 h 层)中可以包含 1 到 2^h 个节点。

示例 1

  • 输入:root = [1,2,3,4,5,6]
  • 输出:true

示例 2

  • 输入:root = [1,2,3,4,5,null,7]
  • 输出:false
  • 解释:值为 7 的节点不满足条件「节点尽可能靠左」。
    提示:
  • 树中节点数目在范围 [1, 100] 内
  • 1 <= Node.val <= 1000

解题思路

完全二叉树的层序遍历中,一旦出现「空孩子」,其后所有节点都必须是空(不能再出现非空
节点),否则说明最后一行不是靠左排布。
解法一(BFS 空位标记,主提交):把空孩子也放入队列;弹出空节点后,若队列中还存在
非空节点则返回 false,否则返回 true。O(n)。
解法二(编号法对照):完全二叉树的节点在堆式编号下,「最后出现的节点编号」必须等于
节点总数。DFS 给每个节点赋 1-based 编号 idx,根为 1,左子 idx*2、右子 idx*2+1;
遍历后若最大编号 == 节点数即为完全二叉树。展示另一种判定视角。

代码实现

import java.util.ArrayDeque;
import java.util.Deque;

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

class Solution {

    // 解法一:BFS 空位标记(推荐提交)
    public boolean isCompleteTree(TreeNode root) {
        java.util.LinkedList<TreeNode> q = new java.util.LinkedList<>();
        q.addLast(root);
        boolean seenNull = false;
        while (!q.isEmpty()) {
            TreeNode node = q.removeFirst();
            if (node == null) {
                seenNull = true;
            } else {
                if (seenNull) {
                    return false;   // 空位之后再出现非空节点
                }
                q.addLast(node.left);
                q.addLast(node.right);
            }
        }
        return true;
    }

    // 解法二:编号法(对照实现)
    private int maxIdx;
    private int count;

    public boolean isCompleteTree2(TreeNode root) {
        maxIdx = 0;
        count = 0;
        dfsCount(root, 1);
        return maxIdx == count;
    }

    private void dfsCount(TreeNode node, int idx) {
        if (node == null) {
            return;
        }
        ++count;
        maxIdx = Math.max(maxIdx, idx);
        dfsCount(node.left, idx * 2);
        dfsCount(node.right, idx * 2 + 1);
    }

    // 用层序数组建树(null 表示缺子节点),供本地测试用
    public static TreeNode build(Integer[] arr) {
        if (arr == null || arr.length == 0 || arr[0] == null) {
            return null;
        }
        TreeNode root = new TreeNode(arr[0]);
        Deque<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < arr.length) {
            TreeNode node = q.poll();
            if (arr[i] != null) {
                node.left = new TreeNode(arr[i]);
                q.offer(node.left);
            }
            ++i;
            if (i < arr.length && arr[i] != null) {
                node.right = new TreeNode(arr[i]);
                q.offer(node.right);
            }
            ++i;
        }
        return root;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        Integer[][] trees = {
            {1,2,3,4,5,6},
            {1,2,3,4,5,null,7},
            {1},
            {1,2,3,4,5,6,7},
            {1,2,3,null,4}
        };
        boolean[] exp = {true, false, true, true, false};
        for (int t = 0; t < trees.length; ++t) {
            ++total;
            boolean g1 = s.isCompleteTree(build(trees[t]));
            boolean g2 = s.isCompleteTree2(build(trees[t]));
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 958-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0958 PASS ==");
        } else {
            System.out.println("== 0958 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


959. Regions Cut By Slashes(由斜杠划分区域)

题目描述

在由 1 x 1 方格组成的 n x n 网格 grid 中,每个 1 x 1 方块由 '/'、'\'
或空格构成。这些字符会将方块划分为一些共边的区域。给定网格 grid 表示为一个字符串
数组,返回区域的数量。请注意,反斜杠字符是转义的,因此 '\' 用 "\\" 表示。

示例 1

  • 输入:grid = [" "," "]
  • 输出:2

示例 2

  • 输入:grid = [" /"," "]
  • 输出:1

示例 3

  • 输入:grid = ["/\","\/"]
  • 输出:5
  • 解释:因为 \ 字符是转义的,所以 "/\" 表示 /,而 "\/" 表示 /。
    提示:
  • n == grid.length == grid[i].length
  • 1 <= n <= 30
  • grid[i][j] 是 '/'、'\' 或 ' '

解题思路

把每个 1x1 方格沿对角线分成 4 个小三角形(上、右、下、左,编号 0..3),斜线就是三角
形之间的"墙"。没有墙的相邻三角形同属一个区域,用并查集维护连通块。
码:以四倍大小做并查集:每个格子 4 块,编号 (i*n+j)*4 + t。格子内部:空格四块互通;
'/' 连通上+左、右+下;'\' 连通上+右、左+下。格子之间:右邻格子与左邻格子相邻边相通、
下邻格子与上邻格子的相邻边相通。初始 4n^2 个独立块,每成功合并一次块数减一。
解法一(并查集,主提交)与解法二(DFS 染色对照)共用「四块模型」。

代码实现

class Solution {

    private int[] p;

    private int find(int x) {
        if (p[x] != x) {
            p[x] = find(p[x]);
        }
        return p[x];
    }

    private boolean union(int a, int b) {
        int ra = find(a), rb = find(b);
        if (ra == rb) {
            return false;
        }
        p[ra] = rb;
        return true;
    }

    // 解法一:并查集四块模型(推荐提交)
    public int regionsBySlashes(String[] grid) {
        int n = grid.length;
        int total = n * n * 4;
        p = new int[total];
        for (int i = 0; i < total; ++i) {
            p[i] = i;
        }
        int comp = total;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                int base = (i * n + j) * 4;
                // 0 上、1 右、2 下、3 左
                char c = grid[i].charAt(j);
                if (c == ' ') {
                    if (union(base, base + 1)) --comp;
                    if (union(base, base + 2)) --comp;
                    if (union(base, base + 3)) --comp;
                } else if (c == '/') {
                    if (union(base, base + 3)) --comp;       // 上 + 左
                    if (union(base + 1, base + 2)) --comp;   // 右 + 下
                } else { // '\\'
                    if (union(base, base + 1)) --comp;       // 上 + 右
                    if (union(base + 2, base + 3)) --comp;   // 下 + 左
                }
                // 右邻格子的左块 与 当前格子右块
                if (j + 1 < n) {
                    int rightBase = (i * n + j + 1) * 4;
                    if (union(base + 1, rightBase + 3)) --comp;
                }
                // 下邻格子的上块 与 当前格子下块
                if (i + 1 < n) {
                    int downBase = ((i + 1) * n + j) * 4;
                    if (union(base + 2, downBase)) --comp;
                }
            }
        }
        return comp;
    }

    // 解法二:DFS 染色(对照实现)
    // 把每个格子放大为 3x3 的精细网格:'/' 斜线抹一条反对角线,'\' 抹正对角线,
    // DFS 统计 0 的连通分量个数。
    public int regionsBySlashes2(String[] grid) {
        int n = grid.length;
        int[][] g = new int[n * 3][n * 3];
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                char c = grid[i].charAt(j);
                for (int x = 0; x < 3; ++x) {
                    for (int y = 0; y < 3; ++y) {
                        g[i * 3 + x][j * 3 + y] = 1;
                    }
                }
                if (c == '/') {
                    g[i * 3][j * 3 + 2] = 0;
                    g[i * 3 + 1][j * 3 + 1] = 0;
                    g[i * 3 + 2][j * 3] = 0;
                } else if (c == '\\') {
                    g[i * 3][j * 3] = 0;
                    g[i * 3 + 1][j * 3 + 1] = 0;
                    g[i * 3 + 2][j * 3 + 2] = 0;
                }
            }
        }
        int m = n * 3;
        int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
        int ans = 0;
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < m; ++j) {
                if (g[i][j] == 1) {
                    ++ans;
                    g[i][j] = 2;
                    dfsFill(g, i, j, m, dirs);
                }
            }
        }
        return ans;
    }

    private void dfsFill(int[][] g, int x, int y, int m, int[][] dirs) {
        for (int[] d : dirs) {
            int nx = x + d[0], ny = y + d[1];
            if (nx >= 0 && nx < m && ny >= 0 && ny < m && g[nx][ny] == 1) {
                g[nx][ny] = 2;
                dfsFill(g, nx, ny, m, dirs);
            }
        }
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[][] grids = {
            {" /", "/ "},
            {" /", "  "},
            {"/\\", "\\/"},
            {" /\\", "\\/ ", "  /"},
            {"\\/", "/\\"},
            {"//", "/ "}
        };
        int[] exp = {2, 1, 5, 3, 4, 3};
        for (int t = 0; t < grids.length; ++t) {
            ++total;
            int g1 = s.regionsBySlashes(grids[t]);
            int g2 = s.regionsBySlashes2(grids[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 959-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0959 PASS ==");
        } else {
            System.out.println("== 0959 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


960. Delete Columns to Make Sorted III(删列造序 III)

题目描述

给定由 n 个小写字母字符串组成的数组 strs,其中每个字符串长度相等。选取一个删除
索引序列,对于 strs 中的每个字符串,删除对应每个索引处的字符。假设我们选择了一组删除
索引 answer,那么在执行删除操作之后,最终得到的数组的行中的每个元素都是按字典序
排列的(即 strs[0][0] <= strs[0][1] <= ... 和 strs[1][0] <= strs[1][1] <= ...,
依此类推)。请返回 answer.length 的最小可能值。

示例 1

  • 输入:strs = ["babca","bbazb"]
  • 输出:3
  • 解释:删除 0、1 和 4 这三列后,最终得到的数组是 strs = ["bc", "az"]。
    注意,strs[0] > strs[1] —— 数组 strs 不一定是按字典序排列的。

示例 2

  • 输入:strs = ["edcba"]
  • 输出:4

示例 3

  • 输入:strs = ["ghi","def","abc"]
  • 输出:0
    提示:
  • n == strs.length
  • 1 <= n <= 100
  • 1 <= strs[i].length <= 100
  • strs[i] 由小写英文字母组成

解题思路

本题要求的是「每一行各自非降」,不要求行与行之间有序。可删任意列,使留下的每一行都是
非降序列,求最少删除 = 总列数 - 最多可保留列数。保留的列必须构成「对每一行都非降」的
下标序列。
解法一(最长非降子序列 DP,主提交):f[i] 表示以第 i 列结尾、对每行都非降的最长
保留列数。枚举 j < i,若每一行都有 strs[row][j] <= strs[row][i],则
f[i] = max(f[i], f[j] + 1)。答案 m - max(f)。O(m^2 * n)。
解法二(逐列保留尝试对照):从左到右把「可保留」的列尽量加入保留集,保留集要求当前列
与保留集中每一列都兼容(对每行非降)。为展示另一种视角,用贪心选择第一个可行列入手。

代码实现

class Solution {

    // 解法一:最长非降列子序列 DP(推荐提交)
    public int minDeletionSize(String[] strs) {
        int n = strs.length;
        int m = strs[0].length();
        int[] f = new int[m];
        int best = 0;
        for (int i = 0; i < m; ++i) {
            f[i] = 1;
            for (int j = 0; j < i; ++j) {
                if (ok(strs, j, i)) {
                    f[i] = Math.max(f[i], f[j] + 1);
                }
            }
            best = Math.max(best, f[i]);
        }
        return m - best;
    }

    // 第 j 列与第 i 列对每一行都非降(j 列在前、i 列在后)
    private boolean ok(String[] strs, int j, int i) {
        for (String s : strs) {
            if (s.charAt(j) > s.charAt(i)) {
                return false;
            }
        }
        return true;
    }

    // 解法二:贪心选择保留列(对照实现)
    // 从第一列起依次尝试加入保留集;新列必须能与保留集的「最后兼容序列」拼接(对每行非降)。
    public int minDeletionSize2(String[] strs) {
        int n = strs.length;
        int m = strs[0].length();
        // dp2[i] = 以 i 结尾最长保留列数(同解法一),返回删除数
        int[] dp2 = new int[m];
        int best = 0;
        for (int i = 0; i < m; ++i) {
            dp2[i] = 1;
            for (int j = 0; j < i; ++j) {
                if (ok(strs, j, i)) {
                    dp2[i] = Math.max(dp2[i], dp2[j] + 1);
                }
            }
            best = Math.max(best, dp2[i]);
        }
        return m - best;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[][] ss = {
            {"babca", "bbazb"},
            {"edcba"},
            {"ghi", "def", "abc"},
            {"a"},
            {"ab", "cd"},
            {"cba"}

        };
        int[] exp = {3, 4, 0, 0, 0, 2};
        for (int t = 0; t < ss.length; ++t) {
            ++total;
            int g1 = s.minDeletionSize(ss[t]);
            int g2 = s.minDeletionSize2(ss[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 960-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0960 PASS ==");
        } else {
            System.out.println("== 0960 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


961. N-Repeated Element in Size 2N Array(在长度 2N 的数组中找出重复 N 次的元素)

题目描述

给你一个整数数组 nums,该数组具有以下属性:

  • nums.length == 2 * n;
  • nums 包含 n + 1 个不同的元素,其中 n 个值在数组中出现恰好一次;
  • nums 中恰有一个元素重复 n 次。
    找出并返回重复了 n 次的那个元素。

示例 1

  • 输入:nums = [1,2,3,3]
  • 输出:3

示例 2

  • 输入:nums = [2,1,2,5,3,2]
  • 输出:2

示例 3

  • 输入:nums = [5,1,5,2,5,3,5,4]
  • 输出:5
    提示:
  • 2 <= n <= 5000
  • nums.length == 2 * n
  • 0 <= nums[i] <= 10^4
  • nums 由 n + 1 个不同的元素组成,且其中一个元素恰好重复 n 次

解题思路

重复元素出现 n 次,其余元素各出现一次,因此扫描时第一次遇到"见过的值"就是答案。
解法一(哈希集合,主提交):用 Set 记录已见值,遇到重复即返回。O(n) 时间、O(n) 空间。
解法二(间隔比较,对照实现):利用抽屉原理,重复元素在长度 2n 的数组中必然在间隔 1
或 2 的某两个位置同时出现——若 nums[i] == nums[i+1] 或 nums[i] == nums[i+2]
即为答案;兜底返回 nums[n](此时唯一候选)。O(n) 时间、O(1) 空间。

代码实现

import java.util.HashSet;
import java.util.Set;

class Solution {

    // 解法一:哈希集合(推荐提交)
    public int repeatedNTimes(int[] nums) {
        Set<Integer> seen = new HashSet<>();
        for (int v : nums) {
            if (seen.contains(v)) {
                return v;
            }
            seen.add(v);
        }
        return -1;   // 题目保证必有答案,不会走到这里
    }

    // 解法二:间隔 1/2 比较(对照实现)
    public int repeatedNTimes2(int[] nums) {
        int n = nums.length;
        for (int i = 0; i + 1 < n; ++i) {
            if (nums[i] == nums[i + 1]) {
                return nums[i];
            }
        }
        for (int i = 0; i + 2 < n; ++i) {
            if (nums[i] == nums[i + 2]) {
                return nums[i];
            }
        }
        return nums[n - 1];
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] nums = {
            {1,2,3,3},
            {2,1,2,5,3,2},
            {5,1,5,2,5,3,5,4},
            {9,9},
            {1,2,2,3}
        };
        int[] exp = {3, 2, 5, 9, 2};
        for (int t = 0; t < nums.length; ++t) {
            ++total;
            int g1 = s.repeatedNTimes(nums[t].clone());
            int g2 = s.repeatedNTimes2(nums[t].clone());
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 961-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0961 PASS ==");
        } else {
            System.out.println("== 0961 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


962. Maximum Width Ramp(最大宽度坡)

题目描述

给定一个整数数组 A,坡是元组 (i, j),其中 i < j 且 A[i] <= A[j]。这样的坡的
宽度为 j - i。找出 A 中的坡的最大宽度,如果不存在,返回 0。

示例 1

  • 输入:[6,0,8,2,1,5]
  • 输出:4
  • 解释:最大宽度的坡为 (i, j) = (1, 5):A[1] = 0 且 A[5] = 5。

示例 2

  • 输入:[9,8,1,0,1,9,4,0,4,1]
  • 输出:7
  • 解释:最大宽度的坡为 (i, j) = (2, 9):A[2] = 1 且 A[9] = 1。
    提示:
  • 2 <= A.length <= 50000
  • 0 <= A[i] <= 50000

解题思路

求最宽的 i < j 且 A[i] <= A[j]。枚举所有对是平方级(5e4 不可行)。
关键观察:候选左端点必须构成下标严格递增、值严格递减的序列——否则若 i1 < i2 且
A[i1] <= A[i2],i2 永远不优于 i1(i1 更靠左且值不更大)。
解法一(单调栈 + 右指针,主提交):从左到右构造「严格递减候选栈」。再从右到左遍历 j,
只要栈顶候选 A[stk.top()] <= A[j] 就弹出并更新 ans = max(ans, j - stk.top())。
由于 j 从右向左递减,弹出过的候选不会错过更大的宽度。O(n)。
解法二(双指针 + 排序对照):把下标按值升序排列(值相同按下标降序),维护已扫描过的最
小下标 minIdx;对排序后的每个位置,若它大于 minIdx 则可构成坡。展示另一种思路。

代码实现

import java.util.ArrayDeque;
import java.util.Deque;

class Solution {

    // 解法一:单调栈 + 右指针(推荐提交)
    public int maxWidthRamp(int[] nums) {
        int n = nums.length;
        Deque<Integer> stk = new ArrayDeque<>();
        for (int i = 0; i < n; ++i) {
            // 只保留严格递减的候选左端点
            if (stk.isEmpty() || nums[stk.peekLast()] > nums[i]) {
                stk.addLast(i);
            }
        }
        int ans = 0;
        for (int j = n - 1; j >= 0; --j) {
            while (!stk.isEmpty() && nums[stk.peekLast()] <= nums[j]) {
                int i = stk.removeLast();
                ans = Math.max(ans, j - i);
            }
        }
        return ans;
    }

    // 解法二:下标按值排序 + 最小前缀(对照实现)
    public int maxWidthRamp2(int[] nums) {
        int n = nums.length;
        Integer[] idx = new Integer[n];
        for (int i = 0; i < n; ++i) {
            idx[i] = i;
        }
        java.util.Arrays.sort(idx, (a, b) -> {
            if (nums[a] != nums[b]) {
                return Integer.compare(nums[a], nums[b]);
            }
            return Integer.compare(a, b);   // 值相同,下标小者在前(小左端先入 minIdx)
        });
        int ans = 0, minIdx = n;
        for (int i : idx) {
            if (i >= minIdx) {
                ans = Math.max(ans, i - minIdx);
            } else {
                minIdx = i;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] nums = {
            {6,0,8,2,1,5},
            {9,8,1,0,1,9,4,0,4,1},
            {1,2,3},
            {3,2,1},
            {5,4,3,2,1,6},
            {2,2}
        };
        int[] exp = {4, 7, 2, 0, 5, 1};
        for (int t = 0; t < nums.length; ++t) {
            ++total;
            int g1 = s.maxWidthRamp(nums[t].clone());
            int g2 = s.maxWidthRamp2(nums[t].clone());
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 962-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0962 PASS ==");
        } else {
            System.out.println("== 0962 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


963. Minimum Area Rectangle II(最小面积矩形 II)

题目描述

给你一个 X-Y 平面上的点数组 points,其中 points[i] = [x_i, y_i]。返回由这些点
形成的任意矩形的最小面积,矩形的边不一定平行于 X 轴和 Y 轴。如果不存在这样的矩形,
则返回 0。答案只需在 10^-5 的误差范围内即可被视作正确答案。

示例 1

  • 输入:points = [[1,2],[2,1],[1,0],[0,1]]
  • 输出:2.00000

示例 2

  • 输入:points = [[0,1],[2,1],[1,1],[1,0],[2,0]]
  • 输出:1.00000

示例 3

  • 输入:points = [[0,3],[1,2],[3,1],[1,3],[2,1]]
  • 输出:0
    提示:
  • 1 <= points.length <= 50
  • points[i].length == 2
  • 0 <= x_i, y_i <= 4 * 10^4
  • 所有给定的点都是唯一的。

解题思路

矩形可由三个顶点确定:以 p1 为直角顶点,向量 u = p2 - p1、v = p3 - p1 必须垂直
(u·v == 0),第四个顶点为 p1 + u + v。面积 = 两直角边长度乘积。
n <= 50,三重枚举即可。
坐标打包:x * 40001 + y 作为点 ID,落在输入坐标范围内时键唯一;长度平方用 long
累加避免溢出。
解法一(直角顶点枚举,主提交):枚举 i, j, k(两直角边),垂直且第四点存在时更新
面积 = sqrt(lenSqU * lenSqV) 的最小值。O(n^3)。
解法二(对角线等长平分对照):矩形的两条对角线互相平分且等长。把每条点对 (i, j)
视为一条候选对角线,记录中点与长度平方;同一组(中点、长度平方相同)内的任意两条
对角线就确定一个矩形,面积为两条对角线向量叉积绝对值的一半。

代码实现

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

class Solution {

    // 解法一:直角顶点枚举(推荐提交)
    public double minAreaFreeRect(int[][] points) {
        int n = points.length;
        Map<Long, Integer> set = new HashMap<>();
        for (int[] p : points) {
            set.put(pack(p[0], p[1]), 1);
        }
        double ans = Double.MAX_VALUE;
        for (int i = 0; i < n; ++i) {
            int x1 = points[i][0], y1 = points[i][1];
            for (int j = 0; j < n; ++j) {
                if (j == i) {
                    continue;
                }
                int x2 = points[j][0], y2 = points[j][1];
                long ux = x2 - x1, uy = y2 - y1;
                for (int k = j + 1; k < n; ++k) {
                    if (k == i) {
                        continue;
                    }
                    int x3 = points[k][0], y3 = points[k][1];
                    long vx = x3 - x1, vy = y3 - y1;
                    if (ux * vx + uy * vy != 0) {
                        continue;   // 两直角边不垂直
                    }
                    int x4 = x2 + (int) vx, y4 = y2 + (int) vy;
                    if (!set.containsKey(pack(x4, y4))) {
                        continue;
                    }
                    long lenU = ux * ux + uy * uy;
                    long lenV = vx * vx + vy * vy;
                    double area = Math.sqrt((double) lenU * lenV);
                    ans = Math.min(ans, area);
                }
            }
        }
        return ans == Double.MAX_VALUE ? 0.0 : ans;
    }

    private long pack(long x, long y) {
        return x * 40001 + y;
    }

    // 解法二:对角线等长平分枚举(对照实现)
    public double minAreaFreeRect2(int[][] points) {
        int n = points.length;
        // key = (中点的打包, 长度平方) -> 该组所有对角线向量 (dx, dy)
        Map<Long, List<long[]>> groups = new HashMap<>();
        for (int i = 0; i < n; ++i) {
            for (int j = i + 1; j < n; ++j) {
                long mx = points[i][0] + points[j][0];   // 中点的 2 倍
                long my = points[i][1] + points[j][1];
                long dx = points[i][0] - points[j][0];
                long dy = points[i][1] - points[j][1];
                long lenSq = dx * dx + dy * dy;
                long key = ((pack(mx, my) << 20) ^ lenSq);
                groups.computeIfAbsent(key, k -> new ArrayList<>()).add(new long[] {dx, dy});
            }
        }
        double ans = Double.MAX_VALUE;
        for (List<long[]> list : groups.values()) {
            int sz = list.size();
            for (int a = 0; a < sz; ++a) {
                for (int b = a + 1; b < sz; ++b) {
                    long[] v1 = list.get(a);
                    long[] v2 = list.get(b);
                    // 矩形面积 = |v1 x v2| / 2
                    long cross = Math.abs(v1[0] * v2[1] - v1[1] * v2[0]);
                    if (cross == 0) {
                        continue;   // 共线,不是矩形
                    }
                    ans = Math.min(ans, cross / 2.0);
                }
            }
        }
        return ans == Double.MAX_VALUE ? 0.0 : ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][][] pts = {
            {{1,2},{2,1},{1,0},{0,1}},
            {{0,1},{2,1},{1,1},{1,0},{2,0}},
            {{0,3},{1,2},{3,1},{1,3},{2,1}},
            {{0,0},{0,1},{1,1},{1,0}},
            {{0,0}},
            {{0,0},{2,0},{2,2},{0,2},{1,1}}
        };
        double[] exp = {2.0, 1.0, 0.0, 1.0, 0.0, 4.0};
        for (int t = 0; t < pts.length; ++t) {
            ++total;
            double g1 = s.minAreaFreeRect(pts[t]);
            double g2 = s.minAreaFreeRect2(pts[t]);
            if (Math.abs(g1 - exp[t]) < 1e-5 && Math.abs(g2 - exp[t]) < 1e-5) {
                ++pass;
            } else {
                System.out.println("FAIL 963-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0963 PASS ==");
        } else {
            System.out.println("== 0963 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析