2502.设计内存分配器

目标

给你一个整数 n ,表示下标从 0 开始的内存数组的大小。所有内存单元开始都是空闲的。

请你设计一个具备以下功能的内存分配器:

  1. 分配 一块大小为 size 的连续空闲内存单元并赋 id mID 。
  2. 释放 给定 id mID 对应的所有内存单元。

注意:

  • 多个块可以被分配到同一个 mID 。
  • 你必须释放 mID 对应的所有内存单元,即便这些内存单元被分配在不同的块中。

实现 Allocator 类:

  • Allocator(int n) 使用一个大小为 n 的内存数组初始化 Allocator 对象。
  • int allocate(int size, int mID) 找出大小为 size 个连续空闲内存单元且位于 最左侧 的块,分配并赋 id mID 。返回块的第一个下标。如果不存在这样的块,返回 -1 。
  • int freeMemory(int mID) 释放 id mID 对应的所有内存单元。返回释放的内存单元数目。

示例:

输入
["Allocator", "allocate", "allocate", "allocate", "freeMemory", "allocate", "allocate", "allocate", "freeMemory", "allocate", "freeMemory"]
[[10], [1, 1], [1, 2], [1, 3], [2], [3, 4], [1, 1], [1, 1], [1], [10, 2], [7]]
输出
[null, 0, 1, 2, 1, 3, 1, 6, 3, -1, 0]

解释
Allocator loc = new Allocator(10); // 初始化一个大小为 10 的内存数组,所有内存单元都是空闲的。
loc.allocate(1, 1); // 最左侧的块的第一个下标是 0 。内存数组变为 [1, , , , , , , , , ]。返回 0 。
loc.allocate(1, 2); // 最左侧的块的第一个下标是 1 。内存数组变为 [1,2, , , , , , , , ]。返回 1 。
loc.allocate(1, 3); // 最左侧的块的第一个下标是 2 。内存数组变为 [1,2,3, , , , , , , ]。返回 2 。
loc.freeMemory(2); // 释放 mID 为 2 的所有内存单元。内存数组变为 [1, ,3, , , , , , , ] 。返回 1 ,因为只有 1 个 mID 为 2 的内存单元。
loc.allocate(3, 4); // 最左侧的块的第一个下标是 3 。内存数组变为 [1, ,3,4,4,4, , , , ]。返回 3 。
loc.allocate(1, 1); // 最左侧的块的第一个下标是 1 。内存数组变为 [1,1,3,4,4,4, , , , ]。返回 1 。
loc.allocate(1, 1); // 最左侧的块的第一个下标是 6 。内存数组变为 [1,1,3,4,4,4,1, , , ]。返回 6 。
loc.freeMemory(1); // 释放 mID 为 1 的所有内存单元。内存数组变为 [ , ,3,4,4,4, , , , ] 。返回 3 ,因为有 3 个 mID 为 1 的内存单元。
loc.allocate(10, 2); // 无法找出长度为 10 个连续空闲内存单元的空闲块,所有返回 -1 。
loc.freeMemory(7); // 释放 mID 为 7 的所有内存单元。内存数组保持原状,因为不存在 mID 为 7 的内存单元。返回 0 。

说明:

  • 1 <= n, size, mID <= 1000
  • 最多调用 allocate 和 free 方法 1000 次

提示:

  • Can you simulate the process?
  • Use brute force to find the leftmost free block and free each occupied memory unit

思路

设计一个内存分配器来管理大小为 n 的内存数组,要求实现初始化、分配与释放方法。内存分配方法返回大小为 size 的连续空闲内存的最左侧下标,并为这些内存分配标识 mID。内存释放则是释放 mID 的所有内存单元。

有网友使用链表来维护空间的分配状态,定义节点属性:起点、大小、是否已分配、下一个节点、mID。分配空间时挨个查找,释放空间类似。使用节点对象表示区间,空间合并起来比较方便。

提示说可以使用暴力解法,暴力解的时间复杂度为 O(qn)

// todo 线段树

代码


/**
 * @date 2025-02-25 10:03
 */
class Allocator {

    private int[] flag;
    private int n;

    public Allocator(int n) {
        this.flag = new int[n];
        this.n = n;
    }

    public int allocate(int size, int mID) {
        int cnt = 0;
        for (int i = 0; i < n; i++) {
            if (flag[i] != 0) {
                cnt = 0;
                continue;
            } else {
                cnt++;
            }
            if (cnt == size) {
                int start = i - size + 1;
                for (; i >= start; i--) {
                    flag[i] = mID;
                }
                return start;
            }
        }
        return -1;
    }

    public int freeMemory(int mID) {
        int cnt = 0;
        for (int i = 0; i < n; i++) {
            if (flag[i] == mID) {
                flag[i] = 0;
                cnt++;
            }
        }
        return cnt;
    }
}

性能

624.数组列表中的最大距离

目标

给定 m 个数组,每个数组都已经按照升序排好序了。

现在你需要从两个不同的数组中选择两个整数(每个数组选一个)并且计算它们的距离。两个整数 a 和 b 之间的距离定义为它们差的绝对值 |a-b| 。

返回最大距离。

示例 1:

输入:[[1,2,3],[4,5],[1,2,3]]
输出:4
解释:
一种得到答案 4 的方法是从第一个数组或者第三个数组中选择 1,同时从第二个数组中选择 5 。

示例 2:

输入:arrays = [[1],[1]]
输出:0

说明:

  • m == arrays.length
  • 2 <= m <= 10^5
  • 1 <= arrays[i].length <= 500
  • -10^4 <= arrays[i][j] <= 10^4
  • arrays[i] 以 升序 排序。
  • 所有数组中最多有 10^5 个整数。

思路

有一个二维数组,其中的每个数组都按升序排列。任选其中两个数组,从每个数组中选两个元素,求这两个元素距离的最大值,距离指差的绝对值。

显然应该选取最大值与最小值的差,但题目限制是从两个数组中取。我们可以求得最大与次最大、最小与次最小,同时记录所属数组。

如果有多个最值相同,从哪个数组中取有影响吗?由于我们从每个组只取首尾两个元素,最值与次最值一定来自不同的组,所以最值的比较可以使用 >=<=。如果最值来自相同的组,那么只需比较最大值与次最小值,次最大值与最小值。

网友题解使用变量记录前面的最大与最小值,那么最大距离为 Math.max(res, Math.max(curMax - preMin, preMax - curMin))

代码


/**
 * @date 2025-02-19 8:57
 */
public class MaxDistance624 {

    public int maxDistance_v1(List<List<Integer>> arrays) {
        int res = 0;
        int preMax = Integer.MIN_VALUE / 2;
        int preMin = Integer.MAX_VALUE / 2;
        for (List<Integer> array : arrays) {
            int n = array.size();
            Integer curMax = array.get(n - 1);
            Integer curMin = array.get(0);
            res = Math.max(res, Math.max(curMax - preMin, preMax - curMin));
            preMax = Math.max(preMax, curMax);
            preMin = Math.min(preMin, curMin);
        }
        return res;
    }

    public int maxDistance(List<List<Integer>> arrays) {
        int[] max = new int[]{Integer.MIN_VALUE, -1};
        int secondMax = Integer.MIN_VALUE;
        int[] min = new int[]{Integer.MAX_VALUE, -1};
        int secondMin = Integer.MAX_VALUE;
        for (int i = 0; i < arrays.size(); i++) {
            List<Integer> array = arrays.get(i);
            int n = array.size();
            if (min[0] >= array.get(0)) {
                secondMin = min[0];
                min[0] = array.get(0);
                min[1] = i;
            } else if (secondMin > array.get(0)) {
                secondMin = array.get(0);
            }
            if (max[0] <= array.get(n - 1)) {
                secondMax = max[0];
                max[0] = array.get(n - 1);
                max[1] = i;
            } else if (secondMax < array.get(n - 1)) {
                secondMax = array.get(n - 1);
            }
        }
        if (max[1] != min[1]) {
            return max[0] - min[0];
        } else {
            return Math.max(max[0] - secondMin, secondMax - min[0]);
        }
    }

}

性能

2080.区间内查询数字的频率

目标

请你设计一个数据结构,它能求出给定子数组内一个给定值的 频率 。

子数组中一个值的 频率 指的是这个子数组中这个值的出现次数。

请你实现 RangeFreqQuery 类:

  • RangeFreqQuery(int[] arr) 用下标从 0 开始的整数数组 arr 构造一个类的实例。
  • int query(int left, int right, int value) 返回子数组 arr[left...right] 中 value 的 频率 。

一个 子数组 指的是数组中一段连续的元素。arr[left...right] 指的是 nums 中包含下标 left 和 right 在内 的中间一段连续元素。

示例 1:

输入:
["RangeFreqQuery", "query", "query"]
[[[12, 33, 4, 56, 22, 2, 34, 33, 22, 12, 34, 56]], [1, 2, 4], [0, 11, 33]]
输出:
[null, 1, 2]

解释:
RangeFreqQuery rangeFreqQuery = new RangeFreqQuery([12, 33, 4, 56, 22, 2, 34, 33, 22, 12, 34, 56]);
rangeFreqQuery.query(1, 2, 4); // 返回 1 。4 在子数组 [33, 4] 中出现 1 次。
rangeFreqQuery.query(0, 11, 33); // 返回 2 。33 在整个子数组中出现 2 次。

说明:

  • 1 <= arr.length <= 10^5
  • 1 <= arr[i], value <= 10^4
  • 0 <= left <= right < arr.length
  • 调用 query 不超过 10^5 次。

思路

设计一个数据结构,能够求出给定元素在子数组中的出现次数。

可以使用哈希表保存每个元素的下标列表(从小到大),查询时根据 value 获取下标列表,二分查找左边界的下界(第一个大于等于左边界的下标)和右边界的上界(最后一个小于等于右边界的下标)。注意二分查找的范围是 0 ~ list.size(),但是比较的则是原数组中的下标。构建时间复杂度为 O(n),查询时间复杂度为 O(logn)

代码


/**
 * @date 2025-02-18 15:57
 */
public  class RangeFreqQuery {

    public Map<Integer, List<Integer>> valueIndexMap = new HashMap<>();

    public RangeFreqQuery(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            valueIndexMap.putIfAbsent(arr[i], new ArrayList<>());
            valueIndexMap.get(arr[i]).add(i);
        }
    }

    public int query(int left, int right, int value) {
        List<Integer> list = valueIndexMap.get(value);
        if (list == null) {
            return 0;
        }
        return getUpperBound(right, list) - getLowerBound(left, list) + 1;
    }

    public int getUpperBound(int target, List<Integer> list) {
        int l = 0, r = list.size() - 1;
        int m = l + (r - l) / 2;
        while (l <= r) {
            if (list.get(m) > target) {
                r = m - 1;
            } else {
                l = m + 1;
            }
            m = l + (r - l) / 2;
        }
        return r;
    }

    public int getLowerBound(int target, List<Integer> list) {
        int l = 0, r = list.size() - 1;
        int m = l + (r - l) / 2;
        while (l <= r) {
            if (list.get(m) < target) {
                l = m + 1;
            } else {
                r = m - 1;
            }
            m = l + (r - l) / 2;
        }
        return l;
    }

}

性能

1706.球会落何处

目标

用一个大小为 m x n 的二维网格 grid 表示一个箱子。你有 n 颗球。箱子的顶部和底部都是开着的。

箱子中的每个单元格都有一个对角线挡板,跨过单元格的两个角,可以将球导向左侧或者右侧。

  • 将球导向右侧的挡板跨过左上角和右下角,在网格中用 1 表示。
  • 将球导向左侧的挡板跨过右上角和左下角,在网格中用 -1 表示。

在箱子每一列的顶端各放一颗球。每颗球都可能卡在箱子里或从底部掉出来。如果球恰好卡在两块挡板之间的 "V" 形图案,或者被一块挡导向到箱子的任意一侧边上,就会卡住。

返回一个大小为 n 的数组 answer ,其中 answer[i] 是球放在顶部的第 i 列后从底部掉出来的那一列对应的下标,如果球卡在盒子里,则返回 -1 。

示例 1:

输入:grid = [[1,1,1,-1,-1],[1,1,1,-1,-1],[-1,-1,-1,1,1],[1,1,1,1,-1],[-1,-1,-1,-1,-1]]
输出:[1,-1,-1,-1,-1]
解释:示例如图:
b0 球开始放在第 0 列上,最终从箱子底部第 1 列掉出。
b1 球开始放在第 1 列上,会卡在第 2、3 列和第 1 行之间的 "V" 形里。
b2 球开始放在第 2 列上,会卡在第 2、3 列和第 0 行之间的 "V" 形里。
b3 球开始放在第 3 列上,会卡在第 2、3 列和第 0 行之间的 "V" 形里。
b4 球开始放在第 4 列上,会卡在第 2、3 列和第 1 行之间的 "V" 形里。

示例 2:

输入:grid = [[-1]]
输出:[-1]
解释:球被卡在箱子左侧边上。

示例 3:

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

说明:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 100
  • grid[i][j] 为 1 或 -1

思路

矩阵 m x n 上方有 n 个球,矩阵元素值为 1 表示格子有 左上右下 的斜线挡板,值为 -1 表示有 左下右上 的斜线挡板。返回 n 个球下落的出口下标,如果被卡在箱子中用 -1 表示。

直接根据题意模拟即可,如果当前格子元素值为 1,判断其右侧格子(如果有的话),如果为 -1 则卡住,否则下落到 (i + 1, j + 1), 当行下标等于 m - 1 时,判断出口记录列下标即可。

代码


/**
 * @date 2025-02-15 20:33
 */
public class FindBall1706 {

    public int[] findBall_v2(int[][] grid) {
        int n = grid[0].length;
        int[] res = new int[n];
        for (int j = 0; j < n; j++) {
            res[j] = fall(grid, j);
        }
        return res;
    }

    public int fall(int[][] grid, int pos){
        int m = grid.length;
        int n = grid[0].length;
        for (int i = 0; i < m; i++) {
            int offset = grid[i][pos];
            pos += offset;
            if (pos == n || pos < 0 || grid[i][pos] != offset) {
                pos = -1;
                break;
            }
        }
        return pos;
    }

    public int[] findBall(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int[] res = new int[n];
        for (int j = 0; j < n; j++) {
            int pos = j;
            for (int i = 0; i < m; i++) {
                if (grid[i][pos] == 1 && (pos == n - 1 || grid[i][pos + 1] == -1)) {
                    pos = -1;
                    break;
                } else if (grid[i][pos] == -1 && (pos == 0 || grid[i][pos - 1] == 1)) {
                    pos = -1;
                    break;
                } else {
                    pos += grid[i][pos];
                }
            }
            res[j] = pos;
        }
        return res;
    }

}

性能

1552.两球之间的磁力

目标

在代号为 C-137 的地球上,Rick 发现如果他将两个球放在他新发明的篮子里,它们之间会形成特殊形式的磁力。Rick 有 n 个空的篮子,第 i 个篮子的位置在 position[i] ,Morty 想把 m 个球放到这些篮子里,使得任意两球间 最小磁力 最大。

已知两个球如果分别位于 x 和 y ,那么它们之间的磁力为 |x - y| 。

给你一个整数数组 position 和一个整数 m ,请你返回最大化的最小磁力。

示例 1:

输入:position = [1,2,3,4,7], m = 3
输出:3
解释:将 3 个球分别放入位于 1,4 和 7 的三个篮子,两球间的磁力分别为 [3, 3, 6]。最小磁力为 3 。我们没办法让最小磁力大于 3 。

示例 2:

输入:position = [5,4,3,2,1,1000000000], m = 2
输出:999999999
解释:我们使用位于 1 和 1000000000 的篮子时最小磁力最大。

说明:

  • n == position.length
  • 2 <= n <= 10^5
  • 1 <= position[i] <= 10^9
  • 所有 position 中的整数 互不相同 。
  • 2 <= m <= position.length

提示:

  • If you can place balls such that the answer is x then you can do it for y where y < x.
  • Similarly if you cannot place balls such that the answer is x then you can do it for y where y > x.
  • Binary search on the answer and greedily see if it is possible.

思路

m 个球放到 n 个空篮子中,任意两个球之间的磁力为 |position[a] - position[b]|,返回最大的磁力最小值。

看到最大化最小值就想到二分,n 个盒子放 m 个球,每个盒子至多放 1 个。假定最小磁力为 y,即相邻球的磁力最少为 y。将 position 排序,累加相邻元素的差即磁力,如果大于 y 则放置一个球,看能否放得下。

代码


/**
 * @date 2025-02-14 9:35
 */
public class MaxDistance1552 {

    public int maxDistance(int[] position, int m) {
        Arrays.sort(position);
        int n = position.length;
        int left = 0, right = position[n - 1] / (m - 1);
        int mid = left + (right - left) / 2;
        while (left <= right) {
            if (check(mid, position, m - 1)) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
            mid = left + (right - left) / 2;
        }
        return right;
    }

    public boolean check(int mid, int[] position, int m) {
        int start = position[0];
        for (int i = 1; i < position.length; i++) {
            if (position[i] >= start + mid) {
                if (--m == 0) {
                    return true;
                }
                start = position[i];
            }
        }
        return false;
    }

}

性能

1760.袋子里最少数目的球

目标

给你一个整数数组 nums ,其中 nums[i] 表示第 i 个袋子里球的数目。同时给你一个整数 maxOperations 。

你可以进行如下操作至多 maxOperations 次:

  • 选择任意一个袋子,并将袋子里的球分到 2 个新的袋子中,每个袋子里都有 正整数 个球。
  • 比方说,一个袋子里有 5 个球,你可以把它们分到两个新袋子里,分别有 1 个和 4 个球,或者分别有 2 个和 3 个球。

你的开销是单个袋子里球数目的 最大值 ,你想要 最小化 开销。

请你返回进行上述操作后的最小开销。

示例 1:

输入:nums = [9], maxOperations = 2
输出:3
解释:
- 将装有 9 个球的袋子分成装有 6 个和 3 个球的袋子。[9] -> [6,3] 。
- 将装有 6 个球的袋子分成装有 3 个和 3 个球的袋子。[6,3] -> [3,3,3] 。
装有最多球的袋子里装有 3 个球,所以开销为 3 并返回 3 。

示例 2:

输入:nums = [2,4,8,2], maxOperations = 4
输出:2
解释:
- 将装有 8 个球的袋子分成装有 4 个和 4 个球的袋子。[2,4,8,2] -> [2,4,4,4,2] 。
- 将装有 4 个球的袋子分成装有 2 个和 2 个球的袋子。[2,4,4,4,2] -> [2,2,2,4,4,2] 。
- 将装有 4 个球的袋子分成装有 2 个和 2 个球的袋子。[2,2,2,4,4,2] -> [2,2,2,2,2,4,2] 。
- 将装有 4 个球的袋子分成装有 2 个和 2 个球的袋子。[2,2,2,2,2,4,2] -> [2,2,2,2,2,2,2,2] 。
装有最多球的袋子里装有 2 个球,所以开销为 2 并返回 2 。

示例 3:

输入:nums = [7,17], maxOperations = 2
输出:7

说明:

1 <= nums.length <= 10^5
1 <= maxOperations, nums[i] <= 10^9

思路

n 个袋子,nums[i] 表示第 i + 1 个袋子中球的数目。每次操作可以任选一个袋子,将其中的球分成非空的两部分并装入两个 袋子。求执行 maxOperations 次操作后袋中球的数量最大值的最小值是多少。

考虑暴力解法,要最小化最大值,我们应该首先操作球最多的袋子,划分方案有 max / 2 种。这里需要枚举所有划分方案,然后重新计算球最多的袋子重复这一处理流程。最坏的情况下所有元素值都相等,时间复杂度为 O((max/2)^maxOperations)。这种解法显然行不通。

既然正向思维行不通,那就考虑逆向思维。假定一个最大值 mid,看能否在 maxOperations 次操作内将所有袋子种的球都降到 mid 以下。

将数字 num 拆分成不大于 mid 的数字最少需要操作几次?这里可以使用贪心思想,将 num 拆分为 num - midmid,然后接着拆分 num - mid 直到它小于等于 mid

关于次数的计算,如果 num % mid == 0,我们只需划分 num / mid - 1 次,因为剩余的部分为 mid。如果余数不为 0,则需要划分 num / mid 次。以上两种情况可以统一写成 (num - 1) / mid

代码


/**
 * @date 2025-02-12 0:03
 */
public class MinimumSize1760 {

    public int minimumSize(int[] nums, int maxOperations) {
        int left = 1, right = 1000000000;
        int mid = left + (right - left) / 2;
        while (left <= right) {
            if (check(nums, mid, maxOperations)) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
            mid = left + (right - left) / 2;
        }
        return left;
    }

    public boolean check(int[] nums, int mid, int maxOperations) {
        for (int num : nums) {
            maxOperations -= (num - 1) / mid;
            if (maxOperations < 0) {
                return false;
            }
        }
        return true;
    }

}

性能

80.删除有序数组中的重复项II

目标

给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

示例 1:

输入:nums = [1,1,1,2,2,3]
输出:5, nums = [1,1,2,2,3]
解释:函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3。 不需要考虑数组中超出新长度后面的元素。

示例 2:

输入:nums = [0,0,1,1,1,1,2,3,3]
输出:7, nums = [0,0,1,1,2,3,3]
解释:函数应返回新长度 length = 7, 并且原数组的前七个元素被修改为 0, 0, 1, 1, 2, 3, 3。不需要考虑数组中超出新长度后面的元素。

说明:

  • 1 <= nums.length <= 3 * 10^4
  • -10^4 <= nums[i] <= 10^4
  • nums 已按升序排列

思路

原地删除有序数组中重复元素超过两个的元素,返回删除后的数组长度。

使用双指针,一个指针指向重复元素的第三个元素或者第一个不重复元素,另一个指向第一个不重复元素,然后将后者的值赋给前者即可。

代码


/**
 * @date 2024-03-07 10:52
 */
public class RemoveDuplicatesII80 {

    public int removeDuplicates(int[] nums) {
        int n = nums.length;
        int i = 1;
        int res = 1;
        while (i < n) {
            int cnt = 1;
            while (i < n && nums[i] == nums[i - 1]) {
                if (cnt < 2) {
                    nums[res++] = nums[i];
                }
                i++;
                cnt++;
            }
            if (i < n) {
                nums[res++] = nums[i++];
            }
        }
        return res;
    }

}

性能

63.不同路径II

目标

给定一个 m x n 的整数数组 grid。一个机器人初始位于 左上角(即 grid[0][0])。机器人尝试移动到 右下角(即 grid[m - 1][n - 1])。机器人每次只能向下或者向右移动一步。

网格中的障碍物和空位置分别用 1 和 0 来表示。机器人的移动路径中不能包含 任何 有障碍物的方格。

返回机器人能够到达右下角的不同路径数量。

测试用例保证答案小于等于 2 * 109。

示例 1:

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

示例 2:

输入:obstacleGrid = [[0,1],[0,0]]
输出:1

说明:

  • m == obstacleGrid.length
  • n == obstacleGrid[i].length
  • 1 <= m, n <= 100
  • obstacleGrid[i][j] 为 0 或 1

思路

有一个 m x n 的二进制矩阵,0 代表空位,1 代表有障碍物。有一个机器人可以向右或向下移动,求从 (0,0) 到 (m - 1, n - 1) 的路径有多少。

定义 dp[i][j] 表示到达坐标 (i - 1, j - 1) 的不同路径数。这样定义可以省去单独初始化第一行第一列。状态转移方程为 当 obstacleGrid[i][j] == 0 时, dp[i][j] = dp[i - 1][j] + dp[i][j - 1],初值为 dp[0][1] = 1,可以视为从 (-1, 0)(0, 0) 的路径数量,如果 (0, 0) 有障碍物则为 0,否则为 1

代码


/**
 * @date 2025-02-01 20:05
 */
public class UniquePathsWithObstacles63 {

     public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[][] dp = new int[m + 1][n + 1];
        dp[0][1] = 1;
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (obstacleGrid[i - 1][j - 1] == 0){
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
                }
            }
        }
        return dp[m][n];
    }

}

性能

59.螺旋矩阵II

目标

给你一个正整数 n ,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。

示例 1:

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

示例 2:

输入:n = 1
输出:[[1]]

说明:

1 <= n <= 20

思路

生成一个 n x n 的正方形矩阵,元素值 1 ~ n^2 按顺时针螺旋顺序排列。

定义 4 个方向,用 direction[i] 表示沿着 i 方向前进的坐标变化量。首先从 (0, 0) 开始向右遍历,预先计算下一步的坐标,如果越界或者已经设置过值则转向。这种解法需要标记已经设置过值的数组,由于填充的元素值都大于 0,数组初值无需特殊处理,只需判断元素是否大于 0 即可。

代码


/**
 * @date 2025-02-07 0:11
 */
public class GenerateMatrix59 {

    public int[][] generateMatrix(int n) {
        int[][] matrix = new int[n][n];
        int[][] direction = new int[][]{{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
        int x = 0, y = 0, num = 1, d = 0;
        for (int i = 0; i < n * n; i++) {
            matrix[x][y] = num++;
            int xNext = x + direction[d][0];
            int yNext = y + direction[d][1];
            if (xNext == n || yNext < 0 || yNext == n || matrix[xNext][yNext] != 0) {
                d = (d + 1) % 4;
            }
            x += direction[d][0];
            y += direction[d][1];
        }
        return matrix;
    }

}

性能

47.全排列II

目标

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

示例 1:

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

示例 2:

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

说明:

  • 1 <= nums.length <= 8
  • -10 <= nums[i] <= 10

思路

返回数组不重复的全排列。

依次枚举第 i 个位置的可选元素,不含重复元素的全排列个数为 n!,收集结果复杂度为 O(n)。重复排列的处理与 与 90.子集II 类似,先排序数组,在每个位置枚举元素时,直接跳过后续相同的元素。

代码


/**
 * @date 2025-02-06 8:41
 */
public class PermuteUnique47 {

    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);
        List<List<Integer>> res = new ArrayList<>();
        int n = nums.length;
        boolean[] visited = new boolean[n];
        List<Integer> path = Arrays.asList(new Integer[n]);
        dfs(0, nums, visited, path, res);
        return res;
    }

    public void dfs(int index, int[] nums, boolean[] visited, List<Integer> path, List<List<Integer>> res) {
        int n = nums.length;
        if (index == n) {
            List<Integer> permute = new ArrayList<>(path);
            res.add(permute);
            return;
        }
        for (int i = 0; i < n; i++) {
            if (visited[i] || (i > 0 && nums[i] == nums[i - 1] && !visited[i - 1])) {
                continue;
            }
            visited[i] = true;
            path.set(index, nums[i]);
            dfs(index + 1, nums, visited, path, res);
            visited[i] = false;
        }
    }

}

性能