【题解】—— LeetCode一周小结12

news2024/12/29 9:05:32

【题解】—— 每日一道题目栏


上接:【题解】—— LeetCode一周小结11

18.区域和检索 - 数组不可变

题目链接:303. 区域和检索 - 数组不可变
1.计算索引 left 和 right (包含 left 和 right)之间的 nums 元素的 和 ,其中 left <= right
实现 NumArray 类:

  • NumArray(int[] nums) 使用数组 nums 初始化对象
  • int sumRange(int i, int j) 返回数组 nums 中索引 left 和 right 之间的元素的 总和 ,包含 left 和 right 两点(也就是 nums[left] + nums[left + 1] + … + nums[right] )

示例 1:

输入:

[“NumArray”, “sumRange”, “sumRange”, “sumRange”]

[[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]

输出:

[null, 1, -1, -3]

解释:

NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);

numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)

numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1))

numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))

提示:

1 <= nums.length <= 104

-105 <= nums[i] <= 105

0 <= i <= j < nums.length

最多调用 104 次 sumRange 方法

题解:
方法:前缀和

        设计一个前缀和数组(s),用于存储给定数组 nums 的前缀和。

        在初始化 NumArray 对象时,首先创建前缀和数组,并计算出给定数组 nums 的每个位置的前缀和,存储在 s 数组中。

        在求解 sumRange 时,只需利用前缀和数组 s,即可在 O(1) 时间内求出指定区间 [left, right] 的和,即 s[right + 1] - s[left]。

class NumArray {
    private int[] s; // 前缀和数组

    // 构造函数,初始化前缀和数组
    public NumArray(int[] nums) {
        s = new int[nums.length + 1];
        for (int i = 0; i < nums.length; i++) {
            s[i + 1] = s[i] + nums[i]; // 计算前缀和
        }
    }

    // 求解给定区间[left, right]的和
    public int sumRange(int left, int right) {
        return s[right + 1] - s[left]; // 返回区间和
    }
}

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray obj = new NumArray(nums);
 * int param_1 = obj.sumRange(left,right);
 */

扩展

  1. 如何计算数组元素到某个数的距离之和?见 2602. 使数组元素全部相等的最少操作次数,题解
  2. 如何计算元素和等于 k 的子数组个数?见 560. 和为 K 的子数组
  3. 把 nums改成二维矩阵,如何计算子矩阵的元素和?见 304. 二维区域和检索 - 矩阵不可变,图解
  4. 如果可以修改 nums的元素值呢?见 307. 区域和检索 - 数组可修改,题解
  5. 对于 53. 最大子数组和,除了 DP 做法外,还可以用 前缀和 解决。这一做法可以扩展到子数组长度有下限/上限,子数组元素和有上限等。

题单:前缀和

560. 和为 K 的子数组
930. 和相同的二元子数组
1524. 和为奇数的子数组数目
974. 和可被 K 整除的子数组
523. 连续的子数组和
3026. 最大好子数组和
525. 连续数组
面试题 17.05. 字母与数字
1124. 表现良好的最长时间段
2488. 统计中位数为 K 的子数组
1590. 使数组和能被 P 整除
2949. 统计美丽子字符串 II
1983. 范围和相等的最宽索引对
2489. 固定比率的子字符串数
2955. 同端子串的数量

题单:异或前缀和

1310. 子数组异或查询
1177. 构建回文串检测
1371. 每个元音包含偶数次的最长子字符串
1542. 找出最长的超赞子字符串
1915. 最美子字符串的数目
2791. 树中可以形成回文的路径数


19.好子数组的最大分数

题目链接:1793. 好子数组的最大分数

给你一个整数数组 nums (下标从 0 开始)和一个整数 k 。

一个子数组 (i, j) 的 分数 定义为 min(nums[i], nums[i+1], …, nums[j]) * (j - i + 1) 。一个 好 子数组的两个端点下标需要满足 i <= k <= j 。

请你返回 好 子数组的最大可能 分数 。

示例 1:

输入:nums = [1,4,3,7,4,5], k = 3

输出:15

解释:最优子数组的左右端点下标是 (1, 5) ,分数为 min(4,3,7,4,5) * (5-1+1) = 3 * 5 = 15 。

示例 2:

输入:nums = [5,5,4,5,4,1,1,1], k = 0

输出:20

解释:最优子数组的左右端点下标是 (0, 4) ,分数为 min(5,5,4,5,4) * (4-0+1) = 4 * 5 = 20 。

提示:

1 <= nums.length <= 105

1 <= nums[i] <= 2 * 104

0 <= k < nums.length

题解:
方法:单调栈
        这个问题可以使用单调栈来解决。

        首先,我们需要求出每个位置 i 左边第一个小于 nums[i] 的位置 left[i],以及右边第一个小于 nums[i] 的位置 right[i]。

        这样,对于每个位置 i,我们可以计算以 nums[i] 作为高度的最大矩形面积,即 (right[i] - left[i] - 1) * nums[i]。

        我们遍历所有位置 i,计算对应的最大面积,并返回最大值即可。

class Solution {
    public int maximumScore(int[] nums, int k) {
        int n = nums.length;
        int[] left = new int[n];
        Deque<Integer> st = new ArrayDeque<>();
        
        // 计算每个位置 i 左边第一个小于 nums[i] 的位置 left[i]
        for (int i = 0; i < n; i++) {
            int x = nums[i];
            while (!st.isEmpty() && x <= nums[st.peek()]) {
                st.pop();
            }
            left[i] = st.isEmpty() ? -1 : st.peek();
            st.push(i);
        }

        // 清空栈,准备计算右边第一个小于 nums[i] 的位置 right[i]
        st.clear();
        
        // 计算每个位置 i 右边第一个小于 nums[i] 的位置 right[i]
        for (int i = n - 1; i >= 0; i--) {
            int x = nums[i];
            while (!st.isEmpty() && x <= nums[st.peek()]) {
                st.pop();
            }
            right[i] = st.isEmpty() ? n : st.peek();
            st.push(i);
        }

        // 计算最大矩形面积
        int ans = 0;
        for (int i = 0; i < n; i++) {
            int h = nums[i];
            int l = left[i];
            int r = right[i];
            if (l < k && k < r) { // 如果 k 位置在 [l, r] 之间
                ans = Math.max(ans, h * (r - l - 1));
            }
        }
        return ans;
    }
}

方法:双指针

        我们使用两个指针 i 和 j 来表示当前矩形的左右边界。

        我们从位置 k 开始,向左右两个方向扩展,每次选择高度较小的边界向内移动,直到两个边界相遇或者超出数组边界。

        在移动过程中,我们不断更新当前的最小高度 minH,并计算以当前最小高度为高的矩形的面积,更新答案。

        最终返回最大面积。

class Solution {
    public int maximumScore(int[] nums, int k) {
        int n = nums.length;
        int ans = nums[k]; // 初始化答案为 nums[k]
        int minH = nums[k]; // 初始化当前最小高度为 nums[k]
        int i = k, j = k; // 初始化两个指针为 k

        // 循环 n-1 次
        for (int t = 0; t < n - 1; t++) {
            // 如果 j 边界到达数组右端或者 i 边界大于 0 且 i-1 位置高度大于 j+1 位置高度
            if (j == n - 1 || (i > 0 && nums[i - 1] > nums[j + 1])) {
                minH = Math.min(minH, nums[--i]); // 向左移动 i 指针
            } else {
                minH = Math.min(minH, nums[++j]); // 向右移动 j 指针
            }
            ans = Math.max(ans, minH * (j - i + 1)); // 计算以当前最小高度为高的矩形面积并更新答案
        }
        return ans;
    }
}

20.数组元素的最小非零乘积

题目链接:1969. 数组元素的最小非零乘积

给你一个正整数 p 。你有一个下标从 1 开始的数组 nums ,这个数组包含范围 [1, 2p - 1] 内所有整数的二进制形式(两端都 包含)。你可以进行以下操作 任意 次:

  • 从 nums 中选择两个元素 x 和 y 。
  • 选择 x 中的一位与 y 对应位置的位交换。对应位置指的是两个整数 相同位置 的二进制位。
    比方说,如果 x = 1101 且 y = 0011 ,交换右边数起第 2 位后,我们得到 x = 1111 和 y = 0001 。

请你算出进行以上操作 任意次 以后,nums 能得到的 最小非零 乘积。将乘积对 109 + 7 取余 后返回。

注意:答案应为取余 之前 的最小值。

示例 1:

输入:p = 1

输出:1

解释:nums = [1] 。

只有一个元素,所以乘积为该元素。

示例 2:

输入:p = 2

输出:6

解释:nums = [01, 10, 11] 。

所有交换要么使乘积变为 0 ,要么乘积与初始乘积相同。

所以,数组乘积 1 * 2 * 3 = 6 已经是最小值。

示例 3:

输入:p = 3

输出:1512

解释:nums = [001, 010, 011, 100, 101, 110, 111]

  • 第一次操作中,我们交换第二个和第五个元素最左边的数位。
    • 结果数组为 [001, 110, 011, 100, 001, 110, 111] 。
  • 第二次操作中,我们交换第三个和第四个元素中间的数位。
    • 结果数组为 [001, 110, 001, 110, 001, 110, 111] 。

数组乘积 1 * 6 * 1 * 6 * 1 * 6 * 7 = 1512 是最小乘积。

提示:

1 <= p <= 60

题解:
方法:贪心
        首先,计算出 2^p - 1 的值,记为 k。然后,计算 k * (k - 1)^(p - 1) 的结果,并对结果取模。在计算过程中,使用快速幂算法来计算幂次方,以避免大数幂的过程中的性能问题。

注释:
- pow方法:快速幂算法,计算 x^p % MOD 的结果。
- minNonZeroProduct方法:根据贪心思路求解最小非零乘积的问题。首先计算出 k = 2^p - 1,然后计算 k * (k - 1)^(p - 1) 的结果,并对结果取模后返回。
public class Solution {
    private static final int MOD = 1_000_000_007;

    // 快速幂算法
    private long pow(long x, int p) {
        x %= MOD;
        long res = 1;
        while (p-- > 0) {
            res = res * x % MOD;
            x = x * x % MOD;
        }
        return res;
    }

    // 求解最小非零乘积的问题
    public int minNonZeroProduct(int p) {
        long k = (1L << p) - 1; // 计算 2^p - 1
        return (int) (k % MOD * pow(k - 1, p - 1) % MOD); // 计算 k * (k - 1)^(p - 1) % MOD 的结果并返回
    }
}

21.频率跟踪器

题目链接:2671. 频率跟踪器

请你设计并实现一个能够对其中的值进行跟踪的数据结构,并支持对频率相关查询进行应答。

实现 FrequencyTracker 类:

  • FrequencyTracker():使用一个空数组初始化 FrequencyTracker 对象。
  • void add(int number):添加一个 number 到数据结构中。
  • void deleteOne(int number):从数据结构中删除一个 number 。数据结构 可能不包含 number ,在这种情况下不删除任何内容。
  • bool hasFrequency(int frequency): 如果数据结构中存在出现 frequency 次的数字,则返回 true,否则返回 false。

示例 1:

输入

[“FrequencyTracker”, “add”, “add”, “hasFrequency”]

[[], [3], [3], [2]]

输出

[null, null, null, true]

解释

FrequencyTracker frequencyTracker = new FrequencyTracker();

frequencyTracker.add(3); // 数据结构现在包含 [3]

frequencyTracker.add(3); // 数据结构现在包含 [3, 3]

frequencyTracker.hasFrequency(2); // 返回 true ,因为 3 出现 2 次

示例 2:

输入

[“FrequencyTracker”, “add”, “deleteOne”, “hasFrequency”]

[[], [1], [1], [1]]

输出

[null, null, null, false]

解释

FrequencyTracker frequencyTracker = new FrequencyTracker();

frequencyTracker.add(1); // 数据结构现在包含 [1]

frequencyTracker.deleteOne(1); // 数据结构现在为空 []

frequencyTracker.hasFrequency(1); // 返回 false ,因为数据结构为空

示例 3:

输入 [“FrequencyTracker”, “hasFrequency”, “add”, “hasFrequency”]

[[], [2], [3], [1]]

输出

[null, false, null, true]

解释

FrequencyTracker frequencyTracker = new FrequencyTracker();

frequencyTracker.hasFrequency(2); // 返回 false ,因为数据结构为空

frequencyTracker.add(3); // 数据结构现在包含 [3]

frequencyTracker.hasFrequency(1); // 返回 true ,因为 3 出现 1 次

提示:

1 <= number <= 105

1 <= frequency <= 105

最多调用 add、deleteOne 和 hasFrequency 共计 2 * 105

题解:
方法:双哈希表
        用哈希表 cnt 统计每个数的出现次数。

class FrequencyTracker {
    private final Map<Integer, Integer> cnt = new HashMap<>(); // number 的出现次数
    private final Map<Integer, Integer> freq = new HashMap<>(); // number 的出现次数的出现次数

    public FrequencyTracker() {}

    public void update(int number, int delta) {
        int c = cnt.merge(number, delta, Integer::sum);
        freq.merge(c - delta, -1, Integer::sum); // 去掉一个旧的 cnt[number]
        freq.merge(c, 1, Integer::sum); // 添加一个新的 cnt[number]
    }

    public void add(int number) {
        update(number, 1);
    }

    public void deleteOne(int number) {
        if (cnt.getOrDefault(number, 0) > 0) {
            update(number, -1);
        }
    }

    public boolean hasFrequency(int frequency) {
        return freq.getOrDefault(frequency, 0) > 0; // 至少有一个 number 的出现次数恰好为 frequency
    }
}

分类题单

滑动窗口(定长/不定长/多指针)
二分算法(二分答案/最小化最大值/最大化最小值/第K小)
单调栈(矩形系列/字典序最小/贡献法)
网格图(DFS/BFS/综合应用)
位运算(基础/性质/拆位/试填/恒等式/贪心/脑筋急转弯)
图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

灵神往期的高质量题解(精选)


22.网格图中最少访问的格子数

题目链接:2617. 网格图中最少访问的格子数

给你一个下标从 0 开始的 m x n 整数矩阵 grid 。你一开始的位置在 左上角 格子 (0, 0) 。

当你在格子 (i, j) 的时候,你可以移动到以下格子之一:

  • 满足 j < k <= grid[i][j] + j 的格子 (i, k) (向右移动),或者

  • 满足 i < k <= grid[i][j] + i 的格子 (k, j) (向下移动)。

请你返回到达 右下角 格子 (m - 1, n - 1) 需要经过的最少移动格子数,如果无法到达右下角格子,请你返回 -1 。

示例 1:

在这里插入图片描述

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

输出:4

解释:上图展示了到达右下角格子经过的 4 个格子。

示例 2:

在这里插入图片描述

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

输出:3

解释:上图展示了到达右下角格子经过的 3 个格子。

示例 3:

在这里插入图片描述

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

输出:-1

解释:无法到达右下角格子。

提示:

m == grid.length

n == grid[i].length

1 <= m, n <= 105

1 <= m * n <= 105

0 <= grid[i][j] < m * n

grid[m - 1][n - 1] == 0

题解:
方法:单调栈优化 DP

class Solution {
    public int minimumVisitedCells(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int mn = 0;
        List<int[]>[] colStacks = new ArrayList[n]; // 每列的单调栈,为了能二分用 ArrayList
        Arrays.setAll(colStacks, i -> new ArrayList<int[]>());
        List<int[]> rowSt = new ArrayList<>(); // 行单调栈
        for (int i = m - 1; i >= 0; i--) {
            rowSt.clear();
            for (int j = n - 1; j >= 0; j--) {
                int g = grid[i][j];
                List<int[]> colSt = colStacks[j];
                mn = i < m - 1 || j < n - 1 ? Integer.MAX_VALUE : 1;
                if (g > 0) {
                    // 在单调栈上二分
                    int k = search(rowSt, j + g);
                    if (k < rowSt.size()) {
                        mn = rowSt.get(k)[0] + 1;
                    }
                    k = search(colSt, i + g);
                    if (k < colSt.size()) {
                        mn = Math.min(mn, colSt.get(k)[0] + 1);
                    }
                }
                if (mn < Integer.MAX_VALUE) {
                    // 插入单调栈
                    while (!rowSt.isEmpty() && mn <= rowSt.get(rowSt.size() - 1)[0]) {
                        rowSt.remove(rowSt.size() - 1);
                    }
                    rowSt.add(new int[]{mn, j});
                    while (!colSt.isEmpty() && mn <= colSt.get(colSt.size() - 1)[0]) {
                        colSt.remove(colSt.size() - 1);
                    }
                    colSt.add(new int[]{mn, i});
                }
            }
        }
        return mn < Integer.MAX_VALUE ? mn : -1; // 最后一个算出的 mn 就是 f[0][0]
    }

    // 开区间二分,见 https://www.bilibili.com/video/BV1AP41137w7/
    private int search(List<int[]> st, int target) {
        int left = -1, right = st.size(); // 开区间 (left, right)
        while (left + 1 < right) { // 区间不为空
            int mid = left + (right - left) / 2;
            if (st.get(mid)[1] <= target) {
                right = mid; // 范围缩小到 (left, mid)
            } else {
                left = mid; // 范围缩小到 (mid, right)
            }
        }
        return right;
    }
}

方法:贪心+最小堆
        类似 Dijkstra 算法

class Solution {
    public int minimumVisitedCells(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int f = 0;
        PriorityQueue<int[]>[] colHeaps = new PriorityQueue[n]; // 每一列的最小堆
        Arrays.setAll(colHeaps, i -> new PriorityQueue<int[]>((a, b) -> a[0] - b[0]));
        PriorityQueue<int[]> rowH = new PriorityQueue<>((a, b) -> a[0] - b[0]); // 行最小堆
        for (int i = 0; i < m; i++) {
            rowH.clear();
            for (int j = 0; j < n; j++) {
                while (!rowH.isEmpty() && rowH.peek()[1] < j) { // 无法到达第 j 列
                    rowH.poll(); // 弹出无用数据
                }
                PriorityQueue<int[]> colH = colHeaps[j];
                while (!colH.isEmpty() && colH.peek()[1] < i) { // 无法到达第 i 行
                    colH.poll(); // 弹出无用数据
                }

                f = i > 0 || j > 0 ? Integer.MAX_VALUE : 1; // 起点算 1 个格子
                if (!rowH.isEmpty()) {
                    f = rowH.peek()[0] + 1; // 从左边跳过来
                }
                if (!colH.isEmpty()) {
                    f = Math.min(f, colH.peek()[0] + 1); // 从上边跳过来
                }

                int g = grid[i][j];
                if (g > 0 && f < Integer.MAX_VALUE) {
                    rowH.offer(new int[]{f, g + j}); // 经过的格子数,向右最远能到达的列号
                    colH.offer(new int[]{f, g + i}); // 经过的格子数,向下最远能到达的行号
                }
            }
        }
        return f < Integer.MAX_VALUE ? f : -1; // 此时的 f 是在 (m-1, n-1) 处算出来的
    }
}

23.统计桌面上的不同数字

题目链接:2549. 统计桌面上的不同数字

给你一个正整数 n ,开始时,它放在桌面上。在 109 天内,每天都要执行下述步骤:

  • 对于出现在桌面上的每个数字 x ,找出符合 1 <= i <= n 且满足 x % i == 1 的所有数字 i 。
  • 然后,将这些数字放在桌面上。

返回在 109天之后,出现在桌面上的 不同 整数的数目。

注意:

  • 一旦数字放在桌面上,则会一直保留直到结束。
  • % 表示取余运算。例如,14 % 3 等于 2 。

示例 1:

输入:n = 5

输出:4

解释:最开始,5 在桌面上。

第二天,2 和 4 也出现在桌面上,因为 5 % 2 == 1 且 5 % 4 == 1 。

再过一天 3 也出现在桌面上,因为 4 % 3 == 1 。

在十亿天结束时,桌面上的不同数字有 2 、3 、4 、5 。

示例 2:

输入:n = 3

输出:2

解释:

因为 3 % 2 == 1 ,2 也出现在桌面上。

在十亿天结束时,桌面上的不同数字只有两个:2 和 3 。

提示:

1 <= n <= 100

题解:
方法:数学 O(1)
        因为 n  mod (n−1)=1 一定满足要求,所以我们可以从 n 开始,生成 n−1,n−2,⋯,最后 [2,n] 中的数字都会在桌面上,这有一共有 n−1 个。

        注意特判 n=1 的情况,此时答案为 1。

class Solution {
    public int distinctIntegers(int n) {
        return Math.max(n - 1, 1);
    }
}

24.零钱兑换

题目链接:322. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11

输出:3

解释:11 = 5 + 5 + 1

示例 2:

输入:coins = [2], amount = 3

输出:-1

示例 3:

输入:coins = [1], amount = 0

输出:0

提示:

1 <= coins.length <= 12

1 <= coins[i] <= 231 - 1

0 <= amount <= 104

题解:

动态规划

方法:记忆化搜索(递归搜索 + 保存计算结果)
        设计一个递归函数dfs用于计算凑齐金额c所需的最少硬币数量,其中i表示当前考虑的硬币种类。

        在dfs函数中,若i小于0,说明没有硬币可用,若c等于0,表示已经凑齐了目标金额,返回0;否则返回一个较大的数(表示无解)。

        若memo数组中已经保存了当前状态的结果,则直接返回memo[i][c]。

        若c小于coins[i],说明当前硬币面额大于剩余金额,无法使用当前硬币,则返回dfs(i - 1, c)的结果。

        否则,当前金额c可以使用当前硬币coins[i],比较不使用当前硬币和使用当前硬币的情况,取其中最小的结果。

class Solution {
    private int[] coins; // 硬币面额数组
    private int[][] memo; // 记忆化数组

    // 构造函数,初始化coins数组和memo数组,并调用dfs函数求解最少硬币数量
    public int coinChange(int[] coins, int amount) {
        this.coins = coins;
        int n = coins.length;
        memo = new int[n][amount + 1];
        for (int[] row : memo)
            Arrays.fill(row, -1); // 初始化memo数组为-1
        int ans = dfs(n - 1, amount); // 调用dfs函数求解最少硬币数量
        return ans < Integer.MAX_VALUE / 2 ? ans : -1; // 返回结果,若无解则返回-1
    }

    // 递归函数,计算凑齐金额c所需的最少硬币数量
    private int dfs(int i, int c) {
        // 若i小于0,表示没有硬币可用;若c等于0,表示已凑齐目标金额,返回0;否则返回一个较大的数(表示无解)
        if (i < 0) return c == 0 ? 0 : Integer.MAX_VALUE / 2;
        // 若memo数组中已保存了当前状态的结果,则直接返回memo[i][c]
        if (memo[i][c] != -1) return memo[i][c];
        // 若c小于coins[i],当前硬币面额大于剩余金额,无法使用当前硬币,则返回dfs(i - 1, c)的结果
        if (c < coins[i]) return memo[i][c] = dfs(i - 1, c);
        // 否则,当前金额c可以使用当前硬币coins[i],比较不使用当前硬币和使用当前硬币的情况,取其中最小的结果
        return memo[i][c] = Math.min(dfs(i - 1, c), dfs(i, c - coins[i]) + 1);
    }
}

方法:递推

        设计一个二维数组f,其中f[i][c]表示使用前i种硬币凑出金额c所需的最少硬币数量。

        初始化f数组,将第一行的所有元素初始化为Integer.MAX_VALUE / 2,除了f[0][0]设为0,表示不需要硬币时的硬币数量为0。

        通过状态转移方程更新f数组的值,即f[i][c] = min(f[i - 1][c], f[i][c - coins[i]] + 1)。

        最终返回f[n][amount],即使用所有硬币凑出金额amount所需的最少硬币数量。

class Solution {
    // 使用动态规划解决硬币找零问题
    public int coinChange(int[] coins, int amount) {
        int n = coins.length; // 获取硬币种类数量
        int[][] f = new int[n + 1][amount + 1]; // 定义二维数组f,用于存储状态转移结果
        Arrays.fill(f[0], Integer.MAX_VALUE / 2); // 初始化第一行的所有元素为Integer.MAX_VALUE / 2
        f[0][0] = 0; // 设置f[0][0]为0,表示不需要硬币时的硬币数量为0

        // 动态规划状态转移过程,更新f数组的值
        for (int i = 0; i < n; ++i) {
            for (int c = 0; c <= amount; ++c) {
                if (c < coins[i]) { // 当前金额小于当前硬币面值时,不选当前硬币
                    f[i + 1][c] = f[i][c];
                } else { // 否则,选取当前硬币或不选取当前硬币中的最小值
                    f[i + 1][c] = Math.min(f[i][c], f[i + 1][c - coins[i]] + 1);
                }
            }
        }
        
        int ans = f[n][amount]; // 获取使用所有硬币凑出金额amount所需的最少硬币数量
        return ans < Integer.MAX_VALUE / 2 ? ans : -1; // 若最优解大于阈值,则返回-1
    }
}

方法:空间优化:滚动数组

        由于状态转移方程只涉及到上一行的值,因此可以使用滚动数组进行空间优化,只需两个一维数组。

        设计两个一维数组f[2][amount + 1],用于存储状态转移结果。

        初始化第一个一维数组f[0][],并且设置f[0][0]为0,表示不需要硬币时的硬币数量为0。

        通过状态转移方程更新第二个一维数组f[(i + 1) % 2][]的值。

        最终返回f[n % 2][amount],即使用所有硬币凑出金额amount所需的最少硬币数量。

class Solution {
    // 使用动态规划解决硬币找零问题(空间优化:滚动数组)
    public int coinChange(int[] coins, int amount) {
        int n = coins.length; // 获取硬币种类数量
        int[][] f = new int[2][amount + 1]; // 定义两个一维数组f,用于存储状态转移结果
        Arrays.fill(f[0], Integer.MAX_VALUE / 2); // 初始化第一个一维数组的所有元素为Integer.MAX_VALUE / 2
        f[0][0] = 0; // 设置f[0][0]为0,表示不需要硬币时的硬币数量为0

        // 动态规划状态转移过程,通过滚动数组更新第二个一维数组的值
        for (int i = 0; i < n; ++i) {
            for (int c = 0; c <= amount; ++c) {
                if (c < coins[i]) { // 当前金额小于当前硬币面值时,不选当前硬币
                    f[(i + 1) % 2][c] = f[i % 2][c];
                } else { // 否则,选取当前硬币或不选取当前硬币中的最小值
                    f[(i + 1) % 2][c] = Math.min(f[i % 2][c], f[(i + 1) % 2][c - coins[i]] + 1);
                }
            }
        }

        int ans = f[n % 2][amount]; // 获取使用所有硬币凑出金额amount所需的最少硬币数量
        return ans < Integer.MAX_VALUE / 2 ? ans : -1; // 若最优解大于阈值,则返回-1
    }
}

方法:空间优化:一个数组

        由于状态转移方程只涉及到前一个状态的值,因此可以只使用一个一维数组进行空间优化。

        设计一个一维数组f,用于存储状态转移结果。

        初始化数组f,将除0元外的所有金额的硬币数量初始化为无穷大(用Integer.MAX_VALUE / 2表示)。

        将f[0]设置为0,表示不需要硬币时的硬币数量为0。

        通过状态转移方程更新数组f的值,即f[c] = Math.min(f[c], f[c - x] + 1),其中x为当前硬币面值。

        最终返回f[amount],即使用所有硬币凑出金额amount所需的最少硬币数量。

class Solution {
    // 使用动态规划解决硬币找零问题(空间优化:一个数组)
    public int coinChange(int[] coins, int amount) {
        int[] f = new int[amount + 1]; // 定义一个一维数组f,用于存储状态转移结果
        Arrays.fill(f, Integer.MAX_VALUE / 2); // 初始化除0元外的所有金额的硬币数量为无穷大(用Integer.MAX_VALUE / 2表示)
        f[0] = 0; // 设置f[0]为0,表示不需要硬币时的硬币数量为0

        // 动态规划状态转移过程,通过一维数组更新硬币数量的值
        for (int x : coins) { // 遍历硬币面值数组
            for (int c = x; c <= amount; ++c) { // 遍历金额范围
                f[c] = Math.min(f[c], f[c - x] + 1); // 更新当前金额所需的最少硬币数量
            }
        }

        int ans = f[amount]; // 获取使用所有硬币凑出金额amount所需的最少硬币数量
        return ans < Integer.MAX_VALUE / 2 ? ans : -1; // 若最优解大于阈值,则返回-1
    }
}

题单:动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)

下接:【题解】—— LeetCode一周小结13


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1543291.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

题目:小蓝的学位运算(蓝桥OJ 3220)

问题描述&#xff1a; 解题思路&#xff1a; 题目计算是将每一个区间的异或值相乘得结果&#xff0c;所以直接枚举每个区间并注意剪枝&#xff0c;结果要开long long。 哥们不懂雀巢原理&#xff0c;只好在每一次计算ans的过程中判断是不是0&#xff0c;是0直接输出0&#xff0…

如何在Linux系统使用Docker本地部署Halo网站并实现无公网IP远程访问

最近&#xff0c;我发现了一个超级强大的人工智能学习网站。它以通俗易懂的方式呈现复杂的概念&#xff0c;而且内容风趣幽默。我觉得它对大家可能会有所帮助&#xff0c;所以我在此分享。点击这里跳转到网站。 文章目录 1. Docker部署Halo1.1 检查Docker版本如果未安装Docker可…

每日学习笔记:C++ STL 容器的杂谈

三种自定义STL容器 string作为STL容器 C风格数组作为STL容器 C11以后 C11以前 容器元素类型是引用 使用智能指针存储元素 使用引用外覆器 各容器使用时机 如何分别用两种不同的排序准则来存储同批数据&#xff1f; 解决方案&#xff1a;将容器元素改为智能指针即可。 根据排…

如何本地搭建群晖虚拟机并实现无quickconnect服务环境远程访问

文章目录 前言本教程解决的问题是&#xff1a;按照本教程方法操作后&#xff0c;达到的效果是前排提醒&#xff1a; 1. 搭建群晖虚拟机1.1 下载黑群晖文件vmvare虚拟机安装包1.2 安装VMware虚拟机&#xff1a;1.3 解压黑群晖虚拟机文件1.4 虚拟机初始化1.5 没有搜索到黑群晖的解…

实战 | 小程序优惠卷遍历

进入小程序&#xff0c;因为是一个小商城&#xff0c;所以照例先查看收货地址是否存在越权&#xff0c;以及能否未授权访问&#xff0c;但是发现不存在这些问题&#xff0c;所以去查看优惠卷 进入领券中心&#xff0c;点击领取优惠券时抓包 发现数据包&#xff0c;存在敏感参数…

CTK插件框架学习-新建插件(02)

CTK插件框架学习-源码下载编译(01)https://mp.csdn.net/mp_blog/creation/editor/136891825 开发环境 window11、vs17、Qt5.14.0、cmake3.27.4 开发流程 新建ctk框架调用工程&#xff08;CTKPlugin&#xff09; 拷贝CTK源码编译完成后的头文件和库文件到工程目录&#xff0…

Springboot 整合Mybatis 实现增删改查(二)

续上篇&#xff1a;Springboot整合Mybatis的详细案例图解分析-CSDN博客 mapper层&#xff08;StudentMapper&#xff09; //通过id查询student方法Student searchStudentById(int id);//通过id删除student方法int deleteStudentById(int id);//通过id增加student方法int inser…

[Java基础揉碎]final关键字

目录 介绍 在某些情况下&#xff0c;程序员可能有以下需求&#xff0c;就会使用到final final注意事项和讨论细节 1) final修饰的属性又叫常量&#xff0c;一般用XX_XX_XX来命名 2) final修饰的属性在定义时&#xff0c;必须赋初值&#xff0c;并且以后不能再修改&#…

[AIGC] SQL中的数据添加和操作:数据类型介绍

SQL&#xff08;结构化查询语言&#xff09;作为一种强大的数据库查询和操作工具&#xff0c;它能够完成从简单查询到复杂数据操作的各种任务。在这篇文章中&#xff0c;我们主要讨论如何在SQL中添加&#xff08;插入&#xff09;数据&#xff0c;以及在数据操作过程中&#xf…

【官方】操作指南,附代码!银河麒麟服务器迁移运维管理平台V2.1中间件及高可用服务部署(4)

1.RocketMQ集群模式 主机配置示例&#xff1a; IP 角色 架构模式 对应配置文件 1.1.1.1 nameserver1 master broker-n0.conf 2.2.2.2 nameserver2 salve1 broker-n1.conf 3.3.3.3 nameserver3 salve2 broker-n2.conf 1.1.安装rocketmq 在服务器上安装rocket…

谷歌seo怎么优化产品推广?

想要在谷歌SEO上优化产品推广&#xff0c;关键在于理解和利用搜索引擎的工作原理来提升你的产品在搜索结果中的可见性&#xff0c;结构化数据就很重要了&#xff0c;它能让谷歌更容易理解你的页面内容&#xff0c;让他知道你这个页面不是文章页&#xff0c;主页&#xff0c;而是…

巧用cpl文件维权和免杀(上)

cpl文件 CPL文件&#xff0c;是Windows控制面板扩展项&#xff0c;CPL全拼为Control Panel Item在system32目录下有一系列的cpl文件,分别对应着各种控制面板的子选项 列入我们winR输入main.cpl 将会打开控制面板中的鼠标属性 cpl文件本质是属于PE文件 但cpl并不像exe,更像是dl…

3月份的倒数第二个周末有感

坐在图书馆的那一刻&#xff0c;忽然感觉时间的节奏开始放缓。今天周末因为我们两都有任务需要完成&#xff0c;所以就选了嘉定图书馆&#xff0c;不得不说嘉定新城远香湖附近的图书馆真的很有感觉。然我不经意回想起学校的时光&#xff0c;那是多么美好且短暂的时光。凝视着窗…

手撕算法-盛最多水的容器

描述 分析 两个板之间能盛下的水的量&#xff0c;取决于短板。想让两个板之间能盛下更多的水&#xff0c;需要改变短板的长度。就像水桶效应&#xff1a;那么用两个指针指向容器的两个板&#xff0c;然后每次移动较短的板即可。移动较短的板&#xff0c;可能会增大容积&#x…

芯片设计工程师必备基本功——《Verilog+HDL应用程序设计实例精讲》

进入芯片行业需要学习哪些基本功呢&#xff1f;其实芯片设计工程师的技能是通过多年的经验学习的。在您开始作为芯片设计工程师工作之前&#xff0c;很难给出一个需要的全面的单一列表&#xff0c;也不可能学习所有内容。话虽如此&#xff0c;但您开始芯片设计师职业生涯时必须…

HDFS的Shell操作及客户端配置方法

HDFS进程启停命令 Hadoop HDFS组件内置了HDFS集群的一键启停脚本。 $HADOOP_HOME/sbin/start-dfs.sh&#xff0c;一键启动HDFS集群$HADOOP_HOME/sbin/stop-dfs.sh&#xff0c;一键关闭HDFS集群 执行原理&#xff1a; 在执行此脚本的机器上&#xff0c;启动&#xff08;关闭&…

Java面试题总结200道(四)

76、ApplicationContext 通常的实现是什么? FileSystemXmlApplicationContext &#xff1a;此容器从一个 XML 文件中加 载 beans 的定义&#xff0c;XML Bean 配置文件的全路径名必须提供给它的构造函数。ClassPathXmlApplicationContext&#xff1a;此容器也从一个 XML 文件…

软件签名不一致会出现的原因和采取的措施

软件签名不一致的问题可能涉及到数字签名、证书、应用程序完整性和安全性等多个方面。这个问题对于软件开发和信息安全都是非常重要的&#xff0c;因此需要进行更加深入的讨论和解释。以下是关于软件签名不一致的可能原因的详细解释&#xff1a; 数字签名的作用和原理&#xff…

权限提升-系统权限提升篇数据库提权PostsqlRedis第三方软件提权密码凭据钓鱼文件

知识点 1、数据库到Linux-数据库提权-Redis 3、数据库到Linux-数据库提权-PostgreSQL 4、计算机用户到系统-第三方软件-各类应用 章节点&#xff1a; 1、Web权限提升及转移 2、系统权限提升及转移 3、宿主权限提升及转移 4、域控权限提升及转移 基础点 0、为什么我们要学习权…

【经验分享】转行如何自学Python并且找到工作,分享自己心得

目前信息化产业发展势头很好&#xff0c;互联网就成为了很多普通人想要涉及的行业&#xff0c;因为相比于传统行业&#xff0c;互联网行业涨薪幅度大&#xff0c;机会也多&#xff0c;所以就会大批的人想要转行来学习Python开发。 首先告诉你的是&#xff0c;应届生零基础开始学…