【LeetCode热题100】打卡第43天:会议室II&完全平方数

【LeetCode热题100】打卡第43天:会议室II&完全平方数

⛅前言

大家好,我是知识汲取者,欢迎来到我的LeetCode热题100刷题专栏!

精选 100 道力扣(LeetCode)上最热门的题目,适合初识算法与数据结构的新手和想要在短时间内高效提升的人,熟练掌握这 100 道题,你就已经具备了在代码世界通行的基本能力。在此专栏中,我们将会涵盖各种类型的算法题目,包括但不限于数组、链表、树、字典树、图、排序、搜索、动态规划等等,并会提供详细的解题思路以及Java代码实现。如果你也想刷题,不断提升自己,就请加入我们吧!QQ群号:827302436。我们共同监督打卡,一起学习,一起进步。

PS:作者水平有限,如有错误或描述不当的地方,恳请及时告诉作者,作者将不胜感激

会议室II

🔒题目

原题链接:253.会议室II

image-20230722192654687

🔑题解

  • 解法一:优先队列

    import java.util.PriorityQueue;
    
    /**
     * @author ghp
     * @title
     */
    class Solution {
        public int minMeetingRooms(int[][] intervals) {
            if (intervals.length == 0) {
                return 0;
            }
            // 升序队列,队头到队尾的元素是从小到大排序的,存储活动的结束时间
            PriorityQueue<Integer> queue = new PriorityQueue<>(intervals.length, (a, b) -> a - b);
            // 往队列中添加第一场会议的结束时间
            queue.offer(intervals[0][1]);
            // 遍历会议的起始时间
            for (int i = 1; i < intervals.length; i++) {
                // 判断当前会议开始时刻,之前开始的会议中是否存在已经结束的会议
                if (intervals[i][0] >= queue.peek()) {
                    // 之前开始的会议中存在已经结束的会议,则将结束的会议从队列中移除
                    queue.poll();
                }
                queue.offer(intervals[i][1]);
            }
            // 所有会议都已经开始了,此时队列中剩余的会议就是存在时间冲突的,需要单独安排会议室
            return queue.size();
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n ) O(n) O(n)
    • 空间复杂度: O ( n + m ) O(n+m) O(n+m)

    其中 n n n 为会议的数量,即数组的行,m为数组的列,也就是2

  • 解法二:暴力

    感觉这个暴力比优先队列还要难以理解,本题解参考:[中等] 253. 会议室 II - 简书 (jianshu.com)

    个人感觉难以理解的是 end 数组,这个暴力我感觉有点巧妙,end数组表示会议室的数量,还同时记录会议的结束时间,同时表示当前已结束的会议

    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    
    /**
     * @author ghp
     * @title
     */
    class Solution {
        public int minMeetingRooms(int[][] intervals) {
            // 记录已结束会议的结束时间
            List<Integer> end = new ArrayList<>();
            // 按开始时间升序
            Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
            // 遍历所有的会议
            for (int[] meeting : intervals) {
                boolean isExit = false;
                // 遍历已结束的会议,判断其中是否存在结束时间早于当前会议起始时间的会议
                for (int i = 0; i < end.size(); i++) {
                    int endTime = end.get(i);
                    if (endTime <= meeting[0]) {
                        // 存在结束时间早于当前会议起始时间的会议,需要更新会议的结束时间
                        end.set(i, meeting[1]);
                        // 找到了当前会议起始时间之前结束的会议,说明不需要新增会议室
                        isExit = true;
                        break;
                    }
                }
                if (!isExit) {
                    // 不存在,说明当前的会议时间与其他会议存在冲突,需要新增会议室
                    end.add(meeting[1]);
                }
            }
            // end中存在的会议都是与其它至少一个会议有冲突的,所以end的大小就是至少需要的会议室数量
            return end.size();
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
    • 空间复杂度: O ( n ) O(n) O(n)

    其中 n n n 为数组中元素的个数


拓展内容

前面我们在解法一中使用优先队列一下就解决了问题,感觉意犹未尽,这里我们提高对自己的要求,我们参考PriorityQueue的源码,直接自己手动实现一个优先队列,顺便学习一下优先队列是如何实现的

class MyPriorityQueue {
    /**
     * 存储队列中的元素
     */
    private int[] arr;
    /**
     * 队列的容量
     */
    private int capacity;
    /**
     * 队列中元素的个数
     */
    private int size;

    public MyPriorityQueue(int capacity) {
        this.arr = new int[capacity];
        this.capacity = capacity;
        this.size = 0;
    }

    /**
     * 添加元素
     * @param val
     */
    public void offer(int val) {
        arr[size] = val;
        size++;
        // 把新加入的元素进行向上调整
        siftUp(arr, size - 1);
    }

    /**
     * 向上调整元素
     * @param arr
     * @param index
     */
    private static void siftUp(int[] arr, int index) {
        int child = index;
        int parent = (child - 1) / 2;
        while (child > 0) {
            if (arr[parent] < arr[child]) {
                // 当前不符合大堆结构,就进行调整
                int temp = arr[parent];
                arr[parent] = arr[child];
                arr[child] = temp;
            } else {
                // 发现当前父节点比子节点大,这时说明整个数组已经符合堆的要求了
                break;
            }
            child = parent;
            parent = (child - 1) / 2;
        }
    }

    /**
     * 移除队尾元素
     * @return
     */
    public int poll() {
        // 下标为0,即队首元素,删除堆顶元素,还要保证剩下的结构依旧为堆
        // 先将队首元素保存下来,先将队首元素保存下来,
        int result = arr[0];
        arr[0] = arr[size - 1];
        size--;
        // 针对队首元素进行向下调整
        siftDown(arr, size, 0);
        // 返回保存的队首元素
        return result;
    }

    private static void siftDown(int[] arr, int size, int index) {
        int parent = index;
        // 通过parent找到child的下标
        int child = 2 * parent + 1;
        while (child < size) {
            //比较左右子树找到较大值
            if (child + 1 < size && arr[child + 1] > arr[child]) {
                child = child + 1;
            }
            // 经过上面的比较已经不知道child是左子树,还是右子树了
            // 但是child下标一定对应左右子树中的较大值下标
            //拿child位置元素与parent位置元素比较
            if (arr[child] > arr[parent]) {
                // 不符合就交换父子节点
                int temp = arr[child];
                arr[child] = arr[parent];
                arr[parent] = temp;
            } else {
                // 调整完毕,不需要再调整
                break;
            }
            // 更新parent和child节点,处理下一层数据
            parent = child;
            child = 2 * parent + 1;
        }
    }

    /**
     * 查看队头元素
     * @return
     */
    public int peek() {
        return arr[0];
    }

    /**
     * 判断队列是否为空
     * @return
     */
    public boolean isEmpty() {
        return size == 0;
    }
}

完全平方数

🔒题目

原题链接:279.完全平方数

image-20230722214319080

🔑题解

  • 解法一:DFS(超时 502 / 588

    暴力DFS思路很简单,我们需要提前对数据进行处理,使得这个题目转换成一个求最短路径的问题

    这里举一例例子:

    比如当我们要计算和为12的完全平方数的个数时,我们可以按照以下思路

    1. 计算完全平方数的个数。我们先对12求平方根,可以得出3,这个3就是能够组成和为12的完全平方根的个数
    2. 计算完全平方数。然后根据这个3,我们可以提前计算出12可以由哪些完全平方数组成,很容易可以得到 能够组成12 的完全平方数有:1,4,9
    3. 利用DFS寻找最小组合数,下方是我画的一个比较简单的图解,希望对你的理解有所帮助

    image-20230723173401703

    /**
     * @author ghp
     * @title
     */
    class Solution {
        // 记录当前和为n的完全平方数的最少数量
        private int min = Integer.MAX_VALUE;
    
        public int numSquares(int n) {
            // 计算完全平方数的个数
            int sqrt = (int) Math.sqrt(n);
            // 计算完全平方数
            int[] square = new int[sqrt];
            for (int i = 1; i <= sqrt; i++) {
                square[i - 1] = i * i;
            }
            // 利用DFS搜寻最短路径
            dfs(square, n, sqrt - 1, 0, 0);
            return min == Integer.MAX_VALUE ? -1 : min;
        }
    
        private void dfs(int[] square, int target, int index, int sum, int count) {
            if (sum == target) {
                // 寻找一种方案,更新min
                min = Math.min(min, count);
                return;
            }
            if (sum > target || count >= min) {
                // 剪枝。当前和已经超过目标值 或 当前参与求和平方数的个数已经大于等于最小值了
                return;
            }
            for (int i = square.length - 1; i > 0; i--) {
                dfs(square, target, i, sum + square[i], count + 1);
            }
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( 2 n ) O(2^n) O(2n),由于引入了剪枝操作实际的耗时会比理论上的低
    • 空间复杂度: O ( n ) O(n) O(n)

    其中 n n n 为square数组中元素的个数

    代码优化:(优化后还是超时😫 551 / 588

    1. 去除了count和isFind等成员变量,直接采用变量的引用传递。直接进行变量的引用传递通常比使用成员变量要更好,因为这样可以减少对成员变量的访问开销,并且可以避免多线程环境下的竞争和同步问题。而且,使用引用传递可以使代码更清晰和易读。
    2. 在dfs方法中增加了一个剪枝条件,当当前步数count已经大于等于min时,停止继续搜索
    3. 在dfs方法中增加了一个next变量,记录下一层最大的值,优化了循环次数(核心优化)

    这个 next 能够使得快速定位到下一个数,比如:square=1,4,9,target=12,第一遍index=2,sum=0,此时 next= (12-0)/9=1,下一层遍历就直接从索引为1开始,直接对索引大于1的进行了一个剪枝

    image-20230723175302113

    /**
     * @author ghp
     * @title
     */
    class Solution {
        // 记录当前和为n的完全平方数的最少数量
        private int min = Integer.MAX_VALUE;
    
        public int numSquares(int n) {
            // 计算完全平方数的个数
            int sqrt = (int) Math.sqrt(n);
            // 计算完全平方数
            int[] square = new int[sqrt];
            for (int i = 1; i <= sqrt; i++) {
                square[i - 1] = i * i;
            }
            // 利用DFS搜寻最短路径
            dfs(square, n, sqrt - 1, 0, 0);
            return min == Integer.MAX_VALUE ? -1 : min;
        }
    
        private void dfs(int[] square, int target, int index, int sum, int count) {
            if (sum == target) {
                // 寻找一种方案,更新min
                min = Math.min(min, count);
                return;
            }
            if (index < 0 || count >= min) {
                // 剪枝。已经到最底层了 或 当前参与求和平方数的个数已经大于等于最小值了
                return;
            }
            // 计算下一层的最大节点
            int maxCount = (target - sum) / square[index];
            for (int i = maxCount; i > 0; i--) {
                dfs(square, target, index - 1, sum + i * square[index], count + i);
            }
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( 2 n ) O(2^n) O(2n),由于引入了剪枝操作实际的耗时会比理论上的低
    • 空间复杂度: O ( n ) O(n) O(n)

    其中 n n n 为square数组中元素的个数

    代码优化:(过啦)

    我们可以发现之前那种先把所有能够组成n的平方和求出的方式比较蠢,既浪费了空间又浪费了时间,我们完全可以在一边DFS遍历的时候,一遍计算平方和,从最大数到最小数开始搜索。解题的图解和前面的是一致的,就是节约了求取平方和的操作,从而大大节约的时间,但是可以发现这样子,能过,提交后空间占比超过该90%,时间占比只超过5%,所以说这一题DFS并不是终极解法

    class Solution {
        private int minCount = Integer.MAX_VALUE;
    
        public int numSquares(int n) {
            dfs(n, 0);
            return minCount;
        }
    
        private void dfs(int target, int count) {
            if (count >= minCount) {
                // 如果当前数量已经超过最小数量,则直接返回
                return;  
            }
            if (target == 0) {
                // 如果目标数为0,更新最小数量并返回
                minCount = Math.min(minCount, count);
                return;  
            }
            for (int i = (int) Math.sqrt(target); i > 0; i--) {
                // 尝试使用完全平方数进行递归搜索
                dfs(target - i * i, count + 1);  
            }
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( 2 n ) O(2^n) O(2n),由于引入了剪枝操作实际的耗时会比理论上的低
    • 空间复杂度: O ( n ) O(n) O(n)

    其中 n n n 为square数组中元素的个数

  • 解法二:BFS(超时 502 / 588

    一般DFS的题目都可以使用BFS解决,同时对于最短路径问题,一般BFS要比DFS要更加快。这里吸取前面的经验,来使用BFS实现

    import java.util.Deque;
    import java.util.LinkedList;
    
    /**
     * @author ghp
     * @title
     */
    class Solution {
    
        public int numSquares(int n) {
            Deque<int[]> queue = new LinkedList<>();
            queue.offer(new int[]{0, 0});
            while (!queue.isEmpty()) {
                // 上一层节点的状态
                int[] pre = queue.poll();
                int sum = pre[0];
                int count = pre[1];
                if (sum == n || count == min) {
                    // BFS一层一层往下搜,最先等于n的,一定是最段路径,所以这里直接就返回了
                    return count;
                }
                // 遍历下一层
                for (int i = (int) Math.sqrt(n); i > 0; i--) {
                    if (sum + i * i <= n) {
                        queue.offer(new int[]{sum + i * i, count + 1});
                    }
                }
            }
            return -1;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n 1.5 ) O(n^{1.5}) O(n1.5),在每一层中,内层循环的次数是随着当前节点的值而减少的,即与n有关的次数。每个节点最多可以生成n个节点,因此总共的节点个数是n^1.5级别的。
    • 空间复杂度: O ( n ) O(n) O(n),主要是用来存储队列和集合的空间消耗,并且最坏情况下,所有的数都需要存储在vis集合中

    其中 n n n 为square数组中元素的个数

    对于最短路径问题,一般BFS要比DFS能容易找到答案

    代码优化:(过啦)

    直接暴力BFS不能过,我们可以进行记忆化搜索从而实现剪枝效果,

    import java.util.Deque;
    import java.util.HashSet;
    import java.util.LinkedList;
    import java.util.Set;
    
    class Solution {
    
        public int numSquares(int n) {
            Deque<int[]> queue = new LinkedList<>();
            // 记录搜索的状态
            Set<Integer> vis = new HashSet<>();
            queue.offer(new int[]{0, 0});
            while (!queue.isEmpty()) {
                // 上一层节点的状态
                int[] pre = queue.poll();
                int sum = pre[0];
                int count = pre[1];
                if (sum == n || count == min) {
                    // BFS一层一层往下搜,最先等于n的,一定是最段路径,所以这里直接就返回了
                    return count;
                }
                // 遍历下一层
                for (int i = (int) Math.sqrt(n); i > 0; i--) {
                    int curSum = sum + i * i;
                    if (!vis.contains(curSum) && curSum <= n) {
                        // 当前层的状态并没有出现,并且当前和也要小于目标值,则可以往下遍历
                        queue.offer(new int[]{curSum, count + 1});
                        // 将当前状态添加到vis集合中
                        vis.add(curSum);
                    }
                }
            }
            return -1;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n 1.5 ) O(n^{1.5}) O(n1.5),在每一层中,内层循环的次数是随着当前节点的值而减少的,即与n有关的次数。每个节点最多可以生成n个节点,因此总共的节点个数是n^1.5级别的。
    • 空间复杂度: O ( n ) O(n) O(n),主要是用来存储队列和集合的空间消耗,并且最坏情况下,所有的数都需要存储在vis集合中

    其中 n n n 为square数组中元素的个数


    这里再提供一种方法,参考这位大佬的,我感觉这个BFS解法相当优雅,这里是直接通过 sqrt 计算出下一层节点的最大值,从而不需要遍历所有的可能,不需要像上面那种方法还单独拿一个 vis 去记录状态,使用 sqrt计算后 压根就不需要去当心会有重复状态的出现,它相当于是跳跃式的遍历,这个有点类似与我之前 DFS 中的方法,至少在DFS我想到了,在这里我又没想到w(゚Д゚)w

    import java.util.LinkedList;
    import java.util.Queue;
    
    class Solution {
        public int numSquares(int n) {
            // 队列记录剩余值
            Queue<Integer> queue = new LinkedList<>();
            queue.add(n);
            int count = 0;
            while (!queue.isEmpty()) {
                count++;
                for (int size = queue.size(); size > 0; size--) {
                    // 回溯到上一个节点
                    int pre = queue.poll();
                    // 计算下一层最大的值
                    int next = (int) Math.sqrt(pre);
                    if (next * next == pre) {
                        return count;
                    }
                    for (int i = next; i > 0; i--) {
                        queue.add(pre - i * i);
                    }
                }
            }
            return -1;
        }
    }
    
  • 解法三:动态规划

    本题本质是一个完全背包问题,我们完全可以参考完全背包问题的解法进行求解

    题解参考:【宫水三叶】详解完全背包一维空间优化推导(附背包问题攻略) - 完全平方数 - 力扣(LeetCode)

    import java.util.Arrays;
    
    class Solution {
        int INF = 0x3f3f3f3f;
    
        public int numSquares(int n) {
            // 计算出所有可能用到的完全平方数
            int m = (int) Math.sqrt(n);
            int[] square = new int[m];
            for (int i = 1; i <= m; i++) {
                square[i - 1] = i * i;
            }
            // f[i][j] 代表考虑前 i 个物品,凑出 j 所使用到的最小元素个数
            int[][] f = new int[m + 1][n + 1];
            // 当没有任何数时,除了 f[0][0] 为 0(花费 0 个数值凑出 0),其他均为无效值
            Arrays.fill(f[0], INF);
            f[0][0] = 0;
            // 处理剩余数的情况
            for (int i = 1; i <= m; i++) {
                int x = square[i - 1];
                for (int j = 0; j <= n; j++) {
                    // 对于不选第 i 个数的情况
                    f[i][j] = f[i - 1][j];
                    // 对于选 k 次第 i 个数的情况
                    for (int k = 1; k * x <= j; k++) {
                        // 能够选择 k 个 x 的前提是剩余的数字 j - k * x 也能被凑出
                        if (f[i - 1][j - k * x] != INF) {
                            f[i][j] = Math.min(f[i][j], f[i - 1][j - k * x] + k);
                        }
                    }
                }
            }
            return f[m][n];
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n 2 ∗ n ) O(n^2*\sqrt{n}) O(n2n )
    • 空间复杂度: O ( n ∗ n ) O(n*\sqrt{n}) O(nn )

    其中 n n n 为square数组中元素的个数

    代码优化:空间优化

    备注:这里我先把题解放这里,今天就先把DFS和BFS的两个解法给消化吸收了,这个动态规划,我把题解和链接先放这里,后面二刷或者后面有时间的时候再来看一看,同样的这个题解也来自【宫水三叶】大佬

    二维转一维

    class Solution {
        public int numSquares(int n) {
            int[] f = new int[n + 1];
            Arrays.fill(f, 0x3f3f3f3f);
            f[0] = 0;
            for (int t = 1; t * t <= n; t++) {
                int x = t * t;
                for (int j = x; j <= n; j++) {
                    f[j] = Math.min(f[j], f[j - x] + 1);
                }
            }
            return f[n];
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n ∗ n ) O(n^*\sqrt{n}) O(nn )
    • 空间复杂度: O ( n ) O(\sqrt{n}) O(n )

    其中 n n n 为square数组中元素的个数

  • 解法四:四平方和定理

    这是本题的究极解法了,感兴趣的可以去LeetCode官方看看(果然算法的尽头就是数学)

    class Solution {
        public int numSquares(int n) {
            if (isPerfectSquare(n)) {
                return 1;
            }
            if (checkAnswer4(n)) {
                return 4;
            }
            for (int i = 1; i * i <= n; i++) {
                int j = n - i * i;
                if (isPerfectSquare(j)) {
                    return 2;
                }
            }
            return 3;
        }
    
        // 判断是否为完全平方数
        public boolean isPerfectSquare(int x) {
            int y = (int) Math.sqrt(x);
            return y * y == x;
        }
    
        // 判断是否能表示为 4^k*(8m+7)
        public boolean checkAnswer4(int x) {
            while (x % 4 == 0) {
                x /= 4;
            }
            return x % 8 == 7;
        }
    }
    
    作者:LeetCode-Solution
    链接:https://leetcode.cn/problems/perfect-squares/solution/wan-quan-ping-fang-shu-by-leetcode-solut-t99c/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    

    复杂度分析:

    • 时间复杂度: O ( n ) O(\sqrt{n}) O(n )
    • 空间复杂度: O ( 1 ) O(1) O(1)

参考题解

在此致谢各位LeetCode的大佬

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知识汲取者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值