LeetCode1941~1960题目解析

LeetCode 1941-1960. 题解合集

文章目录


1941. Check if All Characters Have Equal Number of Occurrences(检查是否所有字符出现次数相同)

题目描述

给你一个字符串 s,如果 s 是一个「好」字符串,请你返回 true,否则请返回 false。

如果 s 中出现过的所有字符的出现次数相同,那么我们称字符串 s 是「好」字符串。

示例 1

  • 输入 :s = "abacbc"
  • 输出 :true
  • 解释 :s 中出现过的字符为 'a'、'b' 和 'c'。s 中所有字符均出现 2 次。

示例 2

  • 输入 :s = "aaabb"
  • 输出 :false
  • 解释 :s 中出现过的字符为 'a' 和 'b'。'a' 出现了 3 次,'b' 出现了 2 次,两者出现次数不同。
    提示:
  • - 1 <= s.length <= 1000
    • s 只包含小写英文字母。

解题思路

解法一:频次数组 + 基准比较,O(n),主提交版本

用 int[26] 统计每个字符出现次数,

取第一个出现过的字符的频次作为基准 base,再遍历 cnt 数组:凡是出现过(cnt[i] > 0)

但次数不等于 base 的字符,直接返回 false。全部一致返回 true。

解法二:频次集合去重,O(n)

同样先统计频次,再用 HashSet<Integer> 收集所有出现过的

字符的频次,最后判断集合大小是否为 1(所有非零频次全部相同)。两解法思路等价,实现角度

不同,用于交叉验证。

代码实现

java 复制代码
import java.util.HashSet;
import java.util.Set;

public class Solution {

    public boolean areOccurrencesEqual(String s) {
        int[] cnt = new int[26];
        for (int i = 0; i < s.length(); i++) {
            cnt[s.charAt(i) - 'a']++;
        }
        int base = 0;
        for (int c : cnt) {
            if (c > 0) {
                base = c;
                break;
            }
        }
        for (int c : cnt) {
            if (c > 0 && c != base) {
                return false;
            }
        }
        return true;
    }

    public boolean areOccurrencesEqualSet(String s) {
        int[] cnt = new int[26];
        for (int i = 0; i < s.length(); i++) {
            cnt[s.charAt(i) - 'a']++;
        }
        Set<Integer> set = new HashSet<>();
        for (int c : cnt) {
            if (c > 0) {
                set.add(c);
            }
        }
        return set.size() == 1;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        String[] ss = {"abacbc", "aaabb", "a", "ab", "abc", "aaabbb", "abacbc"};
        boolean[] exp = {true, false, true, true, true, true, true};
        boolean all = true;
        for (int i = 0; i < ss.length; i++) {
            boolean a = sol.areOccurrencesEqual(ss[i]);
            boolean b = sol.areOccurrencesEqualSet(ss[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " \"" + ss[i] + "\" => " + a + " / set=" + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1942. The Number of the Smallest Unoccupied Chair(最小未被占据椅子的编号)

题目描述

有 n 个朋友在举办一个派对,这些朋友从 0 到 n - 1 编号。派对里有无数张椅子,

编号为 0 到 infinity。当一个朋友到达派对时,他会占据编号最小且未被占据的椅子。

  • 比方说,当一个朋友到达时,如果椅子 0、1 和 5 被占据了,那么他会占据 2 号椅子。
    当一个朋友离开派对时,他的椅子会立刻变成未占据状态。如果同一时刻有另一个朋友到达,
    可以立即占据这张椅子。
    给你一个下标从 0 开始的二维整数数组 times,其中 times[i] = [arrivali, leavingi]
    表示第 i 个朋友到达和离开的时刻,同时给你一个整数 targetFriend。所有到达时间互不相同。
    请你返回编号为 targetFriend 的朋友占据的椅子编号。

示例 1

  • 输入 :times = \[1,4,2,3,4,6], targetFriend = 1
  • 输出:1
  • 解释:
  • 朋友 0 时刻 1 到达,占据椅子 0。
  • 朋友 1 时刻 2 到达,占据椅子 1。
  • 朋友 1 时刻 3 离开,椅子 1 变成未占据。
  • 朋友 0 时刻 4 离开,椅子 0 变成未占据。
  • 朋友 2 时刻 4 到达,占据椅子 0。
    朋友 1 占据椅子 1,所以返回 1。

示例 2

  • 输入 :times = \[3,10,1,5,2,6], targetFriend = 0
  • 输出:2
  • 解释 :朋友 1 时刻 1 到达占据椅子 0;朋友 2 时刻 2 到达占据椅子 1;朋友 0 时刻 3 到达
    占据椅子 2。朋友 1 时刻 5 离开,椅子 0 释放;朋友 2 时刻 6 离开,椅子 1 释放。
    朋友 0 占据椅子 2,所以返回 2。
    提示:
  • - n == times.length
  • - 2 <= n <= 10^4
  • - times[i].length == 2
  • - 1 <= arrivali < leavingi <= 10^5
  • - 0 <= targetFriend <= n - 1
    • 每个 arrivali 时刻互不相同。

解题思路

解法一:按到达时间排序 + 空闲/离开双堆,O(n log n),主提交版本

把朋友按下标与

到达时间绑定,按到达时间升序处理。用一个小顶堆 free 保存所有当前空闲的椅子编号

(初始为 0 ~ n-1),再用一个小顶堆 leaving 保存 (离开时刻, 椅子编号)。处理某位

朋友到达时,先把所有 离开时刻 <= 当前到达时刻 的椅子从 leaving 弹出来放回 free,

然后从 free 弹出编号最小的椅子分配给他;若这位朋友是 targetFriend 直接返回该编号。

处理完所有朋友后不会用到(题目保证所有到达时间不重叠处理即可)。

解法二:时间线模拟,O(n^2)

不排序,直接枚举从最小到最大的所有到达时刻(每次选择

尚未处理的朋友中到达时间最早的),同样先释放已离开的椅子,再从小到大找空闲椅子分配。

实现更朴素,用于交叉验证解法一。

代码实现

java 复制代码
import java.util.Arrays;
import java.util.PriorityQueue;

public class Solution {

    public int smallestChair(int[][] times, int targetFriend) {
        int n = times.length;
        Integer[] order = new Integer[n];
        for (int i = 0; i < n; i++) order[i] = i;
        Arrays.sort(order, (a, b) -> Integer.compare(times[a][0], times[b][0]));

        PriorityQueue<Integer> free = new PriorityQueue<>();
        for (int i = 0; i < n; i++) free.offer(i);
        // 离开堆:按离开时刻升序,(离开时刻, 椅子编号)
        PriorityQueue<int[]> leaving = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
        int[] chairOf = new int[n];

        for (int idx : order) {
            int arrive = times[idx][0];
            // 释放已离开的椅子(离开时刻 <= 当前到达时刻)
            while (!leaving.isEmpty() && leaving.peek()[0] <= arrive) {
                free.offer(leaving.poll()[1]);
            }
            int chair = free.poll();
            chairOf[idx] = chair;
            if (idx == targetFriend) {
                return chair;
            }
            leaving.offer(new int[]{times[idx][1], chair});
        }
        return -1; // 不会到达
    }

    public int smallestChairSim(int[][] times, int targetFriend) {
        int n = times.length;
        boolean[] hasChair = new boolean[n];
        int[] chairOf = new int[n];
        boolean[] done = new boolean[n];
        for (int step = 0; step < n; step++) {
            // 找到当前最早到达且未处理的朋友
            int cur = -1;
            for (int i = 0; i < n; i++) {
                if (!done[i] && (cur == -1 || times[i][0] < times[cur][0])) {
                    cur = i;
                }
            }
            done[cur] = true;
            int arrive = times[cur][0];
            // 释放所有离开时刻 <= arrive 的椅子
            for (int i = 0; i < n; i++) {
                if (hasChair[i] && times[i][1] <= arrive) {
                    hasChair[i] = false;   // 释放
                }
            }
            // 找最小空闲椅子
            int chair = 0;
            while (hasChair[chair]) chair++;
            hasChair[chair] = true;
            chairOf[cur] = chair;
            if (cur == targetFriend) {
                return chair;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][][] tss = {
            {{1, 4}, {2, 3}, {4, 6}},
            {{3, 10}, {1, 5}, {2, 6}},
            {{1, 5}, {2, 3}, {4, 6}, {5, 7}},
            {{1, 4}, {2, 3}, {4, 6}},
            {{33889, 49776}, {13091, 74144}, {50591, 78464}, {40305, 61337}, {30132, 61432}}
        };
        int[] targets = {1, 0, 2, 0, 1};
        int[] exp = {1, 2, 1, 0, 0};
        boolean all = true;
        for (int i = 0; i < tss.length; i++) {
            int a = sol.smallestChair(tss[i], targets[i]);
            int b = sol.smallestChairSim(tss[i], targets[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1943. Describe the Painting(描述绘画结果)

题目描述

给你一个细长的画,用数轴表示。这幅画由若干有重叠的线段表示,每个线段有独一无二的颜色。

给你二维整数数组 segments,其中 segments[i] = [starti, endi, colori] 表示线段为

半开区间 [starti, endi) 且颜色为 colori。

线段间重叠部分的颜色会被混合。如果有两种或者更多颜色混合时,它们会形成一种新的颜色,

用一个集合表示这个混合颜色。比方说,如果颜色 2、4 和 6 被混合,那么结果颜色为 {2,4,6}。

为了简化题目,你不需要输出整个集合,只需要用集合中所有元素的和来表示颜色集合。

你想要用最少数目不重叠半开区间来表示这幅混合颜色的画。这些线段可以用二维数组 painting

表示,其中 painting[j] = [leftj, rightj, mixj] 表示一个半开区间 [leftj, rightj)

的颜色和为 mixj。

请你返回二维数组 painting,它表示最终绘画的结果(没有被涂色的部分不出现在结果中)。

你可以按任意顺序返回最终数组的结果。

示例 1

  • 输入 :segments = \[1,4,5,4,7,7,1,7,9]
  • 输出:\[1,4,14,4,7,16]
  • 解释:[1,4) 颜色为 {5,9}(和为 14);[4,7) 颜色为 {7,9}(和为 16)。

示例 2

  • 输入 :segments = \[1,7,9,6,8,15,8,10,7]
  • 输出:\[1,6,9,6,7,24,7,8,15,8,10,7]

示例 3

  • 输入 :segments = \[1,4,5,1,4,7,4,7,1,4,7,11]
  • 输出:\[1,4,12,4,7,12]
  • 解释 :注意只返回一个单独的线段 [1,7) 是不正确的,因为 [1,4) 的混合颜色集合 {5,7}
    与 [4,7) 的混合颜色集合 {1,11} 不相同(只是和恰好都为 12)。
    提示:
  • - 1 <= segments.length <= 2 * 10^4
  • - segments[i].length == 3
  • - 1 <= starti < endi <= 10^5
  • - 1 <= colori <= 10^9
    • 每种颜色 colori 互不相同。

解题思路

解法一:差分数组 + TreeMap 前缀和,O(n log n),主提交版本

每个线段是半开区间

[start, end),用差分思想:在 start 处颜色和增加 color,在 end 处减少 color。

用 TreeMap<Integer, Long> 收集所有端点(key 为端点,value 为该点的差分增量)。由于

每种颜色独一无二,混合颜色集合只在线段端点处发生变化,因此把所有端点作为切分点即可得到

理论最少区间数。遍历 TreeMap 的相邻端点:维护前缀和 prefix,对区间

[key_i, key_{i+1}),若 prefix > 0(有颜色覆盖)则输出 [key_i, key_{i+1}, prefix],

然后令 prefix += diff[key_{i+1}] 进入下一段。

解法二:暴力枚举切分点,O(n^2),对照

收集全部端点排重排序,对每个相邻端点区间

遍历所有 segments 累加在该区间内覆盖的颜色和,大于 0 则输出。仅用于小规模交叉验证。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public class Solution {

    public List<List<Long>> splitPainting(int[][] segments) {
        TreeMap<Integer, Long> diff = new TreeMap<>();
        for (int[] seg : segments) {
            diff.put(seg[0], diff.getOrDefault(seg[0], 0L) + seg[2]);
            diff.put(seg[1], diff.getOrDefault(seg[1], 0L) - seg[2]);
        }
        List<List<Long>> res = new ArrayList<>();
        long prefix = 0;
        Integer prev = null;
        for (Map.Entry<Integer, Long> e : diff.entrySet()) {
            if (prev != null && prefix > 0) {
                List<Long> part = new ArrayList<>();
                part.add((long) prev);
                part.add((long) e.getKey());
                part.add(prefix);
                res.add(part);
            }
            prev = e.getKey();
            prefix += e.getValue();
        }
        return res;
    }

    public List<List<Long>> splitPaintingBrute(int[][] segments) {
        Set<Integer> pts = new HashSet<>();
        for (int[] seg : segments) {
            pts.add(seg[0]);
            pts.add(seg[1]);
        }
        List<Integer> ps = new ArrayList<>(pts);
        Collections.sort(ps);
        List<List<Long>> res = new ArrayList<>();
        for (int k = 0; k + 1 < ps.size(); k++) {
            long sum = 0;
            for (int[] seg : segments) {
                if (seg[0] <= ps.get(k) && ps.get(k + 1) <= seg[1]) {
                    sum += seg[2];
                }
            }
            if (sum > 0) {
                List<Long> part = new ArrayList<>();
                part.add((long) ps.get(k));
                part.add((long) ps.get(k + 1));
                part.add(sum);
                res.add(part);
            }
        }
        return res;
    }

    private static boolean same(List<List<Long>> a, List<List<Long>> b) {
        if (a.size() != b.size()) return false;
        for (int i = 0; i < a.size(); i++) {
            if (!a.get(i).equals(b.get(i))) return false;
        }
        return true;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][][] segs = {
            {{1, 4, 5}, {4, 7, 7}, {1, 7, 9}},
            {{1, 7, 9}, {6, 8, 15}, {8, 10, 7}},
            {{1, 4, 5}, {1, 4, 7}, {4, 7, 1}, {4, 7, 11}},
            {{1, 4, 5}, {1, 7, 7}},
            {{3, 7, 2}, {1, 5, 8}, {5, 9, 4}}
        };
        boolean all = true;
        for (int i = 0; i < segs.length; i++) {
            List<List<Long>> a = sol.splitPainting(segs[i]);
            List<List<Long>> b = sol.splitPaintingBrute(segs[i]);
            boolean ok = same(a, b);
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1944. Number of Visible People in a Queue(队列中可以看到的人数)

题目描述

有 n 个人排成一个队列,从左到右编号为 0 到 n - 1。给你一个整数数组 heights,

每个整数互不相同,heights[i] 表示第 i 个人的高度。

一个人能看到他右边另一个人的条件是这两人之间的所有人都比他们两人矮。更正式地,第 i

个人能看到第 j 个人的条件是 i < j 且 min(heights[i], heights[j]) > max(heights[i+1], ..., heights[j-1])。

请你返回一个长度为 n 的数组 answer,其中 answer[i] 是第 i 个人在他右侧队列中

能看到的的人数。

示例 1

  • 输入 :heights = 10,6,8,5,11,9
  • 输出:3,1,2,1,1,0
  • 解释:
  • 第 0 个人能看到编号为 1、2 和 4 的人(高度 6、8、11)。
  • 第 1 个人(高度 6)能看到编号为 2 的人(高度 8)。
  • 第 2 个人(高度 8)能看到编号为 3 和 4 的人(高度 5、11)。
  • 第 3 个人(高度 5)能看到编号为 4 的人(高度 11)。
  • 第 4 个人(高度 11)能看到编号为 5 的人(高度 9)。

示例 2

  • 输入 :heights = 5,1,2,3,10
  • 输出 :4,1,1,1,0
    提示:
  • - n == heights.length
  • - 1 <= n <= 10^5
  • - 1 <= heights[i] <= 10^5
    • heights 中所有数互不相同。

解题思路

解法一:从右往左维护严格递减单调栈,O(n),主提交版本

从右向左遍历,栈内保存右侧

尚未被更高的人挡住的人(按高度严格递减,栈底最高)。对当前人 heights[i],他能看到

的右侧人数 = 栈中所有高度严格小于 heights[i] 的人数(这些人都比当前人矮、且中间

没人挡住,各计 1)+ 若栈非空则还能再看到栈顶那第一个比它高的人(计 1,因为更高的

这个人挡住再后面的人)。弹出所有比 heights[i] 矮的栈元素逐个计数,然后若栈非空

cnt++(看到第一个比自己高的),最后把 heights[i] 入栈,它成为新的更矮栈顶。

由于高度互不相同,栈内严格递减即可。

解法二:双向扫描暴力计数,O(n^2),对照

对每个 i,向右逐个扫描 j,记录当前

最大值 mx(初始 heights[i]),每遇到 heights[j] > mx(即能看到且打破记录)

计数加一;当 heights[j] > heights[i] 时停止(被比自己高的挡住)。仅用于小规模交叉验证。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.Deque;

public class Solution {

    public int[] canSeePersonsCount(int[] heights) {
        int n = heights.length;
        int[] ans = new int[n];
        Deque<Integer> stack = new ArrayDeque<>(); // 存高度,栈底到栈顶严格递减
        for (int i = n - 1; i >= 0; i--) {
            int cnt = 0;
            // 所有比当前矮的栈内元素(都能被当前人看到,各计 1)
            while (!stack.isEmpty() && stack.peek() < heights[i]) {
                stack.pop();
                cnt++;
            }
            // 若栈非空,栈顶是右边第一个比当前人高的人,也能看到(计 1,且从此被挡住)
            if (!stack.isEmpty()) {
                cnt++;
            }
            ans[i] = cnt;
            stack.push(heights[i]);
        }
        return ans;
    }

    public int[] canSeePersonsCountBrute(int[] heights) {
        int n = heights.length;
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) {
            int cnt = 0;
            int midMax = 0;   // 已扫过的中间位置的最高高度(i+1..j-1 的最大值)
            for (int j = i + 1; j < n; j++) {
                // 看到 j 的条件:min(heights[i], heights[j]) > max(heights[i+1..j-1])
                if (midMax < Math.min(heights[i], heights[j])) {
                    cnt++;
                }
                midMax = Math.max(midMax, heights[j]);
                // 遇到第一个比自己高的人后,后面所有人都被它挡住,可提前终止
                if (heights[j] > heights[i]) {
                    break;
                }
            }
            ans[i] = cnt;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][] hs = {
            {10, 6, 8, 5, 11, 9},
            {5, 1, 2, 3, 10},
            {1},
            {6, 5, 4, 3, 2, 1},
            {1, 2, 3, 4, 5, 6}
        };
        int[][] exp = {
            {3, 1, 2, 1, 1, 0},
            {4, 1, 1, 1, 0},
            {0},
            {1, 1, 1, 1, 1, 0},
            {1, 1, 1, 1, 1, 0}
        };
        boolean all = true;
        for (int i = 0; i < hs.length; i++) {
            int[] a = sol.canSeePersonsCount(hs[i]);
            int[] b = sol.canSeePersonsCountBrute(hs[i]);
            boolean ok = java.util.Arrays.equals(a, exp[i]) && java.util.Arrays.equals(a, b);
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + java.util.Arrays.toString(a));
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1945. Sum of Digits of String After Convert(字符串转化后的各位数字之和)

题目描述

给你一个由小写字母组成的字符串 s,以及一个整数 k。你的任务是通过一种特殊处理将

字符串转为整数,然后通过重复对它的数位求和 k 次来进行转换。具体地说执行以下步骤:

  1. 用字母在字母表中的位置替换该字母,将 s 转化成一个整数('a' 用 1 替换,
    'b' 用 2 替换,...... 'z' 用 26 替换)。
  2. 接着,将整数转换为其各位数字之和。
  3. 共重复转换操作(第 2 步)k 次。
    例如,如果 s = "zbax" 且 k = 2:转化:"zbax" -> "(26)(2)(1)(24)" -> "262124";
    转换 #1:262124 -> 2+6+2+1+2+4 = 17;转换 #2:17 -> 1+7 = 8。
    返回执行上述操作后得到的结果整数。

示例 1

  • 输入 :s = "iiii", k = 1
  • 输出:36
  • 解释 :转化:"iiii" -> "(9)(9)(9)(9)" -> "9999";转换 #1:9999 -> 36。

示例 2

  • 输入 :s = "leetcode", k = 2
  • 输出:6
  • 解释:转化后为 "12552031545";转换 #1 得 33;转换 #2 得 6。

示例 3

  • 输入 :s = "zbax", k = 2
  • 输出 :8
    提示:
  • - 1 <= s.length <= 100
  • - 1 <= k <= 10
    • s 由小写英文字母组成。

解题思路

解法一:直接模拟,O(k * 位数),主提交版本

先用 StringBuilder 把所有字母的位置数字

(s.charAt(i) - 'a' + 1) 拼接成一个大数字符串,然后循环 k 次:把当前字符串每个字符

对应的数字累加得到 sum,再转成新的字符串。由于 s.length <= 100,第一次拼接后最多

200 个字符,任何一轮的数位和都不超过 9 * 200 = 1800,因此完全在 int 范围内。

解法二:首次求和优化,O(s.length + k)

不需要真的拼接大数字符串------第一次的数位和

可以直接对每个字母的位置数字(1~26)逐位累加(如 26 -> 2+6),得到第一次转换的结果

sum;随后剩余 k-1 轮对 sum 反复做数位求和。两种解法应给出相同答案,用于交叉验证。

代码实现

java 复制代码
public class Solution {

    public int getLucky(String s, int k) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            sb.append(s.charAt(i) - 'a' + 1);
        }
        String t = sb.toString();
        int res = 0;
        for (int r = 0; r < k; r++) {
            int sum = 0;
            for (int i = 0; i < t.length(); i++) {
                sum += t.charAt(i) - '0';
            }
            res = sum;
            t = Integer.toString(sum);
        }
        return res;
    }

    public int getLuckyFast(String s, int k) {
        int sum = 0;
        for (int i = 0; i < s.length(); i++) {
            int v = s.charAt(i) - 'a' + 1;
            sum += v / 10 + v % 10;   // 1~26 的数位和
        }
        for (int r = 1; r < k; r++) {
            int nxt = 0;
            while (sum > 0) {
                nxt += sum % 10;
                sum /= 10;
            }
            sum = nxt;
        }
        return sum;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        String[] ss = {"iiii", "leetcode", "zbax", "a", "zzzz", "abcdefghijklmnopqrstuvwxyz", "hvmhoasabaymnmsd"};
        int[] ks = {1, 2, 2, 1, 3, 1, 1};
        int[] exp = {36, 6, 8, 1, 5, 135, 79};
        boolean all = true;
        for (int i = 0; i < ss.length; i++) {
            int a = sol.getLucky(ss[i], ks[i]);
            int b = sol.getLuckyFast(ss[i], ks[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " \"" + ss[i] + "\" k=" + ks[i] + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1946. Largest Number After Mutating Substring(子字符串突变后可能得到的最大整数)

题目描述

给你一个字符串 num,该字符串表示一个大整数。另给你一个长度为 10 且下标从 0 开始的

整数数组 change,该数组将 0-9 中的每个数字映射到另一个数字。更规范的说法是,数字

d 映射为数字 change[d]。

你可以选择突变 num 的任一子字符串。突变子字符串意味着将每位数字 num[i] 替换为该数字

在 change 中的映射(也就是说,将 num[i] 替换为 change[num[i]])。

请你找出在对 num 的任一子字符串执行突变操作(也可以不执行)后,可能得到的最大整数,

并用字符串表示返回。子字符串是字符串中的一个连续序列。

示例 1

  • 输入 :num = "132", change = 9,8,5,0,3,6,4,2,6,8
  • 输出:"832"
  • 解释 :替换子字符串 "1",1 映射为 change[1] = 8,因此 "132" 变为 "832"。

示例 2

  • 输入 :num = "021", change = 9,4,3,5,7,2,1,9,0,6
  • 输出:"934"
  • 解释:替换子字符串 "021":0->9,2->3,1->4,得到 "934"。

示例 3

  • 输入 :num = "5", change = 1,4,7,5,3,2,5,6,9,4
  • 输出:"5"
  • 解释 :"5" 已经是可以构造的最大整数,所以返回它的字符串表示。
    提示:
  • - 1 <= num.length <= 10^5
    • num 仅由数字 0-9 组成
  • - change.length == 10
  • - 0 <= change[d] <= 9

解题思路

解法一:贪心:找第一个「可增」位置后连续突变,O(n),主提交版本

从左向右扫描,数字

位数越靠前权重越大,因此要让最高位尽量变大。对每位数字 d:若 change[d] > d(突变更

大)则从该位置开始突变;突变要连续执行,一旦开始突变,只要当前位 change[d] >= d(不小

于原值,等于也能保持当前最大前缀)就继续突变,遇到 change[d] < d(会变小)立即停止。

关键点:在已经变大的前缀之后,遇到 change[d] > d 不能跳过继续找后面(那样跳过的位保持

不变,会损失一个高位变大的机会,不是最优),这正是「第一个可增位置开始」+「不小于即续」。

解法二:两遍扫描标记法,O(n),对照

先找出全部「可增」(change[d] > d)的位置,再

找第一个可增位置 L,然后向右扩展直到第一个「会变小」(change[d] < d)的位置之前得到

区间 [L, R),区间内全部突变。实现角度不同,结果应与解法一一致。

代码实现

java 复制代码
public class Solution {

    public String maximumNumber(String num, int[] change) {
        char[] arr = num.toCharArray();
        int n = arr.length;
        boolean started = false;
        for (int i = 0; i < n; i++) {
            int d = arr[i] - '0';
            if (!started) {
                if (change[d] > d) {          // 找到第一个可增位置,开始突变
                    arr[i] = (char) ('0' + change[d]);
                    started = true;
                }
                // 否则保持原样,继续找第一个可增位置
            } else {
                if (change[d] >= d) {         // 已开始突变:不低于原值就继续(保持最大)
                    arr[i] = (char) ('0' + change[d]);
                } else {
                    break;                    // 会变小,立即停止
                }
            }
        }
        return new String(arr);
    }

    public String maximumNumberRange(String num, int[] change) {
        int n = num.length();
        // 找第一个可增位置
        int start = -1;
        for (int i = 0; i < n; i++) {
            if (change[num.charAt(i) - '0'] > num.charAt(i) - '0') {
                start = i;
                break;
            }
        }
        if (start == -1) {
            return num;
        }
        // 从 start 向右扩展,直到第一个会变小的位置为止
        int end = start;
        while (end < n && change[num.charAt(end) - '0'] >= num.charAt(end) - '0') {
            end++;
        }
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            if (i >= start && i < end) {
                sb.append((char) ('0' + change[num.charAt(i) - '0']));
            } else {
                sb.append(num.charAt(i));
            }
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        String[] nums = {"132", "021", "5", "214010", "999", "1234567890", "0123456789", "334111"};
        int[][] changes = {
            {9, 8, 5, 0, 3, 6, 4, 2, 6, 8},
            {9, 4, 3, 5, 7, 2, 1, 9, 0, 6},
            {1, 4, 7, 5, 3, 2, 5, 6, 9, 4},
            {6, 7, 2, 0, 1, 3, 4, 5, 8, 9},
            {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
            {1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
            {9, 8, 7, 6, 5, 4, 3, 2, 1, 0},
            {2, 9, 9, 9, 9, 9, 9, 9, 9, 9}
        };
        String[] exp = {"832", "934", "5", "274010", "999", "1234567891", "9876556789", "999999"};
        boolean all = true;
        for (int i = 0; i < nums.length; i++) {
            String a = sol.maximumNumber(nums[i], changes[i]);
            String b = sol.maximumNumberRange(nums[i], changes[i]);
            boolean ok = a.equals(exp[i]) && a.equals(b);
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " \"" + nums[i] + "\" => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1947. Maximum Compatibility Score Sum(最大兼容性评分和)

题目描述

有一份由 n 个问题组成的调查问卷,每个问题的答案要么是 0(no,否),要么是 1(yes,是)。

这份调查问卷被分发给 m 名学生和 m 名导师,学生和导师的编号都是从 0 到 m - 1。

学生的答案用二维整数数组 students 表示,其中 students[i] 是一个整数数组,包含第 i 名

学生对调查问卷给出的答案。导师的答案用二维整数数组 mentors 表示,其中 mentors[j] 是

一个整数数组,包含第 j 名导师对调查问卷给出的答案。

每个学生都会被分配给一名导师,而每位导师也会分配到一名学生。配对的学生与导师之间的兼容性

评分等于学生和导师答案相同的次数。

  • 例如,学生答案为 1,0,1 而导师答案为 0,0,1,那么他们的兼容性评分为 2。
    请你找出最优的学生与导师的配对方案,以最大程度上提高兼容性评分和。返回可以得到的最大
    兼容性评分和。

示例 1

  • 输入 :students = \[1,1,0,1,0,1,0,0,1], mentors = \[1,0,0,0,0,1,1,1,0]
  • 输出:8
  • 解释 :学生 0 分配给导师 2(评分 3),学生 1 分配给导师 0(评分 2),学生 2 分配给导师 1
    (评分 3),最大评分和为 3+2+3 = 8。

示例 2

  • 输入 :students = \[0,0,0,0,0,0], mentors = \[1,1,1,1,1,1]
  • 输出:0
  • 解释 :任意学生与导师配对的兼容性评分都是 0。
    提示:
  • - m == students.length == mentors.length
  • - n == students[i].length == mentors[j].length
  • - 1 <= m, n <= 8
    • students[i][k] 为 0 或 1
    • mentors[j][k] 为 0 或 1

解题思路

解法一:状态压缩 DP,O(2^m * m),主提交版本

预计算 comp[i][j] = 学生 i 与导师 j

的兼容性评分。令 dp[mask] 表示前 pc(mask) 个学生(下标 0...pc-1)恰好分配了导师集合

mask 时的最大评分和(mask 的二进制位表示哪些导师已被使用)。转移时把第 pc-1 个学生

分配给 mask 中的任意一位导师 j:dp[mask] = max(dp[mask ^ (1<<j)] + comp[pc-1][j])。

最终答案 dp[(1<<m) - 1]。m <= 8 时状态数最多 256,非常快。

解法二:回溯全排列,O(m! * n),对照

递归枚举当前学生分配给哪个尚未使用的导师,

维护当前评分和与全局最大值(可加简单剪枝)。m <= 8 时 40320 个排列,足够小规模交叉验证。

代码实现

java 复制代码
public class Solution {

    public int maxCompatibilitySum(int[][] students, int[][] mentors) {
        int m = students.length, n = students[0].length;
        int[][] comp = new int[m][m];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < m; j++) {
                int c = 0;
                for (int k = 0; k < n; k++) {
                    if (students[i][k] == mentors[j][k]) c++;
                }
                comp[i][j] = c;
            }
        }
        int[] dp = new int[1 << m];
        for (int mask = 1; mask < (1 << m); mask++) {
            int stu = Integer.bitCount(mask) - 1;   // 当前分配到的学生下标
            int best = 0;
            for (int j = 0; j < m; j++) {
                if ((mask & (1 << j)) != 0) {
                    best = Math.max(best, dp[mask ^ (1 << j)] + comp[stu][j]);
                }
            }
            dp[mask] = best;
        }
        return dp[(1 << m) - 1];
    }

    public int maxCompatibilitySumBacktrack(int[][] students, int[][] mentors) {
        int m = students.length, n = students[0].length;
        int[][] comp = new int[m][m];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < m; j++) {
                int c = 0;
                for (int k = 0; k < n; k++) {
                    if (students[i][k] == mentors[j][k]) c++;
                }
                comp[i][j] = c;
            }
        }
        this.best = 0;
        this.used = new boolean[m];
        dfs(comp, 0, 0, m);
        return this.best;
    }

    private int best;
    private boolean[] used;

    private void dfs(int[][] comp, int stu, int sum, int m) {
        if (stu == m) {
            best = Math.max(best, sum);
            return;
        }
        for (int j = 0; j < m; j++) {
            if (!used[j]) {
                used[j] = true;
                dfs(comp, stu + 1, sum + comp[stu][j], m);
                used[j] = false;
            }
        }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][][] st = {
            {{1, 1, 0}, {1, 0, 1}, {0, 0, 1}},
            {{0, 0}, {0, 0}, {0, 0}},
            {{0, 1, 0, 1}, {1, 0, 1, 0}, {1, 1, 0, 0}, {0, 0, 1, 1}},
            {{1}}
        };
        int[][][] mt = {
            {{1, 0, 0}, {0, 0, 1}, {1, 1, 0}},
            {{1, 1}, {1, 1}, {1, 1}},
            {{1, 0, 1, 0}, {0, 1, 0, 1}, {0, 0, 1, 1}, {1, 1, 0, 0}},
            {{0}}
        };
        int[] exp = {8, 0, 16, 0};
        boolean all = true;
        for (int i = 0; i < st.length; i++) {
            int a = sol.maxCompatibilitySum(st[i], mt[i]);
            int b = sol.maxCompatibilitySumBacktrack(st[i], mt[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1948. Delete Duplicate Folders in System(删除系统中的重复文件夹)

题目描述

由于一个漏洞,文件系统中存在许多重复文件夹。给你一个二维数组 paths,其中 paths[i]

是一个表示文件系统中第 i 个文件夹的绝对路径的数组。

  • 例如,"`one`", "`two`", "`three`" 表示路径 "/one/two/three"。
    如果两个文件夹(不需要在同一层级)包含非空且相同的子文件夹集合并具有相同的子文件夹结构,
    则认为这两个文件夹是相同文件夹。相同文件夹的根层级不需要相同。如果存在两个(或两个以上)
    相同文件夹,则需要将这些文件夹和所有它们的子文件夹标记为待删除。
    一旦所有的相同文件夹和它们的子文件夹都被标记为待删除,文件系统将会删除所有上述文件夹。
    文件系统只会执行一次删除操作。执行完这一次删除操作后,不会删除新出现的相同文件夹。
    返回二维数组 ans,该数组包含删除所有标记文件夹之后剩余文件夹的路径。

示例 1

  • 输入 :paths = \["`a`","`c`","`d`","`a`","`b`","`c`","`b`","`d`","`a`"]
  • 输出 :\["`d`","`d`","`a`"]
  • 解释 :文件夹 "/a" 和 "/c"(以及它们的子文件夹)都会被标记为待删除,因为它们都包含名为
    "b" 的空文件夹。

示例 2

  • 输入 :paths = \["`a`","`c`","`a`","`b`","`c`","`b`","`a`","`b`","`x`","`a`","`b`","`x`","`y`","`w`","`w`","`y`"]
  • 输出 :\["`c`","`c`","`b`","`a`","`a`","`b`"]
  • 解释 :文件夹 "/a/b/x" 和 "/w"(以及它们的子文件夹)都会被标记为待删除,因为它们都包含
    名为 "y" 的空文件夹。注意文件夹 "/a" 和 "/c" 在删除后变为相同文件夹,但这两个文件夹不会
    被删除,因为删除只会进行一次。

示例 3

  • 输入 :paths = \["`a`","`b`","`c`","`d`","`c`","`a`"]
  • 输出 :\["`c`","`c`","`d`","`a`","`a`","`b`"]

示例 4

  • 输入 :paths = \["`a`","`a`","`x`","`a`","`x`","`y`","`a`","`z`","`b`","`b`","`x`","`b`","`x`","`y`","`b`","`z`"]
  • 输出:\[\]

示例 5

  • 输入 :paths = \["`a`","`a`","`x`","`a`","`x`","`y`","`a`","`z`","`b`","`b`","`x`","`b`","`x`","`y`","`b`","`z`","`b`","`w`"]
  • 输出 :\["`b`","`b`","`w`","`b`","`z`","`a`","`a`","`z`"]
    提示:
  • - 1 <= paths.length <= 2 * 10^4
  • - 1 <= paths[i].length <= 500
    • path[i][j] 由小写英文字母组成
    • 不会存在两个路径都指向同一个文件夹的情况
    • 对于不在根层级的任意文件夹,其父文件夹也会包含在输入中。

解题思路

解法一:字典树 + 自底向上序列化判重,O(总节点数 * log 总节点数),主提交版本

  1. 建树:虚拟根 root(名字任意占位),对每条路径逐级插入字典树节点(children 按名字
    排序,用 TreeMap 保证序列化稳定)。
  2. 后序遍历序列化:serialize(node) 返回该节点子树的规范化字符串表示,形如
    name(子序列串1)(子序列串2)...,子项按名字有序排好。因为同一父目录下的子目录名唯一,
    所以子树结构 + 目录名共同决定其整体形态。
  3. 判重标记:把每个节点的序列化结果存进 Map<String, TreeNode> seen,若某序列化串已存在
    且当前节点不是虚拟根,则把「第一次出现的那棵子树」和当前这个节点都标记 del = true
    (向 seen.get(key) 和当前节点各打一次标记;实际实现用 dup 集合记录序列化串,后
    序遍历后再看一次:若节点自身的序列化串在 dup 中且节点非根则删)。
    更简洁的做法:第一遍后序遍历得到每个节点的 key 存入 map;第二次遍历,若 key 在 map
    中对应的节点不是自己且该 key 出现过 >= 2 次,则把 map 中已记节点与当前节点都标记。
    为简单实现,这里用:dup 集合装「出现次数 >= 2」的序列化串;随后对每个节点判断
    dup.contains(node.key) 即标为待删(虚拟根不参与)。
  4. 收集答案:按原始路径列出,若任一前缀节点被标记则整个路径跳过;未跳过的存为 List。
    解法二:暴力比较子树,O(n^2 * 子树大小),对照
    先建好同样的树,然后对每对节点用
    「同样形式的序列化」比较是否相同(两两比较序列化串),非根且相同者互相标记删除。
    仅用于小规模交叉验证(n 较小时正确性一致)。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public class Solution {

    static class Node {
        TreeMap<String, Node> children = new TreeMap<>();
        boolean del;         // 是否被标记删除
        String key = "";     // 序列化结果(自身名字 + 子树结构)
    }

    private Node root = new Node();

    public List<List<String>> deleteDuplicateFolder(List<List<String>> paths) {
        root = new Node();   // ⚠️ 类字段 root 必须每次重建,避免多次调用累积旧树
        // 1. 建树
        for (List<String> p : paths) {
            Node cur = root;
            for (String name : p) {
                cur = cur.children.computeIfAbsent(name, k -> new Node());
            }
        }
        // 2. 后序遍历序列化 + 统计每个序列化串的出现次数
        Map<String, Integer> freq = new HashMap<>();
        serialize(root, freq);
        // 3. 收集所有出现 >= 2 次的串(重复子树)
        Set<String> dup = new HashSet<>();
        for (Map.Entry<String, Integer> e : freq.entrySet()) {
            if (e.getValue() >= 2) {
                dup.add(e.getKey());
            }
        }
        // 标记待删除节点(虚拟根不参与)
        mark(root, dup);
        // 4. 收集剩余路径
        List<List<String>> res = new ArrayList<>();
        List<String> cur = new ArrayList<>();
        collect(root, cur, res);
        return res;
    }

    private String serialize(Node node, Map<String, Integer> freq) {
        StringBuilder sb = new StringBuilder();
        // 以子节点名字相对顺序构造;当前节点名字由父节点传入------这里用固定占位,组合时含名字
        for (Map.Entry<String, Node> e : node.children.entrySet()) {
            Node ch = e.getValue();
            sb.append('(').append(e.getKey());
            ch.key = serialize(ch, freq);   // 子节点 key 缓存
            sb.append(ch.key).append(')');
        }
        String key = sb.toString();
        // ⚠️ 判定「文件夹相同」需满足「非空且相同的子文件夹集合」------空文件夹(叶子)的 key
        // 为 "",不参与判重(多个空文件夹不会被看成相同文件夹)。
        if (!key.isEmpty()) {
            freq.put(key, freq.getOrDefault(key, 0) + 1);
        }
        return key;
    }

    private void mark(Node node, Set<String> dup) {
        if (dup.contains(node.key)) {
            node.del = true;
        }
        for (Node ch : node.children.values()) {
            mark(ch, dup);
        }
    }

    private void collect(Node node, List<String> cur, List<List<String>> res) {
        // 若本节点被删,其后代一律不输出
        if (node.del) {
            return;
        }
        for (Map.Entry<String, Node> e : node.children.entrySet()) {
            cur.add(e.getKey());
            if (!e.getValue().del) {
                res.add(new ArrayList<>(cur));
            }
            collect(e.getValue(), cur, res);
            cur.remove(cur.size() - 1);
        }
    }

    public List<List<String>> deleteDuplicateFolderBrute(List<List<String>> paths) {
        // 复用同一棵字典树结构:为公平,重建一棵干净树
        Node r2 = new Node();
        for (List<String> p : paths) {
            Node cur = r2;
            for (String name : p) {
                cur = cur.children.computeIfAbsent(name, k -> new Node());
            }
        }
        // 解法二(暴力比较子树,O(n^2 * 子树大小),对照):两两比较序列化串判定重复,
        // 标记 toDel 后从虚拟根 DFS 收集(父节点被删则整棵子树都不输出)。仅小规模交叉验证。
        Map<Node, List<String>> nodePaths = new HashMap<>();
        collectPaths(r2, new ArrayList<>(), nodePaths);
        List<Node> nodes = new ArrayList<>(nodePaths.keySet());
        // 对每对节点比较序列化串(过滤空 key,且不用自身 key)
        Set<Node> toDel = new HashSet<>();
        Map<Node, String> keyOf = new HashMap<>();
        for (Node nd : nodes) keyOf.put(nd, nodesKey(nd));
        for (int i = 0; i < nodes.size(); i++) {
            for (int j = i + 1; j < nodes.size(); j++) {
                String ki = keyOf.get(nodes.get(i)), kj = keyOf.get(nodes.get(j));
                // 空文件夹(key 为空)不参与判重
                if (!ki.isEmpty() && ki.equals(kj)) {
                    toDel.add(nodes.get(i));
                    toDel.add(nodes.get(j));
                }
            }
        }
        for (Node nd : toDel) nd.del = true;
        // ⚠️ 从虚拟根收集:父节点 del 时整棵子树剪枝(与被删文件夹及其所有子文件夹一并删除)
        List<List<String>> res = new ArrayList<>();
        List<String> cur = new ArrayList<>();
        collectBrute(r2, cur, res);
        return res;
    }

    private void collectBrute(Node node, List<String> cur, List<List<String>> res) {
        for (Map.Entry<String, Node> e : node.children.entrySet()) {
            Node ch = e.getValue();
            if (ch.del) continue;          // 整个子树跳过
            cur.add(e.getKey());
            res.add(new ArrayList<>(cur));
            collectBrute(ch, cur, res);
            cur.remove(cur.size() - 1);
        }
    }

    private String nodesKey(Node node) {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, Node> e : node.children.entrySet()) {
            sb.append('(').append(e.getKey()).append(nodesKey(e.getValue())).append(')');
        }
        return sb.toString();
    }

    private void collectPaths(Node node, List<String> cur, Map<Node, List<String>> out) {
        if (node != root && node != null && cur.size() > 0) {
            // 虚拟根不参与;cur 在递归进入下一层前已加入子目录名
        }
        for (Map.Entry<String, Node> e : node.children.entrySet()) {
            cur.add(e.getKey());
            out.put(e.getValue(), new ArrayList<>(cur));
            collectPaths(e.getValue(), cur, out);
            cur.remove(cur.size() - 1);
        }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        String[][][] raw = {
            {{"a"}, {"c"}, {"d"}, {"a", "b"}, {"c", "b"}, {"d", "a"}},
            {{"a"}, {"c"}, {"a", "b"}, {"c", "b"}, {"a", "b", "x"}, {"a", "b", "x", "y"}, {"w"}, {"w", "y"}},
            {{"a", "b"}, {"c", "d"}, {"c"}, {"a"}},
            {{"a"}, {"a", "x"}, {"a", "x", "y"}, {"a", "z"}, {"b"}, {"b", "x"}, {"b", "x", "y"}, {"b", "z"}},
            {{"a"}, {"a", "x"}, {"a", "x", "y"}, {"a", "z"}, {"b"}, {"b", "x"}, {"b", "x", "y"}, {"b", "z"}, {"b", "w"}}
        };
        boolean all = true;
        for (int i = 0; i < raw.length; i++) {
            List<List<String>> paths = new ArrayList<>();
            for (String[] r : raw[i]) {
                paths.add(java.util.Arrays.asList(r));
            }
            List<List<String>> a = sol.deleteDuplicateFolder(paths);
            List<List<String>> b = sol.deleteDuplicateFolderBrute(paths);
            // 排序后比较(题目允许任意顺序)
            java.util.Collections.sort(a, (x, y) -> x.toString().compareTo(y.toString()));
            java.util.Collections.sort(b, (x, y) -> x.toString().compareTo(y.toString()));
            boolean ok = a.equals(b);
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1949. Strong Friendship(坚定的友谊)

题目类型:数据库(SQL)

题目描述

表:Friendship

复制代码
+-------------+------+
| 列名         | 类型 |
+-------------+------+
| user1_id    | int  |
| user2_id    | int  |
+-------------+------+

(user1_id, user2_id) 是这个表的主键。每一行表示用户 user1_id 和 user2_id 是朋友,且 user1_id < user2_id。

如果 x 和 y 为朋友 且他们至少 有三个共同的朋友,那么 x 和 y 之间的友谊就是坚定的 。写一个解决方案来找到所有的坚定友谊。结果表不应包含重复行,且 user1_id < user2_id,输出还应带共同朋友数 common_friend。

示例 1:

复制代码
输入:
Friendship table:
+----------+----------+
| user1_id | user2_id |
+----------+----------+
| 1        | 2        |
| 1        | 3        |
| 2        | 3        |
| 1        | 4        |
| 2        | 4        |
| 1        | 5        |
| 2        | 5        |
| 1        | 7        |
| 3        | 7        |
| 1        | 6        |
| 3        | 6        |
| 2        | 6        |
+----------+----------+
输出:
+----------+----------+---------------+
| user1_id | user2_id | common_friend |
+----------+----------+---------------+
| 1        | 2        | 4             |
| 1        | 3        | 3             |
+----------+----------+---------------+

解释:用户 1 和 2 有 4 个共同朋友(3、4、5、6);用户 1 和 3 有 3 个共同朋友(2、6、7);用户 2 和 3 只有 2 个共同朋友,不足 3 个。

解题思路

① 将友谊表双向展开成 (user_id, friend_id)(UNION ALL 保留两侧);

② 自连接条件 f1.friend_id = f2.friend_id(同一用户同时是两者的朋友)且 f1.user_id < f2.user_id;

③ 限定 (f1.user_id, f2.user_id) 本身是朋友(用 IN 子查询校验原友谊表),

按朋友对分组 HAVING COUNT(*) >= 3。

代码实现

sql 复制代码
WITH friends AS (
    SELECT user1_id AS user_id, user2_id AS friend_id FROM Friendship
    UNION ALL
    SELECT user2_id, user1_id FROM Friendship
)
SELECT f1.user_id AS user1_id,
       f2.user_id AS user2_id,
       COUNT(*) AS common_friend
FROM friends f1
JOIN friends f2
  ON f1.friend_id = f2.friend_id
 AND f1.user_id < f2.user_id
WHERE (f1.user_id, f2.user_id) IN (SELECT user1_id, user2_id FROM Friendship)
GROUP BY f1.user_id, f2.user_id
HAVING COUNT(*) >= 3;

复杂度分析

本题为数据库查询题,不涉及传统算法的时间/空间复杂度;考察重点为 JOIN / 窗口函数 / 自连接 / 聚合过滤等 SQL 语法与查询逻辑的正确性,执行效率由数据库优化器决定。


1950. Maximum of Minimum Values in All Subarrays(所有子数组最小值中的最大值)

题目描述

给你一个长度为 n 的整数数组 nums,你需要处理 n 个查询。

对于第 i(0 <= i < n)个查询:

  • 你需要先找出 nums 的所有长度为 i + 1 的子数组中的最小值。
  • 在这些最小值中找出最大值作为答案。
    返回一个下标从 0 开始的长度为 n 的整数数组 ans,ans[i] 代表第 i 个查询的答案。

示例 1

  • 输入 :nums = 0,1,2,4
  • 输出:4,2,1,0
  • 解释:
  • 长度为 1 的子数组 4 的最小值最大为 4;
  • 长度为 2 的子数组 2,4 的最小值最大为 2;
  • 长度为 3 的子数组 1,2,4 的最小值最大为 1;
  • 长度为 4 的子数组 0,1,2,4 的最小值最大为 0。

示例 2

  • 输入 :nums = 10,20,50,10
  • 输出 :50,20,10,10
    提示:
  • - n == nums.length
  • - 1 <= n <= 10^5
  • - 0 <= nums[i] <= 10^9

解题思路

解法一:单调栈求每个元素作为最小值的最大覆盖区间,O(n),主提交版本

对每个位置 i,用单调栈求出左边第一个严格小于 nums[i] 的位置 L 和右边第一个小于等于

nums[i] 的位置 R(用「左严格小于 + 右小于等于」保证每个子数组的最小值位置唯一),则

以 nums[i] 为最小值的子数组最大长度为 len = R - L - 1,因此 cand[len-1] = max(cand[len-1], nums[i])。

最后从长到短做后缀最大值传播:因为长度为 len 的子数组能取到的最小值最大值 v,其任意前缀

长度的子数组(取该长区间的一段)最小值最大值不会小于 v,所以 cand[i] = max(cand[i], cand[i+1])。

解法二:暴力枚举,O(n^3),对照,仅小规模

对每个长度 l,枚举所有长度为 l 的子数组,

取各自最小值,再取最大值。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.Deque;

public class Solution {

    public int[] findMaximums(int[] nums) {
        int n = nums.length;
        int[] left = new int[n];   // 左边第一个严格小于 nums[i] 的下标,无则 -1
        Deque<Integer> stack = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            while (!stack.isEmpty() && nums[stack.peek()] >= nums[i]) {
                stack.pop();
            }
            left[i] = stack.isEmpty() ? -1 : stack.peek();
            stack.push(i);
        }
        int[] right = new int[n];  // 右边第一个小于等于 nums[i] 的下标,无则 n
        stack.clear();
        for (int i = n - 1; i >= 0; i--) {
            while (!stack.isEmpty() && nums[stack.peek()] > nums[i]) {
                stack.pop();
            }
            right[i] = stack.isEmpty() ? n : stack.peek();
            stack.push(i);
        }
        int[] cand = new int[n];
        for (int i = 0; i < n; i++) {
            int len = right[i] - left[i] - 1;   // 以 nums[i] 为最小值的最长子数组长度
            cand[len - 1] = Math.max(cand[len - 1], nums[i]);
        }
        // 长区间能取到的值,短区间也能取到(取长区间的一段),从长到短传播
        for (int i = n - 2; i >= 0; i--) {
            cand[i] = Math.max(cand[i], cand[i + 1]);
        }
        return cand;
    }

    public int[] findMaximumsBrute(int[] nums) {
        int n = nums.length;
        int[] ans = new int[n];
        for (int len = 1; len <= n; len++) {
            int best = Integer.MIN_VALUE;
            for (int i = 0; i + len <= n; i++) {
                int mn = Integer.MAX_VALUE;
                for (int j = i; j < i + len; j++) {
                    mn = Math.min(mn, nums[j]);
                }
                best = Math.max(best, mn);
            }
            ans[len - 1] = best;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][] tests = {
            {0, 1, 2, 4},
            {10, 20, 50, 10},
            {1},
            {5, 4, 3, 2, 1},
            {3, 1, 2, 4}
        };
        boolean all = true;
        for (int i = 0; i < tests.length; i++) {
            int[] a = sol.findMaximums(tests[i]);
            int[] b = sol.findMaximumsBrute(tests[i]);
            boolean ok = java.util.Arrays.equals(a, b);
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + java.util.Arrays.toString(a));
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1951. All the Pairs With the Maximum Number of Common Followers(查询具有最多共同关注者的所有两两结对组)

题目类型:数据库(SQL)

题目描述

表:Relations

复制代码
+-------------+------+
| Column Name | Type |
+-------------+------+
| user_id     | int  |
| follower_id | int  |
+-------------+------+

(user_id, follower_id) 是这个表的主键。每一行表示 user_id 的用户与其关注者 follower_id(关注者的 id 即本表的 user_id)。

写出一个查询语句,找到具有最多共同关注者的所有两两结对组:如果有两个用户的共同关注者是最大的,返回所有具有此最大值的两两结对组。结果每一行包含 user1_id 和 user2_id,其中 user1_id < user2_id。

示例:

复制代码
输入:
Relations 表:
+---------+-------------+
| user_id | follower_id |
+---------+-------------+
| 1       | 3           |
| 2       | 3           |
| 7       | 3           |
| 1       | 4           |
| 2       | 4           |
| 7       | 4           |
| 1       | 5           |
| 2       | 6           |
| 7       | 5           |
+---------+-------------+
输出:
+----------+----------+
| user1_id | user2_id |
+----------+----------+
| 1        | 7        |
+----------+----------+

解释:用户 1 和 7 有 3 个共同关注者(3、4、5),是最大值;1 和 2、2 和 7 都只有 2 个。

解题思路

自连接:a.follower_id = b.follower_id 表示存在共同关注者,且 a.user_id < b.user_id 避免重复对;

按用户对分组统计共同关注者数,再过滤出等于全局最大值的对(WHERE cnt = (SELECT MAX(cnt) ...))。

代码实现

sql 复制代码
WITH pairs AS (
    SELECT a.user_id AS u1, b.user_id AS u2, COUNT(*) AS cnt
    FROM Relations a
    JOIN Relations b
      ON a.follower_id = b.follower_id
     AND a.user_id < b.user_id
    GROUP BY a.user_id, b.user_id
)
SELECT u1 AS user1_id, u2 AS user2_id
FROM pairs
WHERE cnt = (SELECT MAX(cnt) FROM pairs);

复杂度分析

本题为数据库查询题,不涉及传统算法的时间/空间复杂度;考察重点为 JOIN / 窗口函数 / 自连接 / 聚合过滤等 SQL 语法与查询逻辑的正确性,执行效率由数据库优化器决定。


1952. Three Divisors(三除数)

题目描述

给你一个整数 n。如果 n 恰好有三个正除数,返回 true;否则,返回 false。

如果存在整数 k,满足 n = k * m,那么整数 m 就是 n 的一个除数。

示例 1

  • 输入 :n = 2
  • 输出 :false
  • 解释:2 只有两个除数:1 和 2。

示例 2

  • 输入 :n = 4
  • 输出 :true
  • 解释 :4 有三个除数:1、2 和 4。
    提示:
  • - 1 <= n <= 10^4

解题思路

解法一:枚举计数,O(sqrt(n)),主提交版本

一个数恰好有三个正除数,当且仅当它是某个

质数的平方。因为任何除数的个数(设为 d(n)):若 n = p1^a1 * p2^a2 * ...,则

d(n) = (a1+1)(a2+1)...。d(n) = 3 只能是单个质因子的平方((2+1) = 3)。

因此枚举 i 从 2 到 sqrt(n)(开根号),检查 i*i == n 且 i 是质数。

解法二:因数计数暴力,O(n),对照

遍历 1..n 计数能被 n 整除的数的个数,等于 3 即真。

代码实现

java 复制代码
public class Solution {

    public boolean isThree(int n) {
        int r = (int) Math.sqrt(n);
        if (r * r != n) {
            return false;         // 不是完全平方数,因数个数必为偶数或 1
        }
        return isPrime(r);        // n = r^2,恰好三个因数 <=> r 是质数
    }

    private boolean isPrime(int x) {
        if (x < 2) return false;
        for (int i = 2; i * i <= x; i++) {
            if (x % i == 0) return false;
        }
        return true;
    }

    public boolean isThreeBrute(int n) {
        int cnt = 0;
        for (int i = 1; i <= n; i++) {
            if (n % i == 0) cnt++;
        }
        return cnt == 3;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[] ns = {2, 4, 1, 3, 9, 25, 6, 49, 12, 9973};
        boolean[] exp = {false, true, false, false, true, true, false, true, false, false};
        boolean all = true;
        for (int i = 0; i < ns.length; i++) {
            boolean a = sol.isThree(ns[i]);
            boolean b = sol.isThreeBrute(ns[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " n=" + ns[i] + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1953. Maximum Number of Weeks for Which You Can Work(你可以工作的最大周数)

题目描述

给你 n 个项目,编号从 0 到 n - 1。同时给你一个整数数组 milestones,其中每个

milestones[i] 表示第 i 个项目中的阶段任务数量。

你可以按下面两个规则参与项目中的工作:

  • 每周,你将会完成某一个项目中的恰好一个阶段任务。你每周都必须工作。
  • 在连续的两周中,你不能参与并完成同一个项目中的两个阶段任务。
    一旦所有项目中的全部阶段任务都完成,或者执行仅剩的一个阶段任务将会导致你违反上面的规则,
    你将停止工作。注意,由于这些条件的限制,你可能无法完成所有阶段任务。
    返回在不违反上面规则的情况下你最多能工作多少周。

示例 1

  • 输入 :milestones = 1,2,3
  • 输出:6
  • 解释:一种可能的情形是:项目 0、2、1、2、1、2 各完成 1 个阶段任务,总周数 6。

示例 2

  • 输入 :milestones = 5,2,1
  • 输出:7
  • 解释 :最多 7 周;第 8 周再做项目 0 会违反规则,因此项目 0 会残留 1 个阶段任务。
    提示:
  • - n == milestones.length
  • - 1 <= n <= 10^5
  • - 1 <= milestones[i] <= 10^9

解题思路

解法一:贪心数学公式,O(n),主提交版本

设任务总数 total,最大值 mx。除最大值项目

外的其他任务 rest = total - mx 可以充当「隔板」把最大项目的任务分隔开,使得任意两个

相同项目任务不相邻。若 mx > rest + 1,则即使把最大项目的任务用尽所有隔板隔开,仍会有

mx - (rest + 1) 个剩余无法安放(每两个同项目任务之间必须至少隔一个其他项目任务),因此

能工作的周数为 2 * rest + 1;否则(mx <= rest + 1),所有任务都能安排完,答案为

total。归纳:ans = min(total, 2 * rest + 1)。

解法二:二叉堆模拟,O(n log n),对照

用最大堆每次取出两个最大的项目各做一个任务(若

只剩一个项目则停下来),模拟周数。该模拟与公式等价,用于交叉验证(数值较大时用 long)。

代码实现

java 复制代码
import java.util.PriorityQueue;

public class Solution {

    public long numberOfWeeks(int[] milestones) {
        long total = 0;
        long mx = 0;
        for (int m : milestones) {
            total += m;
            mx = Math.max(mx, m);
        }
        long rest = total - mx;
        return Math.min(total, 2 * rest + 1);
    }

    public long numberOfWeeksSim(int[] milestones) {
        // 用最大堆存 (剩余任务数, 项目编号),按剩余数降序
        PriorityQueue<long[]> pq = new PriorityQueue<>((a, b) -> Long.compare(b[0], a[0]));
        for (int i = 0; i < milestones.length; i++) {
            pq.offer(new long[]{milestones[i], i});
        }
        long weeks = 0;
        int last = -1;
        while (true) {
            // 拿出剩余最多的
            long[] top = pq.poll();
            if (top == null) break;
            if (top[1] == last) {
                // 与上周相同:尝试用次大的项目
                long[] sec = pq.poll();
                if (sec == null) break;          // 没有其他项目可做,停止
                // 做 sec 项目的一个任务
                weeks++;
                if (sec[0] > 1) pq.offer(new long[]{sec[0] - 1, sec[1]});
                // top 原样放回(剩余没变)
                pq.offer(top);
                last = (int) sec[1];
            } else {
                // 做 top 项目的一个任务
                weeks++;
                if (top[0] > 1) pq.offer(new long[]{top[0] - 1, top[1]});
                last = (int) top[1];
            }
        }
        return weeks;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][] tests = {
            {1, 2, 3},
            {5, 2, 1},
            {1},
            {1, 1, 1, 1},
            {10, 1, 1}
        };
        long[] exp = {6, 7, 1, 4, 5};
        boolean all = true;
        for (int i = 0; i < tests.length; i++) {
            long a = sol.numberOfWeeks(tests[i]);
            long b = sol.numberOfWeeksSim(tests[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1954. Minimum Garden Perimeter to Collect Enough Apples(收集足够苹果的最小花园周长)

题目描述

给你一个用无限二维网格表示的花园,每一个整数坐标处都有一棵苹果树。整数坐标 (i, j) 处的

苹果树有 |i| + |j| 个苹果。

你将会买下正中心坐标是 (0, 0) 的一块正方形土地,且每条边都与两条坐标轴之一平行。

给你一个整数 neededApples,请你返回土地的最小周长,使得至少有 neededApples 个苹果在

土地里面或者边缘上。

|x| 的值定义为:如果 x >= 0,那么值为 x;如果 x < 0,那么值为 -x。

示例 1

  • 输入 :neededApples = 1
  • 输出:8
  • 解释 :边长 1 的正方形只有 (0,0) 一棵树(0 个苹果),不够;边长 2 的正方形覆盖
    -1,1 x -1,1 共 12 个苹果 >= 1,周长 8。

示例 2

  • 输入 :neededApples = 13
  • 输出:16
  • 解释 :d=1 时 S=12 < 13;d=2 时 S=60 >= 13,周长 4*4=16。

示例 3

  • 输入 :neededApples = 1000000000
  • 输出 :5040
    提示:
  • - 1 <= neededApples <= 10^15

解题思路

解法一:数学公式 + 逐层累加,O(cuberoot(neededApples)),主提交版本

设半边长 d

(正方形坐标范围 [-d, d] x [-d, d],边长 2d)。苹果总数:

S(d) = sum_{i=-d}^{d} sum_{j=-d}^{d} (|i| + |j|)

= sum_i |i| * (2d+1) + sum_j |j| * (2d+1) = 2 * (2d+1) * [d(d+1)],

其中 sum_{i=-d}^{d} |i| = 2(1+2+...+d) = d(d+1)。验证 d=1 -> 23 1*2 = 12 ✓。

循环递增 d 直到 S(d) >= neededApples,返回周长 8 * d。

解法二:逐层外壳增量累加,O(cuberoot(neededApples)),对照

第 d 层新增苹果数为

add(d) = S(d) - S(d-1) = 12 * d^2(验证 d=1 -> 12,d=2 -> 48),从 0 开始累加外壳增量

直到总和达到需要值,周长为 8 * d。两解法结果一致,互相验证;代码内另附坐标枚举

countApple(d) 验证公式正确性。

代码实现

java 复制代码
public class Solution {

    public long minimumPerimeter(long neededApples) {
        long d = 0;
        long sum = 0;
        while (sum < neededApples) {
            d++;
            sum = 2L * (2 * d + 1) * d * (d + 1);
        }
        return 8 * d;
    }

    public long minimumPerimeterLoop(long neededApples) {
        long sum = 0;
        long d = 0;
        while (sum < neededApples) {
            d++;
            sum += 12L * d * d;
        }
        return 8 * d;
    }

    private static long countApple(long d) {
        long cnt = 0;
        for (long i = -d; i <= d; i++) {
            for (long j = -d; j <= d; j++) {
                cnt += Math.abs(i) + Math.abs(j);
            }
        }
        return cnt;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        long[] needs = {1, 13, 1000000000, 1000000000000000L, 2, 285};
        long[] exp = {8, 16, 5040, 0, 8, 32};   // exp[i]==0 表示仅交叉验证两解一致
        boolean all = true;
        // 公式自检:S(d) = 2*(2d+1)*d*(d+1) = 12d(d+1) 与坐标枚举一致
        for (long d : new long[]{1, 2, 3, 4, 10}) {
            long s1 = 2L * (2 * d + 1) * d * (d + 1);
            long s2 = countApple(d);
            boolean ok = s1 == s2;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " formulaCheck d=" + d + " => " + s1 + " / enum=" + s2);
        }
        for (int i = 0; i < needs.length; i++) {
            long a = sol.minimumPerimeter(needs[i]);
            long b = sol.minimumPerimeterLoop(needs[i]);
            boolean ok = a == b;
            if (exp[i] > 0) {
                ok &= a == exp[i];
            }
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " needed=" + needs[i] + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1955. Count Number of Special Subsequences(统计特殊子序列的数目)

题目描述

特殊序列是由正整数个 0,紧接着正整数个 1,最后正整数个 2 组成的序列。

  • 比方说,0,1,2 和 0,0,1,1,1,2 是特殊序列。
  • 相反,2,1,0、1 和 0,1,2,0 就不是特殊序列。
    给你一个数组 nums(仅包含整数 0、1 和 2),请你返回不同特殊子序列的数目。由于答案可能
    很大,请你将它对 10^9 + 7 取余后返回。
    一个数组的子序列是从原数组中删除零个或者若干个元素后,剩下元素不改变顺序得到的序列。
    如果两个子序列的下标集合不同,那么这两个子序列是不同的。

示例 1

  • 输入 :nums = 0,1,2,2
  • 输出:3
  • 解释:特殊子序列为 0,1,2,2(用第 3 个 2)、0,1,2,2(用第 4 个 2)、0,1,2,2(含两个 2)。

示例 2

  • 输入 :nums = 2,2,0,0
  • 输出:0
  • 解释:数组 2,2,0,0 中没有特殊子序列。

示例 3

  • 输入 :nums = 0,1,2,0,1,2
  • 输出 :7
    提示:
  • - 1 <= nums.length <= 10^5
  • - 0 <= nums[i] <= 2

解题思路

解法一:三状态 DP,O(n),主提交版本

维护三个状态:

  • dp0:以「0」开头的特殊前缀(形式全为 0)的子序列数------注意必须是「正整数个 0」,
    所以只有遇到 0 时 dp0 = dp0 * 2 + 1(每个已知 0 子序列可选可不选当前 0,另加单元素 0)。
  • dp1:形式为「0...01...1」的前缀子序列数:遇到 1 时,可把当前 1 接到任何 dp0 子序列
    (每个 dp0 子序列都可选或不选当前 1 两次分支?)稳妥写法:dp1 = (dp1 + dp0) % MOD
    表示接到任意一个已有 0 前缀之后;若用 dp1 = dp1 * 2 + dp0 则同时包含「不长在当前 1 上」,
    两者差异在于是否允许多个 1 ------ 这里采用 dp1 = (dp1 + dp0) % MOD,表示以当前 1 结尾扩展,
    但这样会漏掉跳过中间 1 的选择。因此正确转移(每步累积一次)应为:
    dp1 = (dp1 * 2 + dp0) % MOD ------ 对每个已形成的 0...1 前缀可选/不选当前 1(×2),
    再把当前 1 接到任意 0 前缀之后(+dp0)。
  • dp2:完整 0...01...12...2 子序列数:dp2 = (dp2 * 2 + dp1) % MOD(同上)。
    最终答案 dp2。
    解法二:分阶段统计 + 组合计数,对照
    把数组按 0/1/2 分段,用「以 i 结尾的特殊子序列」
    逆推计数也可以,但为代码简洁,解法一与解法二采用同一 DP 结构的两种实现(滚动变量 vs
    数组记录),交叉验证一致性。

代码实现

java 复制代码
public class Solution {

    private static final int MOD = 1_000_000_007;

    public int countSpecialSubsequences(int[] nums) {
        long dp0 = 0, dp1 = 0, dp2 = 0;
        for (int x : nums) {
            if (x == 0) {
                dp0 = (dp0 * 2 + 1) % MOD;          // 已有 0 前缀×2(选/不选当前0),+[0]
            } else if (x == 1) {
                dp1 = (dp1 * 2 + dp0) % MOD;        // 已有 0..1 前缀×2,+把当前1接到任意0前缀
            } else { // x == 2
                dp2 = (dp2 * 2 + dp1) % MOD;        // 已有完整前缀×2,+把当前2接到任意0..1前缀
            }
        }
        return (int) dp2;
    }

    public int countSpecialSubsequencesArr(int[] nums) {
        long[] dp = new long[3];   // dp[0] 0 前缀;dp[1] 0..1;dp[2] 完整特殊子序列
        for (int x : nums) {
            if (x == 0) {
                dp[0] = (dp[0] * 2 + 1) % MOD;
            } else if (x == 1) {
                dp[1] = (dp[1] * 2 + dp[0]) % MOD;
            } else {
                dp[2] = (dp[2] * 2 + dp[1]) % MOD;
            }
        }
        return (int) dp[2];
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][] tests = {
            {0, 1, 2, 2},
            {2, 2, 0, 0},
            {0, 1, 2, 0, 1, 2},
            {0, 1, 2},
            {0, 0, 1, 1, 2, 2},
            {2, 0, 1}
        };
        int[] exp = {3, 0, 7, 1, 27, 0};
        boolean all = true;
        for (int i = 0; i < tests.length; i++) {
            int a = sol.countSpecialSubsequences(tests[i]);
            int b = sol.countSpecialSubsequencesArr(tests[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1956. Minimum Time For K Virus Variants to Spread(感染 K 种病毒所需的最短时间)

题目描述

在无限大的二维平面上有 n 种不同的病毒。给定二维数组 points,第 i 项

points[i] = [xi, yi] 说明第 0 天有一种病毒在点 (xi, yi)。注意初始状态下,可能有多种

病毒在同一点上。

每天,被感染的点会把它感染的病毒传播到上、下、左、右四个邻居点。

现给定一个整数 k,问最少需要多少天,方能找到一点感染至少 k 种病毒?

示例 1

  • 输入 :points = \[1,1,6,1], k = 2
  • 输出:3
  • 解释:在第 3 天,点 (3,1) 与 (4,1) 将感染所有 2 种病毒。

示例 2

  • 输入 :points = \[3,3,1,2,9,2], k = 2
  • 输出:2
  • 解释:在第 2 天,点 (1,2)、(2,2)、(3,2) 等将感染前两种病毒。

示例 3

  • 输入 :points = \[3,3,1,2,9,2], k = 3
  • 输出:4
  • 解释 :在第 4 天,点 (5,2) 会感染所有 3 种病毒。
    提示:
  • - n == points.length
  • - 2 <= n <= 50
  • - points[i].length == 2
  • - 1 <= xi, yi <= 10^9
  • - 2 <= k <= n

解题思路

解法一:坐标变换 + 二分答案 + 候选点枚举,O(n^3 log C),主提交版本

  • 第 d 天病毒 i 能到达的点 = 与 (xi, yi) 曼哈顿距离不超过 d 的整数点(一个菱形)。
  • 目标是求最小的 d,使得存在一个点被至少 k 个菱形覆盖。
  • 曼哈顿距离转切比雪夫:令 u = x + y,v = x - y,则曼哈顿距离
    |x1-x2| + |y1-y2| = max(|u1-u2|, |v1-v2|)(切比雪夫距离)。于是菱形变成边长为 2d、
    轴对齐的正方形 [u_i-d, u_i+d] x [v_i-d, v_i+d]。
  • 若存在一个点被至少 k 个正方形覆盖,则必然存在一个「边界交点」也被同样覆盖(内部区域的
    覆盖集合不变),因此只需检查候选点:所有关键 u(u_i ± d 及 u_i 自身)与关键 v
    (v_i ± d 及 v_i 自身)组合。u、v 奇偶性相同的组合对应整数坐标(且满足
    (u+v)/2、(u-v)/2 均为整数)。
  • 二分 d(上界为所有点对曼哈顿距离最大值 + 1,此时任何点都被至少 2 个菱形覆盖覆盖全部)。
    检查函数:对每个候选点统计曼哈顿距离 <= d 的病毒数,>= k 即可行。
    解法二:不二分的候选天数枚举,对照
    收集所有点对 (i,j) 的 ceil(曼哈顿距离/2) 作为
    候选天数(最优解对应的第 k 近距离必然落在某两个点路径的中点附近),升序检查,取第一个
    可行天数。与解法一独立,交叉验证。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class Solution {

    public int minDayskVariants(int[][] points, int k) {
        int n = points.length;
        long[] u = new long[n];
        long[] v = new long[n];
        long maxDist = 0;
        for (int i = 0; i < n; i++) {
            u[i] = (long) points[i][0] + points[i][1];
            v[i] = (long) points[i][0] - points[i][1];
        }
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                long d = Math.max(Math.abs(u[i] - u[j]), Math.abs(v[i] - v[j]));
                maxDist = Math.max(maxDist, d);
            }
        }
        long lo = 0, hi = maxDist + 1;
        while (lo < hi) {
            long mid = (lo + hi) / 2;
            if (check(points, u, v, k, mid)) {
                hi = mid;
            } else {
                lo = mid + 1;
            }
        }
        return (int) lo;
    }

    private boolean check(int[][] points, long[] u, long[] v, int k, long d) {
        int n = points.length;
        Set<Long> us = new HashSet<>();
        Set<Long> vs = new HashSet<>();
        for (int i = 0; i < n; i++) {
            us.add(u[i]);
            us.add(u[i] - d);
            us.add(u[i] + d);
            vs.add(v[i]);
            vs.add(v[i] - d);
            vs.add(v[i] + d);
        }
        // 候选点 = 关键 u × 关键 v,且 u、v 奇偶一致(保证对应整数坐标)
        List<Long> ul = new ArrayList<>(us);
        List<Long> vl = new ArrayList<>(vs);
        for (long uu : ul) {
            for (long vv : vl) {
                if (((uu - vv) & 1) != 0) {
                    continue;   // x = (u+v)/2 非整数
                }
                long x = (uu + vv) / 2;
                long y = (uu - vv) / 2;
                int cnt = 0;
                for (int i = 0; i < n; i++) {
                    long dist = Math.abs(x - points[i][0]) + Math.abs(y - points[i][1]);
                    if (dist <= d) {
                        cnt++;
                    }
                }
                if (cnt >= k) {
                    return true;
                }
            }
        }
        return false;
    }

    public int minDayskVariantsEnum(int[][] points, int k) {
        int n = points.length;
        Set<Long> cand = new HashSet<>();
        long maxDist = 0;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                long dist = (long) Math.abs(points[i][0] - points[j][0])
                          + Math.abs(points[i][1] - points[j][1]);
                cand.add((dist + 1) / 2);   // ceil(dist/2):覆盖这两点的最早天数
                maxDist = Math.max(maxDist, dist);
            }
        }
        cand.add(0L);
        cand.add(maxDist);
        List<Long> list = new ArrayList<>(cand);
        java.util.Collections.sort(list);
        for (long d : list) {
            if (check2(points, k, d)) {
                return (int) d;
            }
        }
        return (int) maxDist;
    }

    private boolean check2(int[][] points, int k, long d) {
        int n = points.length;
        Set<Long> xs = new HashSet<>();
        Set<Long> ys = new HashSet<>();
        for (int[] p : points) {
            xs.add((long) p[0]);
            ys.add((long) p[1]);
            xs.add((long) p[0] - d);
            xs.add((long) p[0] + d);
            ys.add((long) p[1] - d);
            ys.add((long) p[1] + d);
        }
        for (long x : xs) {
            for (long y : ys) {
                int cnt = 0;
                for (int[] p : points) {
                    if (Math.abs(x - p[0]) + Math.abs(y - p[1]) <= d) {
                        cnt++;
                    }
                }
                if (cnt >= k) {
                    return true;
                }
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][][] pts = {
            {{1, 1}, {6, 1}},
            {{3, 3}, {1, 2}, {9, 2}},
            {{3, 3}, {1, 2}, {9, 2}},
            {{1, 1}, {5, 5}},
            {{1, 1}, {1, 1}, {1, 1}},
            {{0, 0}, {4, 0}, {0, 4}, {4, 4}}
        };
        int[] ks = {2, 2, 3, 2, 3, 2};
        int[] exp = {3, 2, 4, 4, 0, 2};
        boolean all = true;
        for (int i = 0; i < pts.length; i++) {
            int a = sol.minDayskVariants(pts[i], ks[i]);
            int b = sol.minDayskVariantsEnum(pts[i], ks[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1957. Delete Characters to Make Fancy String(删除字符使字符串变好)

题目描述

一个字符串如果没有三个连续相同字符,那么它就是一个好字符串。

给你一个字符串 s,请你从 s 删除最少的字符,使它变成一个好字符串。

请你返回删除后的字符串。题目数据保证答案总是唯一的。

示例 1

  • 输入 :s = "leeetcode"
  • 输出 :"leetcode"
  • 解释 :从第一组 'e' 里面删除一个 'e',得到 "leetcode"。

示例 2

  • 输入 :s = "aaabaaaa"
  • 输出 :"aabaa"
  • 解释 :从第一组 'a' 里面删除一个 'a',得到 "aabaaaa";从第二组 'a' 里面删除两个 'a',
    得到 "aabaa"。

示例 3

  • 输入 :s = "aab"
  • 输出 :"aab"
    提示:
  • - 1 <= s.length <= 10^5
    • s 只包含小写英文字母。

解题思路

解法一:贪心保留,O(n),主提交版本

从左到右扫描,每遇到一个字符,若「已保留的最后两个

字符」都等于当前字符(即会出现三个连续相同),则跳过(删除)当前字符;否则保留它(追加)。

这样每个字符最多被删除一次且保留的字符串是唯一的(贪心保前缀是唯一最优解)。

解法二:数连续段,O(n),对照

把字符串按连续相同字符分段,每段长度 seg,该段最多保留

min(seg, 2) 个字符,拼接即得结果。两解法结果一致。

代码实现

java 复制代码
public class Solution {

    public String makeFancyString(String s) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            int n = sb.length();
            // 若最后两个字符都是 c,则不能保留(会形成三个连续)
            if (n >= 2 && sb.charAt(n - 1) == c && sb.charAt(n - 2) == c) {
                continue;
            }
            sb.append(c);
        }
        return sb.toString();
    }

    public String makeFancyStringSeg(String s) {
        StringBuilder sb = new StringBuilder();
        int i = 0;
        while (i < s.length()) {
            int j = i;
            while (j < s.length() && s.charAt(j) == s.charAt(i)) {
                j++;
            }
            int take = Math.min(j - i, 2);   // 每段最多保留 2 个
            for (int k = 0; k < take; k++) {
                sb.append(s.charAt(i));
            }
            i = j;
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        String[] tests = {"leeetcode", "aaabaaaa", "aab", "a", "aaaaaa", "abbcccddddeeeee"};
        String[] exp = {"leetcode", "aabaa", "aab", "a", "aa", "abbccddee"};
        boolean all = true;
        for (int i = 0; i < tests.length; i++) {
            String a = sol.makeFancyString(tests[i]);
            String b = sol.makeFancyStringSeg(tests[i]);
            boolean ok = a.equals(exp[i]) && a.equals(b);
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " \"" + tests[i] + "\" => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1958. Check if Move is Legal(检查操作是否合法)

题目描述

给你一个下标从 0 开始的 8 x 8 网格 board,其中 board[r][c] 表示游戏棋盘上的格子

(r, c)。棋盘上空格用 '.' 表示,白色格子用 'W' 表示,黑色格子用 'B' 表示。

游戏中每次操作步骤为:选择一个空格子,将它变成你正在执行的颜色(要么白色,要么黑色)。

但是,合法操作必须满足:涂色后这个格子是好线段的一个端点(好线段可以是水平的、竖直的

或者是对角线)。

好线段指的是一个包含三个或者更多格子(包含端点格子)的线段,线段两个端点格子为同一种

颜色,且中间剩余格子的颜色都为另一种颜色(线段上不能有任何空格子)。

给你两个整数 rMove 和 cMove 以及一个字符 color,表示你正在执行操作的颜色(白或者黑),

如果将格子 (rMove, cMove) 变成颜色 color 后是一个合法操作,那么返回 true,否则返回

false。

示例 1

  • 输入 :board = \[".",".",".","`B`",".",".",".",".",".",".",".","`W`",".",".",".",".",
    ".",".",".","W",".",".",".",".",".",".",".","W",".",".",".",".",
    "W","B","B",".","W","W","W","B",".",".",".","B",".",".",".",".",
    ".",".",".","B",".",".",".",".",".",".",".","W",".",".",".","."],
    rMove = 4, cMove = 3, color = "B"
  • 输出 :true

示例 2

  • 输入 :board = \[".",".",".",".",".",".",".",".",".","`B`",".",".","`W`",".",".",".",
    ".",".","W",".",".",".",".",".",".",".",".","W","B",".",".",".",
    ".",".",".",".",".",".",".",".",".",".",".",".","B","W",".",".",
    ".",".",".",".",".",".","W",".",".",".",".",".",".",".",".","B"],
    rMove = 4, cMove = 4, color = "W"
  • 输出 :false
  • 解释 :涂色广播后虽然产生了好线段,但选中格子是作为中间格子,没有以选中格子为端点。
    提示:
  • - board.length == board[r].length == 8
  • - 0 <= rMove, cMove < 8
  • - board[rMove][cMove] == '.'
    • color 要么是 'B' 要么是 'W'。

解题思路

解法一:八方向扩展校验,O(1),主提交版本

把 (rMove, cMove) 涂成 color 后,沿八个

方向(水平 2、垂直 2、对角 4)逐一延伸:第一步必须碰到「另一种颜色」(否则线段长度为 1 或

直接是相同颜色,不构成端点),随后继续延伸,若碰到空格则此方向失败;若碰到与端点同色的

格子则此方向构成好线段(中间全是另一种颜色、长度 >= 3)返回 true。八个方向都失败返回

false。

解法二:预涂色 + 全线段枚举,对照

先把格子涂色,然后枚举所有可能的好线段------对任意

方向,尝试以 (rMove, cMove) 为一端构造长度 >= 3 的线段,检查端点同色且中间全为另一色。

实现上与解法一本质相同,只是用「先涂色再检查」的方式组织代码,用于交叉验证。

代码实现

java 复制代码
public class Solution {

    private static final int[][] DIRS = {
        {0, 1}, {0, -1}, {1, 0}, {-1, 0},        // 水平、垂直
        {1, 1}, {1, -1}, {-1, 1}, {-1, -1}       // 对角
    };

    public boolean checkMove(char[][] board, int rMove, int cMove, char color) {
        char other = color == 'W' ? 'B' : 'W';
        for (int[] d : DIRS) {
            int r = rMove + d[0], c = cMove + d[1];
            int step = 1;
            // 第一步必须是对手颜色(否则不成线段)
            if (!inBoard(r, c) || board[r][c] != other) {
                continue;
            }
            // 继续延伸,直到棋盘边界
            while (true) {
                r += d[0];
                c += d[1];
                if (!inBoard(r, c)) {
                    break;           // 出界,此方向失败
                }
                if (board[r][c] == '.') {
                    break;           // 空格,失败
                }
                step++;
                if (board[r][c] == color) {
                    // 碰到同色端点:线段长度 step+1 >= 2(含首步对手色),且中间全是对手色
                    return true;
                }
                // 仍是 other 色,继续
            }
        }
        return false;
    }

    private boolean inBoard(int r, int c) {
        return r >= 0 && r < 8 && c >= 0 && c < 8;
    }

    public boolean checkMoveFill(char[][] board, int rMove, int cMove, char color) {
        char other = color == 'W' ? 'B' : 'W';
        char[][] b = new char[8][8];
        for (int i = 0; i < 8; i++) {
            b[i] = board[i].clone();   // 复制棋盘避免污染
        }
        b[rMove][cMove] = color;
        for (int[] d : DIRS) {
            // 第一步必须是对手色(否则不成线段)
            int r = rMove + d[0], c = cMove + d[1];
            if (!inBoard(r, c) || b[r][c] != other) {
                continue;
            }
            // 继续延伸:只能是对手色,遇到同色端点即构成好线段,空格/出界则失败
            r += d[0];
            c += d[1];
            while (inBoard(r, c)) {
                if (b[r][c] == '.') {
                    break;
                }
                if (b[r][c] == color) {
                    return true;    // 线段长度 >= 3 且中间全为对手色
                }
                r += d[0];
                c += d[1];
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        char[][] b1 = {
            {'.', '.', '.', 'B', '.', '.', '.', '.'},
            {'.', '.', '.', 'W', '.', '.', '.', '.'},
            {'.', '.', '.', 'W', '.', '.', '.', '.'},
            {'.', '.', '.', 'W', '.', '.', '.', '.'},
            {'W', 'B', 'B', '.', 'W', 'W', 'W', 'B'},
            {'.', '.', '.', 'B', '.', '.', '.', '.'},
            {'.', '.', '.', 'B', '.', '.', '.', '.'},
            {'.', '.', '.', 'W', '.', '.', '.', '.'}
        };
        char[][] b2 = {
            {'.', '.', '.', '.', '.', '.', '.', '.'},
            {'.', 'B', '.', '.', 'W', '.', '.', '.'},
            {'.', '.', 'W', '.', '.', '.', '.', '.'},
            {'.', '.', '.', 'W', 'B', '.', '.', '.'},
            {'.', '.', '.', '.', '.', '.', '.', '.'},
            {'.', '.', '.', '.', 'B', 'W', '.', '.'},
            {'.', '.', '.', '.', '.', '.', 'W', '.'},
            {'.', '.', '.', '.', '.', '.', '.', 'B'}
        };
        boolean[][] runs = {
            {sol.checkMove(b1, 4, 3, 'B'), sol.checkMoveFill(b1, 4, 3, 'B'), true},
            {sol.checkMove(b2, 4, 4, 'W'), sol.checkMoveFill(b2, 4, 4, 'W'), false}
        };
        boolean all = true;
        for (int i = 0; i < runs.length; i++) {
            boolean ok = runs[i][0] == runs[i][2] && runs[i][0] == runs[i][1];
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + runs[i][0] + " / " + runs[i][1]);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1959. Minimum Total Space Wasted With K Resizing Operations(K 次调整数组大小浪费的最小总空间)

题目描述

你正在设计一个动态数组。给你一个下标从 0 开始的整数数组 nums,其中 nums[i] 是 i

时刻数组中的元素数目。除此以外,你还有一个整数 k,表示你可以调整数组大小的最多次数

(每次都可以调整成任意大小)。

t 时刻数组的大小 size_t 必须大于等于 nums[t],因为数组需要有足够的空间容纳所有元素。

t 时刻浪费的空间为 size_t - nums[t],总浪费空间为所有时刻浪费空间之和。

在任意时刻调整数组大小,会产生一些浪费空间。请你返回在不超过 k 次调整的情况下,总浪费

空间的最小值。

示例 1

  • 输入 :nums = 10,20, k = 0
  • 输出:10
  • 解释:无法调整大小,全程大小为 20,浪费 = (20-10) + (20-20) = 10。

示例 2

  • 输入 :nums = 10,20,30, k = 1
  • 输出:10
  • 解释 :可以调整 1 次:初始大小 30(浪费 20+10),再调整到 30(浪费 0+0),共 20 不行;
    最优:大小 10(浪费 0),再调整为 30(浪费 0+0)?不对------大小必须是 >= nums[t]。
    最优方案:0~1 时刻大小 20(浪费 10+0),2 时刻调整为 30(浪费 0),共 10。

示例 3

  • 输入 :nums = 10,20,30,50, k = 2
  • 输出:30
  • 解释 :最优:时刻 0~2 大小 30(浪费 20+10+0),时刻 3 调整为 50(浪费 0),共 30。
    提示:
  • - 1 <= nums.length <= 200
  • - 1 <= nums[i] <= 10^6
  • - 0 <= k <= nums.length - 1

解题思路

解法一:区间 DP,O(n^2 * k),主提交版本

把时间轴分成至多 k+1 段,每段内统一用一个

固定大小(该段最大值),段的浪费 = 段长 * 段最大值 - 段元素和。设 cost[i][j] = 区间

[i, j] 作为一段(不调整)时的浪费空间:len * max - sum。令 dp[t][i] = 前 i+1 个

时刻(下标 0...i)用了 t 次调整(即形成 t+1 段)的最小总浪费。转移:

dp[t][i] = min over j < i of dp[t-1][j] + cost[j+1][i],初始化 dp[0][i] = cost[0][i]。

答案 dp[k][n-1]。

解法二:记忆化 DFS,对照

dfs(i, segs) 表示下标 0...i 还有 segs 次调整时的最小浪费,

递归枚举最后一段的起点,与解法一等价,用于交叉验证。

代码实现

java 复制代码
public class Solution {

    public int minSpaceWastedKResizing(int[] nums, int k) {
        int n = nums.length;
        int[][] cost = new int[n][n];
        for (int i = 0; i < n; i++) {
            int mx = 0, sum = 0;
            for (int j = i; j < n; j++) {
                mx = Math.max(mx, nums[j]);
                sum += nums[j];
                cost[i][j] = mx * (j - i + 1) - sum;
            }
        }
        final int INF = Integer.MAX_VALUE / 4;
        // dp[t][i]:0..i 用了 t 次调整的最小浪费
        int[][] dp = new int[k + 1][n];
        for (int i = 0; i < n; i++) {
            dp[0][i] = cost[0][i];
        }
        for (int t = 1; t <= k; t++) {
            for (int i = 0; i < n; i++) {
                int best = INF;
                for (int j = 0; j < i; j++) {
                    if (dp[t - 1][j] != INF) {
                        best = Math.min(best, dp[t - 1][j] + cost[j + 1][i]);
                    }
                }
                // 也可不用满次数:0..i 划分成 <= t+1 段(即本次不额外调整)
                dp[t][i] = Math.min(best, dp[t - 1][i]);
            }
        }
        return dp[k][n - 1];
    }

    public int minSpaceWastedKResizingMem(int[] nums, int k) {
        int n = nums.length;
        int[][] cost = new int[n][n];
        for (int i = 0; i < n; i++) {
            int mx = 0, sum = 0;
            for (int j = i; j < n; j++) {
                mx = Math.max(mx, nums[j]);
                sum += nums[j];
                cost[i][j] = mx * (j - i + 1) - sum;
            }
        }
        memo = new int[n][k + 2];
        for (int[] row : memo) java.util.Arrays.fill(row, -1);
        return dfs(nums, cost, n, n - 1, k + 1);   // segs = k+1 个段可用
    }

    private int[][] memo;

    private int dfs(int[] nums, int[][] cost, int n, int i, int segs) {
        if (i < 0) return 0;
        if (segs <= 0) return Integer.MAX_VALUE / 4;
        if (memo[i][segs] != -1) return memo[i][segs];
        int best = Integer.MAX_VALUE / 4;
        for (int j = -1; j < i; j++) {
            int prev = dfs(nums, cost, n, j, segs - 1);
            if (prev < Integer.MAX_VALUE / 4) {
                best = Math.min(best, prev + cost[j + 1][i]);
            }
        }
        return memo[i][segs] = best;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][] nums = {{10, 20}, {10, 20, 30}, {10, 20, 30, 50}, {5, 4, 3, 2, 1}, {1, 100, 1}};
        int[] ks = {0, 1, 2, 2, 1};
        int[] exp = {10, 10, 10, 2, 99};
        boolean all = true;
        for (int i = 0; i < nums.length; i++) {
            int a = sol.minSpaceWastedKResizing(nums[i], ks[i]);
            int b = sol.minSpaceWastedKResizingMem(nums[i], ks[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1960. Maximum Product of the Length of Two Palindromic Substrings(两个回文子字符串长度的最大乘积)

题目描述

给你一个下标从 0 开始的字符串 s,你需要找到两个不重叠的回文子字符串,它们的长度都必须为

奇数,使得它们长度的乘积最大。

更正式地,你想要选择四个整数 i、j、k、l,使得 0 <= i <= j < k <= l < s.length,

且 s[i..j] 与 s[k..l] 都是回文串(长度均为奇数)。两个子字符串不重叠指 j < k。

返回两个回文子字符串长度的最大乘积。

示例 1

  • 输入 :s = "ababbb"
  • 输出:9
  • 解释 :选择 "aba"(下标 0...2,长度 3)和 "bbb"(下标 3...5,长度 3),乘积 3*3 = 9。

示例 2

  • 输入 :s = "zaaaxbbby"
  • 输出:9
  • 解释 :选择 "aaa"(下标 1...3)和 "bbb"(下标 5...7),乘积 9。
    提示:
  • - 2 <= s.length <= 10^5
    • s 只包含小写英文字母。

解题思路

解法一:Manacher + 前后缀最长奇回文,O(n),主提交版本

  1. Manacher 求以每个位置为中心的最长回文半径(在插入 '#' 的新串上跑),得到半径数组 d。
  2. 对每个原始下标 i,以它为中心能形成的最长奇回文长度为 L = 2*r - 1(r 为原始半径),
    覆盖区间 [i - (r-1), i + (r-1)]。
  3. 用差分/扫描把「以位置 end 结尾的最长奇回文长度」e[end] 与「以位置 start 开头的最长奇
    回文长度」b[start] 求出来:对每个中心 i 半径 r,可更新 e[i+r-1] = max(..., 2r-1)、
    b[i-r+1] = max(..., 2r-1);由于回文可截短,还需做后缀/前缀 max 传播:
    e[i] = max(e[i], e[i+1] - 2)(若 i+1 处有长度 L 的奇回文,则以 i 结尾也有 L-2 的奇回文),
    同理 b[i] = max(b[i], b[i-1] - 2)。
  4. 枚举切分点:ans = max over i of e[i] * b[i+1](左侧以 i 结尾、右侧以 i+1 开头,保证不重叠)。
    解法二:中心扩展暴力 + 同样前后缀,O(n^2),对照,仅小规模
    直接对每个中心向两边扩展求
    最长奇回文半径,其余步骤与解法一相同。用于交叉验证。

代码实现

java 复制代码
public class Solution {

    public long maxProduct(String s) {
        int n = s.length();
        // 1. Manacher
        char[] t = new char[2 * n + 3];
        t[0] = '^';
        for (int i = 0; i < n; i++) {
            t[2 * i + 1] = '#';
            t[2 * i + 2] = s.charAt(i);
        }
        t[2 * n + 1] = '#';
        t[2 * n + 2] = '$';
        int m = t.length;
        int[] p = new int[m];
        int c = 0, R = 0;
        for (int i = 1; i < m - 1; i++) {
            int mirror = 2 * c - i;
            p[i] = R > i ? Math.min(R - i, p[mirror]) : 0;
            while (t[i + p[i] + 1] == t[i - p[i] - 1]) {
                p[i]++;
            }
            if (i + p[i] > R) {
                c = i;
                R = i + p[i];
            }
        }
        // 2. 以原始下标 i 为中心的最长奇回文半径 r(不含中心,扩展长度 p[2i+2]/2)
        int[] r = new int[n];
        for (int i = 0; i < n; i++) {
            r[i] = p[2 * i + 2] / 2;   // 半径(半边长度),最长奇回文长度 = 2*r+1
        }
        // 3. e[i]:以 i 结尾的最长奇回文长度
        int[] e = new int[n];
        int[] b = new int[n];          // b[i]:以 i 开头的最长奇回文长度
        for (int i = 0; i < n; i++) {
            int len = 2 * r[i] + 1;    // 奇数
            int L = i - r[i], RR = i + r[i];
            if (e[RR] < len) e[RR] = len;
            if (b[L] < len) b[L] = len;
        }
        // 传播:末尾向前,长度 -2
        for (int i = n - 2; i >= 0; i--) {
            if (e[i + 1] >= 2 && e[i] < e[i + 1] - 2) {
                e[i] = e[i + 1] - 2;
            }
        }
        for (int i = 1; i < n; i++) {
            if (b[i - 1] >= 2 && b[i] < b[i - 1] - 2) {
                b[i] = b[i - 1] - 2;
            }
        }
        // 4. 枚举切分点:左侧回文结束于任意 <= i 的位置,右侧回文开始于任意 >= i+1 的位置
        //    (不重叠),因此用「前缀最大 end」×「后缀最大 start」。
        int[] left = new int[n];
        left[0] = e[0];
        for (int i = 1; i < n; i++) {
            left[i] = Math.max(left[i - 1], e[i]);
        }
        int[] right = new int[n];
        right[n - 1] = b[n - 1];
        for (int i = n - 2; i >= 0; i--) {
            right[i] = Math.max(right[i + 1], b[i]);
        }
        long ans = 0;
        for (int i = 0; i + 1 < n; i++) {
            if (left[i] > 0 && right[i + 1] > 0) {
                ans = Math.max(ans, (long) left[i] * right[i + 1]);
            }
        }
        return ans;
    }

    public long maxProductExpand(String s) {
        int n = s.length();
        int[] e = new int[n];
        int[] b = new int[n];
        for (int i = 0; i < n; i++) {
            int len = 1;
            int l = i, r = i;
            while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)) {
                len = r - l + 1;
                if (e[r] < len) e[r] = len;
                if (b[l] < len) b[l] = len;
                l--;
                r++;
            }
        }
        // 传播
        for (int i = n - 2; i >= 0; i--) {
            if (e[i + 1] >= 2 && e[i] < e[i + 1] - 2) {
                e[i] = e[i + 1] - 2;
            }
        }
        for (int i = 1; i < n; i++) {
            if (b[i - 1] >= 2 && b[i] < b[i - 1] - 2) {
                b[i] = b[i - 1] - 2;
            }
        }
        int[] left = new int[n];
        left[0] = e[0];
        for (int i = 1; i < n; i++) {
            left[i] = Math.max(left[i - 1], e[i]);
        }
        int[] right = new int[n];
        right[n - 1] = b[n - 1];
        for (int i = n - 2; i >= 0; i--) {
            right[i] = Math.max(right[i + 1], b[i]);
        }
        long ans = 0;
        for (int i = 0; i + 1 < n; i++) {
            if (left[i] > 0 && right[i + 1] > 0) {
                ans = Math.max(ans, (long) left[i] * right[i + 1]);
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        String[] ss = {"ababbb", "zaaaxbbby", "aba", "aaa", "ab", "abccba", "aaaaaa", "abcdeedcba", "bbbbbcc"};
        long[] exp = {9, 9, 1, 1, 1, 1, 9, 1, 5};
        boolean all = true;
        for (int i = 0; i < ss.length; i++) {
            long a = sol.maxProduct(ss[i]);
            long b = sol.maxProductExpand(ss[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " \"" + ss[i] + "\" => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析

相关推荐
txxzjmzlh37 分钟前
SpringBoot 统⼀功能处理
java·spring boot·后端
nice_lcj5202 小时前
Java Lambda 表达式入门:怎么写、怎么用、常用方法有哪些
java·开发语言·后端·面试
码农小河663 小时前
基于SSM+JSP+MySQL的在线医疗服务系统的设计与实现
java·开发语言·mysql
SL_staff4 小时前
从配置失效到逻辑复用:JVS-Logic 如何构建可治理的业务编排中枢
java·spring·产品
特立独行的猫A4 小时前
一份 Markdown 发十个平台:cspub 一稿多投
java·后端·python
文丑颜不良啊5 小时前
LeetCode1961~1980题目解析
java·leetcode
文丑颜不良啊5 小时前
LeetCode2001~2020题目解析
java·leetcode
用户3721574261355 小时前
Java 批量提取 Word 文档中的图片并保存到本地
java
SL_staff6 小时前
BI项目失败率90%?JVS如何用三阶闭环构建可落地的数据生产力平台
java·开源·全栈