LeetCode1881~1990题目解析

LeetCode 1881-1900. 题解合集

文章目录


1881. Maximum Value after Insertion(插入后的最大值)

题目描述

给你一个非常大的整数 n 和一个整数数字 x,大整数 n 用一个字符串表示。n 中每一位数字和数字 x

都处于闭区间 1, 9 中,且 n 可能表示一个负数。

你打算通过在 n 的十进制表示的任意位置插入 x 来最大化 n 的数值。但不能在负号的左边插入 x。

例如,如果 n = 73 且 x = 6,那么最佳方案是将 6 插入 7 和 3 之间,使 n = 763。

如果 n = -55 且 x = 2,那么最佳方案是将 2 插在第一个 5 之前,使 n = -255。

返回插入操作后,用字符串表示的 n 的最大值。

示例 1

  • 输入 :n = "99", x = 9
  • 输出:"999"

示例 2

  • 输入 :n = "-13", x = 2
  • 输出:"-123"
  • 解释 :插入 x 可以得到 -213、-123 或者 -132,三者中最大的是 -123。
    提示:
  • - 1 <= n.length <= 10^5
  • - 1 <= x <= 9
    • n 中每一位的数字都在闭区间 1, 9 中。
    • n 代表一个有效的整数。
    • 当 n 表示负数时,将会以字符 '-' 开始。

解题思路

正数:想让数尽可能大,插入位置要尽量靠左,即找到第一个小于 x 的数字,把 x 插在它前面;

若没有,插到末尾。

负数:绝对值越小数值越大,等价于想让"正数部分"尽可能小,即找到第一个大于 x 的数字,

把 x 插在它前面(使该位之后数位右移、整体前段保持较小);若没有,插到末尾。

解法一:一次扫描贪心,主提交版本

区分正负号后找插入点,StringBuilder 插入。

O(n) 时间、O(n) 空间。

解法二:枚举全部位置取最大,对照

把 x 插入每个可行位置生成全部候选串,正数取数值/字典序

最大、负数取最小绝对值;仅作小规模验证,O(n^2)。

代码实现

java 复制代码
public class Solution {

    public String maxValue(String n, int x) {
        boolean negative = n.charAt(0) == '-';
        char xc = (char) ('0' + x);
        int i;
        if (negative) {
            // 负数:绝对值越小越好 → 找到第一个大于 x 的数字,插在其前
            i = 1;
            while (i < n.length() && n.charAt(i) <= xc) {
                i++;
            }
        } else {
            // 正数:找到第一个小于 x 的数字,插在其前
            i = 0;
            while (i < n.length() && n.charAt(i) >= xc) {
                i++;
            }
        }
        return n.substring(0, i) + xc + n.substring(i);
    }

    public String maxValueAll(String n, int x) {
        String best = null;
        char xc = (char) ('0' + x);
        int start = n.charAt(0) == '-' ? 1 : 0;
        boolean negative = start == 1;
        for (int i = start; i <= n.length(); i++) {
            String cand = n.substring(0, i) + xc + n.substring(i);
            if (best == null) {
                best = cand;
                continue;
            }
            // 负数比"绝对值",即去掉负号后字典序最小
            if (negative) {
                String a = cand.substring(1);
                String b = best.substring(1);
                if (a.compareTo(b) < 0) best = cand;
            } else {
                if (cand.compareTo(best) > 0) best = cand;
            }
        }
        return best;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        String[][] cases = {
                {"99", "9"},
                {"-13", "2"},
                {"73", "6"},
                {"-55", "2"},
                {"1", "1"},
                {"-9", "1"},
                {"123", "9"},
                {"987", "1"},
                {"-555", "5"},
                {"999", "5"}
        };
        boolean ok = true;
        for (String[] c : cases) {
            String r1 = s.maxValue(c[0], Integer.parseInt(c[1]));
            String r2 = s.maxValueAll(c[0], Integer.parseInt(c[1]));
            ok &= r1.equals(r2);
            System.out.println("n=" + c[0] + " x=" + c[1] + " -> " + r1 + " | " + r2 + (r1.equals(r2) ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析


1882. Process Tasks Using Servers(使用服务器处理任务)

题目描述

给你两个下标从 0 开始的整数数组 servers 和 tasks,长度分别为 n 和 m。servers[i] 是第 i 台

服务器的权重,而 tasks[j] 是处理第 j 项任务所需要的时间(秒)。

你正在运行一个仿真系统,在处理完所有任务后,该系统将会关闭。每台服务器只能同时处理一项任务。

第 0 项任务在第 0 秒可以开始处理,第 j 项任务在第 j 秒可以开始处理。处理第 j 项任务时,你

需要为它分配一台权重最小的空闲服务器。如果存在多台相同权重的空闲服务器,请选择下标最小的

服务器。如果一台空闲服务器在第 t 秒分配到第 j 项任务,那么在 t + tasks[j] 时它将恢复空闲。

如果没有空闲服务器,则必须等待,直到出现一台空闲服务器,并尽可能早地处理剩余任务。如果有多项

任务等待分配,则按照下标递增的顺序完成分配。如果同一时刻存在多台空闲服务器,可以同时将多项

任务分别分配给它们。

构建长度为 m 的答案数组 ans,其中 ans[j] 是第 j 项任务分配的服务器的下标。返回 ans。

示例 1

  • 输入 :servers = 3,3,2, tasks = 1,2,3,2,1,2
  • 输出:2,2,0,2,1,2

示例 2

  • 输入 :servers = 5,1,4,3,2, tasks = 2,1,2,4,5,2,1
  • 输出 :1,4,1,4,1,3,2
    提示:
    • servers.length == n,tasks.length == m
  • - 1 <= n, m <= 10^5
  • - 1 <= servers[i], tasks[j] <= 10^5

解题思路

解法一:双优先队列模拟,主提交版本

维护"空闲服务器堆"(按权重、下标排序)与"忙碌服务器堆"

(按结束时刻排序)。时间从 t=0 递增:先把所有按时结束的忙碌服务器归还到空闲堆,再给任务

分配:若空闲堆非空直接取最小权重服务器;否则让时间推进到忙碌堆最早结束时刻,归还后可分配。

注意同一空闲时刻可同时喂给多个等待任务。O((n+m) log n)。

解法二:预推进时间 + 队列,对照

不显式推进"等待时刻",而是对每个任务 j,用"当前时间

accTime"记录下一个可开始处理任务的时刻:若空闲堆空则 accTime = 最早结束时刻并归还;

否则在 accTime 与 j 都满足时直接分配。实现略有差异但语义相同,作为对照。

代码实现

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

public class Solution {

    public int[] assignTasks(int[] servers, int[] tasks) {
        int n = servers.length, m = tasks.length;
        // 空闲堆:权重小优先、下标小优先
        PriorityQueue<int[]> idle = new PriorityQueue<>((a, b) ->
                a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
        // 忙碌堆:结束时刻早优先
        PriorityQueue<int[]> busy = new PriorityQueue<>((a, b) -> a[0] - b[0]);
        for (int i = 0; i < n; i++) {
            idle.offer(new int[]{servers[i], i});
        }
        int[] ans = new int[m];
        int time = 0;
        for (int j = 0; j < m; j++) {
            time = Math.max(time, j);            // 任务 j 最早 j 秒可开始
            // 归还截止到 time 的所有忙碌服务器
            while (!busy.isEmpty() && busy.peek()[0] <= time) {
                int[] sv = busy.poll();
                idle.offer(new int[]{sv[1], sv[2]});   // [权重, 下标]
            }
            if (idle.isEmpty()) {
                // 没有空闲服务器:跳到最早结束时刻
                time = busy.peek()[0];
                while (!busy.isEmpty() && busy.peek()[0] <= time) {
                    int[] sv = busy.poll();
                    idle.offer(new int[]{sv[1], sv[2]});
                }
            }
            int[] choose = idle.poll();
            ans[j] = choose[1];
            busy.offer(new int[]{time + tasks[j], choose[0], choose[1]});
        }
        return ans;
    }

    public int[] assignTasksAlt(int[] servers, int[] tasks) {
        int n = servers.length, m = tasks.length;
        PriorityQueue<int[]> idle = new PriorityQueue<>((a, b) ->
                a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
        PriorityQueue<int[]> busy = new PriorityQueue<>((a, b) -> a[0] - b[0]);
        for (int i = 0; i < n; i++) {
            idle.offer(new int[]{servers[i], i});
        }
        int[] ans = new int[m];
        long acc = 0;                              // 当前可分配时刻(虚拟时钟)
        for (int j = 0; j < m; j++) {
            long cur = Math.max(acc, j);
            while (!busy.isEmpty() && busy.peek()[0] <= cur) {
                int[] sv = busy.poll();
                idle.offer(new int[]{sv[1], sv[2]});
            }
            if (idle.isEmpty()) {
                acc = busy.peek()[0];              // 直接推到最早空闲时刻
                while (!busy.isEmpty() && busy.peek()[0] <= acc) {
                    int[] sv = busy.poll();
                    idle.offer(new int[]{sv[1], sv[2]});
                }
            } else {
                acc = cur;
            }
            int[] choose = idle.poll();
            ans[j] = choose[1];
            busy.offer(new int[]{(int) (acc + tasks[j]), choose[0], choose[1]});
        }
        return ans;
    }

    private static String str(int[] a) {
        return Arrays.toString(a);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] servers = {{3, 3, 2}, {5, 1, 4, 3, 2}, {1, 1}, {2, 1, 1}};
        int[][] tasks = {{1, 2, 3, 2, 1, 2}, {2, 1, 2, 4, 5, 2, 1}, {1, 1, 1, 1}, {3, 3, 3, 3, 3}};
        boolean ok = true;
        for (int t = 0; t < servers.length; t++) {
            int[] r1 = s.assignTasks(servers[t], tasks[t]);
            int[] r2 = s.assignTasksAlt(servers[t], tasks[t]);
            ok &= Arrays.equals(r1, r2);
            System.out.println(str(r1) + " | " + str(r2) + (Arrays.equals(r1, r2) ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析


1883. Minimum Skips to Arrive at Meeting On Time

题目描述

(准时抵达会议现场的最小跳过休息次数)

给你一个整数 hoursBefore,表示你要前往会议所剩下的可用小时数。要想成功抵达会议现场,你必须

途经 n 条道路。道路的长度用一个长度为 n 的整数数组 dist 表示,其中 dist[i] 表示第 i 条道路的

长度(千米)。另给你一个整数 speed,表示你在道路上前进的速度(千米每小时)。

当你通过第 i 条路之后,就必须休息并等待,直到下一个整数小时才能开始继续通过下一条道路。

注意:你不需要在通过最后一条道路后休息,因为那时你已经抵达会议现场。

例如,如果你通过一条道路用去 1.4 小时,那你必须停下来等待,到 2 小时才可以继续通过下一条道路。

如果通过一条道路恰好用去 2 小时,就无需等待,可以直接继续。

然而,为了能准时到达,你可以选择跳过一些路的休息时间,这意味着你不必等待下一个整数小时。

返回准时抵达会议现场所需要的最小跳过次数;如果无法准时参会,返回 -1。

示例 1

  • 输入 :dist = 1,3,2, speed = 4, hoursBefore = 2
  • 输出:1

示例 2

  • 输入 :dist = 7,3,5,5, speed = 2, hoursBefore = 10
  • 输出:2

示例 3

  • 输入 :dist = 7,3,5,5, speed = 1, hoursBefore = 10
  • 输出 :-1
    提示:
  • - n == dist.length
  • - 1 <= n <= 1000
  • - 1 <= dist[i] <= 10^5
  • - 1 <= speed <= 10^6
  • - 1 <= hoursBefore <= 10^7

解题思路

把时间单位放大 speed 倍(即用"每千米所需时间单位"计):第 i 条路耗时 disti 单位,

休息到下一整数小时等价于"向上取整到 speed 的倍数"(单位小时 * speed)。

设 dpik = 走完前 i 条路、跳过 k 次休息后的最短用时(整数单位,hoursBefore*speed 可直接比较)。

转移:不跳 → dpik = ceilToUnit(dpi-1k + disti);跳过 → dpik = dpi-1k-1 + disti。

边界 dp00 = 0;i 从 1...n,最后一趟不休息(不取整)。

最后找最小 k 使 dpnk <= hoursBefore * speed,否则 -1。

解法一:滚动数组 DP,主提交版本

O(n^2) 时间、O(n) 空间;用 long 防溢出。

解法二:二维表 DP,对照

不滚动、直接二维表,便于对照阅读。

代码实现

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

public class Solution {

    public int minSkips(int[] dist, int speed, int hoursBefore) {
        int n = dist.length;
        long limit = (long) hoursBefore * speed;   // 单位化的时限
        long[] dp = new long[n + 1];               // dp[k]:跳过 k 次时的最短用时
        Arrays.fill(dp, Long.MAX_VALUE / 4);
        dp[0] = 0;
        for (int i = 0; i < n; i++) {
            long[] ndp = new long[n + 1];
            Arrays.fill(ndp, Long.MAX_VALUE / 4);
            boolean last = (i == n - 1);
            for (int k = 0; k <= i; k++) {
                if (dp[k] == Long.MAX_VALUE / 4) continue;
                // 不跳过本次休息(最后一条路无休息)
                long cost = dp[k] + dist[i];
                if (!last) {
                    cost = (cost + speed - 1) / speed * speed;   // 取整到 speed 的倍数
                }
                ndp[k] = Math.min(ndp[k], cost);
                // 跳过本次休息
                if (!last && k + 1 <= n) {
                    ndp[k + 1] = Math.min(ndp[k + 1], dp[k] + dist[i]);
                }
            }
            dp = ndp;
        }
        for (int k = 0; k <= n; k++) {
            if (dp[k] <= limit) {
                return k;
            }
        }
        return -1;
    }

    public int minSkips2D(int[] dist, int speed, int hoursBefore) {
        int n = dist.length;
        long limit = (long) hoursBefore * speed;
        long[][] dp = new long[n + 1][n + 1];
        for (long[] row : dp) {
            Arrays.fill(row, Long.MAX_VALUE / 4);
        }
        dp[0][0] = 0;
        for (int i = 0; i < n; i++) {
            boolean last = (i == n - 1);
            for (int k = 0; k <= i; k++) {
                if (dp[i][k] == Long.MAX_VALUE / 4) continue;
                long cost = dp[i][k] + dist[i];
                if (!last) {
                    cost = (cost + speed - 1) / speed * speed;
                }
                dp[i + 1][k] = Math.min(dp[i + 1][k], cost);
                if (!last) {
                    dp[i + 1][k + 1] = Math.min(dp[i + 1][k + 1], dp[i][k] + dist[i]);
                }
            }
        }
        for (int k = 0; k <= n; k++) {
            if (dp[n][k] <= limit) {
                return k;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] ds = {{1, 3, 2}, {7, 3, 5, 5}, {7, 3, 5, 5}, {1, 1, 1}, {5, 5, 5, 5}};
        int[] speeds = {4, 2, 1, 1, 2};
        int[] limits = {2, 10, 10, 3, 10};
        boolean ok = true;
        for (int t = 0; t < ds.length; t++) {
            int r1 = s.minSkips(ds[t], speeds[t], limits[t]);
            int r2 = s.minSkips2D(ds[t], speeds[t], limits[t]);
            ok &= (r1 == r2);
            System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析


1884. Egg Drop With 2 Eggs and N Floors(鸡蛋掉落-两枚鸡蛋)

题目描述

给你 2 枚相同的鸡蛋,和一栋从第 1 层到第 n 层共有 n 层楼的建筑。

已知存在楼层 f,满足 0 <= f <= n,任何从高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的

楼层落下的鸡蛋都不会碎。

每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,

你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中重复使用这枚鸡蛋。

请你计算并返回要确定 f 确切的值的最小操作次数是多少?

示例 1

  • 输入 :n = 2
  • 输出:2

示例 2

  • 输入 :n = 100
  • 输出 :14
    提示:
  • - 1 <= n <= 1000

解题思路

关键观察:第一枚鸡蛋"探路"采取递增间隔策略。若第一次从 k 层扔、第二次从 k+(k-1) 层扔、

第三次从 k+(k-1)+(k-2) 层扔......则每多一次试探可多覆盖一层;第 k 次试探可覆盖的总楼层数为

k*(k+1)/2。因此答案是使 k*(k+1)/2 >= n 的最小 k。

解法一:数学直接求解,主提交版本

对 k 从 1 递增(或解二次方程),找最小 k 满足

k(k+1)/2 >= n,k 最大不超过 45(1000 层)。O(sqrt(n))。

解法二:DP 一般化推导,对照

用更通用的"鸡蛋掉落"DP:dpte = 用 e 枚鸡蛋最多可

尝试 t 次时能确定的最大楼层数;dpte = dpt-1e-1 + dpt-1e + 1。

找到最小的 t 使 dpt2 >= n。对应第一类斯特林类似递推,便于推广到多枚鸡蛋。

代码实现

java 复制代码
public class Solution {

    public int twoEggDrop(int n) {
        int k = 1;
        while (k * (k + 1) / 2 < n) {
            k++;
        }
        return k;
    }

    public int twoEggDropDP(int n) {
        // dp[t][e] = 用 e 枚鸡蛋尝试 t 次能确定的最大楼层数
        // dp[t][e] = dp[t-1][e-1] + dp[t-1][e] + 1;dp[t][1] = t
        int t = 0;
        long prev1 = 0;   // dp[0][1] = 0
        long cur2 = 0;    // dp[0][2] = 0
        while (cur2 < n) {
            t++;
            long next = prev1 + cur2 + 1;   // dp[t][2]
            prev1 = t;                      // dp[t][1] = t
            cur2 = next;
        }
        return t;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[] cases = {1, 2, 5, 10, 100, 1000, 45, 36};
        boolean ok = true;
        for (int c : cases) {
            int r1 = s.twoEggDrop(c);
            int r2 = s.twoEggDropDP(c);
            ok &= (r1 == r2);
            System.out.println("n=" + c + " -> " + r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析


1885. Count Pairs in Two Arrays(统计数对)

题目描述

给你两个长度为 n 的整数数组 nums1 和 nums2,找出所有满足 i < j 且

nums1[i] + nums1[j] > nums2[i] + nums2[j] 的数对 (i, j)。

返回满足条件数对的个数。

示例 1

  • 输入 :nums1 = 2,1,2,1, nums2 = 1,2,1,2
  • 输出:1
  • 解释:只有 (0,2) 满足 2+2 > 1+1。

示例 2

  • 输入 :nums1 = 1,10,6,2, nums2 = 1,4,1,5
  • 输出 :5
    提示:
  • - n == nums1.length == nums2.length
  • - 1 <= n <= 10^5
  • - 1 <= nums1[i], nums2[i] <= 10^5

解题思路

移项:nums1i + nums1j > nums2i + nums2j

<=> (nums1i - nums2i) + (nums1j - nums2j) > 0。

记 di = nums1i - nums2i,问题变成"有序数组 d 中 i < j 且 di + dj > 0 的数对数"。

排序后经典双指针:l 从 0、r 从 n-1,若 dl + dr > 0,则 l...r-1 与 r 都配对,

累加 (r - l) 后 r--;否则 l++。

解法一:排序 + 双指针,主提交版本

O(n log n) 时间、O(n) 空间(diff 数组)。

解法二:排序 + 二分,对照

排序后对每个 i,用二分找满足 dj > -di 的最小下标 j,

累加 n - j,注意排除自身(j 必须 > i 不需特别排除,因为 i < j 条件通过下标关系体现:

遍历时只统计 j > i 的部分)。实现直观但对 O(n log n) 二分常数更大。

代码实现

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

public class Solution {

    public long countPairs(int[] nums1, int[] nums2) {
        int n = nums1.length;
        int[] d = new int[n];
        for (int i = 0; i < n; i++) {
            d[i] = nums1[i] - nums2[i];
        }
        Arrays.sort(d);
        long ans = 0;
        int l = 0, r = n - 1;
        while (l < r) {
            if (d[l] + d[r] > 0) {
                ans += r - l;      // d[l..r-1] 与 d[r] 均满足
                r--;
            } else {
                l++;
            }
        }
        return ans;
    }

    public long countPairsBinary(int[] nums1, int[] nums2) {
        int n = nums1.length;
        int[] d = new int[n];
        for (int i = 0; i < n; i++) {
            d[i] = nums1[i] - nums2[i];
        }
        Arrays.sort(d);
        long ans = 0;
        for (int i = 0; i < n; i++) {
            // 找第一个 d[j] > -d[i] 的下标;j 可能等于 i,需保证 j > i。
            // 由于二分找的是全数组第一个 > -d[i] 的下标 pos,pos 可能 <= i,
            // 但 d[i] 与它配对数不依赖下标序,排序后"所有优于互补"的元素都可配对,
            // 而 i<j 的约束通过等价形式(每个有序对选较小的处理一次)自然满足:
            int lo = 0, hi = n;
            while (lo < hi) {
                int mid = (lo + hi) >>> 1;
                if (d[mid] > -d[i]) hi = mid;
                else lo = mid + 1;
            }
            // 真正的配对数为 max(0, pos 右侧元素个数 - (自配对校正))
            // 自配对校正:当 i 恰好落在满足区间内时需扣 1(d[i]+d[j]>0 且 j==i 是自己)
            long add = n - lo;
            if (d[i] > -d[i]) {
                add--;              // 自身被计入,扣除
            }
            ans += add;
        }
        return ans / 2;             // 每个有序对 (i,j) 被计两次
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] a = {{2, 1, 2, 1}, {1, 10, 6, 2}, {1, 1, 1}, {5, 5, 5, 5}, {10, 20, 30}};
        int[][] b = {{1, 2, 1, 2}, {1, 4, 1, 5}, {1, 1, 1}, {1, 1, 1, 1}, {30, 20, 10}};
        boolean ok = true;
        for (int t = 0; t < a.length; t++) {
            long r1 = s.countPairs(a[t], b[t]);
            long r2 = s.countPairsBinary(a[t], b[t]);
            ok &= (r1 == r2);
            System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析


1886. Determine Whether Matrix Can Be Obtained By Rotation

题目描述

(判断矩阵经轮转后是否一致)

给你两个大小为 n x n 的二进制矩阵 mat 和 target。现以 90 度顺时针轮转矩阵 mat 中的元素

若干次,如果能够使 mat 与 target 一致,返回 true;否则,返回 false。

示例 1

  • 输入 :mat = \[0,1,1,0], target = \[1,0,0,1]
  • 输出 :true
  • 解释 :顺时针轮转 90 度一次可以使 mat 和 target 一致。

示例 2

  • 输入 :mat = \[0,1,1,1], target = \[1,0,0,1]
  • 输出 :false

示例 3

  • 输入 :mat = \[0,0,0,0,1,0,1,1,1], target = \[1,1,1,0,1,0,0,0,0]
  • 输出 :true
  • 解释 :顺时针轮转 90 度两次可以使 mat 和 target 一致。
    提示:
  • - n == mat.length == target.length
  • - n == mat[i].length == target[i].length
  • - 1 <= n <= 10
    • mat[i][j] 和 target[i][j] 不是 0 就是 1

解题思路

顺时针旋转 90° 一次:rotrc = matn-1-cr。旋转 0、1、2、3 次逐一与 target 比较。

解法一:显式旋转函数,主提交版本

实现 rotate90 返回新矩阵,比较 0~3 次旋转结果。

O(n^2) 空间与时间(每次旋转 O(n^2),共 4 次)。

解法二:坐标映射直接比较,对照

对旋转 k 次(k = 0...3),检查所有 (r, c) 是否满足

mat映射坐标 == targetrc,无需真实构造旋转矩阵,常数更小。

代码实现

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

public class Solution {

    public boolean findRotation(int[][] mat, int[][] target) {
        int n = mat.length;
        int[][] cur = mat;
        for (int k = 0; k < 4; k++) {
            if (Arrays.deepEquals(cur, target)) {
                return true;
            }
            cur = rotate90(cur);
        }
        return false;
    }

    private int[][] rotate90(int[][] m) {
        int n = m.length;
        int[][] r = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                r[j][n - 1 - i] = m[i][j];
            }
        }
        return r;
    }

    public boolean findRotationMap(int[][] mat, int[][] target) {
        int n = mat.length;
        for (int k = 0; k < 4; k++) {
            boolean same = true;
            for (int i = 0; i < n && same; i++) {
                for (int j = 0; j < n; j++) {
                    int si = i, sj = j;
                    // 旋转 k 次后 target 的 (i,j) 来自原始 mat 的位置
                    for (int t = 0; t < k; t++) {
                        int ni = sj;
                        int nj = n - 1 - si;
                        si = ni;
                        sj = nj;
                    }
                    if (mat[si][sj] != target[i][j]) {
                        same = false;
                        break;
                    }
                }
            }
            if (same) {
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][][] mats = {
                {{0, 1}, {1, 0}},
                {{0, 1}, {1, 1}},
                {{0, 0, 0}, {0, 1, 0}, {1, 1, 1}},
                {{1}},
                {{1, 2}, {3, 4}}
        };
        int[][][] targets = {
                {{1, 0}, {0, 1}},
                {{1, 0}, {0, 1}},
                {{1, 1, 1}, {0, 1, 0}, {0, 0, 0}},
                {{1}},
                {{3, 1}, {4, 2}}
        };
        boolean ok = true;
        for (int t = 0; t < mats.length; t++) {
            boolean r1 = s.findRotation(mats[t], targets[t]);
            boolean r2 = s.findRotationMap(mats[t], targets[t]);
            ok &= (r1 == r2);
            System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析


1887. Reduction Operations to Make the Array Elements Equal

题目描述

(使数组元素相等的减少操作次数)

给你一个整数数组 nums,你的目标是令 nums 中的所有元素相等。完成一次减少操作需要遵照下面的

几个步骤:

  • 找出 nums 中的最大值。记这个值为 largest 并取其下标 i(如果有多个元素都是最大值,则取最小
    的下标 i)。
  • 找出 nums 中的下一个最大值,这个值严格小于 largest,记为 nextLargest。
  • 将 nums[i] 减少到 nextLargest。
    返回使 nums 中的所有元素相等的操作次数。

示例 1

  • 输入 :nums = 5,1,3
  • 输出:3

示例 2

  • 输入 :nums = 1,1,1
  • 输出:0

示例 3

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

解题思路

观察:每次操作都把"当前最大值"降到"次大值"。若排序后有若干不同取值 v1 < v2 < ... < vm

(升序),那么取值 vj 的元素最终都要一路降到 v1,每个 vj 层的元素需要执行

(该层及更大层的元素个数 - 1) 次操作合计。等价地:从大到小扫,每次遇到新值切换层级时,

累加"前缀(较大的)元素个数"作为一次整层降低的代价。

解法一:排序 + 层级计数,主提交版本

升序排序后从右往左遍历,每当发现新值层级变化,

累加 i(该值及其右边所有更大值的个数);O(n log n)。

解法二:值域计数 + 前缀累计,对照

不排序,用值域数组统计频率,从大到小遍历不同取值,

用"已见更大元素数"累计操作;O(n + maxVal)。

代码实现

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

public class Solution {

    public int reductionOperations(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        int ans = 0;
        int start = n - 1;               // 当前层的起始(最右)下标
        for (int i = n - 2; i >= 0; i--) {
            if (nums[i] != nums[i + 1]) {
                // 遇到更小的新层级:nums[i+1..] 这一整批(共 start - i 个... 实际是 n-1-i 个)
                // 需要执行 1 次"降到 nums[i]";而更多层迭代会叠加
                ans += n - 1 - i;
            }
        }
        return ans;
    }

    public int reductionOperationsCount(int[] nums) {
        int max = 0;
        for (int v : nums) {
            if (v > max) max = v;
        }
        int[] cnt = new int[max + 1];
        for (int v : nums) cnt[v]++;
        int ans = 0;
        int bigger = 0;                  // 已经处理过的、更大的元素个数
        // 从大到小遍历取值
        for (int v = max; v >= 0; v--) {
            if (cnt[v] > 0) {
                if (bigger > 0) {
                    ans += bigger;       // 把这一整层(bigger 个元素)降一级的次数
                }
                bigger += cnt[v];
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] cases = {{5, 1, 3}, {1, 1, 1}, {1, 1, 2, 2, 3}, {1, 2, 3, 4}, {2, 2, 2, 2}, {1, 2}, {1, 1, 1, 2}};
        boolean ok = true;
        for (int[] c : cases) {
            int r1 = s.reductionOperations(c);
            int r2 = s.reductionOperationsCount(c);
            ok &= (r1 == r2);
            System.out.println(Arrays.toString(c) + " -> " + r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析


1888. Minimum Number of Flips to Make the Binary String Alternating

题目描述

(使二进制字符串字符交替的最少反转次数)

给你一个二进制字符串 s。你可以按任意顺序执行以下两种操作任意次:

  • 类型 1:删除字符串 s 的第一个字符并将它添加到字符串结尾。
  • 类型 2:选择字符串 s 中任意一个字符并将该字符反转,即如果值为 '0',则反转得到 '1',
    反之亦然。
    请你返回使 s 变成交替字符串的前提下,类型 2 的最少操作次数。
    我们称一个字符串是交替的,需要满足任意相邻字符都不同。比方说,字符串 "010" 和 "1010"
    都是交替的,但是字符串 "0100" 不是。

示例 1

  • 输入 :s = "111000"
  • 输出:2
  • 解释 :执行第一种操作两次,得到 s = "100011",然后对第三和第六个字符执行第二种操作,
    得到 "101010"。

示例 2

  • 输入 :s = "010"
  • 输出:0

示例 3

  • 输入 :s = "1110"
  • 输出 :1
    提示:
  • - 1 <= s.length <= 10^5
    • s[i] 要么是 '0',要么是 '1'

解题思路

类型 1 可执行任意次,等价于"考虑 s 的所有循环移位"。设 t = s + s(长度为 2n)。

交替串只有两种形态:pattern0 = "0101..."、pattern1 = "1010..."。对任意窗口起点 l

(0 <= l < n),窗口 l, l+n-1 即 s 的一种循环移位,其最少反转次数 =

min(该窗口与 pattern0 的差异数, 与 pattern1 的差异数)。

关键推导:对 pattern0,定义 ai = (ti 与"按全局下标 i 奇偶的 pattern0"不符) ? 1 : 0。

窗口起点 l 的差异数 d0(l) = Σ_{i=l}^{l+n-1} t\[i 与"按窗口内位置奇偶的 pattern0"不符]。

由于"窗口内位置奇偶"与"全局位置奇偶"的关系是:l 偶时二者一致、l 奇时互补,

因此 d0(l) = l 偶 ? sumA(l) : n - sumA(l),其中 sumA(l)=Σ_{i=l}^{l+n-1} ai。

同 理 d1(l) 用 bi(按全局下标奇偶的 pattern1 差异)计算。滑动窗口 O(1) 维护 sumA/sumB。

解法一:滑动窗口差分,主提交版本

O(n) 时间、O(n) 空间。

解法二:逐起点枚举,对照

对每个起点直接逐位统计差异,O(n^2),小规模验证用。

代码实现

java 复制代码
public class Solution {

    public int minFlips(String s) {
        int n = s.length();
        String t = s + s;
        int m = 2 * n;
        int[] a = new int[m];   // a[i]:t[i] 与"全局下标奇偶定 pattern0"的差异
        int[] b = new int[m];   // b[i]:t[i] 与"全局下标奇偶定 pattern1"的差异
        for (int i = 0; i < m; i++) {
            char c = t.charAt(i);
            if (c != (i % 2 == 0 ? '0' : '1')) a[i] = 1;
            if (c != (i % 2 == 0 ? '1' : '0')) b[i] = 1;
        }
        int sumA = 0, sumB = 0;
        for (int i = 0; i < n; i++) {
            sumA += a[i];
            sumB += b[i];
        }
        // 起点 l = 0(原串不移位)
        int ans = Math.min(Math.min(sumA, n - sumA), Math.min(sumB, n - sumB));
        // 起点 l = 1..n-1
        for (int l = 1; l < n; l++) {
            sumA += a[l + n - 1] - a[l - 1];
            sumB += b[l + n - 1] - b[l - 1];
            int d0 = (l % 2 == 0) ? sumA : n - sumA;
            int d1 = (l % 2 == 0) ? sumB : n - sumB;
            ans = Math.min(ans, Math.min(d0, d1));
        }
        return ans;
    }

    public int minFlipsBrute(String s) {
        int n = s.length();
        String t = s + s;
        int ans = Integer.MAX_VALUE;
        for (int l = 0; l < n; l++) {
            int d0 = 0, d1 = 0;
            for (int k = 0; k < n; k++) {
                char c = t.charAt(l + k);
                if (c != (k % 2 == 0 ? '0' : '1')) d0++;
                if (c != (k % 2 == 0 ? '1' : '0')) d1++;
            }
            ans = Math.min(ans, Math.min(d0, d1));
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        String[] cases = {"111000", "010", "1110", "000", "111", "101", "01010", "1001", "1", "0",
                "010010", "0011001", "1010101", "11001011"};
        boolean ok = true;
        for (String c : cases) {
            int r1 = s.minFlips(c);
            int r2 = s.minFlipsBrute(c);
            ok &= (r1 == r2);
            System.out.println(c + " -> " + r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析


1889. Minimum Space Wasted From Packaging(装包裹的最小浪费空间)

题目描述

给你 n 个包裹,你需要把它们装在箱子里,每个箱子装一个包裹。总共有 m 个供应商提供不同尺寸的

箱子(每个规格都有无数个箱子)。如果一个包裹的尺寸小于等于一个箱子的尺寸,那么这个包裹就可以

放入这个箱子之中。

包裹的尺寸用一个整数数组 packages 表示,其中 packages[i] 是第 i 个包裹的尺寸。供应商用二维

数组 boxes 表示,其中 boxes[j] 是第 j 个供应商提供的所有箱子尺寸的数组。

你想要选择一个供应商并只使用该供应商提供的箱子,使得总浪费空间最小。对于每个装了包裹的箱子,

我们定义浪费的空间等于 箱子的尺寸 - 包裹的尺寸。总浪费空间为所有箱子中浪费空间的总和。

请你选择最优箱子供应商,使得总浪费空间最小。如果无法将所有包裹放入箱子中,请你返回 -1。

由于答案可能会很大,请返回它对 10^9 + 7 取余的结果。

示例 1

  • 输入 :packages = 2,3,5, boxes = \[4,8,2,8]
  • 输出:6

示例 2

  • 输入 :packages = 2,3,5, boxes = \[1,4,2,3,3,4]
  • 输出:-1

示例 3

  • 输入 :packages = 3,5,8,10,11,12, boxes = \[12,11,9,10,5,14]
  • 输出 :9
    提示:
  • - n == packages.length
  • - m == boxes.length
  • - 1 <= n <= 10^5
    • 1 <= packages[i], boxes[j] 中各元素 <= 10^5
  • - sum(boxes[j].length) <= 10^5
    • boxes[j] 中的元素互不相同

解题思路

对每个供应商,其最优装法:包裹排升序、箱子排升序,从小到大依次用最小的能装下包裹的箱子装。

浪费和 = Σ 每个箱子尺寸 * 用它装的包裹数 - Σ 包裹总尺寸。后者是常量,因此只需最小化

"Σ 箱子尺寸 * 装入数"。

对某供应商:把箱子排序,对每个箱子 b,用二分(排序后的包裹)找到"用前一个箱子装不下的首

个包裹下标"与"能装进 b 的最后一个包裹下标",区间内的包裹都用 b 装,贡献

b * 区间个数,用包裹前缀和快速得到包裹尺寸和,累加贡献与包裹和之差即浪费。

解法一:排序 + 二分 + 前缀和(主提交版本)

O((n + sum|boxes|) log n)。

解法二:相同框架、用指针扫描替代二分,对照

箱子升序时"能装下的包裹区间"用双指针维护,

避免重复二分,便于对照理解。

代码实现

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

public class Solution {
    private static final int MOD = 1000000007;

    public int minWastedSpace(int[] packages, int[][] boxes) {
        Arrays.sort(packages);
        int n = packages.length;
        long[] pref = new long[n + 1];
        for (int i = 0; i < n; i++) {
            pref[i + 1] = pref[i] + packages[i];
        }
        long total = pref[n];
        long best = Long.MAX_VALUE;
        for (int[] box : boxes) {
            Arrays.sort(box);
            if (box[box.length - 1] < packages[n - 1]) {
                continue;                      // 该供应商装不下最大包裹
            }
            long used = 0;
            int last = 0;                      // 已装入的包裹数
            for (int b : box) {
                // 找到 packages 中 <= b 的最大下标(全局二分)
                int hi = upperBound(packages, b);
                if (hi <= last) continue;      // 这个箱子没有新的可用包裹
                used += (long) b * (hi - last);
                last = hi;
                if (last == n) break;
            }
            if (last < n) {
                continue;                      // 装不完(理论上不会,因为最大箱已覆盖)
            }
            best = Math.min(best, used - total);
        }
        return best == Long.MAX_VALUE ? -1 : (int) (best % MOD);
    }

    private int upperBound(int[] a, int key) {
        int lo = 0, hi = a.length;
        while (lo < hi) {
            int mid = (lo + hi) >>> 1;
            if (a[mid] <= key) lo = mid + 1;
            else hi = mid;
        }
        return lo;
    }

    public int minWastedSpaceTwoPointer(int[] packages, int[][] boxes) {
        Arrays.sort(packages);
        int n = packages.length;
        long[] pref = new long[n + 1];
        for (int i = 0; i < n; i++) {
            pref[i + 1] = pref[i] + packages[i];
        }
        long total = pref[n];
        long best = Long.MAX_VALUE;
        for (int[] box : boxes) {
            Arrays.sort(box);
            if (box[box.length - 1] < packages[n - 1]) {
                continue;
            }
            long used = 0;
            int last = 0;
            for (int b : box) {
                int hi = last;
                while (hi < n && packages[hi] <= b) {
                    hi++;
                }
                if (hi > last) {
                    used += (long) b * (hi - last);
                    last = hi;
                }
                if (last == n) break;
            }
            if (last == n) {
                best = Math.min(best, used - total);
            }
        }
        return best == Long.MAX_VALUE ? -1 : (int) (best % MOD);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] packages = {{2, 3, 5}, {2, 3, 5}, {3, 5, 8, 10, 11, 12}, {1, 1}, {10}};
        int[][][] boxes = {
                {{4, 8}, {2, 8}},
                {{1, 4}, {2, 3}, {3, 4}},
                {{12}, {11, 9}, {10, 5, 14}},
                {{1, 2}, {2, 2}},
                {{5}, {10}}
        };
        boolean ok = true;
        for (int t = 0; t < packages.length; t++) {
            int r1 = s.minWastedSpace(packages[t], boxes[t]);
            int r2 = s.minWastedSpaceTwoPointer(packages[t], boxes[t]);
            ok &= (r1 == r2);
            System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析


1890. The Latest Login in 2020(2020年最后一次登录)

题目类型:数据库(SQL)

题目描述

表:Logins

复制代码
+----------------+----------+
| 列名           | 类型      |
+----------------+----------+
| user_id        | int      |
| time_stamp     | datetime |
+----------------+----------+

(user_id, time_stamp) 是这个表的主键。每一行包含的信息是 user_id 这个用户的登录时间。

编写解决方案以获取在 2020 年登录过的所有用户的本年度最后一次登录时间。结果集不包含 2020 年没有登录过的用户。

示例 1:

复制代码
输入:
Logins 表:
+---------+---------------------+
| user_id | time_stamp          |
+---------+---------------------+
| 6       | 2020-06-30 15:06:07 |
| 6       | 2021-04-21 14:06:06 |
| 6       | 2019-03-07 00:18:15 |
| 8       | 2020-02-01 05:10:53 |
| 8       | 2020-12-30 00:46:50 |
| 2       | 2020-01-16 02:49:50 |
| 2       | 2019-08-25 07:59:08 |
| 14      | 2019-07-14 09:00:00 |
| 14      | 2021-01-06 11:59:59 |
+---------+---------------------+
输出:
+---------+---------------------+
| user_id | last_stamp          |
+---------+---------------------+
| 6       | 2020-06-30 15:06:07 |
| 8       | 2020-12-30 00:46:50 |
| 2       | 2020-01-16 02:49:50 |
+---------+---------------------+

解题思路

必须先过滤 2020 年再取最大值 :先 WHERE time_stamp 限定在 2020 年内,再按 user_id 分组

取 MAX(time_stamp);否则用户 6 的 2021-04-21 会污染结果。

代码实现

sql 复制代码
SELECT user_id, MAX(time_stamp) AS last_stamp
FROM Logins
WHERE time_stamp BETWEEN '2020-01-01 00:00:00' AND '2020-12-31 23:59:59'
GROUP BY user_id;

复杂度分析

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


1891. Cutting Ribbons(割绳子)

题目描述

给定一个整数数组 ribbons 和一个整数 k,数组每项 ribbons[i] 表示第 i 条绳子的长度。对于每条

绳子,你可以将任意切割成一系列长度为正整数的部分,或者选择不进行切割。

例如,如果给你一条长度为 4 的绳子,你可以保持长度为 4 不变,或切割成 3+1、2+2、2+1+1、

1+1+1+1。

你的任务是找出最大 x 值,要求满足可以裁切出至少 k 条长度均为 x 的绳子。你可以丢弃裁切后剩余

的任意长度的绳子。如果不可能切割出 k 条相同长度的绳子,返回 0。

示例 1

  • 输入 :ribbons = 9,7,5, k = 3
  • 输出:5
  • 解释:第一条切成 5+4,第二条切成 5+2,第三条不切,得到 3 条长度为 5 的绳子。

示例 2

  • 输入 :ribbons = 7,5,9, k = 4
  • 输出:4

示例 3

  • 输入 :ribbons = 5,7,9, k = 22
  • 输出:0
  • 解释 :由于绳子长度需要为正整数,无法得到 22 条长度相同的绳子。
    提示:
  • - 1 <= ribbons.length <= 10^5
  • - 1 <= ribbons[i] <= 10^5
  • - 1 <= k <= 10^9

解题思路

对目标长度 x,能从所有绳子上切出的段数为 Σ (ribbonsi / x);x 越大段数越少,单调可二分。

二分下界 1、上界 max(ribbons);若 x=1 时段数都 < k,返回 0。

解法一:二分答案,主提交版本

O(n log maxLen)。

解法二:线性递降,对照

从 maxLen 逐 1 下降尝试(小规模),验证二分正确性。

代码实现

java 复制代码
public class Solution {

    public int maxLength(int[] ribbons, int k) {
        int max = 0;
        for (int v : ribbons) {
            if (v > max) max = v;
        }
        int lo = 1, hi = max, ans = 0;
        while (lo <= hi) {
            int mid = (lo + hi) >>> 1;
            long cnt = 0;
            for (int v : ribbons) {
                cnt += v / mid;
                if (cnt >= k) break;   // 提前终止
            }
            if (cnt >= k) {
                ans = mid;
                lo = mid + 1;
            } else {
                hi = mid - 1;
            }
        }
        return ans;
    }

    public int maxLengthLinear(int[] ribbons, int k) {
        int max = 0;
        for (int v : ribbons) {
            if (v > max) max = v;
        }
        for (int x = max; x >= 1; x--) {
            long cnt = 0;
            for (int v : ribbons) {
                cnt += v / x;
            }
            if (cnt >= k) return x;
        }
        return 0;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] rbs = {{9, 7, 5}, {7, 5, 9}, {5, 7, 9}, {10, 10, 10}, {1}, {100000}};
        int[] ks = {3, 4, 22, 5, 1, 50000};
        boolean ok = true;
        for (int t = 0; t < rbs.length; t++) {
            int r1 = s.maxLength(rbs[t], ks[t]);
            int r2 = s.maxLengthLinear(rbs[t], ks[t]);
            ok &= (r1 == r2);
            System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析


1892. Page Recommendations II(页面推荐Ⅱ)

题目类型:数据库(SQL)

题目描述

表:Friendship

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| user1_id      | int     |
| user2_id      | int     |
+---------------+---------+

(user1_id, user2_id) 是 Friendship 表的主键。该表的每一行表示用户 user1_id 和 user2_id 是好友。

表:Likes

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

(user_id, page_id) 是 Likes 表的主键。该表的每一行表示 user_id 喜欢 page_id。

如果页面被 user_id 的至少一个朋友喜欢 ,而不被 user_id 喜欢 ,系统将推荐该页面给 user_id。编写解决方案查找针对每个用户的所有可能的页面建议:user_id(系统向其提出建议的用户)、page_id(推荐页面)、friends_likes(喜欢该页面的好友数)。

示例 1:

复制代码
输入:
Friendship 表:                  Likes 表:
+----------+----------+   +---------+---------+
| user1_id | user2_id |   | user_id | page_id |
+----------+----------+   +---------+---------+
| 1        | 2        |   | 1       | 88      |
| 1        | 3        |   | 2       | 23      |
| 1        | 4        |   | 3       | 24      |
| 2        | 3        |   | 4       | 56      |
| 2        | 4        |   | 5       | 11      |
| 2        | 5        |   | 6       | 33      |
| 6        | 1        |   | 2       | 77      |
+----------+----------+   | 3       | 77      |
                          | 6       | 88      |
                          +---------+---------+
输出:
+---------+---------+---------------+
| user_id | page_id | friends_likes |
+---------+---------+---------------+
| 1       | 77      | 2             |
| 1       | 23      | 1             |
| ...     | ...     | ...           |
| 5       | 77      | 1             |
| 5       | 23      | 1             |
+---------+---------+---------------+

解释:以用户 1 为例,其好友为 2、3、4、6;好友喜欢的页面中排除自己已喜欢的 88,得到 77(2 个好友喜欢)、23、24、56、33。

解题思路

① 把 Friendship 双向展开成 (user_id, friend_id) 视图(UNION 去重);

② 连接 Likes 取好友喜欢的页面,并按 (user_id, page_id) 分组统计好友数;

③ 用 LEFT JOIN 自己的 Likes 排除自己已喜欢的页面。

代码实现

sql 复制代码
WITH friends AS (
    SELECT user1_id AS user_id, user2_id AS friend_id FROM Friendship
    UNION
    SELECT user2_id, user1_id FROM Friendship
)
SELECT f.user_id, l.page_id, COUNT(*) AS friends_likes
FROM friends f
JOIN Likes l ON f.friend_id = l.user_id
LEFT JOIN Likes own
  ON f.user_id = own.user_id AND own.page_id = l.page_id
WHERE own.user_id IS NULL
GROUP BY f.user_id, l.page_id;

复杂度分析

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


1893. Check if All the Integers in a Range Are Covered

题目描述

(检查是否区域内所有整数都被覆盖)

给你一个二维整数数组 ranges 和两个整数 left 和 right。每个 ranges[i] = `starti`, `endi`

表示一个从 starti 到 endi 的闭区间。

如果闭区间 `left`, `right` 内每个整数都被 ranges 中至少一个区间覆盖,那么请你返回 true,

否则返回 false。

已知区间 ranges[i] = `starti`, `endi`,如果整数 x 满足 starti <= x <= endi,那么我们称整数 x

被覆盖了。

示例 1

  • 输入 :ranges = \[1,2,3,4,5,6], left = 2, right = 5
  • 输出 :true

示例 2

  • 输入 :ranges = \[1,10,10,20], left = 21, right = 21
  • 输出 :false
    提示:
  • - 1 <= ranges.length <= 50
  • - 1 <= starti <= endi <= 50
  • - 1 <= left <= right <= 50

解题思路

解法一:布尔数组覆盖标记,主提交版本

对每个区间标记覆盖点(值域 <= 50),最后检查

left, right 内每个点是否都被标记。O(50 * ranges.length)。

解法二:差分数组,对照

用差分数组对区间 +1/-1,做前缀和得到每个点的覆盖次数,

再检查 left, right 全 > 0;O(maxVal + ranges.length),语义更工程化。

代码实现

java 复制代码
public class Solution {

    public boolean isCovered(int[][] ranges, int left, int right) {
        boolean[] cover = new boolean[55];
        for (int[] r : ranges) {
            for (int x = r[0]; x <= r[1]; x++) {
                cover[x] = true;
            }
        }
        for (int x = left; x <= right; x++) {
            if (!cover[x]) {
                return false;
            }
        }
        return true;
    }

    public boolean isCoveredDiff(int[][] ranges, int left, int right) {
        int[] diff = new int[55];
        for (int[] r : ranges) {
            diff[r[0]]++;
            diff[r[1] + 1]--;
        }
        int cur = 0;
        for (int x = 1; x <= 50; x++) {
            cur += diff[x];
            if (x >= left && x <= right && cur <= 0) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][][] rng = {
                {{1, 2}, {3, 4}, {5, 6}},
                {{1, 10}, {10, 20}},
                {{1, 50}},
                {{2, 4}},
                {{1, 3}, {5, 8}}
        };
        int[] ls = {2, 21, 1, 1, 4};
        int[] rs = {5, 21, 50, 4, 4};
        boolean ok = true;
        for (int t = 0; t < rng.length; t++) {
            boolean r1 = s.isCovered(rng[t], ls[t], rs[t]);
            boolean r2 = s.isCoveredDiff(rng[t], ls[t], rs[t]);
            ok &= (r1 == r2);
            System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析


1894. Find the Student that Will Replace the Chalk

题目描述

(找到需要补充粉笔的学生编号)

一个班级里有 n 个学生,编号为 0 到 n - 1。每个学生会依次回答问题,编号为 0 的学生先回答,

然后是编号为 1 的学生,以此类推,直到编号为 n - 1 的学生,然后老师会重复这个过程,重新从

编号为 0 的学生开始回答问题。

给你一个长度为 n 且下标从 0 开始的整数数组 chalk 和一个整数 k。一开始粉笔盒里总共有 k 支粉笔。

当编号为 i 的学生回答问题时,他会消耗 chalk[i] 支粉笔。如果剩余粉笔数量严格小于 chalk[i],

那么学生 i 需要补充粉笔。

请你返回需要补充粉笔的学生编号。

示例 1

  • 输入 :chalk = 5,1,5, k = 22
  • 输出:0

示例 2

  • 输入 :chalk = 3,4,1,2, k = 25
  • 输出 :1
    提示:
  • - chalk.length == n
  • - 1 <= n <= 10^5
  • - 1 <= chalk[i] <= 10^5
  • - 1 <= k <= 10^9

解题思路

一轮完整循环消耗 sum(chalk)。k 可以先对 sum 取余:k %= sum,此时 k < sum,只需走不到一轮。

解法一:取余 + 线性扫描,主提交版本

先求总消耗并对 k 取余,然后顺序遍历 chalk 累减,

找到第一个 k < chalki 的学生。O(n)。

解法二:前缀和 + 二分,对照

构造前缀和(模 sum 后),对剩余 k 二分找第一个前缀和 > k

的下标即为答案。O(n) 建表 + O(log n) 查询。

代码实现

java 复制代码
public class Solution {

    public int chalkReplacer(int[] chalk, int k) {
        long sum = 0;
        for (int v : chalk) {
            sum += v;
        }
        long remain = k % sum;   // 完整循环后剩余的粉笔
        for (int i = 0; i < chalk.length; i++) {
            if (remain < chalk[i]) {
                return i;
            }
            remain -= chalk[i];
        }
        return 0;   // 理论上走不到
    }

    public int chalkReplacerBinary(int[] chalk, int k) {
        int n = chalk.length;
        long[] pref = new long[n];
        pref[0] = chalk[0];
        for (int i = 1; i < n; i++) {
            pref[i] = pref[i - 1] + chalk[i];
        }
        long remain = k % pref[n - 1];
        // 找第一个 pref[i] > remain 的下标
        int lo = 0, hi = n - 1;
        while (lo < hi) {
            int mid = (lo + hi) >>> 1;
            if (pref[mid] > remain) hi = mid;
            else lo = mid + 1;
        }
        return lo;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] chalks = {{5, 1, 5}, {3, 4, 1, 2}, {1, 1, 1}, {100000}, {2, 3, 5, 7}};
        int[] ks = {22, 25, 1, 999999, 10};
        boolean ok = true;
        for (int t = 0; t < chalks.length; t++) {
            int r1 = s.chalkReplacer(chalks[t], ks[t]);
            int r2 = s.chalkReplacerBinary(chalks[t], ks[t]);
            ok &= (r1 == r2);
            System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析


1895. Largest Magic Square(最大的幻方)

题目描述

一个 k x k 的幻方指的是一个 k x k 填满整数的方格阵,且每一行、每一列以及两条对角线的和

全部相等。幻方中的整数不需要互不相同。显然,每个 1 x 1 的方格都是一个幻方。

给你一个 m x n 的整数矩阵 grid,请你返回矩阵中最大幻方的尺寸(即边长 k)。

示例 1

  • 输入 :grid = \[7,1,4,5,6,2,5,1,6,4,1,5,4,3,2,1,2,7,3,4]
  • 输出:3
  • 解释:最大幻方尺寸为 3,其行、列、对角线之和都等于 12。

示例 2

  • 输入 :grid = \[5,1,3,1,9,3,3,1,1,3,3,8]
  • 输出 :2
    提示:
  • - m == grid.length
  • - n == grid[i].length
  • - 1 <= m, n <= 50
  • - 1 <= grid[i][j] <= 10^5

解题思路

解法一:行/列前缀和 + 两条对角线前缀和,主提交版本

预计算行前缀 rowPref、列前缀 colPref

以及主、副对角线前缀 diag1/diag2。从大到小枚举边长 k,对每个左上角 (i, j) 检查:

行和、列和(用前缀 O(1))、两条对角线和(用 diagonal 前缀 O(1))都等于首行和基准。

找到的第一个可行 k 即最大。O(mn min(m,n))。

解法二:暴力逐格校验,对照

对每个候选方阵直接累加各行/列/对角线,O(k) 每次校验,

O(mnmin(m,n)^2),小规模对照验证。

代码实现

java 复制代码
public class Solution {

    public int largestMagicSquare(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        long[][] rowPref = new long[m][n + 1];
        long[][] colPref = new long[m + 1][n];
        long[][] d1 = new long[m][n];   // 主对角线(↘)前缀和
        long[][] d2 = new long[m][n];   // 副对角线(↙)前缀和
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                rowPref[i][j + 1] = rowPref[i][j] + grid[i][j];
                colPref[i + 1][j] = colPref[i][j] + grid[i][j];
                d1[i][j] = grid[i][j] + (i - 1 >= 0 && j - 1 >= 0 ? d1[i - 1][j - 1] : 0);
                d2[i][j] = grid[i][j] + (i - 1 >= 0 && j + 1 < n ? d2[i - 1][j + 1] : 0);
            }
        }
        for (int k = Math.min(m, n); k >= 1; k--) {
            for (int i = 0; i + k <= m; i++) {
                for (int j = 0; j + k <= n; j++) {
                    // 基准 = 首行和
                    long base = rowPref[i][j + k] - rowPref[i][j];
                    boolean ok = true;
                    // 各行
                    for (int r = i; r < i + k; r++) {
                        if (rowPref[r][j + k] - rowPref[r][j] != base) { ok = false; break; }
                    }
                    if (!ok) continue;
                    // 各列
                    for (int c = j; c < j + k; c++) {
                        if (colPref[i + k][c] - colPref[i][c] != base) { ok = false; break; }
                    }
                    if (!ok) continue;
                    // 主对角线:从 (i,j) 到 (i+k-1,j+k-1)
                    long dd1 = d1[i + k - 1][j + k - 1]
                            - (i - 1 >= 0 && j - 1 >= 0 ? d1[i - 1][j - 1] : 0);
                    if (dd1 != base) continue;
                    // 副对角线:从 (i,j+k-1) 到 (i+k-1,j)
                    long dd2 = d2[i + k - 1][j]
                            - (i - 1 >= 0 && j + k < n ? d2[i - 1][j + k] : 0);
                    if (dd2 != base) continue;
                    return k;
                }
            }
        }
        return 1;
    }

    public int largestMagicSquareBrute(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        for (int k = Math.min(m, n); k >= 1; k--) {
            for (int i = 0; i + k <= m; i++) {
                for (int j = 0; j + k <= n; j++) {
                    long base = 0;
                    for (int c = j; c < j + k; c++) base += grid[i][c];
                    boolean ok = true;
                    for (int r = i; r < i + k; r++) {
                        long s = 0;
                        for (int c = j; c < j + k; c++) s += grid[r][c];
                        if (s != base) { ok = false; break; }
                    }
                    if (!ok) continue;
                    for (int c = j; c < j + k; c++) {
                        long s = 0;
                        for (int r = i; r < i + k; r++) s += grid[r][c];
                        if (s != base) { ok = false; break; }
                    }
                    if (!ok) continue;
                    long dd1 = 0, dd2 = 0;
                    for (int t = 0; t < k; t++) {
                        dd1 += grid[i + t][j + t];
                        dd2 += grid[i + t][j + k - 1 - t];
                    }
                    if (dd1 == base && dd2 == base) return k;
                }
            }
        }
        return 1;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] g1 = {{7, 1, 4, 5, 6}, {2, 5, 1, 6, 4}, {1, 5, 4, 3, 2}, {1, 2, 7, 3, 4}};
        int[][] g2 = {{5, 1, 3, 1}, {9, 3, 3, 1}, {1, 3, 3, 8}};
        int[][] g3 = {{2, 2, 2, 2}, {2, 2, 2, 2}, {2, 2, 2, 2}};
        int[][] g4 = {{1}};
        int[][][] cases = {g1, g2, g3, g4};
        boolean ok = true;
        for (int[][] c : cases) {
            int r1 = s.largestMagicSquare(c);
            int r2 = s.largestMagicSquareBrute(c);
            ok &= (r1 == r2);
            System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析


1896. Minimum Cost to Change the Final Value of Expression

题目描述

(反转表达式值的最少操作次数)

给你一个有效的布尔表达式,用字符串 expression 表示。这个字符串包含字符 '1'、'0'、'&'

(按位与运算)、'|'(按位或运算)、'(' 和 ')'。

比方说,"()1|1" 和 "(1)&()" 不是有效布尔表达式。而 "1"、"(((1))|(0))" 和 "1|(0&(1))"

是有效布尔表达式。

你的目标是将布尔表达式的值反转(也就是将 0 变为 1,或者将 1 变为 0),请你返回达成目标需要的

最少操作次数。可执行的操作如下:

  • 将一个 '1' 变成一个 '0'。
  • 将一个 '0' 变成一个 '1'。
  • 将一个 '&' 变成一个 '|'。
  • 将一个 '|' 变成一个 '&'。
    注意:'&' 的运算优先级与 '|' 相同。计算表达式时,括号优先级最高,然后按照从左到右的顺序运算。

示例 1

  • 输入 :expression = "1&(0|1)"
  • 输出:1
  • 解释:将 '|' 变成 '&',得到 "1&(0&1)",值为 0。

示例 2

  • 输入 :expression = "(0&0)&(0&0&0)"
  • 输出:3

示例 3

  • 输入 :expression = "(0|(1|0&1))"
  • 输出 :1
    提示:
  • - 1 <= expression.length <= 10^5
    • expression 只包含 '1'、'0'、'&'、'|'、'(' 和 ')'
    • 所有括号都有与之匹配的对应括号。
    • 不会有空的括号(也就是说 "()" 不是 expression 的子字符串)。

解题思路

自底向上 DP:对每个子表达式维护 (z, o) 两个数------z 是把它的值改成 0 的最小操作数,

o 是把它的值改成 1 的最小操作数。

  • 叶子 '0':(0, 1);叶子 '1':(1, 0)。
  • 组合 A op B(op 为 '&' 或 '|'):
    & 的结果为 0 当且仅当至少一侧为 0:
    z = min(A.z + min(B.z, B.o), min(A.z, A.o) + B.z)
    & 的结果为 1 当且仅当两侧都为 1(或把运算符改成 '|' 后至少一侧为 1,代价 +1):
    o = min(A.o + B.o, 1 + min(A.o + min(B.z, B.o), min(A.z, A.o) + B.o))
    | 对称:
    o = min(A.o + min(B.z, B.o), min(A.z, A.o) + B.o)
    z = min(A.z + B.z, 1 + min(A.z + min(B.z, B.o), min(A.z, A.o) + B.z))
    表达式求值用两个栈模拟(值栈 + 运算符栈),'&' 与 '|' 同级从左到右结合,遇新运算符先合
    并前一个。最终答案:若根表达式当前值为 v,翻转代价 = v == 1 ? root.z : root.o。
    解法一:栈模拟 + DP 合并(主提交版本)
    O(n) 时间、O(n) 空间。
    解法二:递归下降解析 + 同一 DP,对照
    按括号递归求值,便于阅读理解组合规则。

代码实现

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

public class Solution {

    private static class Node {
        int z;
        int o;

        Node(int z, int o) {
            this.z = z;
            this.o = o;
        }
    }

    public int minOperationsToFlip(String expression) {
        Deque<Node> vals = new ArrayDeque<>();
        Deque<Character> ops = new ArrayDeque<>();
        for (char c : expression.toCharArray()) {
            if (c == '0' || c == '1') {
                vals.push(c == '0' ? new Node(0, 1) : new Node(1, 0));
            } else if (c == '(') {
                ops.push(c);
            } else if (c == ')') {
                if (!ops.isEmpty() && ops.peek() != '(') {
                    char op = ops.pop();
                    Node right = vals.pop();
                    Node left = vals.pop();
                    vals.push(merge(left, right, op));
                }
                ops.pop();                     // 弹出 '('
            } else {                           // '&' 或 '|'
                if (!ops.isEmpty() && ops.peek() != '(') {
                    char op = ops.pop();       // 同优先级从左到右:先合并前一运算符
                    Node right = vals.pop();
                    Node left = vals.pop();
                    vals.push(merge(left, right, op));
                }
                ops.push(c);
            }
        }
        while (!ops.isEmpty()) {               // 收尾:合并剩余
            char op = ops.pop();
            Node right = vals.pop();
            Node left = vals.pop();
            vals.push(merge(left, right, op));
        }
        Node root = vals.pop();
        int curV = root.z == 0 ? 0 : 1;        // 当前值
        return curV == 1 ? root.z : root.o;
    }

    private Node merge(Node a, Node b, char op) {
        if (op == '&') {
            int z = Math.min(a.z + Math.min(b.z, b.o), Math.min(a.z, a.o) + b.z);
            int o = Math.min(a.o + b.o,
                    1 + Math.min(a.o + Math.min(b.z, b.o), Math.min(a.z, a.o) + b.o));
            return new Node(z, o);
        }
        // '|'
        int o = Math.min(a.o + Math.min(b.z, b.o), Math.min(a.z, a.o) + b.o);
        int z = Math.min(a.z + b.z,
                1 + Math.min(a.z + Math.min(b.z, b.o), Math.min(a.z, a.o) + b.z));
        return new Node(z, o);
    }

    public int minOperationsToFlipParse(String expression) {
        int[] idx = {0};
        Node root = parse(expression, idx);
        int curV = root.z == 0 ? 0 : 1;
        return curV == 1 ? root.z : root.o;
    }

    private Node parse(String s, int[] idx) {
        Node left = null;
        char pending = 0;
        while (idx[0] < s.length()) {
            char c = s.charAt(idx[0]++);
            if (c == '(') {
                Node sub = parse(s, idx);
                left = combine(left, sub, pending);
            } else if (c == ')') {
                break;
            } else if (c == '0' || c == '1') {
                Node num = new Node(c == '0' ? 0 : 1, c == '0' ? 1 : 0);
                left = combine(left, num, pending);
            } else {                           // '&' 或 '|'
                pending = c;
            }
        }
        return left;
    }

    private Node combine(Node a, Node b, char op) {
        if (a == null) return b;
        return merge(a, b, op);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        String[] cases = {
                "1&(0|1)",          // 1
                "(0&0)&(0&0&0)",    // 3
                "(0|(1|0&1))",      // 1
                "1",                // 1
                "0",                // 1
                "1|1",              // 1
                "1&1",              // 1
                "0&1",              // 1
                "((1))",            // 1
                "1|0",              // 1
                "(1|0)&(0&1)",      // ?
                "0|0"               // 1
        };
        boolean ok = true;
        for (String c : cases) {
            int r1 = s.minOperationsToFlip(c);
            int r2 = s.minOperationsToFlipParse(c);
            ok &= (r1 == r2);
            System.out.println(c + " -> " + r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析


1897. Redistribute Characters to Make All Strings Equal

题目描述

(重新分配字符使所有字符串都相等)

给你一个字符串数组 words(下标从 0 开始计数)。

在一步操作中,需先选出两个不同下标 i 和 j,其中 words[i] 是一个非空字符串,接着将 words[i]

中的任一字符移动到 words[j] 中的任一位置上。

如果执行任意步操作可以使 words 中的每个字符串都相等,返回 true;否则,返回 false。

示例 1

  • 输入 :words = "`abc`","`aabc`","`bc`"
  • 输出 :true
  • 解释 :将 words[1] 中的第一个 'a' 移动到 words[2] 的最前面,所有字符串都等于 "abc"。

示例 2

  • 输入 :words = "`ab`","`a`"
  • 输出 :false
  • 解释 :执行操作无法使所有字符串都相等。
    提示:
  • - 1 <= words.length <= 100
  • - 1 <= words[i].length <= 100
    • words[i] 由小写英文字母组成

解题思路

字符可以在任意字符串之间自由移动,因此"能否全部相等"等价于"每个字符的总出现次数都能被

words.length 整除"(最终每个字符串会得到恰好 wordsi.length == 总长/m 个字符,

且每个字母均匀分配)。

解法一:26 字母频次整除判断,主提交版本

统计所有字符串中每个字母出现的总次数,

检查每个频次 % m == 0。O(总字符数)。

解法二:HashMap 等价实现,对照

用 Map<Character,Integer> 统计后遍历判断可整除,

与解法一逻辑完全一致、数据结构不同,便于理解。

代码实现

java 复制代码
import java.util.HashMap;
import java.util.Map;

public class Solution {

    public boolean makeEqual(String[] words) {
        int m = words.length;
        int[] cnt = new int[26];
        for (String w : words) {
            for (char c : w.toCharArray()) {
                cnt[c - 'a']++;
            }
        }
        for (int v : cnt) {
            if (v % m != 0) {
                return false;
            }
        }
        return true;
    }

    public boolean makeEqualMap(String[] words) {
        int m = words.length;
        Map<Character, Integer> freq = new HashMap<>();
        for (String w : words) {
            for (char c : w.toCharArray()) {
                freq.put(c, freq.getOrDefault(c, 0) + 1);
            }
        }
        for (int v : freq.values()) {
            if (v % m != 0) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        String[][] cases = {
                {"abc", "aabc", "bc"},
                {"ab", "a"},
                {"a", "a", "a"},
                {"aa", "bb"},
                {"ab", "ab", "cd"},
                {"x"}
        };
        boolean ok = true;
        for (String[] c : cases) {
            boolean r1 = s.makeEqual(c);
            boolean r2 = s.makeEqualMap(c);
            ok &= (r1 == r2);
            System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析


1898. Maximum Number of Removable Characters(可移除字符的最大数目)

题目描述

给你两个字符串 s 和 p,其中 p 是 s 的一个子序列。同时,给你一个元素互不相同且下标从 0 开始

计数的整数数组 removable,该数组是 s 中下标的一个子集(s 的下标也从 0 开始计数)。

请你找出一个整数 k(0 <= k <= removable.length),使得在按顺序移除 removable 中前 k 个

下标对应的字符后,p 仍然是 s 的一个子序列。返回你可以找到的最大 k。

字符串的一个子序列是一个由原字符串生成的新字符串,生成过程中可能会移除原字符串中的一些字符

(也可能不移除)但不改变剩余字符之间的相对顺序。

示例 1

  • 输入 :s = "abcacb", p = "ab", removable = 3,1,0
  • 输出:2

示例 2

  • 输入 :s = "abcbddddd", p = "abcd", removable = 3,2,1,4,5,6
  • 输出:1

示例 3

  • 输入 :s = "abcab", p = "abc", removable = 0,1,2,3,4
  • 输出 :0
    提示:
  • - 1 <= p.length < s.length <= 10^5
  • - 0 <= removable.length < s.length
  • - 0 <= removable[i] < s.length
    • p 是 s 的一个子序列
    • s 和 p 都由小写英文字母组成
    • removable 中的元素互不相同

解题思路

k 可行则 k-1 也可行(单调性),二分答案。check(k):标记 removable0...k-1 的下标为已删除,

然后双指针判断 p 是否仍是 s 的子序列。

解法一:二分 + 标记数组,主提交版本

O((n+m) log |removable|),标记用 boolean 每次重建。

解法二:二分 + 序号版本,对照

用额外数组 pos 标记"下标 i 的删除序号",check 时判断

当前字符是否属于前 k 个删除(posi < k),避免重复清空 boolean;语义等价。

代码实现

java 复制代码
public class Solution {

    public int maximumRemovals(String s, String p, int[] removable) {
        int lo = 0, hi = removable.length, ans = 0;
        while (lo <= hi) {
            int mid = (lo + hi) >>> 1;
            if (check(s, p, removable, mid)) {
                ans = mid;
                lo = mid + 1;
            } else {
                hi = mid - 1;
            }
        }
        return ans;
    }

    private boolean check(String s, String p, int[] removable, int k) {
        int n = s.length();
        boolean[] removed = new boolean[n];
        for (int i = 0; i < k; i++) {
            removed[removable[i]] = true;
        }
        int j = 0;
        for (int i = 0; i < n && j < p.length(); i++) {
            if (!removed[i] && s.charAt(i) == p.charAt(j)) {
                j++;
            }
        }
        return j == p.length();
    }

    public int maximumRemovalsSet(String s, String p, int[] removable) {
        int n = s.length();
        int lo = 0, hi = removable.length, ans = 0;
        while (lo <= hi) {
            int mid = (lo + hi) >>> 1;
            java.util.Set<Integer> removed = new java.util.HashSet<>();
            for (int i = 0; i < mid; i++) {
                removed.add(removable[i]);
            }
            int j = 0;
            for (int i = 0; i < n && j < p.length(); i++) {
                if (!removed.contains(i) && s.charAt(i) == p.charAt(j)) {
                    j++;
                }
            }
            if (j == p.length()) {
                ans = mid;
                lo = mid + 1;
            } else {
                hi = mid - 1;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        String[] ss = {"abcacb", "abcbddddd", "abcab", "qobftgcueho"};
        String[] ps = {"ab", "abcd", "abc", "obue"};
        int[][] rem = {{3, 1, 0}, {3, 2, 1, 4, 5, 6}, {0, 1, 2, 3, 4}, {5, 3, 0, 6, 4, 9, 10, 7, 2, 8}};
        boolean ok = true;
        for (int t = 0; t < ss.length; t++) {
            int r1 = s.maximumRemovals(ss[t], ps[t], rem[t]);
            int r2 = s.maximumRemovalsSet(ss[t], ps[t], rem[t]);
            ok &= (r1 == r2);
            System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析


1899. Merge Triplets to Form Target Triplet(合并若干三元组以形成目标三元组)

题目描述

三元组是一个由三个整数组成的数组。给你一个二维整数数组 triplets,其中 triplets[i] = `ai`, `bi`, `ci`

表示第 i 个三元组。同时,给你一个整数数组 target = `x`, `y`, `z`,表示你想要得到的三元组。

为了得到 target,你需要对 triplets 执行下面的操作任意次(可能零次):

  • 选出两个下标 i 和 j(i != j),并更新 triplets[j] 为 `max`(`ai`, `aj`), `max`(`bi`, `bj`), `max`(`ci`, `cj`)。
    例如,triplets[i] = 2,5,3 且 triplets[j] = 1,7,5,triplets[j] 将会更新为 2,7,5。
    如果通过以上操作我们可以使得目标三元组 target 成为 triplets 的一个元素,则返回 true;否则,
    返回 false。

示例 1

  • 输入 :triplets = \[2,5,3,1,8,4,1,7,5], target = 2,7,5
  • 输出 :true

示例 2

  • 输入 :triplets = \[1,3,4,2,5,8], target = 2,5,8
  • 输出 :true

示例 3

  • 输入 :triplets = \[2,5,3,2,3,4,1,2,5,5,2,3], target = 5,5,5
  • 输出 :true

示例 4

  • 输入 :triplets = \[3,4,5,4,5,6], target = 3,2,5
  • 输出 :false
    提示:
  • - 1 <= triplets.length <= 10^5
  • - triplets[i].length == target.length == 3
  • - 1 <= ai, bi, ci, x, y, z <= 1000

解题思路

由于操作只会取 max,任何三元组只要某一维 > target 对应维,它就"有害"(参与合并会把该维

顶超过 target),必须丢弃。剩下的"候选"三元组每个维都 <= target,且它们通过多次合并可以

把各维叠到 target:只要候选并集中每一维的 max 恰好覆盖 target 即可(因为候选们共同参与

合并最终得到的三元组 = max(ai), max(bi), max(ci))。

解法一:筛选候选 + 逐维覆盖检查,主提交版本

一趟过滤 + 三个布尔/求最大值。O(n)。

解法二:全程维护叠加结果,对照

对每个候选三元组直接与"当前累计结果"取 max(类似模拟

合并),最终检查累计结果是否恰为 target;常数略大、可读性更好。

代码实现

java 复制代码
public class Solution {

    public boolean mergeTriplets(int[][] triplets, int[] target) {
        int x = target[0], y = target[1], z = target[2];
        int maxA = -1, maxB = -1, maxC = -1;
        for (int[] t : triplets) {
            if (t[0] <= x && t[1] <= y && t[2] <= z) {
                maxA = Math.max(maxA, t[0]);
                maxB = Math.max(maxB, t[1]);
                maxC = Math.max(maxC, t[2]);
            }
        }
        return maxA == x && maxB == y && maxC == z;
    }

    public boolean mergeTripletsAccum(int[][] triplets, int[] target) {
        int[] acc = new int[3];
        for (int[] t : triplets) {
            // 只有全部维度都不超 target 的候选才可参与
            if (t[0] <= target[0] && t[1] <= target[1] && t[2] <= target[2]) {
                for (int d = 0; d < 3; d++) {
                    acc[d] = Math.max(acc[d], t[d]);
                }
            }
        }
        return acc[0] == target[0] && acc[1] == target[1] && acc[2] == target[2];
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][][] tripletsCases = {
                {{2, 5, 3}, {1, 8, 4}, {1, 7, 5}},
                {{1, 3, 4}, {2, 5, 8}},
                {{2, 5, 3}, {2, 3, 4}, {1, 2, 5}, {5, 2, 3}},
                {{3, 4, 5}, {4, 5, 6}},
                {{1, 1, 1}},
                {{2, 2, 2}}
        };
        int[][] targetCases = {{2, 7, 5}, {2, 5, 8}, {5, 5, 5}, {3, 2, 5}, {1, 1, 1}, {1, 1, 1}};
        boolean ok = true;
        for (int t = 0; t < tripletsCases.length; t++) {
            boolean r1 = s.mergeTriplets(tripletsCases[t], targetCases[t]);
            boolean r2 = s.mergeTripletsAccum(tripletsCases[t], targetCases[t]);
            ok &= (r1 == r2);
            System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析


1900. The Earliest and Latest Rounds Where Players Compete

题目描述

(最佳运动员的比拼回合)

n 名运动员参与一场锦标赛,所有运动员站成一排,并根据最开始的站位从 1 到 n 编号。

锦标赛由多个回合组成(从回合 1 开始)。每一回合中,这一排从前往后数的第 i 名运动员需要与

从后往前数的第 i 名运动员比拼,获胜者将会进入下一回合。如果当前回合中运动员数目为奇数,那么

中间那位运动员将轮空晋级下一回合。

每回合结束后,获胜者将会基于最开始分配给他们的原始顺序(升序)重新排成一排。

编号为 firstPlayer 和 secondPlayer 的运动员是本场锦标赛中的最佳运动员。在他们开始比拼之前,

完全可以战胜任何其他运动员。而任意两个其他运动员进行比拼时,其中任意一个都有获胜的可能,

因此你可以裁定谁是这一回合的获胜者。

给你三个整数 n、firstPlayer 和 secondPlayer。返回一个由两个值组成的整数数组,分别表示两位

最佳运动员在本场锦标赛中比拼的最早回合数和最晚回合数。

示例 1

  • 输入 :n = 11, firstPlayer = 2, secondPlayer = 4
  • 输出:3,4

示例 2

  • 输入 :n = 5, firstPlayer = 1, secondPlayer = 5
  • 输出:1,1
  • 解释 :两名最佳运动员 1 和 5 在回合 1 直接比拼。
    提示:
  • - 2 <= n <= 28
  • - 1 <= firstPlayer < secondPlayer <= n

解题思路

把两名最佳运动员记为 F、S(0 基位置 l、r)。DFS 状态 (n, l, r) 表示"当前还有 n 名运动员,

F 在第 l 位、S 在第 r 位",返回 最早相遇回合, 最晚相遇回合(从当前回合起算)。

  • 若 l + r == n - 1(两人本回合就被配成对称对手),返回 1, 1。
  • 否则,本回合有 m = floor(n/2) 对比赛(奇数时中间位轮空直接晋级)。其他运动员的胜负可以
    任意裁定:用 m 位二进制掩码枚举所有比赛结果,并强制 F、S 获胜(其对手淘汰),
    由此可唯一确定两人在下一回合中的位置(新位置 = 排在该选手之前晋级的人数)。
    对每个分支递归得到下一回合的 e, l,用 e+1 / l+1 更新当前状态的 最早, 最晚。
    子状态规模极小(n <= 28 且每次约减半),配合记忆化即可。
    解法一:位掩码枚举比赛结果 + 记忆化 DFS(主提交版本)
    主提交版本。
    解法二:回溯式逐对枚举胜者,对照
    对每个普通配对依次选择胜者(DFS 递归),
    与位掩码枚举完全等价,写法更贴近"递归枚举"的直觉,便于交叉验证。

代码实现

java 复制代码
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class Solution {

    private final Map<Integer, int[]> memo = new HashMap<>();

    public int[] earliestAndLatest(int n, int firstPlayer, int secondPlayer) {
        memo.clear();
        return dfs(n, firstPlayer - 1, secondPlayer - 1);
    }

    private int[] dfs(int n, int l, int r) {
        int key = (n << 10) | (l << 5) | r;
        int[] cached = memo.get(key);
        if (cached != null) {
            return cached;
        }
        if (l + r == n - 1) {                       // 本回合直接对决
            int[] res = {1, 1};
            memo.put(key, res);
            return res;
        }
        int pairs = n / 2;                          // 本回合比赛对数
        int minE = Integer.MAX_VALUE, maxL = 1;
        for (int mask = 0; mask < (1 << pairs); mask++) {
            boolean[] win = new boolean[n];
            for (int j = 0; j < pairs; j++) {       // 配对 (j, n-1-j)
                if ((mask & (1 << j)) != 0) {
                    win[j] = true;
                } else {
                    win[n - 1 - j] = true;
                }
            }
            if ((n & 1) == 1) {
                win[pairs] = true;                  // 中间位轮空晋级
            }
            // 两名最佳运动员必胜
            win[l] = true;
            win[r] = true;
            // 他们的对手必败(注意避开轮空位与选手自身)
            int ol = n - 1 - l;
            int orr = n - 1 - r;
            if (ol != l && ol != r) win[ol] = false;
            if (orr != r && orr != l) win[orr] = false;
            // 计算两人下一回合的位置(排在前面且晋级的人数)
            int a = 0, b = 0, cnt = 0;
            for (int j = 0; j < n; j++) {
                if (j == l) a = cnt;
                if (j == r) b = cnt;
                if (win[j]) cnt++;
            }
            int[] sub = dfs((n + 1) / 2, a, b);
            minE = Math.min(minE, sub[0] + 1);
            maxL = Math.max(maxL, sub[1] + 1);
        }
        int[] res = {minE, maxL};
        memo.put(key, res);
        return res;
    }

    public int[] earliestAndLatestBacktrack(int n, int firstPlayer, int secondPlayer) {
        return dfs2(n, firstPlayer - 1, secondPlayer - 1);
    }

    private final Map<Integer, int[]> memo2 = new HashMap<>();

    private int[] dfs2(int n, int l, int r) {
        int key = (n << 10) | (l << 5) | r;
        int[] cached = memo2.get(key);
        if (cached != null) {
            return cached;
        }
        if (l + r == n - 1) {
            int[] res = {1, 1};
            memo2.put(key, res);
            return res;
        }
        int[] ans = {Integer.MAX_VALUE, 1};
        // 回溯枚举:按配对 (0,n-1), (1,n-2), ... 依次决定"前半赢/后半赢"
        backtrack(0, n, l, r, new boolean[n], ans);
        int[] res = {ans[0], ans[1]};
        memo2.put(key, res);
        return res;
    }

    private void backtrack(int pi, int n, int l, int r, boolean[] win, int[] ans) {
        int pairs = n / 2;
        if (pi == pairs) {
            if ((n & 1) == 1) win[pairs] = true;    // 中间轮空
            win[l] = true;
            win[r] = true;
            int ol = n - 1 - l;
            int orr = n - 1 - r;
            if (ol != l && ol != r) win[ol] = false;
            if (orr != r && orr != l) win[orr] = false;
            int a = 0, b = 0, cnt = 0;
            for (int j = 0; j < n; j++) {
                if (j == l) a = cnt;
                if (j == r) b = cnt;
                if (win[j]) cnt++;
            }
            int[] sub = dfs2((n + 1) / 2, a, b);
            ans[0] = Math.min(ans[0], sub[0] + 1);
            ans[1] = Math.max(ans[1], sub[1] + 1);
            return;
        }
        // 配对 (pi, n-1-pi):左侧赢
        win[pi] = true;
        win[n - 1 - pi] = false;
        backtrack(pi + 1, n, l, r, win, ans);
        win[pi] = false;
        win[n - 1 - pi] = true;
        backtrack(pi + 1, n, l, r, win, ans);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] cases = {
                {11, 2, 4}, {5, 1, 5}, {2, 1, 2}, {4, 1, 2},
                {6, 1, 6}, {8, 3, 4}, {28, 1, 28}, {28, 13, 14},
                {7, 2, 6}, {10, 3, 8}, {3, 1, 3}, {3, 1, 2}
        };
        boolean ok = true;
        for (int[] c : cases) {
            int[] r1 = s.earliestAndLatest(c[0], c[1], c[2]);
            int[] r2 = s.earliestAndLatestBacktrack(c[0], c[1], c[2]);
            boolean same = Arrays.equals(r1, r2);
            ok &= same;
            System.out.println("n=" + c[0] + " f=" + c[1] + " s=" + c[2] + " -> "
                    + Arrays.toString(r1) + " | " + Arrays.toString(r2)
                    + (same ? " OK" : " FAIL"));
        }
        System.out.println(ok ? "ALL OK" : "HAS FAIL");
    }
}

复杂度分析

相关推荐
十碗饭吃不饱1 小时前
后端运维:网络联通、进程查询、Java服务启动脚本
java·运维·网络
T_Wizard1 小时前
后端转 AI 应用开发,我先把这 12 个名词理清了:一张图看懂架构、RAG、Tool Calling 与 MCP
java·ai编程
杨航 AI1 小时前
JEV的用法
java·前端·数据库
灿飞2 小时前
Kotlin与Java虚拟机七:空安全:把十亿美元的错误消灭在类型系统里
java·kotlin
知识搬运临时工2 小时前
JVM 删掉偏向锁之后,一次无竞争锁从 0.3 纳秒变成 5.4 纳秒
java
IT枫斗者枫哥2 小时前
用了BigDecimal还差一分?用四组Java代码查清原因
java·后端
一条泥憨鱼2 小时前
【消息队列】ACK确认了什么:消息队列可靠投递的完整链路
java·后端·kafka·消息队列·rabbitmq
从琳开始9893 小时前
网络原理入门(二)------OSI七层模型?TCP/IP五层(四层)模型原来是这样的
java·网络·tcp/ip·java-ee·php
文丑颜不良啊3 小时前
LeetCode1861~1880题目解析
java·leetcode