算法刷题记录——LeetCode篇(9.1) [第801~810题]

更新时间:2025-03-29

优先整理热门100及面试150,不定期持续更新,欢迎关注!


801. 使序列递增的最小交换次数

我们有两个长度相等且不为空的整型数组 nums1nums2 。在一次操作中,我们可以交换 nums1[i]nums2[i] 的元素。

例如,如果 nums1 = [1,2,3,8]nums2 =[5,6,7,4] ,你可以交换 i = 3 处的元素,得到 nums1 =[1,2,3,4]nums2 =[5,6,7,8]

返回 使 nums1nums2 严格递增 所需操作的最小次数 。
数组 arr 严格递增 且 arr[0] < arr[1] < arr[2] < ... < arr[arr.length - 1]

注意:
用例保证可以实现操作。

示例 1:

输入: nums1 = [1,3,5,4], nums2 = [1,2,3,7]
输出: 1

解释:
交换 A[3] 和 B[3] 后,两个数组如下:
A = [1, 3, 5, 7] , B = [1, 2, 3, 4]
两个数组均为严格递增的。

示例 2:

输入: nums1 = [0,3,5,8,9], nums2 = [2,1,4,6,9]
输出: 1

提示:

  • 2 <= nums1.length <= 10^5
  • nums2.length == nums1.length
  • 0 <= nums1[i], nums2[i] <= 2 * 10^5

方法:动态规划

使用动态规划来维护两个状态:swap 表示在第 i 个位置交换元素所需的最小交换次数,noswap 表示不交换时的最小交换次数。

通过遍历数组,检查每个位置是否可以从前一个位置的两种状态(交换或不交换)转移而来,并更新当前状态。

  1. 初始化:初始时,swap 为 1(交换第一个元素),noswap 为 0(不交换第一个元素)。
  2. 遍历数组:从第二个元素开始,遍历每个位置 i
  3. 状态转移
    • 检查是否可以从 i-1 不交换的状态转移而来:
      • 不交换当前元素的条件:nums1[i] > nums1[i-1]nums2[i] > nums2[i-1]
      • 交换当前元素的条件:nums2[i] > nums1[i-1]nums1[i] > nums2[i-1]
    • 检查是否可以从 i-1 交换的状态转移而来:
      • 不交换当前元素的条件:nums1[i] > nums2[i-1]nums2[i] > nums1[i-1]
      • 交换当前元素的条件:nums2[i] > nums2[i-1]nums1[i] > nums1[i-1]
  4. 更新状态:根据上述条件,更新当前 swapnoswap 的值。
  5. 返回结果:遍历结束后,返回 swapnoswap 中的最小值。

代码实现(Java):

class Solution {
    public int minSwap(int[] nums1, int[] nums2) {
        int n = nums1.length;
        int swap = 1, noswap = 0;
      
        for (int i = 1; i < n; i++) {
            int newSwap = Integer.MAX_VALUE;
            int newNoswap = Integer.MAX_VALUE;
          
            // 检查是否可以从i-1不交换转移而来
            if (nums1[i] > nums1[i-1] && nums2[i] > nums2[i-1]) {
                newNoswap = Math.min(newNoswap, noswap);
            }
            if (nums2[i] > nums1[i-1] && nums1[i] > nums2[i-1]) {
                newSwap = Math.min(newSwap, noswap + 1);
            }
          
            // 检查是否可以从i-1交换转移而来
            if (nums1[i] > nums2[i-1] && nums2[i] > nums1[i-1]) {
                newNoswap = Math.min(newNoswap, swap);
            }
            if (nums2[i] > nums2[i-1] && nums1[i] > nums1[i-1]) {
                newSwap = Math.min(newSwap, swap + 1);
            }
          
            swap = newSwap;
            noswap = newNoswap;
        }
      
        return Math.min(swap, noswap);
    }
}
复杂度分析
  • 时间复杂度:O(n),遍历数组一次。
  • 空间复杂度:O(1),仅使用常量空间维护两个状态变量。

802. 找到最终的安全状态

有一个有 n 个节点的有向图,节点按 0n - 1 编号。图由一个 索引从 0 开始的 2D 整数数组 graph表示, graph[i]是与节点 i 相邻的节点的整数数组,这意味着从节点 igraph[i]中的每个节点都有一条边。

如果一个节点没有连出的有向边,则该节点是 终端节点 。如果从该节点开始的所有可能路径都通向 终端节点 ,则该节点为 安全节点 。

返回一个由图中所有 安全节点 组成的数组作为答案。答案数组中的元素应当按 升序 排列。

示例 1:

输入:graph = [[1,2],[2,3],[5],[0],[5],[],[]]
输出:[2,4,5,6]

解释:
节点 5 和节点 6 是终端节点,因为它们都没有出边。
从节点 2、4、5 和 6 开始的所有路径都指向节点 5 或 6 。

示例 2:

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

解释:
只有节点 4 是终端节点,从节点 4 开始的所有路径都通向节点 4 。

提示:

  • n == graph.length
  • 1 <= n <= 10^4
  • 0 <= graph[i].length <= n
  • 0 <= graph[i][j] <= n - 1
  • graph[i] 按严格递增顺序排列
  • 图中可能包含自环
  • 图中边的数目在范围 [1, 4 * 10^4] 内

方法:拓扑排序(出度处理)

安全节点的条件是其所有路径最终都到达终端节点(无出边或路径无环)。通过拓扑排序,从终端节点(出度为0)开始,反向处理所有指向它们的节点,逐步将出度减为0的节点标记为安全。

  1. 构建反向图:记录每个节点被哪些节点指向。
  2. 计算初始出度:每个节点的出边数目。
  3. 初始化队列:将出度为0的节点加入队列并标记为安全。
  4. 拓扑排序处理:从队列取出节点,更新其反向图中前驱节点的出度,若减至0则加入队列并标记安全。
  5. 收集结果:所有标记为安全的节点排序后输出。

代码实现(Java):

class Solution {
    public List<Integer> eventualSafeNodes(int[][] graph) {
        int n = graph.length;
        List<List<Integer>> reverseGraph = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            reverseGraph.add(new ArrayList<>());
        }
        for (int v = 0; v < n; v++) {
            for (int u : graph[v]) {
                reverseGraph.get(u).add(v);
            }
        }
      
        int[] outDegree = new int[n];
        for (int i = 0; i < n; i++) {
            outDegree[i] = graph[i].length;
        }
      
        Queue<Integer> queue = new LinkedList<>();
        boolean[] safe = new boolean[n];
        for (int i = 0; i < n; i++) {
            if (outDegree[i] == 0) {
                queue.offer(i);
                safe[i] = true;
            }
        }
      
        while (!queue.isEmpty()) {
            int u = queue.poll();
            for (int v : reverseGraph.get(u)) {
                outDegree[v]--;
                if (outDegree[v] == 0) {
                    queue.offer(v);
                    safe[v] = true;
                }
            }
        }
      
        List<Integer> result = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if (safe[i]) {
                result.add(i);
            }
        }
        Collections.sort(result);
        return result;
    }
}
复杂度分析
  • 时间复杂度:O(n + e),构建反向图和处理队列各需线性时间。
  • 空间复杂度:O(n + e),存储反向图和队列所需空间。

803. 打砖块

有一个 m x n 的二元网格 grid ,其中 1 表示砖块,0 表示空白。砖块 稳定(不会掉落)的前提是:

  • 一块砖直接连接到网格的顶部,或者
  • 至少有一块相邻(4 个方向之一)砖块 稳定 不会掉落时

给你一个数组 hits ,这是需要依次消除砖块的位置。每当消除 hits[i] = (rowi, coli) 位置上的砖块时,对应位置的砖块(若存在)会消失,然后其他的砖块可能因为这一消除操作而 掉落 。一旦砖块掉落,它会 立即 从网格 grid 中消失(即,它不会落在其他稳定的砖块上)。

返回一个数组 result ,其中 result[i] 表示第 i 次消除操作对应掉落的砖块数目。

注意,消除可能指向是没有砖块的空白位置,如果发生这种情况,则没有砖块掉落。

示例 1:

输入:grid = [[1,0,0,0],[1,1,1,0]], hits = [[1,0]]
输出:[2]

解释:
网格开始为:[[1,0,0,0], [1,1,1,0]]
消除 (1,0) 处加粗的砖块,得到网格:[[1,0,0,0] [0,1,1,0]]
两个加粗的砖不再稳定,因为它们不再与顶部相连,也不再与另一个稳定的砖相邻,因此它们将掉落。得到网格:[[1,0,0,0], [0,0,0,0]]
因此,结果为 [2] 。

示例 2:

输入:grid = [[1,0,0,0],[1,1,0,0]], hits = [[1,1],[1,0]]
输出:[0,0]

解释:
网格开始为:[[1,0,0,0], [1,1,0,0]]
消除 (1,1) 处加粗的砖块,得到网格:[[1,0,0,0], [1,0,0,0]]
剩下的砖都很稳定,所以不会掉落。网格保持不变:[[1,0,0,0], [1,0,0,0]]
接下来消除 (1,0) 处加粗的砖块,得到网格:[[1,0,0,0], [0,0,0,0]]
剩下的砖块仍然是稳定的,所以不会有砖块掉落。
因此,结果为 [0,0] 。

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 200
  • grid[i][j] 为 0 或 1
  • 1 <= hits.length <= 4 * 10^4
  • hits[i].length == 2
  • 0 <= xi <= m - 1
  • 0 <= yi <= n - 1
  • 所有 (xi, yi) 互不相同

方法:逆向并查集

逆向处理敲击操作,将问题转换为逐步添加砖块。使用并查集维护连通性,并统计每次添加后连接到顶部的砖块数目变化。通过比较添加前后的尺寸变化,计算掉落的砖块数。

  1. 预处理:复制原网格并移除所有被敲击的砖块。
  2. 初始化并查集:将预处理后的网格中所有砖块合并,顶部砖块连接到虚拟节点。
  3. 逆序处理敲击:从最后一次敲击开始,逐步恢复砖块,合并相邻砖块,并计算每次恢复后连接到顶部的砖块数目变化。
  4. 计算结果:变化量即为当前敲击导致的掉落数目。

代码实现(Java):

class Solution {
    public int[] hitBricks(int[][] grid, int[][] hits) {
        int m = grid.length;
        int n = grid[0].length;
        int[][] copy = new int[m][n];
        for (int i = 0; i < m; i++) {
            System.arraycopy(grid[i], 0, copy[i], 0, n);
        }
      
        // 预处理,移除所有被敲击的砖块
        for (int[] hit : hits) {
            int x = hit[0], y = hit[1];
            if (copy[x][y] == 1) copy[x][y] = 0;
        }
      
        int size = m * n;
        UnionFind uf = new UnionFind(size + 1);
      
        // 初始化顶部砖块连接到虚拟节点
        for (int j = 0; j < n; j++) {
            if (copy[0][j] == 1) {
                uf.union(j, size);
            }
        }
      
        // 构建初始连通性
        for (int i = 1; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (copy[i][j] == 1) {
                    // 上方有砖块则合并
                    if (copy[i-1][j] == 1) {
                        uf.union(i * n + j, (i-1) * n + j);
                    }
                    // 左方有砖块则合并
                    if (j > 0 && copy[i][j-1] == 1) {
                        uf.union(i * n + j, i * n + j - 1);
                    }
                }
            }
        }
      
        int[] ans = new int[hits.length];
        int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
      
        // 逆序恢复砖块
        for (int i = hits.length - 1; i >= 0; i--) {
            int x = hits[i][0], y = hits[i][1];
            if (grid[x][y] == 0) {
                ans[i] = 0;
                continue;
            }
          
            copy[x][y] = 1;
            int original = uf.getSize(size);
          
            // 若当前砖块在顶部,连接到虚拟节点
            if (x == 0) {
                uf.union(x * n + y, size);
            }
          
            // 合并相邻砖块
            for (int[] d : dirs) {
                int nx = x + d[0], ny = y + d[1];
                if (nx >= 0 && nx < m && ny >= 0 && ny < n && copy[nx][ny] == 1) {
                    uf.union(x * n + y, nx * n + ny);
                }
            }
          
            int current = uf.getSize(size);
            ans[i] = Math.max(current - original - 1, 0);
        }
      
        return ans;
    }
  
    class UnionFind {
        private int[] parent;
        private int[] size;
      
        public UnionFind(int n) {
            parent = new int[n];
            size = new int[n];
            for (int i = 0; i < n; i++) {
                parent[i] = i;
                size[i] = 1;
            }
        }
      
        public int find(int x) {
            if (parent[x] != x) {
                parent[x] = find(parent[x]);
            }
            return parent[x];
        }
      
        public void union(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);
            if (rootX == rootY) return;
          
            if (rootX == parent.length - 1) {
                parent[rootY] = rootX;
                size[rootX] += size[rootY];
            } else {
                parent[rootX] = rootY;
                size[rootY] += size[rootX];
            }
        }
      
        public int getSize(int x) {
            int root = find(x);
            return root == parent.length - 1 ? size[root] : 0;
        }
    }
}
复杂度分析
  • 时间复杂度:O(N + K),其中 N 是网格中的元素总数,K 是敲击次数。预处理和并查集操作均接近线性。
  • 空间复杂度:O(N),用于存储并查集结构。

804. 唯一摩尔斯密码词

国际摩尔斯密码定义一种标准编码方式,将每个字母对应于一个由一系列点和短线组成的字符串, 比如:

  • 'a' 对应 ".-"
  • 'b' 对应 "-..."
  • 'c' 对应 "-.-." ,以此类推。

为了方便,所有 26 个英文字母的摩尔斯密码表如下:
[".-","-...","-.-.","-..",".","..-.","--.","....","..",".---","-.-",".-..","--","-.","---",".--.","--.-",".-.","...","-","..-","...-",".--","-..-","-.--","--.."]

给你一个字符串数组 words ,每个单词可以写成每个字母对应摩尔斯密码的组合。

例如,"cab" 可以写成 "-.-..--..." ,(即 "-.-." + ".-" + "-..." 字符串的结合)。我们将这样一个连接过程称作 单词翻译
words 中所有单词进行单词翻译,返回不同 单词翻译 的数量。

示例 1:

输入: words = ["gin", "zen", "gig", "msg"]
输出: 2

解释:
各单词翻译如下:
“gin” -> “–…-.”
“zen” -> “–…-.”
“gig” -> “–…–.”
“msg” -> “–…–.”
共有 2 种不同翻译, “–…-.” 和 “–…–.”。

示例 2:

输入:words = ["a"]
输出:1

提示:

  • 1 <= words.length <= 100
  • 1 <= words[i].length <= 12
  • words[i] 由小写英文字母组成

方法:哈希集合

将每个单词转换为对应的摩尔斯密码字符串,利用哈希集合自动去重的特性统计不同密码的数量。摩尔斯密码表预先存储每个字母的对应编码。

  1. 初始化摩尔斯密码表:按顺序存储26个字母对应的密码。
  2. 转换每个单词:遍历单词中的每个字符,拼接对应的摩尔斯密码。
  3. 使用哈希集合去重:将转换后的字符串存入集合,集合大小即为答案。

代码实现(Java):

class Solution {
    public int uniqueMorseRepresentations(String[] words) {
        String[] morseCodes = {
            ".-","-...","-.-.","-..",".","..-.","--.","....","..",".---",
            "-.-",".-..","--","-.","---",".--.","--.-",".-.","...","-",
            "..-","...-",".--","-..-","-.--","--.."
        };
        Set<String> uniqueMorse = new HashSet<>();
      
        for (String word : words) {
            StringBuilder sb = new StringBuilder();
            for (char c : word.toCharArray()) {
                sb.append(morseCodes[c - 'a']);
            }
            uniqueMorse.add(sb.toString());
        }
      
        return uniqueMorse.size();
    }
}
复杂度分析
  • 时间复杂度:O(N*L),其中 N 是单词数量,L 是单词平均长度。每个字符处理时间为常数。
  • 空间复杂度:O(N*L),哈希集合存储所有不同的摩尔斯密码字符串。

805. 数组的均值分割

给定你一个整数数组 nums,我们要将 nums 数组中的每个元素移动到 A 数组 或者 B 数组中,使得 A 数组和 B 数组不为空,并且 average(A) == average(B)

如果可以完成则返回true , 否则返回 false

注意: 对于数组 arr , average(arr)arr 的所有元素的和除以 arr 长度。

示例 1:

输入: nums = [1,2,3,4,5,6,7,8]
输出: true

解释: 我们可以将数组分割为 [1,4,5,8] 和 [2,3,6,7], 他们的平均值都是4.5。

示例 2:

输入: nums = [3,1]
输出: false

提示:

  • 1 <= nums.length <= 30
  • 0 <= nums[i] <= 10^4

方法:折半搜索

将数组分成两部分,分别生成所有可能子集的元素数目和总和。对于每个可能的子集长度k,检查是否存在两部分子集的总和之和等于目标值,从而判断是否可以将数组分割成两个平均值相等的非空子集。

  1. 计算总和:确定整个数组的总和。
  2. 分割数组:将数组分成两个部分,减少计算量。
  3. 生成子集信息:对每个部分生成所有可能子集的元素数目和总和的映射。
  4. 遍历可能的k:检查每个k是否满足条件,利用两部分的映射快速查询。

代码实现(Java):

class Solution {
    public boolean splitArraySameAverage(int[] nums) {
        int n = nums.length;
        if (n == 1) return false;
        int totalSum = Arrays.stream(nums).sum();
      
        int lenA = n / 2, lenB = n - lenA;
        int[] A = Arrays.copyOfRange(nums, 0, lenA);
        int[] B = Arrays.copyOfRange(nums, lenA, n);
      
        Map<Integer, Set<Integer>> mapA = generateSubsetSums(A);
        Map<Integer, Set<Integer>> mapB = generateSubsetSums(B);
      
        for (int k = 1; k < n; k++) {
            if ((totalSum * k) % n != 0) continue;
            int target = (totalSum * k) / n;
          
            for (int a = Math.max(0, k - lenB); a <= Math.min(k, lenA); a++) {
                int b = k - a;
                if (!mapA.containsKey(a) || !mapB.containsKey(b)) continue;
              
                Set<Integer> sumsA = mapA.get(a);
                Set<Integer> sumsB = mapB.get(b);
                for (int sumA : sumsA) {
                    if (sumsB.contains(target - sumA)) return true;
                }
            }
        }
        return false;
    }
  
    private Map<Integer, Set<Integer>> generateSubsetSums(int[] arr) {
        Map<Integer, Set<Integer>> map = new HashMap<>();
        int n = arr.length;
        for (int mask = 0; mask < (1 << n); mask++) {
            int bits = Integer.bitCount(mask);
            int sum = 0;
            for (int i = 0; i < n; i++) {
                if ((mask & (1 << i)) != 0) sum += arr[i];
            }
            map.computeIfAbsent(bits, k -> new HashSet<>()).add(sum);
        }
        return map;
    }
}
复杂度分析
  • 时间复杂度:预处理部分为 O(2^(n/2) * n),查询部分为 O(n^2 * 2^(n/2)),整体在 n=30 时可行。
  • 空间复杂度:O(2^(n/2) * n),存储各子集的总和。

806. 写字符串需要的行数

我们要把给定的字符串 S 从左到右写到每一行上,每一行的最大宽度为100个单位,如果我们在写某个字母的时候会使这行超过了100 个单位,那么我们应该把这个字母写到下一行。我们给定了一个数组 widths,这个数组 widths[0] 代表 'a' 需要的单位, widths[1] 代表 'b' 需要的单位,…, widths[25] 代表 'z' 需要的单位。

**现在回答两个问题:**至少多少行能放下S,以及最后一行使用的宽度是多少个单位?将你的答案作为长度为2的整数列表返回。

示例 1:

输入: 
widths = [10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10]
S = "abcdefghijklmnopqrstuvwxyz"
输出: [3, 60]

解释:
所有的字符拥有相同的占用单位10。所以书写所有的26个字母,
我们需要2个整行和占用60个单位的一行。

示例 2:

输入: 
widths = [4,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10]
S = "bbbcccdddaaa"
输出: [2, 4]

解释:
除去字母’a’所有的字符都是相同的单位10,并且字符串 “bbbcccdddaa” 将会覆盖 9 * 10 + 2 * 4 = 98 个单位.
最后一个字母 ‘a’ 将会被写到第二行,因为第一行只剩下2个单位了。
所以,这个答案是2行,第二行有4个单位宽度。

注:

  • 字符串 S 的长度在 [1, 1000] 的范围。
  • S 只包含小写字母。
  • widths 是长度为 26的数组。
  • widths[i] 值的范围在 [2, 10]。

方法:逐字符遍历

遍历字符串中的每个字符,逐字符计算当前行的宽度。当当前行无法容纳下一个字符时,换行并重置当前行的宽度。最后统计总行数和最后一行的宽度。

  1. 初始化变量:行数 lines 初始为1,当前行宽度 currentWidth 初始为0。
  2. 遍历每个字符:对于每个字符,获取对应的摩尔斯密码宽度。
  3. 判断换行:如果当前行加上该字符的宽度超过100,则换行,否则累加宽度。
  4. 返回结果:最终返回行数和最后一行的宽度。

代码实现(Java):

class Solution {
    public int[] numberOfLines(int[] widths, String s) {
        int lines = 1;
        int currentWidth = 0;
        for (char c : s.toCharArray()) {
            int w = widths[c - 'a'];
            if (currentWidth + w > 100) {
                lines++;
                currentWidth = w;
            } else {
                currentWidth += w;
            }
        }
        return new int[]{lines, currentWidth};
    }
}
复杂度分析
  • 时间复杂度:O(n),其中 n 是字符串的长度。每个字符处理一次。
  • 空间复杂度:O(1),仅使用常数额外空间。

807. 保持城市天际线

给你一座由 n x n 个街区组成的城市,每个街区都包含一座立方体建筑。给你一个下标从 0 开始的 n x n 整数矩阵 grid ,其中 grid[r][c] 表示坐落于 r 行 c 列的建筑物的 高度 。

城市的 天际线 是从远处观察城市时,所有建筑物形成的外部轮廓。从东、南、西、北四个主要方向观测到的 天际线 可能不同。

我们被允许为 任意数量的建筑物 的高度增加 任意增量(不同建筑物的增量可能不同) 。 高度为 0 的建筑物的高度也可以增加。然而,增加的建筑物高度 不能影响 从任何主要方向观察城市得到的 天际线

不改变 从任何主要方向观测到的城市 天际线 的前提下,返回建筑物可以增加的 最大高度增量总和 。

示例 1:

输入:grid = [[3,0,8,4],[2,4,5,7],[9,2,6,3],[0,3,1,0]]
输出:35

解释:
用红色绘制从不同方向观看得到的天际线。
在不影响天际线的情况下,增加建筑物的高度:
gridNew = [ [8, 4, 8, 7], [7, 4, 7, 7], [9, 4, 8, 7], [3, 3, 3, 3] ]

示例 2:

输入:grid = [[0,0,0],[0,0,0],[0,0,0]]
输出:0

解释:增加任何建筑物的高度都会导致天际线的变化。

提示:

  • n == grid.length
  • n == grid[r].length
  • 2 <= n <= 50
  • 0 <= grid[r][c] <= 100

方法:行列最大值约束

每个建筑物的高度增量受其所在行和列的最大高度限制。新高度不能超过行和列最大值中的较小值,以保持天际线不变。总增量即为所有建筑物允许增加的高度之和。

  1. 计算行最大值:遍历每行,记录每行的最大值。
  2. 计算列最大值:遍历每列,记录每列的最大值。
  3. 计算总增量:对于每个建筑物,其允许的最大高度为行和列最大值的较小者,累计其与原高度的差值。

代码实现(Java):

class Solution {
    public int maxIncreaseKeepingSkyline(int[][] grid) {
        int n = grid.length;
        int[] rowMax = new int[n];
        int[] colMax = new int[n];
      
        // 计算每行的最大值
        for (int r = 0; r < n; r++) {
            int max = 0;
            for (int c = 0; c < n; c++) {
                max = Math.max(max, grid[r][c]);
            }
            rowMax[r] = max;
        }
      
        // 计算每列的最大值
        for (int c = 0; c < n; c++) {
            int max = 0;
            for (int r = 0; r < n; r++) {
                max = Math.max(max, grid[r][c]);
            }
            colMax[c] = max;
        }
      
        // 计算总增量
        int sum = 0;
        for (int r = 0; r < n; r++) {
            for (int c = 0; c < n; c++) {
                int limit = Math.min(rowMax[r], colMax[c]);
                sum += limit - grid[r][c];
            }
        }
        return sum;
    }
}
复杂度分析
  • 时间复杂度:O(n²),遍历矩阵三次:计算行最大值、列最大值和总增量,每次均为O(n²)。
  • 空间复杂度:O(n),存储行和列的最大值,各需O(n)空间。

808. 分汤

有 A 和 B 两种类型 的汤。一开始每种类型的汤有 n 毫升。有四种分配操作:

  • 提供 100ml 的 汤A 和 0ml 的 汤B
  • 提供 75ml 的 汤A 和 25ml 的 汤B
  • 提供 50ml 的 汤A 和 50ml 的 汤B
  • 提供 25ml 的 汤A 和 75ml 的 汤B

当我们把汤分配给某人之后,汤就没有了。每个回合,我们将从四种概率同为 0.25 的操作中进行分配选择。如果汤的剩余量不足以完成某次操作,我们将尽可能分配。当两种类型的汤都分配完时,停止操作。

注意:不存在先分配 100 ml 汤B 的操作。

需要返回的值: 汤A 先分配完的概率 + 汤A和汤B 同时分配完的概率 / 2。返回值在正确答案 10-5 的范围内将被认为是正确的。

示例 1:

输入: n = 50
输出: 0.62500

解释:
- 如果我们选择前两个操作,A 首先将变为空。
- 对于第三个操作,A 和 B 会同时变为空。
- 对于第四个操作,B 首先将变为空。
- 所以 A 变为空的总概率加上 A 和 B 同时变为空的概率的一半是 0.25 *(1 + 1 + 0.5 + 0)= 0.625。

示例 2:

输入: n = 100
输出: 0.71875

提示:

  • 0 <= n <= 10^9​​​​​​​

方法:动态规划

当汤的剩余量较大时,每次操作平均消耗的汤A比汤B多,因此汤A更可能先耗尽。通过数学分析,当n≥4800时,结果趋近于1。对于较小的n,使用动态规划计算所有可能状态的转移概率。

  1. 阈值处理:当n≥4800时,直接返回1.0。
  2. 单位转换:将汤量转换为25ml的单位,减少状态数目。
  3. 动态规划:按自底向上方式计算每个状态的转移概率。

代码实现(Java):

class Solution {
    public double soupServings(int n) {
        if (n == 0) return 0.5;
        if (n >= 4800) return 1.0;
      
        int m = (n + 24) / 25; // 转换为25ml单位,向上取整
        double[][] dp = new double[m + 1][m + 1];
      
        for (int a = 0; a <= m; a++) {
            for (int b = 0; b <= m; b++) {
                int realA = a * 25;
                int realB = b * 25;
                if (realA > n) realA = n;
                if (realB > n) realB = n;
              
                if (realA == 0 && realB == 0) {
                    dp[a][b] = 0.5;
                } else if (realA == 0) {
                    dp[a][b] = 1.0;
                } else if (realB == 0) {
                    dp[a][b] = 0.0;
                } else {
                    // 处理四种操作
                    int[][] ops = {{4, 0}, {3, 1}, {2, 2}, {1, 3}};
                    double sum = 0.0;
                    for (int[] op : ops) {
                        int newA = a - op[0];
                        int newB = b - op[1];
                        newA = Math.max(newA, 0);
                        newB = Math.max(newB, 0);
                        if (newA >= dp.length) newA = dp.length - 1;
                        if (newB >= dp[0].length) newB = dp[0].length - 1;
                        sum += dp[newA][newB];
                    }
                    dp[a][b] = sum * 0.25;
                }
            }
        }
        return dp[m][m];
    }
}
复杂度分析
  • 时间复杂度:O(m²),其中m = n/25。对于较小的n,m最多为192(当n=4800时),时间复杂度可接受。
  • 空间复杂度:O(m²),存储动态规划表。

809. 情感丰富的文字

有时候人们会用重复写一些字母来表示额外的感受,比如 "hello" -> "heeellooo", "hi" -> "hiii"。我们将相邻字母都相同的一串字符定义为相同字母组,例如:"h", "eee", "ll", "ooo"

对于一个给定的字符串 S ,如果另一个单词能够通过将一些字母组扩张从而使其和 S 相同,我们将这个单词定义为可扩张的(stretchy)。扩张操作定义如下:选择一个字母组(包含字母 c ),然后往其中添加相同的字母 c 使其长度达到 3 或以上。

例如,以 "hello" 为例,我们可以对字母组 "o" 扩张得到 "hellooo",但是无法以同样的方法得到 "helloo" 因为字母组 "oo" 长度小于 3。此外,我们可以进行另一种扩张 "ll" -> "lllll" 以获得 "helllllooo"。如果 s = "helllllooo",那么查询词 "hello" 是可扩张的,因为可以对它执行这两种扩张操作使得 query = "hello" -> "hellooo" -> "helllllooo" = s

输入一组查询单词,输出其中可扩张的单词数量。

示例:

输入: 
s = "heeellooo"
words = ["hello", "hi", "helo"]
输出:1

解释:
- 我们能通过扩张 “hello” 的 “e” 和 “o” 来得到 “heeellooo”。
- 我们不能通过扩张 “helo” 来得到 “heeellooo” 因为 “ll” 的长度小于 3 。

提示:

  • 1 <= s.length, words.length <= 100
  • 1 <= words[i].length <= 100
  • s 和所有在 words 中的单词都只由小写字母组成。

方法:字母组分解

将字符串分解为连续相同字符的字母组,比较每个字母组的字符和长度是否符合扩展条件。只有当所有对应的字母组满足字符相同且长度条件时,查询单词才是可扩张的。

  1. 分解字母组:将字符串转换为由字符及其连续出现次数组成的字母组列表。
  2. 比较字母组
    • 检查两个字符串的字母组数目是否相同。
    • 逐个比较字母组的字符是否相同。
    • 检查每个字母组的长度是否满足条件:原长度等于目标长度,或目标长度≥原长度且≥3。

代码实现(Java):

class Solution {
    public int expressiveWords(String s, String[] words) {
        List<Group> sGroups = getGroups(s);
        int count = 0;
        for (String word : words) {
            List<Group> wordGroups = getGroups(word);
            if (sGroups.size() != wordGroups.size()) continue;
            boolean valid = true;
            for (int i = 0; i < sGroups.size(); i++) {
                Group sGroup = sGroups.get(i);
                Group wordGroup = wordGroups.get(i);
                if (sGroup.c != wordGroup.c) {
                    valid = false;
                    break;
                }
                int sCount = sGroup.count;
                int wCount = wordGroup.count;
                if (sCount != wCount) {
                    if (sCount < wCount || sCount < 3) {
                        valid = false;
                        break;
                    }
                }
            }
            if (valid) {
                count++;
            }
        }
        return count;
    }
  
    private List<Group> getGroups(String s) {
        List<Group> groups = new ArrayList<>();
        if (s.isEmpty()) return groups;
        char prev = s.charAt(0);
        int count = 1;
        for (int i = 1; i < s.length(); i++) {
            char curr = s.charAt(i);
            if (curr == prev) {
                count++;
            } else {
                groups.add(new Group(prev, count));
                prev = curr;
                count = 1;
            }
        }
        groups.add(new Group(prev, count));
        return groups;
    }
  
    static class Group {
        char c;
        int count;
        Group(char c, int count) {
            this.c = c;
            this.count = count;
        }
    }
}
复杂度分析
  • 时间复杂度:O(k * (m + n)),其中k是单词数量,m是s的长度,n是单词的平均长度。分解每个字符串的时间为O(m)或O(n)。
  • 空间复杂度:O(m + n),存储字母组列表。

810. 黑板异或游戏

黑板上写着一个非负整数数组 nums[i]

Alice 和 Bob 轮流从黑板上擦掉一个数字,Alice 先手。如果擦除一个数字后,剩余的所有数字按位异或运算得出的结果等于 0 的话,当前玩家游戏失败。 另外,如果只剩一个数字,按位异或运算得到它本身;如果无数字剩余,按位异或运算结果为 0

并且,轮到某个玩家时,如果当前黑板上所有数字按位异或运算结果等于 0 ,这个玩家获胜。

假设两个玩家每步都使用最优解,当且仅当 Alice 获胜时返回 true。

示例 1:

输入: nums = [1,1,2]
输出: false

解释:
1. Alice 有两个选择: 擦掉数字 1 或 2。
2. 如果擦掉 1, 数组变成 [1, 2]。剩余数字按位异或得到 1 XOR 2 = 3。那么 Bob 可以擦掉任意数字,因为 Alice 会成为擦掉最后一个数字的人,她总是会输。
3. 如果 Alice 擦掉 2,那么数组变成[1, 1]。剩余数字按位异或得到 1 XOR 1 = 0。Alice 仍然会输掉游戏。

示例 2:

输入: nums = [0,1]
输出: true

示例 3:

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

提示:

  • 1 <= nums.length <= 1000
  • 0 <= nums[i] < 2^16

方法:数学条件判断

根据游戏规则推导,如果初始时所有数字的异或和为0,则Alice直接获胜。否则,当且仅当数组长度为偶数时,Alice可以通过最优策略获胜。

  1. 计算数组中所有元素的异或和。
  2. 若异或和为0,返回true
  3. 否则,检查数组长度是否为偶数,返回对应的结果。

代码实现(Java):

class Solution {
    public boolean xorGame(int[] nums) {
        int xorSum = 0;
        for (int num : nums) {
            xorSum ^= num;
        }
        return xorSum == 0 || nums.length % 2 == 0;
    }
}
复杂度分析
  • 时间复杂度:O(n),其中n是数组长度。遍历数组一次计算异或和。
  • 空间复杂度:O(1),仅使用常数空间。

声明

  1. 本文版权归 CSDN 用户 Allen Wurlitzer 所有,遵循CC-BY-SA协议发布,转载请注明出处。
  2. 本文题目来源 力扣-LeetCode ,著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值