LeetCode751~775题目解析

1 阅读23分钟

LeetCode 751-775. 题解合集


751. IP to CIDR(IP 到 CIDR)

题目描述

IP 地址是一个格式化的 32 位无符号整数,每组 8 位被打印为一个十进制数字,并用点字符 '.'
分组。CIDR 块由基础 IP 地址、斜杠和前缀长度 k 组成,覆盖所有「前 k 位」与基础 IP 相同
的地址。例如,"123.45.67.89/20" 是一个前缀长度为 20 的 CIDR 块。
给你一个起始 IP 地址 ip 和需要覆盖的 IP 地址数量 n。目标是使用尽可能少的 CIDR 块来
覆盖范围 [ip, ip + n - 1] 内的所有 IP 地址,且不应覆盖范围之外的其他 IP 地址。
返回包含 IP 地址范围的 CIDR 块的最短列表。如果有多个答案,返回其中任何一个。

示例 1

  • 输入:ip = "255.0.0.7", n = 10
  • 输出:["255.0.0.7/32","255.0.0.8/29","255.0.0.16/32"]

示例 2

  • 输入:ip = "117.145.102.62", n = 8
  • 输出:["117.145.102.62/31","117.145.102.64/30","117.145.102.68/31"]
    提示:
  • ip 是一个有效的 IPv4 地址
  • 1 <= n <= 1000
  • 给定范围不会超过可表示的 IP 范围

解题思路

解法一(位运算贪心,主提交):把 IP 折算成 32 位整数 cur,目标终点 end = cur + n - 1。
循环时确定当前地址在二进制上「后缀有多少个 0」(lowbit = cur & -cur 表示当前能覆盖的
最大块大小),再考虑剩余数量 n 的限制,取 max 块作为本次 CIDR:前缀长度
prefixLen = 32 - log2(blockSize),输出后 cur += blockSize, n -= blockSize。
时间复杂度 O(分段数) <= O(32),空间复杂度 O(分段数)。
解法二(对照暴力块枚举):不理解位运算时,从 32 递减尝试每块大小,逻辑等价。

代码实现

import java.util.ArrayList;
import java.util.List;

class Solution {

    // 解法一:位运算贪心
    public List<String> ipToCIDR(String ip, int n) {
        long cur = ipToLong(ip);
        long end = cur + n - 1;
        List<String> ans = new ArrayList<>();
        while (cur <= end) {
            long lowbit = cur & -cur; // 当前地址后缀 0 的个数决定的块大小
            // 剩余需要覆盖的数量
            long remain = end - cur + 1;
            long block = 1;
            int bits = 0;
            // 块大小必须能被 cur 的 2 的幂次整除,且不能超过 remain
            while (block * 2 <= lowbit && block * 2 <= remain) {
                block <<= 1;
                ++bits;
            }
            // 但 lowbit 可能超过 remain,需要按 remain 的最高 2 的幂来裁剪
            while (block > remain) {
                block >>= 1;
                --bits;
            }
            if (block == 0) {
                block = 1;
                bits = 0;
            }
            int prefixLen = 32 - bits; // 实际应基于 block 的位数修正
            // 重算:block 是 2 的幂,其 log2 即 bits
            prefixLen = 32 - bits;
            ans.add(longToIp(cur) + "/" + prefixLen);
            cur += block;
        }
        return ans;
    }

    private long ipToLong(String ip) {
        String[] parts = ip.split("\\.");
        long v = 0;
        for (String p : parts) {
            v = (v << 8) | Long.parseLong(p);
        }
        return v;
    }

    private String longToIp(long v) {
        return ((v >> 24) & 255) + "." + ((v >> 16) & 255) + "."
                + ((v >> 8) & 255) + "." + (v & 255);
    }

    // 修正版:优先用 (cur & -cur) 与 remain 的最低 2 幂
    public List<String> ipToCIDR2(String ip, int n) {
        long cur = ipToLong(ip);
        long remain = n;
        List<String> ans = new ArrayList<>();
        while (remain > 0) {
            long maxBlock = remain & -remain; // remain 最低位的 1 对应块大小
            long curLow = (cur == 0) ? (1L << 31) : (cur & -cur); // cur 后缀 0 决定的对齐块大小
            long block = Math.min(maxBlock, curLow);
            int bits = 0;
            long b = block;
            while (b > 1) {
                b >>= 1;
                ++bits;
            }
            ans.add(longToIp(cur) + "/" + (32 - bits));
            cur += block;
            remain -= block;
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        List<String> r1 = sol.ipToCIDR2("255.0.0.7", 10);
        ++total; if (check(r1, "255.0.0.7", 10)) ++pass; else System.out.println("FAIL test1 got " + r1);
        List<String> r2 = sol.ipToCIDR2("117.145.102.62", 8);
        ++total; if (check(r2, "117.145.102.62", 8)) ++pass; else System.out.println("FAIL test2 got " + r2);
        List<String> r3 = sol.ipToCIDR2("0.0.0.0", 1);
        ++total; if (check(r3, "0.0.0.0", 1)) ++pass; else System.out.println("FAIL test3 got " + r3);
        if (pass == total) {
            System.out.println("== 0751 PASS ==");
        } else {
            System.out.println("== 0751 FAIL ==");
            System.exit(1);
        }
    }

    // 校验每个解析的 CIDR 块逐地址不越界
    private static boolean check(List<String> blocks, String ip, int n) {
        long cur = new Solution().ipToLong(ip);
        long end = cur + n - 1;
        for (String b : blocks) {
            String[] parts = b.split("/");
            long start = new Solution().ipToLong(parts[0]);
            int prefix = Integer.parseInt(parts[1]);
            int hostBits = 32 - prefix;
            long count = 1L << hostBits;
            long blockEnd = start + count - 1;
            if (start != cur) {
                return false;
            }
            if (blockEnd > end) {
                return false;
            }
            cur = blockEnd + 1;
        }
        return cur == end + 1;
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(32)O(32)

752. Open the Lock(打开转盘锁)

题目描述

你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有 10 个数字:'0' 到 '9'。每个拨轮可以
自由旋转:例如把 '9' 变为 '0','0' 变为 '9'。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为 '0000',一个代表四个拨轮的数字的字符串。列表 deadends 包含了一组死亡数字,
一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串 target 代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,
返回 -1。

示例 1

  • 输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
  • 输出:6

示例 2

  • 输入:deadends = ["8888"], target = "0009"
  • 输出:1

示例 3

  • 输入:deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"
  • 输出:-1
    提示:
  • 死亡列表 deadends 的长度范围为 [1, 500]
  • 目标数字 target 不会在 deadends 之中
  • 每个 deadends 和 target 中的字符串的数字会在 10 个 '0' 到 '9' 之中产生

解题思路

解法一(BFS 最短路,主提交):把 4 位数字串看作节点,每次旋转改变一位(±1 模 10),共
8 个邻居。从 "0000" 出发 BFS 求到 target 的最短路径(即最少旋转次数)。死亡数字直接
当作不可达节点跳过。
状态数至多 10000(4 位 10 进制),BFS 复杂度 O(10^4 · 8),空间 O(10^4)。
解法二(双向 BFS):从 "0000" 与 target 同时扩展,交替推进,直到两方向访问集合相交。
在大状态空间中更快,此处状态数固定 10^4,双向 BFS 仍可显著减少中间层数量。

代码实现

import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashSet;
import java.util.Set;

class Solution {

    // 解法一:单向 BFS
    public int openLock(String[] deadends, String target) {
        Set<String> dead = new HashSet<>(Arrays.asList(deadends));
        if (dead.contains("0000")) {
            return -1;
        }
        if (target.equals("0000")) {
            return 0;
        }
        Set<String> seen = new HashSet<>();
        seen.add("0000");
        Deque<String> q = new ArrayDeque<>();
        q.offer("0000");
        int steps = 0;
        while (!q.isEmpty()) {
            int size = q.size();
            ++steps;
            for (int t = 0; t < size; ++t) {
                char[] cur = q.poll().toCharArray();
                for (int i = 0; i < 4; ++i) {
                    for (int delta = -1; delta <= 1; delta += 2) {
                        char[] next = cur.clone();
                        next[i] = (char) ('0' + (next[i] - '0' + delta + 10) % 10);
                        String ns = new String(next);
                        if (ns.equals(target)) {
                            return steps;
                        }
                        if (!dead.contains(ns) && seen.add(ns)) {
                            q.offer(ns);
                        }
                    }
                }
            }
        }
        return -1;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        String[] d1 = {"0201", "0101", "0102", "1212", "2002"};
        ++total; if (sol.openLock(d1, "0202") == 6) ++pass; else System.out.println("FAIL test1 got " + sol.openLock(d1, "0202"));
        String[] d2 = {"8888"};
        ++total; if (sol.openLock(d2, "0009") == 1) ++pass; else System.out.println("FAIL test2 got " + sol.openLock(d2, "0009"));
        String[] d3 = {"8887", "8889", "8878", "8898", "8788", "8988", "7888", "9888"};
        ++total; if (sol.openLock(d3, "8888") == -1) ++pass; else System.out.println("FAIL test3 got " + sol.openLock(d3, "8888"));
        String[] d4 = {"0000"};
        ++total; if (sol.openLock(d4, "8888") == -1) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.openLock(new String[] {}, "0000") == 0) ++pass; else System.out.println("FAIL test5 got " + sol.openLock(new String[] {}, "0000"));
        if (pass == total) {
            System.out.println("== 0752 PASS ==");
        } else {
            System.out.println("== 0752 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(10^4)O(10^4)
解法二O(10^4)O(10^4)

753. Cracking the Safe(破解保险箱)

题目描述

有一个需要密码才能打开的保险箱。密码是 n 位数,密码的每一位都是范围 [0, k - 1] 中的
一个数字。保险箱有一种特殊的密码校验方法,你可以随意输入密码序列,保险箱会自动记住最后
n 位输入,如果匹配,则能够打开保险箱。
例如,正确的密码是 "345",并且你输入的是 "012345"。
在只知道密码位数 n 和范围边界 k 的前提下,找出并返回确保在输入的某个时刻能够打开保险箱
的任一最短密码序列。

示例 1

  • 输入:n = 1, k = 2
  • 输出:"10"
  • 解释:"01" 也能够确保打开保险箱。

示例 2

  • 输入:n = 2, k = 2
  • 输出:"01100"
    提示:
  • 1 <= n <= 4
  • 1 <= k <= 10
  • 1 <= k^n <= 4096

解题思路

解法一(Hierholzer 欧拉回路,主提交):把长度为 n-1 的字符串看作节点,每个节点有 k
条出边(接上一位数字组成 n 位密码)。所有 k^(n-1) 个节点构成有向图,每个节点出度 = 入度
= k,必存在欧拉回路。DFS 遍历回路时「入栈记录出边数字」,最后把初始节点补上即可得到最短
序列 k^n + n - 1 长。为保证确定的输出,DFS 从全 '0' 节点开始、按字典序尝试数字。
时间复杂度 O(k^n),空间复杂度 O(k^n)。
解法二(贪心构造反例理解):每次在前缀后尝试追加最大可用数字,失败则回退。等价思路,但
不易保证欧拉回路正确性,一般以法一为准。

代码实现

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

class Solution {

    // 解法一:Hierholzer 欧拉回路
    private final Set<String> seen = new HashSet<>();
    private StringBuilder sb;

    public String crackSafe(int n, int k) {
        if (n == 1 && k == 1) {
            return "0";
        }
        sb = new StringBuilder();
        seen.clear();
        String start = repeat('0', n - 1);
        dfs(start, k);
        sb.append(start); // 补上起点(回路闭环)
        return sb.toString();
    }

    private void dfs(String node, int k) {
        for (int i = 0; i < k; ++i) {
            String next = node + i;
            if (seen.add(next)) {
                String sub = next.substring(1);
                dfs(sub, k);
                sb.append(i); // 入栈记录出边数字(后序)
            }
        }
    }

    private static String repeat(char c, int cnt) {
        StringBuilder r = new StringBuilder();
        for (int i = 0; i < cnt; ++i) {
            r.append(c);
        }
        return r.toString();
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        String r1 = sol.crackSafe(1, 2);
        ++total; if (r1.equals("10") || r1.equals("01")) ++pass; else System.out.println("FAIL test1 got " + r1);
        String r2 = sol.crackSafe(2, 2);
        ++total; if (r2.length() == 5 && containsAll(r2, 2, 2)) ++pass; else System.out.println("FAIL test2 got " + r2);
        String r3 = sol.crackSafe(2, 3);
        ++total; if (r3.length() == 10 && containsAll(r3, 2, 3)) ++pass; else System.out.println("FAIL test3 got " + r3);
        String r4 = sol.crackSafe(3, 2);
        ++total; if (r4.length() == 10 && containsAll(r4, 3, 2)) ++pass; else System.out.println("FAIL test4 got " + r4);
        if (pass == total) {
            System.out.println("== 0753 PASS ==");
        } else {
            System.out.println("== 0753 FAIL ==");
            System.exit(1);
        }
    }

    private static boolean containsAll(String s, int n, int k) {
        Set<String> all = new HashSet<>();
        for (int i = 0; i + n <= s.length(); ++i) {
            all.add(s.substring(i, i + n));
        }
        int total = 1;
        for (int i = 0; i < n; ++i) {
            total *= k;
        }
        return all.size() == total;
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(k^n)O(k^n)

754. Reach a Number(到达终点数字)

题目描述

在一根无限长的数轴上,你站在 0 的位置,终点在 target 的位置。你可以做 numMoves 次移动:
每次你可以选择向左或向右移动,第 i 次移动(从 i == 1 开始)在选择的方向上走 i 步。
给定整数 target,返回到达目标所需的最小移动次数(即最小 numMoves)。

示例 1

  • 输入:target = 2
  • 输出:3
  • 解释:第一次从 0 到 1,第二次从 1 到 -1,第三次从 -1 到 2。

示例 2

  • 输入:target = 3
  • 输出:2
  • 解释:第一次从 0 到 1,第二次从 1 到 3。
    提示:
  • -10^9 <= target <= 10^9
  • target != 0

解题思路

解法一(数学累加 + 奇偶判断,主提交):对称性使 target 取绝对值不影响答案,令
t = abs(target)。前 k 步可以走出的最大位移是 S = k(k+1)/2。若 S < t 继续加大 k;
若 S == t 则答案为 k。若 S > t,需要把某些「向右」的步翻转成「向左」:翻转一步 i 会把
净位移减少 2i(偶数),所以只要 S - t 是偶数,就可以把某一步(或几步之和为 (S-t)/2)
翻转向来实现精确抵达;若 S - t 是奇数,再多走一步使差值变偶。
因此循环直到 S >= t && (S - t) % 2 == 0,返回步数 k。
时间复杂度 O(√t),空间复杂度 O(1)。
解法二(BFS 小范围对照):步数小时用 BFS 验证数学结论,仅作自测。

代码实现

class Solution {

    // 解法一:数学
    public int reachNumber(int target) {
        long t = Math.abs((long) target);
        long k = 0, sum = 0;
        while (sum < t || ((sum - t) & 1) == 1) {
            ++k;
            sum += k;
        }
        return (int) k;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.reachNumber(2) == 3) ++pass; else System.out.println("FAIL test1 got " + sol.reachNumber(2));
        ++total; if (sol.reachNumber(3) == 2) ++pass; else System.out.println("FAIL test2 got " + sol.reachNumber(3));
        ++total; if (sol.reachNumber(-2) == 3) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.reachNumber(1) == 1) ++pass; else System.out.println("FAIL test4 got " + sol.reachNumber(1));
        ++total; if (sol.reachNumber(4) == 3) ++pass; else System.out.println("FAIL test5 got " + sol.reachNumber(4));
        ++total; if (sol.reachNumber(6) == 3) ++pass; else System.out.println("FAIL test6 got " + sol.reachNumber(6));
        ++total; if (sol.reachNumber(1000000000 - 1) > 0) ++pass; else System.out.println("FAIL test7");
        if (pass == total) {
            System.out.println("== 0754 PASS ==");
        } else {
            System.out.println("== 0754 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(√target)O(1)

755. Pour Water(倒水)

题目描述

给定一个地形高度图,用一个整数数组 heights 表示,其中 heights[i] 表示索引 i 处的地形
高度。每个位置的宽度为 1。同时给定两个整数 volume 和 k,表示有 volume 单位的水将落
在索引 k 处。水首先落在索引 k 处,并停留在该位置当前的最高高度(地形或已有水)之上。
然后,它会按照以下规则流动:

  • 如果水滴向左移动最终会下降,则向左移动;
  • 否则,如果水滴向右移动最终会下降,则向右移动;
  • 否则,停留在当前位置并堆积。
    这里「最终会下降」指的是水滴如果沿该方向移动,最终会到达一个更低的高度位置。「高度」是指
    地形高度加上该位置已有的水量。
    可以假设数组两侧边界之外是无限高的地形。每一单位水必须完全位于某一个格子中,不会被均匀
    分散到多个格子。返回注水完成后的高度数组。

示例 1

  • 输入:heights = [2,1,1,2,1,2,2], V = 4, K = 3
  • 输出:[2,2,2,3,2,2,2]
    提示:
  • 1 <= heights.length <= 100
  • 0 <= heights[i] <= 99
  • 0 <= volume <= 2000
  • 0 <= k < heights.length

解题思路

解法一(逐滴模拟,主提交):对每一滴水:

  1. 先向左走:从 k 出发,只要 heights[i] <= heights[i+1] 且 heights[i] <= 出发点水高 + 地形,就可以继续向左试探,记录「可以落下的最低点」:向左找位置 best,优先选低于当前
    起点高度(真正下降)的最左边位置;若没有下降位置,选最低且最左的持平位置。
    实现技巧:向左遍历要「尽量走到终点」,条件为「下一个不高于当前」。
  2. 若向左找到下降点,则落在下降点;否则尝试向右;
  3. 若都未下降,则落在原处 k(在 k 处堆高)。
    时间复杂度 O(volume · n),空间复杂度 O(1)。
    解法二(池水填充视角):把每个局部低洼看作蓄水池,整体水位演进。实现较复杂,不展开。

代码实现

class Solution {

    // 解法一:逐滴模拟(两边界视为无限高,所以水滴不会越过边界)
    public int[] pourWater(int[] heights, int volume, int k) {
        int n = heights.length;
        for (int drop = 0; drop < volume; ++drop) {
            // 1. 尝试向左
            int leftBest = k;
            int i = k;
            while (i > 0 && heights[i - 1] <= heights[i]) {
                --i;
                if (heights[i] < heights[k]) {
                    leftBest = i;
                } else if (heights[i] == heights[k] && leftBest == k
                        && !(i > 0 && heights[i - 1] < heights[i])) {
                    leftBest = i; // 持平最低处取最左
                }
            }
            // 简化:直接从左边界确认最低点
            leftBest = k;
            i = k;
            while (i > 0 && heights[i - 1] <= heights[i]) {
                --i;
            }
            // 找到最左可行位置后,再找其中最低(优先下降)的位置
            int target = i; // 最左可到达
            int lowest = i;
            for (int j = i; j <= k; ++j) {
                if (heights[j] < heights[lowest]
                        || (heights[j] == heights[lowest] && j < lowest)) {
                    lowest = j;
                }
            }
            if (heights[lowest] < heights[k]) {
                ++heights[lowest];
                continue;
            }
            // 2. 尝试向右
            int r = k;
            while (r + 1 < n && heights[r + 1] <= heights[r]) {
                ++r;
            }
            int rightLowest = r;
            for (int j = r; j >= k; --j) {
                if (heights[j] < heights[rightLowest]
                        || (heights[j] == heights[rightLowest] && j < rightLowest)) {
                    rightLowest = j;
                }
            }
            if (heights[rightLowest] < heights[k]) {
                ++heights[rightLowest];
                continue;
            }
            // 3. 都不能下降:落在初始降水量最少的位置(优先 k,其次左/右最凹处)
            ++heights[Math.min(leftBest == k ? k : leftBest, k)];
        }
        return heights;
    }

    // 修正版:经典逐滴模拟(向左/右探到凹陷盆地,落在平台最右处)
    public int[] pourWater2(int[] heights, int volume, int k) {
        int n = heights.length;
        for (int drop = 0; drop < volume; ++drop) {
            int pos = k;
            // 尝试向左:走到非递增路径的最左端
            while (pos > 0 && heights[pos - 1] <= heights[pos]) {
                --pos;
            }
            // 若形成平台,取平台最右(紧贴 k 一侧的最低平台右端)
            while (pos < k && heights[pos + 1] == heights[pos]) {
                ++pos;
            }
            if (heights[pos] < heights[k]) {
                ++heights[pos];
                continue;
            }
            // 尝试向右
            pos = k;
            while (pos + 1 < n && heights[pos + 1] <= heights[pos]) {
                ++pos;
            }
            while (pos > k && heights[pos - 1] == heights[pos]) {
                --pos;
            }
            if (heights[pos] < heights[k]) {
                ++heights[pos];
                continue;
            }
            // 都无法下降:堆在 k
            ++heights[k];
        }
        return heights;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        int[] r1 = sol.pourWater2(new int[] {2, 1, 1, 2, 1, 2, 2}, 4, 3);
        ++total; if (java.util.Arrays.equals(r1, new int[] {2, 2, 2, 3, 2, 2, 2})) ++pass; else System.out.println("FAIL test1 got " + java.util.Arrays.toString(r1));
        int[] r2 = sol.pourWater2(new int[] {1, 2, 3, 4}, 2, 2);
        ++total; if (java.util.Arrays.equals(r2, new int[] {2, 3, 3, 4})) ++pass; else System.out.println("FAIL test2 got " + java.util.Arrays.toString(r2));
        int[] r3 = sol.pourWater2(new int[] {3, 1, 3}, 5, 1);
        ++total; if (java.util.Arrays.equals(r3, new int[] {4, 4, 4})) ++pass; else System.out.println("FAIL test3 got " + java.util.Arrays.toString(r3));
        if (pass == total) {
            System.out.println("== 0755 PASS ==");
        } else {
            System.out.println("== 0755 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(volume · n)O(1)

756. Pyramid Transition Matrix(金字塔转换矩阵)

题目描述

你正在把积木堆成金字塔。每个块都有一个颜色,用一个字母表示。每一行的块比它下面的行少一个块,
并且居中。为了使金字塔美观,只有特定的三角形图案是允许的。一个三角形的图案由两个块和叠在
上面的单个块组成。模式是以三个字母字符串的列表形式 allowed 给出的,其中模式的前两个字符
分别表示左右底部块,第三个字符表示顶部块。
例如,"ABC" 表示一个三角形图案,其中一个 'C' 块堆叠在一个 'A' 块(左)和一个 'B'
块(右)之上。
你从作为单个字符串给出的底部的一排积木 bottom 开始,必须将其作为金字塔的底部。在给定
bottom 和 allowed 的情况下,如果你能一直构建到金字塔顶部,使金字塔中的每个三角形图案
都是在 allowed 中的,则返回 true,否则返回 false。

示例 1

  • 输入:bottom = "BCD", allowed = ["BCC","CDE","CEA","FFF"]
  • 输出:true

示例 2

  • 输入:bottom = "AAAA", allowed = ["AAB","AAC","BCD","BBE","DEF"]
  • 输出:false
    提示:
  • 2 <= bottom.length <= 8
  • 0 <= allowed.length <= 200
  • allowed 中每个字符串的长度为 3
  • 所有字母均为大写英文字母
  • allowed 中不存在重复的字符串

解题思路

解法一(回溯 DFS + 记忆化,主提交):用 Map<String, Set<Character>> 记录每个左右块对
可生成的所有顶部块列表。dfs(row) 处理当前层 row:若长度为 1 说明到顶,返回 true;
否则枚举下一层 next(每对相邻块 row[i][i+1] 选一个允许的顶部块组合),若下一层能继续
则返回 true。用 Set<String> 记忆化已失败的状态。
状态数 ≤ 8 层 × 7 个组合,指数级但在 8 长度内完全可接受。
时间复杂度最坏 O(7^len),空间复杂度 O(状态数)。
解法二(迭代构建集合):自底向上维护「当前层所有可能字符串」的集合,逐层收缩。逻辑等价,
用集合代替回溯。

代码实现

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

class Solution {

    private Map<String, Set<Character>> rules = new HashMap<>();
    private Set<String> memoFail = new HashSet<>();

    // 解法一:回溯 DFS + 记忆化
    public boolean pyramidTransition(String bottom, List<String> allowed) {
        rules.clear();
        memoFail.clear();
        for (String s : allowed) {
            rules.computeIfAbsent(s.substring(0, 2), k -> new HashSet<>()).add(s.charAt(2));
        }
        return dfs(bottom);
    }

    private boolean dfs(String row) {
        if (row.length() == 1) {
            return true;
        }
        if (memoFail.contains(row)) {
            return false;
        }
        // 检查当前层是否所有相邻对都有规则
        for (int i = 0; i + 1 < row.length(); ++i) {
            if (!rules.containsKey(row.substring(i, i + 2))) {
                memoFail.add(row);
                return false;
            }
        }
        StringBuilder sb = new StringBuilder();
        if (!buildNext(row, 0, sb)) {
            memoFail.add(row);
            return false;
        }
        return true;
    }

    // 枚举下一层所有可能,遇到可行则提前返回 true
    private boolean buildNext(String row, int idx, StringBuilder sb) {
        if (idx + 1 == row.length()) {
            return dfs(sb.toString());
        }
        String key = row.substring(idx, idx + 2);
        Set<Character> tops = rules.get(key);
        if (tops == null) {
            return false;
        }
        for (char c : tops) {
            sb.append(c);
            if (buildNext(row, idx + 1, sb)) {
                return true;
            }
            sb.deleteCharAt(sb.length() - 1);
        }
        return false;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.pyramidTransition("BCD", java.util.Arrays.asList("BCC", "CDE", "CEA", "FFF"))) ++pass; else System.out.println("FAIL test1");
        ++total; if (!sol.pyramidTransition("AAAA", java.util.Arrays.asList("AAB", "AAC", "BCD", "BBE", "DEF"))) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.pyramidTransition("A", java.util.Arrays.asList())) ++pass; else System.out.println("FAIL test3");
        ++total; if (!sol.pyramidTransition("ABC", java.util.Arrays.asList("ABD", "BCE"))) ++pass; else System.out.println("FAIL test4");
        if (pass == total) {
            System.out.println("== 0756 PASS ==");
        } else {
            System.out.println("== 0756 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(指数)O(指数)
解法二O(指数)O(指数)

757. Set Intersection Size At Least Two(设置交集大小至少为2)

题目描述

给你一个二维整数数组 intervals,其中 intervals[i] = [starti, endi] 表示从 starti
到 endi 的所有整数,包括 starti 和 endi。包含集合是一个名为 nums 的数组,并满足
intervals 中的每个区间都至少有两个整数在 nums 中。返回包含集合可能的最小大小。

示例 1

  • 输入:intervals = [[1,3],[3,7],[8,9]]
  • 输出:5
  • 解释:nums = [2, 3, 4, 8, 9]

示例 2

  • 输入:intervals = [[1,3],[1,4],[2,5],[3,5]]
  • 输出:3
  • 解释:nums = [2, 3, 4]

示例 3

  • 输入:intervals = [[1,2],[2,3],[2,4],[4,5]]
  • 输出:5
    提示:
  • 1 <= intervals.length <= 3000
  • intervals[i].length == 2
  • 0 <= starti < endi <= 10^8

解题思路

解法一(贪心 + 双计数,主提交):先把区间按右端点升序(右端点相同按左端点降序)排序,然后
维护已选集合 nums 中「最后两个元素」a < b(初始无)。对每个区间 [l, r]:

  • 若 b 在区间内且 a 也在区间内,已满足 2 个,跳过;
  • 若只有 b 在区间内(或 ab 都已在区间内但不足 2 个),只需补 1 个,贪心取 r - 1 或新
    的 b;
  • 若一个都不在,需要补 2 个,取 r-1, r。
    用「计数当前集合与区间的交集个数」判断即可:统计 cnt = (a >= l && a <= r) + (b >= l && b <= r)。
    按 cnt 分三种情况更新 a/b。
    时间复杂度 O(m·log m),空间复杂度 O(m)。
    解法二(动态规划对照):区间端点离散化后求最小点数,能保证正确但繁琐,作为参考思路。

代码实现

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Solution {

    // 解法一:贪心 + 维护最后两个已选元素
    public int intersectionSizeTwo(int[][] intervals) {
        // 按右端点升序;相等时左端点降序(尽量让后面的区间覆盖更多已选点)
        Arrays.sort(intervals, (x, y) -> x[1] != y[1] ? x[1] - y[1] : y[0] - x[0]);
        int ans = 0;
        int a = -1, b = -1; // 已选集合中最后两个元素,a < b
        for (int[] iv : intervals) {
            int l = iv[0], r = iv[1];
            int cnt = 0;
            if (a >= l && a <= r) ++cnt;
            if (b >= l && b <= r) ++cnt;
            if (cnt >= 2) {
                continue;
            }
            if (cnt == 1) {
                // 还需补 1 个:优先补 r(r 能覆盖后续尽可能多的区间)
                // 新集合末尾两个为 b 和 r(若 b 原本不在区间内,则 a?b 都更新)
                if (b >= l && b <= r) {
                    // b 在区间内,a 不在:补一个 r
                    a = b;
                    b = r;
                    ++ans;
                } else {
                    // a 在区间内,b 不在:说明 b 是上一次补在 r'(> 当前 r?不可能,因为排序右端点非降)
                    // 此时 b > r,按理不会发生(按右端点排序);为健壮,直接补 r
                    a = b;
                    b = r;
                    ++ans;
                }
            } else {
                // 一个都不在:补 r-1, r
                a = r - 1;
                b = r;
                ans += 2;
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        int[][] i1 = {{1, 3}, {3, 7}, {8, 9}};
        ++total; if (sol.intersectionSizeTwo(i1) == 5) ++pass; else System.out.println("FAIL test1 got " + sol.intersectionSizeTwo(i1));
        int[][] i2 = {{1, 3}, {1, 4}, {2, 5}, {3, 5}};
        ++total; if (sol.intersectionSizeTwo(i2) == 3) ++pass; else System.out.println("FAIL test2 got " + sol.intersectionSizeTwo(i2));
        int[][] i3 = {{1, 2}, {2, 3}, {2, 4}, {4, 5}};
        ++total; if (sol.intersectionSizeTwo(i3) == 5) ++pass; else System.out.println("FAIL test3 got " + sol.intersectionSizeTwo(i3));
        int[][] i4 = {{0, 1}};
        ++total; if (sol.intersectionSizeTwo(i4) == 2) ++pass; else System.out.println("FAIL test4 got " + sol.intersectionSizeTwo(i4));
        if (pass == total) {
            System.out.println("== 0757 PASS ==");
        } else {
            System.out.println("== 0757 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(m·log m)O(m)

758. Bold Words in String(字符串中的加粗单词)

题目描述

给定一个关键词集合 words 和一个字符串 s,将所有 s 中出现的关键词 words[i] 加粗。
所有在标签 <b> 和 </b> 中的字母都会加粗。加粗后返回 s。返回的字符串需要使用尽可能少
的标签,当然标签应形成有效的组合。

示例 1

  • 输入:words = ["ab","bc"], s = "aabcd"
  • 输出:"aabcd"
  • 解释:注意返回 "aabcd" 会使用更多的标签,因此是错误的。

示例 2

  • 输入:words = ["ab","cb"], s = "aabcd"
  • 输出:"aabcd"
    提示:
  • 1 <= s.length <= 500
  • 0 <= words.length <= 50
  • 1 <= words[i].length <= 10
  • s 和 words[i] 由小写英文字母组成

解题思路

解法一(差分标记 + 区间合并输出,主提交):先算出每个位置是否应加粗。把每个关键词在 s 中
出现的区间 [l, r) 用差分数组 +1/-1 标记,再求前缀和,bold[i] = 前缀和 > 0。最后按
bold 的连续段拼出带标签的字符串:段起点前加 <b>,段终点后加 </b>。
时间复杂度 O(|words| · |s| · L),空间复杂度 O(|s|)。
解法二(朴素区间合并):用布尔数组记录加粗位置后,扫描合并。逻辑与法一同构,实现直接。

代码实现

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

class Solution {

    // 解法一:差分标记 + 前缀和
    public String boldWords(String[] words, String s) {
        int n = s.length();
        int[] diff = new int[n + 1];
        for (String w : words) {
            if (w.isEmpty()) {
                continue;
            }
            int idx = s.indexOf(w);
            while (idx != -1) {
                ++diff[idx];
                --diff[idx + w.length()];
                idx = s.indexOf(w, idx + 1);
            }
        }
        boolean[] bold = new boolean[n];
        int run = 0;
        for (int i = 0; i < n; ++i) {
            run += diff[i];
            bold[i] = run > 0;
        }
        StringBuilder sb = new StringBuilder();
        int i = 0;
        while (i < n) {
            if (bold[i]) {
                int j = i;
                while (j < n && bold[j]) {
                    ++j;
                }
                sb.append("<b>").append(s, i, j).append("</b>");
                i = j;
            } else {
                sb.append(s.charAt(i));
                ++i;
            }
        }
        return sb.toString();
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        String r1 = sol.boldWords(new String[] {"ab", "bc"}, "aabcd");
        ++total; if (r1.equals("a<b>abc</b>d")) ++pass; else System.out.println("FAIL test1 got " + r1);
        String r2 = sol.boldWords(new String[] {"ab", "cb"}, "aabcd");
        ++total; if (r2.equals("a<b>ab</b>cd")) ++pass; else System.out.println("FAIL test2 got " + r2);
        String r3 = sol.boldWords(new String[] {"a", "b"}, "abc");
        ++total; if (r3.equals("<b>ab</b>c")) ++pass; else System.out.println("FAIL test3 got " + r3);
        String r4 = sol.boldWords(new String[] {}, "abc");
        ++total; if (r4.equals("abc")) ++pass; else System.out.println("FAIL test4 got " + r4);
        String r5 = sol.boldWords(new String[] {"aaa"}, "aaab");
        ++total; if (r5.equals("<b>aaa</b>b")) ++pass; else System.out.println("FAIL test5 got " + r5);
        if (pass == total) {
            System.out.println("== 0758 PASS ==");
        } else {
            System.out.println("== 0758 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(50·s·L)O(s)

759. Employee Free Time(员工空闲时间)

题目描述

给定员工的 schedule 列表,表示每个员工的工作时间。每个员工都有一个非重叠的时间段
Intervals 列表,这些时间段已经排好序。返回表示所有员工的共同、正数长度的空闲时间的有限
时间段列表,同样需要排好序。
例如,schedule = [[[1,2],[5,6]],[[1,3]],[[4,10]]] 的共同空闲时间为 [[3,4]]。

示例 1

  • 输入:schedule = [[[1,2],[5,6]],[[1,3]],[[4,10]]]
  • 输出:[[3,4]]

示例 2

  • 输入:schedule = [[[1,3],[6,7]],[[2,4]],[[2,5],[9,12]]]
  • 输出:[[5,6],[7,9]]
    提示:
  • 1 <= schedule.length, schedule[i].length <= 50
  • 0 <= interval.start < interval.end <= 10^8
  • 每个员工的工作时间段互不重叠且已排序

解题思路

解法一(合并所有工作时间段 + 找空隙,主提交):把所有员工的工作区间收集到一个列表,按
start 升序(相同按 end 升序)排序后线性合并,得到所有员工「至少一人在工作」的合并区间
列表 merged。相邻两个合并区间 [A.end, B.start] 之间若 start < end 即为共同空闲区间。
时间复杂度 O(T·log T),空间复杂度 O(T),T 为总区间数。
解法二(最小堆多路归并):模拟时间线推进,用最小堆按 start 取最早结束的工作区间,维护
当前最晚结束时间,遇到空隙即空闲段。实现略繁,逻辑等价。

代码实现

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

class Solution {

    // 力扣该题提供的 Interval 结构
    private static class Interval {
        int start;
        int end;
        Interval() { start = 0; end = 0; }
        Interval(int s, int e) { start = s; end = e; }
    }

    // 解法一:合并所有工作区间 + 找空隙
    public List<Interval> employeeFreeTime(List<List<Interval>> schedule) {
        List<Interval> all = new ArrayList<>();
        for (List<Interval> emp : schedule) {
            all.addAll(emp);
        }
        all.sort((a, b) -> a.start != b.start ? a.start - b.start : a.end - b.end);
        List<Interval> merged = new ArrayList<>();
        for (Interval iv : all) {
            if (merged.isEmpty() || iv.start > merged.get(merged.size() - 1).end) {
                merged.add(iv);
            } else {
                Interval last = merged.get(merged.size() - 1);
                last.end = Math.max(last.end, iv.end);
            }
        }
        List<Interval> ans = new ArrayList<>();
        for (int i = 1; i < merged.size(); ++i) {
            int s = merged.get(i - 1).end;
            int e = merged.get(i).start;
            if (s < e) {
                ans.add(new Interval(s, e));
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    private static List<Interval> mk(int[][] arr) {
        List<Interval> list = new ArrayList<>();
        for (int[] a : arr) {
            list.add(new Interval(a[0], a[1]));
        }
        return list;
    }

    private static String fmt(List<Interval> list) {
        StringBuilder sb = new StringBuilder("[");
        for (Interval iv : list) {
            sb.append("[").append(iv.start).append(",").append(iv.end).append("],");
        }
        if (sb.length() > 1) sb.setLength(sb.length() - 1);
        sb.append("]");
        return sb.toString();
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        List<List<Interval>> s1 = new ArrayList<>();
        s1.add(mk(new int[][] {{1, 2}, {5, 6}}));
        s1.add(mk(new int[][] {{1, 3}}));
        s1.add(mk(new int[][] {{4, 10}}));
        List<Interval> r1 = sol.employeeFreeTime(s1);
        ++total; if (fmt(r1).equals("[[3,4]]")) ++pass; else System.out.println("FAIL test1 got " + fmt(r1));
        List<List<Interval>> s2 = new ArrayList<>();
        s2.add(mk(new int[][] {{1, 3}, {6, 7}}));
        s2.add(mk(new int[][] {{2, 4}}));
        s2.add(mk(new int[][] {{2, 5}, {9, 12}}));
        List<Interval> r2 = sol.employeeFreeTime(s2);
        ++total; if (fmt(r2).equals("[[5,6],[7,9]]")) ++pass; else System.out.println("FAIL test2 got " + fmt(r2));
        if (pass == total) {
            System.out.println("== 0759 PASS ==");
        } else {
            System.out.println("== 0759 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(T·log T)O(T)
解法二O(T·log K)O(K)

760. Find Anagram Mappings(找出变位映射)

题目描述

给你两个整数数组 nums1 和 nums2,其中 nums2 是 nums1 的一个变位词。两个数组都
可能包含重复元素。返回一个下标映射数组 mapping,它将 nums1 映射到 nums2,其中
mapping[i] = j 表示 nums1 中的第 i 个元素出现在 nums2 的第 j 个下标上。如果有
多个答案,返回任意一个。
数组 a 是数组 b 的一个变位词意味着 b 是通过将 a 中元素的顺序随机打乱生成的。

示例 1

  • 输入:nums1 = [12,28,46,32,50], nums2 = [50,12,32,46,28]
  • 输出:[1,4,3,2,0]

示例 2

  • 输入:nums1 = [84,46], nums2 = [84,46]
  • 输出:[0,1]
    提示:
  • 1 <= nums1.length <= 100
  • nums2.length == nums1.length
  • 0 <= nums1[i], nums2[i] <= 10^5
  • nums2 是 nums1 的一个变位词

解题思路

解法一(值 -> 下标列表 + 队列消费,主提交):由于存在重复元素,用 Map<Integer, Deque>
记录 nums2 中每个值出现的所有下标(从前到后)。遍历 nums1,对每个值弹出其下一个未使用
的下标作为映射。
时间复杂度 O(n),空间复杂度 O(n)。
解法二(值 -> 下标计数游标):给 nums2 每个值记录「当前消费到第几个下标」,用
Map<Integer, int[]> 或两个独立计数器配合列表实现。逻辑等价。

代码实现

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

class Solution {

    // 解法一:Map<值, 下标队列>
    public int[] anagramMappings(int[] nums1, int[] nums2) {
        Map<Integer, java.util.Deque<Integer>> pos = new HashMap<>();
        for (int j = 0; j < nums2.length; ++j) {
            pos.computeIfAbsent(nums2[j], k -> new java.util.ArrayDeque<>()).offer(j);
        }
        int[] ans = new int[nums1.length];
        for (int i = 0; i < nums1.length; ++i) {
            ans[i] = pos.get(nums1[i]).poll();
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        int[] n1 = {12, 28, 46, 32, 50};
        int[] n2 = {50, 12, 32, 46, 28};
        int[] r1 = sol.anagramMappings(n1, n2);
        ++total; if (Arrays.equals(r1, new int[] {1, 4, 3, 2, 0})) ++pass; else System.out.println("FAIL test1 got " + Arrays.toString(r1));
        int[] r2 = sol.anagramMappings(new int[] {84, 46}, new int[] {84, 46});
        ++total; if (Arrays.equals(r2, new int[] {0, 1})) ++pass; else System.out.println("FAIL test2 got " + Arrays.toString(r2));
        int[] r3 = sol.anagramMappings(new int[] {1, 1, 2}, new int[] {2, 1, 1});
        // 校验映射合法性:nums2[r3[i]] == nums1[i]
        boolean ok = r3.length == 3;
        for (int i = 0; i < 3 && ok; ++i) {
            if (n2.length == 3) ok = false; // 用独立数组校验
        }
        int[] nn1 = {1, 1, 2}, nn2 = {2, 1, 1};
        ok = r3.length == 3;
        for (int i = 0; i < 3 && ok; ++i) {
            if (nn2[r3[i]] != nn1[i]) {
                ok = false;
            }
        }
        ++total; if (ok) ++pass; else System.out.println("FAIL test3 got " + Arrays.toString(r3));
        if (pass == total) {
            System.out.println("== 0760 PASS ==");
        } else {
            System.out.println("== 0760 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(n)
解法二O(n)O(n)

761. Special Binary String(特殊的二进制字符串)

题目描述

特殊的二进制字符串是具有以下两个性质的二进制序列:

  • 0 的数量与 1 的数量相等;
  • 二进制序列的每一个前缀中 1 的数量要大于等于 0 的数量。
    给定一个特殊的二进制字符串 s。一次移动操作包括选择字符串 s 中的两个连续的、非空的、
    特殊子串,并交换它们。返回在字符串上应用任意次操作后可能得到的字典序最大的字符串。

示例 1

  • 输入:S = "11011000"
  • 输出:"11100100"

示例 2

  • 输入:s = "10"
  • 输出:"10"
    提示:
  • 1 <= s.length <= 50

解题思路

解法一(递归分治 + 排序拼接,主提交):任何特殊二进制串都形如 "1" + A + "0",其中 A
是若干特殊子串的拼接。一次移动可以交换两个相邻的特殊子串,因此同一层级内可以通过冒泡交换
把子串按字典序降序排列。递归处理每一层,最后返回 "1" + (排序后的内层) + "0"。
实现:用计数器扫描,遇到 '1' 加 1、遇到 '0' 减 1,当归零时就把这一整段递归处理。
时间复杂度 O(n²)(每层排序),空间复杂度 O(n)。
解法二(暴力交换最大):直接模拟所有相邻特殊子串交换找最大值,状态较多,仅作理论参考。

代码实现

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

class Solution {

    // 解法一:递归分治
    public String makeLargestSpecial(String s) {
        if (s.length() <= 2) {
            return s;
        }
        int n = s.length();
        List<String> parts = new ArrayList<>();
        int cnt = 0, start = 0;
        for (int i = 0; i < n; ++i) {
            cnt += s.charAt(i) == '1' ? 1 : -1;
            if (cnt == 0) {
                // 完整特殊子串 [start, i]
                parts.add("1" + makeLargestSpecial(s.substring(start + 1, i)) + "0");
                start = i + 1;
            }
        }
        parts.sort(Collections.reverseOrder());
        StringBuilder sb = new StringBuilder();
        for (String p : parts) {
            sb.append(p);
        }
        return sb.toString();
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.makeLargestSpecial("11011000").equals("11100100")) ++pass; else System.out.println("FAIL test1 got " + sol.makeLargestSpecial("11011000"));
        ++total; if (sol.makeLargestSpecial("10").equals("10")) ++pass; else System.out.println("FAIL test2 got " + sol.makeLargestSpecial("10"));
        ++total; if (sol.makeLargestSpecial("101100").equals("110010")) ++pass; else System.out.println("FAIL test3 got " + sol.makeLargestSpecial("101100"));
        ++total; if (sol.makeLargestSpecial("1").equals("1")) ++pass; else System.out.println("FAIL test4");
        if (pass == total) {
            System.out.println("== 0761 PASS ==");
        } else {
            System.out.println("== 0761 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n²)O(n)

762. Prime Number of Set Bits in Binary Representation

题目描述

给你两个整数 left 和 right,在闭区间 [left, right] 范围内,统计并返回「计算置位位数
为质数」的整数个数。计算置位位数就是二进制表示中 1 的个数。
例如,21 的二进制表示 10101 有 3 个计算置位。

示例 1

  • 输入:left = 6, right = 10
  • 输出:4
  • 解释:6(110, 2 个)、7(111, 3 个)、9(1001, 2 个)、10(1010, 2 个),共 4 个。

示例 2

  • 输入:left = 10, right = 15
  • 输出:5
    提示:
  • 1 <= left <= right <= 10^6
  • 0 <= right - left <= 10^4

解题思路

解法一(位计数 + 质数表,主提交):对区间内每个数用 Integer.bitCount 求置位个数。因为
right <= 10^6 < 2^20,置位个数最多 20,用布尔数组预生成 [0, 20] 的质数标记,查表判断。
时间复杂度 O((right - left) · 1),空间复杂度 O(1)。
解法二(手写 lowbit 计数):不用 Integer.bitCount,用 n &= n - 1 循环数 1,逻辑等价,
便于教学。

代码实现

class Solution {

    // 解法一:bitCount + 质数表
    public int countPrimeSetBits(int left, int right) {
        boolean[] prime = new boolean[21];
        for (int x : new int[] {2, 3, 5, 7, 11, 13, 17, 19}) {
            prime[x] = true;
        }
        int ans = 0;
        for (int v = left; v <= right; ++v) {
            if (prime[Integer.bitCount(v)]) {
                ++ans;
            }
        }
        return ans;
    }

    // 解法二:手工 lowbit 计数
    public int countPrimeSetBits2(int left, int right) {
        boolean[] prime = new boolean[21];
        for (int x : new int[] {2, 3, 5, 7, 11, 13, 17, 19}) {
            prime[x] = true;
        }
        int ans = 0;
        for (int v = left; v <= right; ++v) {
            int cnt = 0, x = v;
            while (x != 0) {
                x &= x - 1;
                ++cnt;
            }
            if (prime[cnt]) {
                ++ans;
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.countPrimeSetBits(6, 10) == 4) ++pass; else System.out.println("FAIL test1 got " + sol.countPrimeSetBits(6, 10));
        ++total; if (sol.countPrimeSetBits(10, 15) == 5) ++pass; else System.out.println("FAIL test2 got " + sol.countPrimeSetBits(10, 15));
        ++total; if (sol.countPrimeSetBits2(6, 10) == 4) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.countPrimeSetBits2(10, 15) == 5) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.countPrimeSetBits(1, 1) == 0) ++pass; else System.out.println("FAIL test5");
        if (pass == total) {
            System.out.println("== 0762 PASS ==");
        } else {
            System.out.println("== 0762 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(区间长度)O(1)
解法二O(区间长度 · log val)O(1)

763. Partition Labels(划分字母区间)

题目描述

给你一个字符串 s。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
例如,字符串 "ababcc" 能够被分为 ["abab", "cc"],但类似 ["aba", "bcc"] 的划分是
非法的。注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s。
返回一个表示每个字符串片段的长度的列表。

示例 1

  • 输入:s = "ababcbacadefegdehijhklij"
  • 输出:[9,7,8]
  • 解释:划分结果为 "ababcbaca"、"defegde"、"hijhklij",每个字母最多出现在一个片段中。

示例 2

  • 输入:s = "eccbbbbdec"
  • 输出:[10]
    提示:
  • 1 <= s.length <= 500
  • s 仅由小写英文字母组成

解题思路

解法一(贪心 + 最右位置,主提交):先扫描一遍记录每个字母最后一次出现的下标 last[26]。
再从左往右扫描,维护当前片段的起始 start 和「当前片段内所有字母的最右出现位置」end。
当 i == end 时说明当前片段可以结束,长度 end - start + 1 加入答案,start = i + 1。
贪心正确性:每个片段必须延伸至其中任一字母的最后出现位置,因此在 end 处切分是最早的合法
切分点,保证片段数最多。
时间复杂度 O(n),空间复杂度 O(1)。
解法二(区间合并视角):把每个字母首次/末次出现视为区间,等价于合并重叠区间后取连通块。

代码实现

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Solution {

    // 解法一:贪心 + 最右位置
    public List<Integer> partitionLabels(String s) {
        int[] last = new int[26];
        int n = s.length();
        for (int i = 0; i < n; ++i) {
            last[s.charAt(i) - 'a'] = i;
        }
        List<Integer> ans = new ArrayList<>();
        int start = 0, end = 0;
        for (int i = 0; i < n; ++i) {
            end = Math.max(end, last[s.charAt(i) - 'a']);
            if (i == end) {
                ans.add(end - start + 1);
                start = i + 1;
            }
        }
        return ans;
    }

    // 解法二:区间合并视角
    public List<Integer> partitionLabels2(String s) {
        int[][] seg = new int[26][2];
        for (int[] r : seg) {
            r[0] = Integer.MAX_VALUE;
            r[1] = -1;
        }
        for (int i = 0; i < s.length(); ++i) {
            int c = s.charAt(i) - 'a';
            seg[c][0] = Math.min(seg[c][0], i);
            seg[c][1] = Math.max(seg[c][1], i);
        }
        List<int[]> intervals = new ArrayList<>();
        for (int[] r : seg) {
            if (r[0] <= r[1]) {
                intervals.add(r);
            }
        }
        intervals.sort((a, b) -> a[0] - b[0]);
        List<Integer> ans = new ArrayList<>();
        int curL = intervals.get(0)[0], curR = intervals.get(0)[1];
        for (int i = 1; i < intervals.size(); ++i) {
            int[] r = intervals.get(i);
            if (r[0] < curR) {
                curR = Math.max(curR, r[1]);
            } else {
                ans.add(curR - curL + 1);
                curL = r[0];
                curR = r[1];
            }
        }
        ans.add(curR - curL + 1);
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        List<Integer> r1 = sol.partitionLabels("ababcbacadefegdehijhklij");
        ++total; if (r1.equals(Arrays.asList(9, 7, 8))) ++pass; else System.out.println("FAIL test1 got " + r1);
        List<Integer> r2 = sol.partitionLabels("eccbbbbdec");
        ++total; if (r2.equals(Arrays.asList(10))) ++pass; else System.out.println("FAIL test2 got " + r2);
        List<Integer> r3 = sol.partitionLabels2("ababcbacadefegdehijhklij");
        ++total; if (r3.equals(Arrays.asList(9, 7, 8))) ++pass; else System.out.println("FAIL test3 got " + r3);
        List<Integer> r4 = sol.partitionLabels("caedbdedda");
        ++total; if (r4.equals(Arrays.asList(1, 9))) ++pass; else System.out.println("FAIL test4 got " + r4);
        if (pass == total) {
            System.out.println("== 0763 PASS ==");
        } else {
            System.out.println("== 0763 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(1)
解法二O(n + 26·log26)O(26)

764. Largest Plus Sign(最大加号标志)

题目描述

在一个 n x n 的矩阵 grid 中,除了在数组 mines 中给出的元素为 0,其他每个元素都为 1。 mines[i] = [xi, yi] 表示 grid[xi][yi] == 0。返回 grid 中包含 1 的最大的轴对齐加号 标志的阶数。如果未找到加号标志,则返回 0。 一个 k 阶由 1 组成的「轴对称」加号标志具有中心网格 grid[r][c] == 1,以及 4 个从中心 向上、向下、向左、向右延伸、长度为 k-1、由 1 组成的臂。注意,只有加号标志的所有网格要求 为 1,别的网格可能为 0 也可能为 1。

示例 1

  • 输入:n = 5, mines = [[4, 2]]
  • 输出:2

示例 2

  • 输入:n = 1, mines = [[0, 0]]
  • 输出:0 提示:
  • 1 <= n <= 500
  • 1 <= mines.length <= 5000
  • 0 <= xi, yi < n
  • 每一对 (xi, yi) 都不重复

解题思路

解法一(四方向 DP,主提交):用四个矩阵分别记录每个格子向上、下、左、右连续 1 的最长长度 (不含自身或含自身视写法而定,统一处理边界)。up[i][j] 等可以由邻居递推:

  • up[i][j] = (grid[i][j] == 1) ? up[i-1][j] + 1 : 0;
  • 同理 down/left/right。 某格子能作为中心的最大阶数 = min(up, down, left, right),取全局最大。 时间复杂度 O(n²),空间复杂度 O(n²)。 解法二(每个中心暴力扩展):对每个非雷格子向四个方向数 1 的长度,最坏 O(n³),作对照。

代码实现

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

class Solution {

    // 解法一:四方向 DP
    public int orderOfLargestPlusSign(int n, int[][] mines) {
        int[][] grid = new int[n][n];
        for (int[] row : grid) {
            Arrays.fill(row, 1);
        }
        for (int[] m : mines) {
            grid[m[0]][m[1]] = 0;
        }
        int[][] up = new int[n][n];
        int[][] down = new int[n][n];
        int[][] left = new int[n][n];
        int[][] right = new int[n][n];
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == 1) {
                    up[i][j] = (i > 0 ? up[i - 1][j] : 0) + 1;
                    left[i][j] = (j > 0 ? left[i][j - 1] : 0) + 1;
                }
            }
        }
        for (int i = n - 1; i >= 0; --i) {
            for (int j = n - 1; j >= 0; --j) {
                if (grid[i][j] == 1) {
                    down[i][j] = (i + 1 < n ? down[i + 1][j] : 0) + 1;
                    right[i][j] = (j + 1 < n ? right[i][j + 1] : 0) + 1;
                }
            }
        }
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                int k = Math.min(Math.min(up[i][j], down[i][j]),
                        Math.min(left[i][j], right[i][j]));
                ans = Math.max(ans, k);
            }
        }
        return ans;
    }

    // 解法二:中心扩展暴力对照
    public int orderOfLargestPlusSign2(int n, int[][] mines) {
        int[][] g = new int[n][n];
        for (int[] row : g) {
            Arrays.fill(row, 1);
        }
        for (int[] m : mines) {
            g[m[0]][m[1]] = 0;
        }
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (g[i][j] == 0) {
                    continue;
                }
                int k = 1;
                while (i - k >= 0 && i + k < n && j - k >= 0 && j + k < n
                        && g[i - k][j] == 1 && g[i + k][j] == 1
                        && g[i][j - k] == 1 && g[i][j + k] == 1) {
                    ++k;
                }
                ans = Math.max(ans, k);
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        int[][] m1 = {{4, 2}};
        ++total; if (sol.orderOfLargestPlusSign(5, m1) == 2) ++pass; else System.out.println("FAIL test1 got " + sol.orderOfLargestPlusSign(5, m1));
        int[][] m2 = {{0, 0}};
        ++total; if (sol.orderOfLargestPlusSign(1, m2) == 0) ++pass; else System.out.println("FAIL test2");
        int[][] m3 = {};
        ++total; if (sol.orderOfLargestPlusSign(2, m3) == 1) ++pass; else System.out.println("FAIL test3 got " + sol.orderOfLargestPlusSign(2, m3));
        ++total; if (sol.orderOfLargestPlusSign2(5, m1) == 2) ++pass; else System.out.println("FAIL test4");
        if (pass == total) {
            System.out.println("== 0764 PASS ==");
        } else {
            System.out.println("== 0764 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n²)O(n²)
解法二O(n³)O(n²)

765. Couples Holding Hands(情侣牵手)

题目描述

n 对情侣坐在连续排列的 2n 个座位上,想要牵到对方的手。人和座位由一个整数数组 row 表示,其中 row[i] 是坐在第 i 个座位上的人的 ID。情侣们按顺序编号,第一对是 (0, 1), 第二对是 (2, 3),以此类推,最后一对是 (2n - 2, 2n - 1)。 返回最少交换座位的次数,以便每对情侣可以并肩坐在一起。每次交换可选择任意两人,让他们站起来 交换座位。

示例 1

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

示例 2

  • 输入:row = [3,2,0,1]
  • 输出:0 提示:
  • 2n == row.length
  • 2 <= n <= 30
  • n 是偶数
  • 0 <= row[i] < 2n
  • row 中所有元素均无重复

解题思路

解法一(座位对位置映射 + 贪心交换,主提交):先记录每个人的座位下标 pos[person]。从左到右 每两个座位 (2*i, 2*i+1) 检查是否为情侣:

  • 若 row[2i] 的伴侣已在旁边则跳过;
  • 否则找到伴侣所在下标 j = pos[partner],把 row[2i+1] 与 row[j] 交换,同时更新 pos。每对错位贡献 1 次交换。 时间复杂度 O(n),空间复杂度 O(n)。 解法二(并查集环计数):把「占位错误」建模成置换环,每个大小为 c 的环需要 c-1 次交换, 答案 = Σ(c - 1)。两两凑对视角等价,实现略繁。

代码实现

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

class Solution {

    // 解法一:位置映射 + 贪心交换
    public int minSwapsCouples(int[] row) {
        int n = row.length;
        int[] pos = new int[n];
        for (int i = 0; i < n; ++i) {
            pos[row[i]] = i;
        }
        int ans = 0;
        for (int i = 0; i < n; i += 2) {
            int p = row[i];
            int partner = (p % 2 == 0) ? p + 1 : p - 1;
            if (row[i + 1] == partner) {
                continue;
            }
            // 找到 partner 的位置并交换到 i+1
            int j = pos[partner];
            int tmp = row[i + 1];
            row[i + 1] = partner;
            row[j] = tmp;
            pos[partner] = i + 1;
            pos[tmp] = j;
            ++ans;
        }
        return ans;
    }

    // 解法二:并查集(环)
    private int[] parent;

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

    public int minSwapsCouples2(int[] row) {
        int n = row.length / 2;
        parent = new int[n];
        for (int i = 0; i < n; ++i) {
            parent[i] = i;
        }
        Map<Integer, Integer> seatToCouple = new HashMap<>();
        for (int i = 0; i < row.length; ++i) {
            seatToCouple.put(row[i], row[i] / 2);
        }
        for (int i = 0; i < row.length; i += 2) {
            int a = seatToCouple.get(row[i]);
            int b = seatToCouple.get(row[i + 1]);
            int ra = find(a), rb = find(b);
            if (ra != rb) {
                parent[ra] = rb;
            }
        }
        // 每个连通块需要 size-1 次交换
        int[] size = new int[n];
        int blocks = 0;
        boolean[] vis = new boolean[n];
        for (int i = 0; i < n; ++i) {
            int r = find(i);
            if (size[r] == 0) {
                ++blocks;
            }
            ++size[r];
        }
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            if (size[i] > 0) {
                ans += size[i] - 1;
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        int[] r1 = {0, 2, 1, 3};
        ++total; if (sol.minSwapsCouples(r1) == 1) ++pass; else System.out.println("FAIL test1 got " + sol.minSwapsCouples(r1));
        int[] r2 = {3, 2, 0, 1};
        ++total; if (sol.minSwapsCouples(r2) == 0) ++pass; else System.out.println("FAIL test2");
        int[] r3 = {0, 2, 1, 3};
        ++total; if (sol.minSwapsCouples2(r3) == 1) ++pass; else System.out.println("FAIL test3");
        int[] r4 = {5, 4, 2, 6, 3, 1, 0, 7};
        ++total; if (sol.minSwapsCouples(r4) == 2) ++pass; else System.out.println("FAIL test4 got " + sol.minSwapsCouples(r4));
        int[] r5 = {5, 4, 2, 6, 3, 1, 0, 7};
        ++total; if (sol.minSwapsCouples2(r5) == 2) ++pass; else System.out.println("FAIL test5 got " + sol.minSwapsCouples2(r5));
        if (pass == total) {
            System.out.println("== 0765 PASS ==");
        } else {
            System.out.println("== 0765 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(n)
解法二O(n·α)O(n)

766. Toeplitz Matrix(托普利茨矩阵)

题目描述

给你一个 m x n 的矩阵 matrix。如果这个矩阵是托普利茨矩阵,返回 true;否则返回 false。 如果矩阵上每一条由左上到右下的对角线上的元素都相同,那么这个矩阵是托普利茨矩阵。

示例 1

  • 输入:matrix = [[1,2,3,4],[5,1,2,3],[9,5,1,2]]
  • 输出:true
  • 解释:各条对角线均为 [9]、[5,5]、[1,1,1]、[2,2,2]、[3,3]、[4]。

示例 2

  • 输入:matrix = [[1,2],[2,2]]
  • 输出:false 提示:
  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 20
  • 0 <= matrix[i][j] <= 99

解题思路

解法一(逐元素比较左上方邻居,主提交):托普利茨矩阵等价于每个元素与它左上方的元素相等, 即 matrix[i][j] == matrix[i-1][j-1](对 i >= 1, j >= 1)。一次双重循环检查即可, 每条对角线自然被该条件覆盖。 时间复杂度 O(m·n),空间复杂度 O(1)。 解法二(逐对角线比较):显式枚举每条从首行或首列出发的斜线,逐条比较。逻辑直观,实现略长。

代码实现

class Solution {

    // 解法一:比较左上邻居
    public boolean isToeplitzMatrix(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        for (int i = 1; i < m; ++i) {
            for (int j = 1; j < n; ++j) {
                if (matrix[i][j] != matrix[i - 1][j - 1]) {
                    return false;
                }
            }
        }
        return true;
    }

    // 解法二:逐对角线比较
    public boolean isToeplitzMatrix2(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        // 首行出发的对角线
        for (int j = 0; j < n; ++j) {
            int val = matrix[0][j];
            for (int i = 1, c = j + 1; i < m && c < n; ++i, ++c) {
                if (matrix[i][c] != val) {
                    return false;
                }
            }
        }
        // 首列出发的对角线
        for (int i = 1; i < m; ++i) {
            int val = matrix[i][0];
            for (int r = i + 1, c = 1; r < m && c < n; ++r, ++c) {
                if (matrix[r][c] != val) {
                    return false;
                }
            }
        }
        return true;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        int[][] m1 = {{1, 2, 3, 4}, {5, 1, 2, 3}, {9, 5, 1, 2}};
        ++total; if (sol.isToeplitzMatrix(m1)) ++pass; else System.out.println("FAIL test1");
        int[][] m2 = {{1, 2}, {2, 2}};
        ++total; if (!sol.isToeplitzMatrix(m2)) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.isToeplitzMatrix2(m1)) ++pass; else System.out.println("FAIL test3");
        ++total; if (!sol.isToeplitzMatrix2(m2)) ++pass; else System.out.println("FAIL test4");
        int[][] m3 = {{1}};
        ++total; if (sol.isToeplitzMatrix(m3)) ++pass; else System.out.println("FAIL test5");
        if (pass == total) {
            System.out.println("== 0766 PASS ==");
        } else {
            System.out.println("== 0766 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(m·n)O(1)
解法二O(m·n)O(1)

767. Reorganize String(重构字符串)

题目描述

给定一个字符串 s,检查是否能重新排布其中的字母,使得任意两个相邻字符不相同。返回 s 的 任意可能的重新排列。若不可行,返回空字符串 ""。

示例 1

  • 输入:s = "aab"
  • 输出:"aba"

示例 2

  • 输入:s = "aaab"
  • 输出:"" 提示:
  • 1 <= s.length <= 500
  • s 只包含小写字母

解题思路

解法一(贪心 + 优先队列,主提交):若某字母出现次数超过 (n + 1) / 2 则必不可能(鸽巢 原理)。否则用大根堆按「出现次数」维护字母,每次取两个最多的字母交替放置,避免相邻相同。 若最后只剩一个字母则补到末尾(其剩余次数必为 1)。 时间复杂度 O(n·log 26) = O(n),空间复杂度 O(26)。 解法二(排序重排):把字符按「先频次、后字母」排序后重新交错摆放;若某字母频次过半也返回空。 思路直观,实现略绕。

代码实现

import java.util.PriorityQueue;

class Solution {

    // 解法一:贪心 + 优先队列
    public String reorganizeString(String s) {
        int[] cnt = new int[26];
        for (char c : s.toCharArray()) {
            ++cnt[c - 'a'];
        }
        int n = s.length();
        for (int c : cnt) {
            if (c > (n + 1) / 2) {
                return ""; // 无法重排
            }
        }
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> b[1] - a[1]); // {字母, 次数}
        for (int i = 0; i < 26; ++i) {
            if (cnt[i] > 0) {
                pq.offer(new int[] {i, cnt[i]});
            }
        }
        StringBuilder sb = new StringBuilder();
        while (pq.size() >= 2) {
            int[] a = pq.poll();
            int[] b = pq.poll();
            sb.append((char) ('a' + a[0]));
            sb.append((char) ('a' + b[0]));
            if (--a[1] > 0) {
                pq.offer(a);
            }
            if (--b[1] > 0) {
                pq.offer(b);
            }
        }
        if (!pq.isEmpty()) {
            sb.append((char) ('a' + pq.poll()[0]));
        }
        // 二次校验(防御)
        String res = sb.toString();
        for (int i = 1; i < res.length(); ++i) {
            if (res.charAt(i) == res.charAt(i - 1)) {
                return "";
            }
        }
        return res;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        String res1 = sol.reorganizeString("aab");
        ++total; if (res1.equals("aba")) ++pass; else System.out.println("FAIL test1 got " + res1);
        ++total; if (sol.reorganizeString("aaab").equals("")) ++pass; else System.out.println("FAIL test2 got " + sol.reorganizeString("aaab"));
        String res3 = sol.reorganizeString("vvvlo");
        ++total; if (res3.equals("vlvov") || res3.equals("vovlv") || res3.equals("volvv") == false) ++pass; else System.out.println("FAIL test3 got " + res3);
        String res4 = sol.reorganizeString("aaabc");
        ++total; if (!res4.equals("")) ++pass; else System.out.println("FAIL test4 got " + res4);
        if (pass == total) {
            System.out.println("== 0767 PASS ==");
        } else {
            System.out.println("== 0767 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(26)
解法二O(n·log n)O(n)

768. Max Chunks To Make Sorted II(最多能完成排序的块 II)

题目描述

给你一个整数数组 arr。将 arr 分割成若干块,并将这些块分别进行排序。之后再连接起来, 使得连接的结果和按升序排序后的原数组相同。返回能将数组分成的最多块数。

示例 1

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

示例 2

  • 输入:arr = [2,1,3,4,4]
  • 输出:4
  • 解释:分成 [2,1]、[3]、[4]、[4] 可以得到最多的块数。 提示:
  • 1 <= arr.length <= 2000
  • 0 <= arr[i] <= 10^8

解题思路

解法一(前缀最大 ≤ 后缀最小,主提交):位置 i 与 i+1 之间可以作为切分点,当且仅当 arr[0..i] 中的最大值 <= arr[i+1..n-1] 中的最小值。预处理后缀最小值数组 suffixMin, 一遍扫描计数。 时间复杂度 O(n),空间复杂度 O(n)。 解法二(单调栈):维护一个单调非减栈,栈内元素代表每个块排序后的最大值;当新元素小于栈顶 时,需要把前面的块合并。代码简练,但理解门槛略高。

代码实现

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

class Solution {

    // 解法一:前缀最大 <= 后缀最小
    public int maxChunksToSorted(int[] arr) {
        int n = arr.length;
        int[] suffixMin = new int[n + 1];
        suffixMin[n] = Integer.MAX_VALUE;
        for (int i = n - 1; i >= 0; --i) {
            suffixMin[i] = Math.min(arr[i], suffixMin[i + 1]);
        }
        int ans = 0, curMax = -1;
        for (int i = 0; i < n; ++i) {
            curMax = Math.max(curMax, arr[i]);
            if (curMax <= suffixMin[i + 1]) {
                ++ans; // 可以在此处切分
            }
        }
        return ans;
    }

    // 解法二:单调栈(栈底为块的最大值)
    public int maxChunksToSorted2(int[] arr) {
        Deque<Integer> stk = new ArrayDeque<>();
        for (int v : arr) {
            if (stk.isEmpty() || v >= stk.peek()) {
                stk.push(v);
            } else {
                int mx = stk.pop();
                while (!stk.isEmpty() && v < stk.peek()) {
                    stk.pop();
                }
                stk.push(mx); // 合并后的块最大值保持为原先的 mx
            }
        }
        return stk.size();
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        int[] a1 = {5, 4, 3, 2, 1};
        ++total; if (sol.maxChunksToSorted(a1) == 1) ++pass; else System.out.println("FAIL test1 got " + sol.maxChunksToSorted(a1));
        int[] a2 = {2, 1, 3, 4, 4};
        ++total; if (sol.maxChunksToSorted(a2) == 4) ++pass; else System.out.println("FAIL test2 got " + sol.maxChunksToSorted(a2));
        ++total; if (sol.maxChunksToSorted2(a1) == 1) ++pass; else System.out.println("FAIL test3 got " + sol.maxChunksToSorted2(a1));
        ++total; if (sol.maxChunksToSorted2(a2) == 4) ++pass; else System.out.println("FAIL test4");
        int[] a5 = {1, 1, 0, 0, 1};
        ++total; if (sol.maxChunksToSorted(a5) == 2) ++pass; else System.out.println("FAIL test5 got " + sol.maxChunksToSorted(a5));
        if (pass == total) {
            System.out.println("== 0768 PASS ==");
        } else {
            System.out.println("== 0768 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(n)
解法二O(n)O(n)

769. Max Chunks To Make Sorted(最多能完成排序的块)

题目描述

给定一个长度为 n 的整数数组 arr,它表示在 [0, n - 1] 范围内的整数的排列(每个整数 恰好出现一次)。我们将 arr 分割成若干块(即分区),并对每个块单独排序。将它们连接起来后, 使得连接的结果和按升序排序后的原数组相同。返回数组能分成的最多块数量。

示例 1

  • 输入:arr = [4,3,2,1,0]
  • 输出:1

示例 2

  • 输入:arr = [1,0,2,3,4]
  • 输出:4
  • 解释:分成 [1, 0], [2], [3], [4] 可以得到最多的块数,排序后连接为 [0, 1, 2, 3, 4]。 提示:
  • n == arr.length
  • 1 <= n <= 10
  • 0 <= arr[i] < n
  • arr 中每个元素都不同

解题思路

解法一(前缀最大值,主提交):因为 arr 是 [0, n-1] 的排列,若位置 i 处能成为块的右 边界,则该块内元素的最大值必须恰好等于 i(这样块排序后恰好是 [left..i] 的连续整数)。 从左往右维护前缀最大值 curMax,每当 curMax == i 时 ans++。 时间复杂度 O(n),空间复杂度 O(1)。 解法二(分治交换验证):模拟「每组块排序后连接」的过程,枚举切分点并验证,最坏 O(n²), 作对照参考。

代码实现

import java.util.ArrayList;
import java.util.List;

class Solution {

    // 解法一:前缀最大值
    public int maxChunksToSorted(int[] arr) {
        int ans = 0, curMax = 0;
        for (int i = 0; i < arr.length; ++i) {
            curMax = Math.max(curMax, arr[i]);
            if (curMax == i) {
                ++ans; // 前 i+1 个元素的集合恰为 [0..i]
            }
        }
        return ans;
    }

    // 解法二:朴素枚举验证(对照组)
    public int maxChunksToSorted2(int[] arr) {
        int n = arr.length, ans = 1;
        for (int mask = 0; mask < (1 << (n - 1)); ++mask) {
            // 用位掩码枚举切分点,验证每个块最小值/最大值排序拼接后整体有序
            int prevMax = -1, ok = 1;
            for (int i = 0; i < n; ) {
                int j = i, mx = -1, mn = n + 1;
                int m = mask;
                while (true) {
                    mx = Math.max(mx, arr[j]);
                    mn = Math.min(mn, arr[j]);
                    boolean cut = (j == n - 1) || ((m >> j) & 1) == 1;
                    ++j;
                    if (cut) {
                        break;
                    }
                }
                if (mn < prevMax) {
                    ok = 0;
                    break;
                }
                // 块内元素应构成连续区间 [mn, mx] 且长度 = mx - mn + 1 = j - i
                if (mx - mn + 1 != j - i) {
                    ok = 0;
                    break;
                }
                prevMax = mx;
                i = j;
            }
            if (ok == 1) {
                ans = Math.max(ans, Integer.bitCount(mask) + 1);
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        int[] a1 = {4, 3, 2, 1, 0};
        ++total; if (sol.maxChunksToSorted(a1) == 1) ++pass; else System.out.println("FAIL test1 got " + sol.maxChunksToSorted(a1));
        int[] a2 = {1, 0, 2, 3, 4};
        ++total; if (sol.maxChunksToSorted(a2) == 4) ++pass; else System.out.println("FAIL test2 got " + sol.maxChunksToSorted(a2));
        ++total; if (sol.maxChunksToSorted2(a1) == 1) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.maxChunksToSorted2(a2) == 4) ++pass; else System.out.println("FAIL test4");
        int[] a3 = {0};
        ++total; if (sol.maxChunksToSorted(a3) == 1) ++pass; else System.out.println("FAIL test5");
        int[] a4 = {1, 2, 0, 3};
        ++total; if (sol.maxChunksToSorted(a4) == 2) ++pass; else System.out.println("FAIL test6 got " + sol.maxChunksToSorted(a4));
        if (pass == total) {
            System.out.println("== 0769 PASS ==");
        } else {
            System.out.println("== 0769 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(1)

770. Basic Calculator IV(基本计算器 IV)

题目描述

给定一个表达式如 expression = "e + 8 - a + 5" 和一个求值映射,如 {"e": 1}(给定的形式 为 evalvars = ["e"] 和 evalints = [1]),返回表示简化表达式的标记列表,例如 ["-1*a","14"]。

  • 表达式交替使用块和符号,每个块和符号之间有一个空格;
  • 块要么是括号中的表达式,要么是变量,要么是非负整数;
  • 变量是一个由小写字母组成的字符串(不包括数字)。变量可以有多个字母,且从不具有前导系数 或一元运算符;
  • 表达式按通常顺序求值:先算括号,然后乘法,再算加法和减法;
  • 输出格式:对于系数非零的每个自变量项,按字典序将自变量写在一个项中(如 a*b*c),项中 变量写在一起;项的次数等于变量个数;按次数降序、字典序升序排序输出;前三系数直接放在 左边,用星号分隔(前导系数 1 仍要打印);常数项输出为纯数字。

示例 1

  • 输入:expression = "e + 8 - a + 5", evalvars = ["e"], evalints = [1]
  • 输出:["-1*a","14"]

示例 2

  • 输入:expression = "e - 8 + temperature - pressure", evalvars = ["e","temperature"], evalints = [1,12]
  • 输出:["-1*pressure","5"]

示例 3

  • 输入:expression = "(e + 8) * (e - 8)", evalvars = [], evalints = []
  • 输出:["1ee","-64"] 提示:
  • 表达式长度不超过 200
  • 表达式只包含小写字母、数字、+、-、*、() 和空格
  • 表达式中变量/数字个数不超过 100
  • 单独变量的长度不超过 20

解题思路

解法一(多项式表示 + 递归下降,主提交):把每个表达式看作「多项式」——各项记为 Map<List<String>, Integer>,键为按字典序排好的变量列表(如 {"a","a","b"}),值为系数。 用递归下降或栈解析:

  • expr() 处理加减:带符号合并多项式;
  • term() 处理乘除(此处只有乘):多项式乘法 = 键列表合并 + 系数相乘;
  • factor() 处理数字 / 变量(用 eval 映射替换为数字)/ (expr)。 加减时变量列表为空的项就是常数项。最后把所有 系数 != 0 的项按「次数降序、字典序升序」 排序输出:常数项排最后,k*a*a、14 等格式按题意拼接。 时间复杂度 O(项数²) 量级(多项式膨胀),表达式中元素 <= 100,可接受。 解法二(迭代双栈 + 多项式栈):用双栈模拟,逻辑等价,代码更工程化。

代码实现

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

class Solution {

    private final Map<String, Integer> varmap = new HashMap<>();
    private List<String> tokens;
    private int pos;

    public List<String> basicCalculatorIV(String expression, String[] evalvars, int[] evalints) {
        varmap.clear();
        for (int i = 0; i < evalvars.length; ++i) {
            varmap.put(evalvars[i], evalints[i]);
        }
        // 分词:把 "(", ")", "+", "-", "*", 数字, 变量 拆为 token
        tokens = new ArrayList<>();
        String e = expression.replace(" ", "");
        int n = e.length();
        for (int i = 0; i < n; ) {
            char c = e.charAt(i);
            if (c == '(' || c == ')' || c == '+' || c == '-' || c == '*') {
                tokens.add(String.valueOf(c));
                ++i;
            } else {
                int j = i;
                while (j < n && Character.isLetterOrDigit(e.charAt(j))) {
                    ++j;
                }
                tokens.add(e.substring(i, j));
                i = j;
            }
        }
        pos = 0;
        Map<List<String>, Integer> poly = expr();
        // 收集结果
        List<String> ans = new ArrayList<>();
        List<Map.Entry<List<String>, Integer>> items = new ArrayList<>();
        for (Map.Entry<List<String>, Integer> en : poly.entrySet()) {
            if (en.getValue() != 0) {
                items.add(en);
            }
        }
        // 常量项先摘出
        List<Map.Entry<List<String>, Integer>> constItems = new ArrayList<>();
        List<Map.Entry<List<String>, Integer>> varItems = new ArrayList<>();
        for (Map.Entry<List<String>, Integer> en : items) {
            if (en.getKey().isEmpty()) {
                constItems.add(en);
            } else {
                varItems.add(en);
            }
        }
        varItems.sort((a, b) -> {
            int d = b.getKey().size() - a.getKey().size(); // 次数降序
            if (d != 0) return d;
            for (int i = 0; i < a.getKey().size(); ++i) {
                int cmp = a.getKey().get(i).compareTo(b.getKey().get(i));
                if (cmp != 0) return cmp;
            }
            return 0;
        });
        for (Map.Entry<List<String>, Integer> en : varItems) {
            int coeff = en.getValue();
            StringBuilder sb = new StringBuilder();
            sb.append(coeff);
            for (String v : en.getKey()) {
                sb.append('*').append(v);
            }
            ans.add(sb.toString());
        }
        for (Map.Entry<List<String>, Integer> en : constItems) {
            ans.add(String.valueOf(en.getValue()));
        }
        if (ans.isEmpty()) {
            ans.add("0");
        }
        return ans;
    }

    private Map<List<String>, Integer> expr() {
        Map<List<String>, Integer> res = term();
        while (pos < tokens.size() && (tokens.get(pos).equals("+") || tokens.get(pos).equals("-"))) {
            String op = tokens.get(pos++);
            Map<List<String>, Integer> next = term();
            for (Map.Entry<List<String>, Integer> en : next.entrySet()) {
                int sign = op.equals("-") ? -1 : 1;
                res.merge(en.getKey(), sign * en.getValue(), (a, b) -> a + b);
            }
        }
        return res;
    }

    private Map<List<String>, Integer> term() {
        Map<List<String>, Integer> res = factor();
        while (pos < tokens.size() && tokens.get(pos).equals("*")) {
            ++pos;
            Map<List<String>, Integer> r2 = factor();
            res = multiply(res, r2);
        }
        return res;
    }

    private Map<List<String>, Integer> factor() {
        String tok = tokens.get(pos++);
        if (tok.equals("(")) {
            Map<List<String>, Integer> inner = expr();
            // 跳过右括号
            while (pos < tokens.size() && !tokens.get(pos).equals(")")) {
                ++pos;
            }
            if (pos < tokens.size() && tokens.get(pos).equals(")")) {
                ++pos;
            }
            return inner;
        }
        if (Character.isDigit(tok.charAt(0))) {
            int v = Integer.parseInt(tok);
            Map<List<String>, Integer> m = new HashMap<>();
            m.put(new ArrayList<>(), v);
            return m;
        }
        // 变量:替换为数值或保留为单项式
        Integer val = varmap.get(tok);
        if (val != null) {
            Map<List<String>, Integer> m = new HashMap<>();
            m.put(new ArrayList<>(), val);
            return m;
        }
        List<String> vars = new ArrayList<>();
        vars.add(tok);
        Map<List<String>, Integer> m = new HashMap<>();
        m.put(vars, 1);
        return m;
    }

    private Map<List<String>, Integer> multiply(Map<List<String>, Integer> a, Map<List<String>, Integer> b) {
        Map<List<String>, Integer> res = new HashMap<>();
        for (Map.Entry<List<String>, Integer> ea : a.entrySet()) {
            for (Map.Entry<List<String>, Integer> eb : b.entrySet()) {
                List<String> vars = new ArrayList<>(ea.getKey());
                vars.addAll(eb.getKey());
                vars.sort(String::compareTo);
                res.merge(vars, ea.getValue() * eb.getValue(), (x, y) -> x + y);
            }
        }
        return res;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        List<String> r1 = sol.basicCalculatorIV("e + 8 - a + 5", new String[] {"e"}, new int[] {1});
        ++total; if (r1.equals(java.util.Arrays.asList("-1*a", "14"))) ++pass; else System.out.println("FAIL test1 got " + r1);
        List<String> r2 = sol.basicCalculatorIV("e - 8 + temperature - pressure", new String[] {"e", "temperature"}, new int[] {1, 12});
        ++total; if (r2.equals(java.util.Arrays.asList("-1*pressure", "5"))) ++pass; else System.out.println("FAIL test2 got " + r2);
        List<String> r3 = sol.basicCalculatorIV("(e + 8) * (e - 8)", new String[] {}, new int[] {});
        ++total; if (r3.equals(java.util.Arrays.asList("1*e*e", "-64"))) ++pass; else System.out.println("FAIL test3 got " + r3);
        List<String> r4 = sol.basicCalculatorIV("a * b * c + b * a * c * 4", new String[] {}, new int[] {});
        ++total; if (r4.equals(java.util.Arrays.asList("5*a*b*c"))) ++pass; else System.out.println("FAIL test4 got " + r4);
        List<String> r5 = sol.basicCalculatorIV("7 - 7", new String[] {}, new int[] {});
        ++total; if (r5.equals(java.util.Arrays.asList("0"))) ++pass; else System.out.println("FAIL test5 got " + r5);
        if (pass == total) {
            System.out.println("== 0770 PASS ==");
        } else {
            System.out.println("== 0770 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(P²)O(P)

771. Jewels and Stones(宝石与石头)

题目描述

给你一个字符串 jewels 代表石头中宝石的类型,另有一个字符串 stones 代表你拥有的石头。 stones 中每个字符代表了一种你拥有的石头的类型,你想知道你拥有的石头中有多少是宝石。 字母区分大小写,因此 "a" 和 "A" 是不同类型的石头。

示例 1

  • 输入:jewels = "aA", stones = "aAAbbbb"
  • 输出:3

示例 2

  • 输入:jewels = "z", stones = "ZZ"
  • 输出:0 提示:
  • 1 <= jewels.length, stones.length <= 50
  • jewels 和 stones 仅由英文字母组成
  • jewels 中的所有字符都是唯一的

解题思路

解法一(哈希集合统计,主提交):把 jewels 中所有字符放入 HashSet<Character>,然后遍历 stones 统计命中的字符个数。 时间复杂度 O(|jewels| + |stones|),空间复杂度 O(|jewels|)。 解法二(布尔数组):字符范围仅英文字母(大小写共 52 个),用 boolean[128] 记录宝石类型, 免除哈希开销。

代码实现

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

class Solution {

    // 解法一:哈希集合
    public int numJewelsInStones(String jewels, String stones) {
        Set<Character> set = new HashSet<>();
        for (char c : jewels.toCharArray()) {
            set.add(c);
        }
        int ans = 0;
        for (char c : stones.toCharArray()) {
            if (set.contains(c)) {
                ++ans;
            }
        }
        return ans;
    }

    // 解法二:布尔数组
    public int numJewelsInStones2(String jewels, String stones) {
        boolean[] isJewel = new boolean[128];
        for (char c : jewels.toCharArray()) {
            isJewel[c] = true;
        }
        int ans = 0;
        for (char c : stones.toCharArray()) {
            if (isJewel[c]) {
                ++ans;
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.numJewelsInStones("aA", "aAAbbbb") == 3) ++pass; else System.out.println("FAIL test1 got " + sol.numJewelsInStones("aA", "aAAbbbb"));
        ++total; if (sol.numJewelsInStones("z", "ZZ") == 0) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.numJewelsInStones2("aA", "aAAbbbb") == 3) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.numJewelsInStones2("z", "ZZ") == 0) ++pass; else System.out.println("FAIL test4");
        if (pass == total) {
            System.out.println("== 0771 PASS ==");
        } else {
            System.out.println("== 0771 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(J + S)O(J)
解法二O(J + S)O(1)

772. Basic Calculator III(基本计算器 III)

题目描述

实现一个基本的计算器来计算简单的表达式字符串。表达式字符串只包含非负整数,算符 +、-、 *、/,左括号 ( 和右括号 )。整数除法需要向下截断。可以假定给定的表达式总是有效的。 所有的中间结果的范围均满足 [-2^31, 2^31 - 1]。不能使用任何将字符串作为表达式求值的内置 函数,比如 eval()。

示例 1

  • 输入:s = "1+1"
  • 输出:2

示例 2

  • 输入:s = "6-4/2"
  • 输出:4

示例 3

  • 输入:s = "2*(5+5*2)/3+(6/2+8)"
  • 输出:21 提示:
  • 1 <= s.length <= 10^5
  • s 由整数、算符 ('+','-','*','/')、左右括号 '('、')' 和空格 ' ' 组成
  • s 表示一个有效的表达式
  • 所有整数都在 [1, 2^31 - 1] 范围内(题目有的版本允许 0/负,这里按题意非负整数)

解题思路

解法一(双栈:数字栈 + 符号栈,主提交):用两个栈模拟表达式求值:

  • 数字:累积数字;
  • '(':压栈并打标记(用栈中元素计数或压入特殊符号);
  • ')':弹出直到遇到 '(';
  • 运算符 + - * /:当「栈顶运算符优先级 >= 当前运算符」时先完成一次运算(左结合),再压入 当前运算符(遇到 ( 前停止)。 处理完后清空符号栈完成剩余运算。 除法用整除(Java 对正数即向下截断;遇负数按题意均正即可)。 时间复杂度 O(n),空间复杂度 O(n)。 解法二(递归下降 + 全局指针):把 expr() 拆成 term()(乘除)与 factor()(数字/括号) 的经典递归下降,遇到括号递归求值。代码结构更清晰,同样 O(n)。

代码实现

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

class Solution {

    // 解法一:双栈
    public int calculate(String s) {
        Deque<Integer> nums = new ArrayDeque<>();
        Deque<Character> ops = new ArrayDeque<>();
        int i = 0, n = s.length();
        while (i < n) {
            char c = s.charAt(i);
            if (c == ' ') {
                ++i;
            } else if (Character.isDigit(c)) {
                int v = 0;
                while (i < n && Character.isDigit(s.charAt(i))) {
                    v = v * 10 + (s.charAt(i) - '0');
                    ++i;
                }
                nums.push(v);
            } else if (c == '(') {
                ops.push(c);
                ++i;
            } else if (c == ')') {
                while (!ops.isEmpty() && ops.peek() != '(') {
                    calc(nums, ops);
                }
                ops.pop(); // 弹出 '('
                ++i;
            } else { // + - * /
                while (!ops.isEmpty() && precedence(ops.peek()) >= precedence(c)) {
                    calc(nums, ops);
                }
                ops.push(c);
                ++i;
            }
        }
        while (!ops.isEmpty()) {
            calc(nums, ops);
        }
        return nums.peek();
    }

    private int precedence(char c) {
        if (c == '+' || c == '-') return 1;
        if (c == '*' || c == '/') return 2;
        return 0;
    }

    private void calc(Deque<Integer> nums, Deque<Character> ops) {
        char op = ops.pop();
        int b = nums.pop();
        int a = nums.isEmpty() ? 0 : nums.pop();
        int r;
        switch (op) {
            case '+': r = a + b; break;
            case '-': r = a - b; break;
            case '*': r = a * b; break;
            default: r = a / b; break;
        }
        nums.push(r);
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.calculate("1+1") == 2) ++pass; else System.out.println("FAIL test1 got " + sol.calculate("1+1"));
        ++total; if (sol.calculate("6-4/2") == 4) ++pass; else System.out.println("FAIL test2 got " + sol.calculate("6-4/2"));
        ++total; if (sol.calculate("2*(5+5*2)/3+(6/2+8)") == 21) ++pass; else System.out.println("FAIL test3 got " + sol.calculate("2*(5+5*2)/3+(6/2+8)"));
        ++total; if (sol.calculate(" 3/2 ") == 1) ++pass; else System.out.println("FAIL test4 got " + sol.calculate(" 3/2 "));
        ++total; if (sol.calculate("1 + 1") == 2) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.calculate("2-1-1") == 0) ++pass; else System.out.println("FAIL test6 got " + sol.calculate("2-1-1"));
        ++total; if (sol.calculate("(1+(4+5+2)-3)+(6+8)") == 23) ++pass; else System.out.println("FAIL test7 got " + sol.calculate("(1+(4+5+2)-3)+(6+8)"));
        ++total; if (sol.calculate("1*2-3/4+5*6-7*8+9/10") == -24) ++pass; else System.out.println("FAIL test8 got " + sol.calculate("1*2-3/4+5*6-7*8+9/10"));
        if (pass == total) {
            System.out.println("== 0772 PASS ==");
        } else {
            System.out.println("== 0772 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(n)
解法二O(n)O(n)

773. Sliding Puzzle(滑动谜题)

题目描述

在一个 2 x 3 的板上(board)有 5 块砖瓦,用数字 1~5 来表示,以及一块空缺用 0 来表示。 一次移动定义为选择 0 与一个相邻的数字(上下左右)进行交换。最终当板 board 的结果是 [[1,2,3],[4,5,0]] 谜板被解开。给出一个谜板的初始状态 board,返回最少可以通过多少次移动 解开谜板,如果不能解开谜板,则返回 -1。

示例 1

  • 输入:board = [[1,2,3],[4,0,5]]
  • 输出:1

示例 2

  • 输入:board = [[1,2,3],[5,4,0]]
  • 输出:-1

示例 3

  • 输入:board = [[4,1,2],[5,0,3]]
  • 输出:5 提示:
  • board.length == 2
  • board[i].length == 3
  • board[i][j] 是 0 到 5 的排列

解题思路

解法一(BFS 状态最短路,主提交):把 2x3 板展平成长度 6 的字符串/整型状态,0 的每个位置 有固定的相邻位置表。从初始状态 BFS,交换 0 与邻居生成新状态,首次到达目标状态 "123450" 的层数即为答案;队列耗尽则不可解返回 -1。 状态数至多 6! = 720,BFS 复杂度 O(720·邻域)。 解法二(A* / 双向 BFS):用启发式(曼哈顿距离和)加速,思路更高级;状态空间小,单向 BFS 已经足够,此处双向 BFS 作为对照。

代码实现

import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

class Solution {

    // 0 在每个下标 (展平) 时可交换的相邻位置
    private static final int[][] NEIGHBOR = {
        {1, 3}, {0, 2, 4}, {1, 5},
        {0, 4}, {1, 3, 5}, {2, 4}
    };

    // 解法一:BFS
    public int slidingPuzzle(int[][] board) {
        StringBuilder start = new StringBuilder();
        for (int[] row : board) {
            for (int v : row) {
                start.append(v);
            }
        }
        String target = "123450";
        if (start.toString().equals(target)) {
            return 0;
        }
        Set<String> seen = new HashSet<>();
        seen.add(start.toString());
        Deque<String> q = new ArrayDeque<>();
        q.offer(start.toString());
        int steps = 0;
        while (!q.isEmpty()) {
            int size = q.size();
            ++steps;
            for (int t = 0; t < size; ++t) {
                String cur = q.poll();
                int zero = cur.indexOf('0');
                for (int nb : NEIGHBOR[zero]) {
                    char[] arr = cur.toCharArray();
                    arr[zero] = arr[nb];
                    arr[nb] = '0';
                    String next = new String(arr);
                    if (next.equals(target)) {
                        return steps;
                    }
                    if (seen.add(next)) {
                        q.offer(next);
                    }
                }
            }
        }
        return -1;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        int[][] b1 = {{1, 2, 3}, {4, 0, 5}};
        ++total; if (sol.slidingPuzzle(b1) == 1) ++pass; else System.out.println("FAIL test1 got " + sol.slidingPuzzle(b1));
        int[][] b2 = {{1, 2, 3}, {5, 4, 0}};
        ++total; if (sol.slidingPuzzle(b2) == -1) ++pass; else System.out.println("FAIL test2 got " + sol.slidingPuzzle(b2));
        int[][] b3 = {{4, 1, 2}, {5, 0, 3}};
        ++total; if (sol.slidingPuzzle(b3) == 5) ++pass; else System.out.println("FAIL test3 got " + sol.slidingPuzzle(b3));
        int[][] b4 = {{1, 2, 3}, {4, 5, 0}};
        ++total; if (sol.slidingPuzzle(b4) == 0) ++pass; else System.out.println("FAIL test4");
        if (pass == total) {
            System.out.println("== 0773 PASS ==");
        } else {
            System.out.println("== 0773 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(720)O(720)
解法二O(720)O(720)

774. Minimize Max Distance to Gas Station(最小化去加油站的最大距离)

题目描述

整数数组 stations 表示水平数轴上各个加油站的位置。给你一个整数 k。请你在数轴上增设 k 个加油站,新增加油站可以位于水平数轴上的任意位置,而不必放在整数位置上。设 penalty() 是增设 k 个新加油站后,相邻两个加油站间的最大距离。请你返回 penalty() 可能的最小值。 与实际答案误差在 10^-6 范围内的答案将被视作正确答案。

示例 1

  • 输入:stations = [1,2,3,4,5,6,7,8,9,10], k = 9
  • 输出:0.50000

示例 2

  • 输入:stations = [23,24,36,39,46,56,57,65,84,98], k = 1
  • 输出:14.00000 提示:
  • 10 <= stations.length <= 2000
  • 0 <= stations[i] <= 10^8
  • stations 按严格递增顺序排列
  • 1 <= k <= 10^6

解题思路

解法一(二分答案 + 贪心检查,主提交):「最大距离最小」是典型的二分答案模型。二分最大 相邻距离 mid,检查「每个原始间距 gap = stations[i+1] - stations[i] 至少需要插入 ceil(gap / mid) - 1 个新站才能让该段内相邻距离 <= mid」,总需要插入数 total <= k 则可行,缩小上界;否则放大下界。 注意 gap == mid 的整数倍时 ceil(gap/mid) - 1 处理:可用 (gap + mid - eps) / mid - 1 避免浮点边界,或直接用 Math.ceil(gap / mid) - 1。为满足 1e-6 精度,循环约 60 次。 时间复杂度 O(n · iterations),迭代常数约 60,足以覆盖 1e-6 精度。 解法二(优先队列每次插入最贪点):每次把新站加在「当前最长段」中点,用大根堆放段长;每次 插入把段长均匀拆分。O(k·log n) 在 k 可达 1e6 时会超时,仅作理论对照。

代码实现

import java.util.Arrays;

class Solution {

    // 解法一:二分答案
    public double minmaxGasDist(int[] stations, int k) {
        double lo = 0, hi = 0;
        for (int i = 1; i < stations.length; ++i) {
            hi = Math.max(hi, (double) (stations[i] - stations[i - 1]));
        }
        for (int iter = 0; iter < 60; ++iter) {
            double mid = (lo + hi) / 2;
            long need = 0; // 需要插入的站数
            for (int i = 1; i < stations.length; ++i) {
                double gap = stations[i] - stations[i - 1];
                need += (long) Math.ceil(gap / mid) - 1;
            }
            if (need <= k) {
                hi = mid; // 可行,收缩上界
            } else {
                lo = mid; // 不可行,放大下界
            }
        }
        return hi;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        int[] s1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        double r1 = sol.minmaxGasDist(s1, 9);
        ++total; if (Math.abs(r1 - 0.5) < 1e-6) ++pass; else System.out.println("FAIL test1 got " + r1);
        int[] s2 = {23, 24, 36, 39, 46, 56, 57, 65, 84, 98};
        double r2 = sol.minmaxGasDist(s2, 1);
        ++total; if (Math.abs(r2 - 14.0) < 1e-6) ++pass; else System.out.println("FAIL test2 got " + r2);
        int[] s3 = {10, 20, 30};
        double r3 = sol.minmaxGasDist(s3, 1);
        ++total; if (Math.abs(r3 - 10.0) < 1e-6) ++pass; else System.out.println("FAIL test3 got " + r3);
        if (pass == total) {
            System.out.println("== 0774 PASS ==");
        } else {
            System.out.println("== 0774 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n·60)O(1)
解法二O(k·log n)O(n)

775. Global and Local Inversions(全局倒置与局部倒置)

题目描述

给你一个长度为 n 的整数数组 nums,表示由范围 [0, n - 1] 内所有整数组成的一个排列。 全局倒置的数目等于满足下述条件不同下标对 (i, j) 的数目:0 <= i < j < n 且 nums[i] > nums[j]。局部倒置的数目等于满足下述条件的下标 i 的数目:0 <= i < n - 1 且 nums[i] > nums[i + 1]。 当数组 nums 中全局倒置的数量等于局部倒置的数量时,返回 true;否则返回 false。

示例 1

  • 输入:nums = [1,0,2]
  • 输出:true
  • 解释:有 1 个全局倒置,和 1 个局部倒置。

示例 2

  • 输入:nums = [1,2,0]
  • 输出:false
  • 解释:有 2 个全局倒置((1,2)、(2,0)),和 1 个局部倒置((2,0))。 提示:
  • n == nums.length
  • 1 <= n <= 10^5
  • 0 <= nums[i] < n
  • nums 中所有元素都是唯一的

解题思路

解法一(等价转化:无「跨步非法倒置」,主提交):局部倒置一定是全局倒置,因此要两者数量相等, 只需保证「不存在非相邻的全局倒置」,即对于任意 i < j - 1,都有 nums[i] < nums[j]。 等价的说法是:扫描时维护 nums[0..i-2] 的最大值 mx,只要出现 mx > nums[i](i 从 2 起) 就说明存在非局部倒置,返回 false。因为排列中一个数只要「跳级」就不会等于局部倒置数。 注意这里 nums 是 [0, n-1] 的排列,可进一步简化:非局部倒置存在当且仅当某个 nums[i] == i + 1 与 nums[i+1] == i 分别拆开,但直接维护前缀最大更通用。 时间复杂度 O(n),空间复杂度 O(1)。 解法二(暴力对照):直接数全局/局部倒置个数,O(n²),仅作正确性对照。

代码实现

class Solution {

    // 解法一:维护前缀最大,检查非相邻倒置
    public boolean isIdealPermutation(int[] nums) {
        int n = nums.length;
        int maxBefore = Integer.MIN_VALUE; // nums[0..i-2] 的最大值(i >= 2 时有效)
        for (int i = 0; i < n; ++i) {
            if (i >= 2 && maxBefore > nums[i]) {
                return false; // 存在非相邻的全局倒置 (j, i),j <= i-2
            }
            if (i >= 1) {
                maxBefore = Math.max(maxBefore, nums[i - 1]);
            }
        }
        return true;
    }

    // 解法二:暴力对照
    public boolean isIdealPermutation2(int[] nums) {
        int n = nums.length, global = 0, local = 0;
        for (int i = 0; i < n; ++i) {
            if (i + 1 < n && nums[i] > nums[i + 1]) {
                ++local;
            }
            for (int j = i + 1; j < n; ++j) {
                if (nums[i] > nums[j]) {
                    ++global;
                }
            }
        }
        return global == local;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        int[] n1 = {1, 0, 2};
        ++total; if (sol.isIdealPermutation(n1)) ++pass; else System.out.println("FAIL test1");
        int[] n2 = {1, 2, 0};
        ++total; if (!sol.isIdealPermutation(n2)) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.isIdealPermutation2(n1)) ++pass; else System.out.println("FAIL test3");
        ++total; if (!sol.isIdealPermutation2(n2)) ++pass; else System.out.println("FAIL test4");
        int[] n5 = {0};
        ++total; if (sol.isIdealPermutation(n5)) ++pass; else System.out.println("FAIL test5");
        int[] n6 = {2, 0, 1};
        ++total; if (!sol.isIdealPermutation(n6)) ++pass; else System.out.println("FAIL test6");
        if (pass == total) {
            System.out.println("== 0775 PASS ==");
        } else {
            System.out.println("== 0775 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(1)
解法二O(n²)O(1)