当前位置: 首页 > news >正文

基础算法训练7

目录

库存管理II

翻转对

合并K个升序链表

存在重复元素II

字符串相乘

字符串解码

在每个树行中找最大值

数据流的中位数

被包围的区域

为高尔夫比赛砍树

库存管理II

LCR 159. 库存管理 III - 力扣(LeetCode)

解法一:先进行排序,接着返回要的个数即可

class Solution {
    public int[] inventoryManagement(int[] stock, int cnt) {
        Arrays.sort(stock);
        int[] ret = new int[cnt];
        for(int i = 0; i < cnt; i++){
            ret[i] = stock[i];
        }
        return ret;
    }
}

解法二:维护一个优先级队列,由于是要最小值,所以建最小堆,把元素都加入堆之后,前cnt个即可

class Solution {
    public int[] inventoryManagement(int[] stock, int cnt) {
        if(cnt==0)return new int[0]; //特殊情况处理
        Queue<Integer> q = new PriorityQueue<>(cnt);
        int[] ret = new int[cnt];
        for(int i = 0; i < stock.length; i++){
            q.offer(stock[i]);
        }
        for(int i = 0; i < cnt; i++) ret[i] = q.poll();
        return ret;
    }
}

翻转对

493. 翻转对 - 力扣(LeetCode)

要找的是这样一对数,其中前面的数大于后面数字的两倍。这个问题的解决思路与寻找逆序对有相似之处,都是利用归并排序的思想。在逆序对的计算中,我们可以在合并数组的过程中同时计算逆序对的个数;而在这个问题中,必须先计算翻转对的个数。
 
在计算翻转对个数时,可以采用归并的降序和升序两种方式。其核心思想和逆序对是一致的,需要特别注意的是,判断条件不能简单地按照题目要求,直接判断一个数是否比后一个数的两倍大,因为这样在计算过程中可能会发生溢出。因此,我们改为比较当前数的二分之一是否比后一个数大,如果出现溢出情况,则直接终止循环,因为此时前一个数肯定不可能比溢出后的数更大。
 
降序情况:
在降序排列的过程中,当满足当前数的二分之一大于后一个数,或者右区间端点越界时,就退出循环。如果是右区间端点越界,直接终止循环即可,因为这意味着后面不存在比当前数的二分之一更大的数了;如果没有越界,则更新翻转对的个数,并移动右区间的指针。
 
升序情况:
在升序排列的过程中,当满足当前数的二分之一大于后一个数,或者左区间端点越界时,就退出循环。若左区间端点越界,直接结束循环,因为这表明在当前左区间之后不存在比当前数的二分之一更小的数(由于是升序,左区间后面的数只会更大)。若没有越界,则更新翻转对的个数,并移动左区间的指针。

降序排序:

class Solution {
    public int reversePairs(int[] nums) {
        int len = nums.length;
        int[] tmp = new int[len];
        return _merge(nums,tmp,0,len-1);
    }
    public int _merge(int[] nums, int[] tmp, int left, int right){
        if(left >= right) return 0 ;// 递归终止条件:区间只有一个元素或无元素
        int mid = left + (right-left) / 2; //计算中间值
        int ret = 0;// 当前区间的逆序对计数
        // 1. 递归处理左半部分和右半部分
        ret += _merge(nums,tmp,left,mid);
        ret += _merge(nums,tmp,mid+1,right);
        // 2. 统计重要逆序对(nums[i] > 2*nums[j])
        int begin1 = left,begin2 = mid+1;
        while(begin1 <= mid){
            // 移动右指针,直到找到 nums[begin1] > 2*nums[begin2]
            // 使用 nums[begin1]/2.0 比较避免乘法溢出
            while(begin2<=right && nums[begin1] / 2.0<= nums[begin2]) {
                begin2++;
            }
            // 如果右指针越界,说明左半部分剩余元素都不满足条件 直接break即可
            if(begin2>right) {
                break;
            }
            begin1++;
            ret += right-begin2 + 1; //更新翻转对个数
             
        }
        // 3. 合并两个已排序的子数组(降序排序)
        begin1 = left;begin2 = mid+1;
        int index = left;
        while(begin1<=mid && begin2<=right){
            tmp[index++] = nums[begin1]<=nums[begin2]?nums[begin2++]:nums[begin1++];
        }
        // 处理剩余元素
        while (begin1<=mid)  tmp[index++] = nums[begin1++];
        while (begin2<=right)  tmp[index++] = nums[begin2++];
        for(int i = left; i <= right; i++){
            nums[i] = tmp[i];
        }
        return ret;
    }
}

升序:

        while(begin2 <= right){
            // 移动左指针,直到找到 nums[begin1] > 2*nums[begin2]
            // 使用 nums[begin1]/2.0 比较避免乘法溢出
            while(begin1<=mid && nums[begin1] / 2.0<= nums[begin2]) {
                begin1++;
            }
            // 如果左指针越界, 直接break即可
            if(begin1>mid) {
                break;
            }
            begin2++;
            ret += mid-begin1 + 1; //更新翻转对个数
        }
        // 3. 合并两个已排序的子数组(降序排序)
        begin1 = left;begin2 = mid+1;
        int index = left;
        while(begin1<=mid && begin2<=right){
            tmp[index++] = nums[begin1]<=nums[begin2]?nums[begin1++]:nums[begin2++];
        }

合并K个升序链表

23. 合并 K 个升序链表 - 力扣(LeetCode)

解法一:借助数据结构堆来实现。具体而言,使用最小堆,基于优先级队列来构建这个堆结构。在此创建优先级队列时要自定义比较器,通过它能够依据特定规则对堆中的元素进行排序。
为了简化对边界情况的处理,引入一个虚拟头结点。这样一来,在后续的操作中,就无需针对特殊情况编写额外的逻辑。同时,创建一个尾指针方便在链表尾部插入新节点。
算法的执行流程如下:首先,遍历给定的每一个链表。在遍历每个链表时,针对链表中的每一个节点,将其加入到优先级队列中。需要特别注意的是,由于链表可能存在环,因此在遍历过程中,必须断开每个节点的  next  指针,以避免出现无限循环的问题。
当所有节点都加入到优先级队列后,开始从队列中取出元素。每次取出堆顶元素,将其连接到尾指针所指向节点的后面,然后更新尾指针,使其指向新添加的节点。重复这一过程,直到优先级队列为空。

最终,返回虚拟头结点的下一个节点,该节点即为经过处理后的链表的头节点。

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        // 1. 创建最小堆优先队列,按节点的val值升序排序
        Queue<ListNode> queue = new PriorityQueue<>((o1,o2) -> o1.val-o2.val);
        // 2. 创建虚拟头节点和尾指针,用于构建结果链表
        ListNode ret = new ListNode();
        ListNode end = ret;
        // 3. 遍历所有链表,将节点加入优先队列
        for(ListNode head : lists){  // head是每个链表的头节点
            //拿到每一个链表
            while(head != null){
                ListNode next = head.next;
                // 断开当前节点的next指针,避免链表成环
                head.next = null;
                queue.offer(head); // 将当前节点加入优先队列
                head = next; //更新节点
            }
        }
        // 4. 从优先队列中取出最小节点,构建结果链表
        while(!queue.isEmpty()){
            // 取出堆顶元素,也就是当前最小节点
            end.next = queue.poll(); // 将节点链接到结果链表的尾部
            end = end.next;
        }
        // 5. 返回结果链表的头节点(跳过虚拟头节点)
        return ret.next;
    }
}

2.也可以先将每个链表的头节点加入优先级队列(不需要提前遍历整个链表,需要过滤空链表),接着遍历优先级队列,依次取出节点最小值,加入到结果链表中,如果此时节点还有后继节点咋加入到优先级队列中,直到优先级队列为空。

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        // 1. 创建最小堆优先队列,按节点的val值升序排序
        Queue<ListNode> queue = new PriorityQueue<>((o1,o2) -> o1.val-o2.val);
        // 2. 创建虚拟头节点和尾指针,用于构建结果链表
        ListNode ret = new ListNode();
        ListNode end = ret;
        // 3. 遍历所有链表,将每个链表头节点加入优先队列
        // 这里只需要加入每个链表的头节点,不需要遍历整个链表
        for(ListNode head : lists){ 
            if(head!=null)
            queue.offer(head); // 将非空链表的头节点加入优先队列
        }
        // 4. 从优先队列中取出最小节点,构建结果链表
        while(!queue.isEmpty()){
            ListNode t = queue.poll();
            end.next = t;
            end = t;
            // 如果取出的节点还有后续节点,将后续节点加入优先队列
            if(t.next!=null)queue.offer(t.next);
        }
        // 5. 返回结果链表的头节点(跳过虚拟头节点)
        return ret.next;
    }
}

解法二:归并思想

运用归并思想处理链表。其核心思路是将链表持续二分,把复杂问题拆解为规模更小的子问题。
 在拆分过程中,当子问题里仅包含一个链表时,直接返回该链表;若子问题中没有链表,就返回 null 。当子问题包含两个链表时,执行链表合并操作,将这两个链表有序整合。随后,把合并后的链表作为结果,用于上一层的归并操作。持续这一过程,直至完成对整个链表的归并处理。

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        // 调用分治合并方法,初始范围为整个数组
        return merge(lists,0,lists.length-1);
    }
    public ListNode merge(ListNode[] lists, int left, int right){
        if(left > right) return null;
        // 递归终止条件2:范围内只有一个链表,直接返回
        if(left == right) return lists[left];
        int mid = left + (right - left) / 2;
        // 递归合并左半部分链表
        ListNode node1 = merge(lists, left, mid);
        // 递归合并右半部分链表
        ListNode node2 = merge(lists, mid+1, right);
        //合并两个已排序的链表
        ListNode ret = new ListNode(); // 创建虚拟头节点(简化边界条件处理)
        ListNode tmp = ret;
        while(node1!=null && node2 !=null){
            if(node1.val <node2.val){
                tmp.next = node1;
                tmp = tmp.next;
                node1 = node1.next;
            }else {
                tmp.next = node2;
                tmp = tmp.next;
                node2 = node2.next;          
            }
        }
        tmp.next = node1==null ? node2 : node1;
        return ret.next;
    }
}

存在重复元素II

219. 存在重复元素 II - 力扣(LeetCode)

借助哈希表解决这一问题,哈希表的每个元素由两部分构成:一是数组中的元素本身,二是该元素在数组中的下标。
遍历整个数组时,每次先检查哈希表中是否已经存在当前元素。若存在,取出其下标,并与当前元素下标进行运算,判断是否满足设定条件。一旦满足,立即返回 true 。倘若不满足,或者哈希表中不存在该元素,就将当前元素及其下标更新到哈希表中。这是由于题目要求 i - j 的绝对值小于等于 k ,即需要找到距离当前元素最近的重复元素,因此用当前元素的下标覆盖哈希表中原有的下标。当遍历完整个数组后,若仍未找到符合条件的元素,说明不存在这样的一对元素,直接返回 false 。

class Solution {
    public boolean containsNearbyDuplicate(int[] nums, int k) {
        Map<Integer,Integer> hash = new HashMap<>();
        int len = nums.length;
        for(int i = 0; i < len; i++){
            //如果元素存在则取出下标和当前下标进行运算 判断是否<=key
            if(hash.containsKey(nums[i])){
                if(Math.abs(hash.get(nums[i])-i) <=k) {
                    return true;
                } 
            }
            //不存在或者条件不成立则更新hash表中该元素的下标
            hash.put(nums[i],i);
        }
        return false;
    }
}

字符串相乘

43. 字符串相乘 - 力扣(LeetCode)

该题只能直接模拟竖式乘法,主要步骤就是先无进位相乘,然后处理进位,最后处理前导零(当元结果为全0时,只需返回一个零);

竖式乘法运算过程中,首先需要一个数组来存放无进位相乘的结果。由于两个长度分别为 m 和 n 的数字相乘,结果的最大长度为 m + n - 1。因此,创建一个大小为 m + n - 1 的数组。这是因为当两个数字最高位相乘时,产生的结果处于结果数字的最高位开始的位置,而后续的计算会涉及到当前数字除最高位以外的低位部分。

字符串预处理:为了方便从最低位开始计算,将输入的两个字符串进行翻转,然后将它们转换为字符数组。这样,在后续计算中,可以像实际竖式乘法一样,从最低位开始逐位相乘。

将两个字符串先翻转,方便先从最低为进行计算,接着转换成char数组

双重循环模拟竖式乘法

  1. 外层循环遍历第一个数的每一位
  2. 内层循环遍历第二个数的每一位
  3. 将每一位相乘的结果累加到数组的对应位置(i+j)
  • 例如:计算123×456时,3×6的结果放在add[0]()这就是要翻转的原因,翻转之后可以直接从前向后一次遍历从低位到高位),2×5的结果放在add[1]等

处理进位

  1. 循环处理直到所有数字处理完毕且没有进位
  2. 每次取出当前位的值加上进位值
  3. 取个位数加入结果字符串
  4. 计算新的进位值
  • 例如:某位计算得到15,则保留5,进位1

处理前导零

  1. 由于结果是逆序的,实际处理的是最高位的零
  2. 删除多余的前导零,但要保留最后一个零(即结果本身就是0的情况)
  • 例如:处理"000"变为"0"

由于之前对字符串进行了翻转,最后需要将结果数组逆序,转换为字符串形式并返回。

class Solution {
    public String multiply(String num1, String num2) {
        // 两个数相乘的最大位数是m+n-1(不考虑进位时)
        int m = num1.length(), n=num2.length();
        int[] add = new int[m+n-1];  // 用于创建int数组存放无进位相乘的结果
        // 翻转字符串,方便从低位开始计算
        char[] tmp1 = new StringBuilder(num1).reverse().toString().toCharArray();
        char[] tmp2 = new StringBuilder(num2).reverse().toString().toCharArray();
        // 进行无进位相乘
        // 模拟竖式乘法,将每一位相乘的结果存入对应的位置
        for(int i=0; i<m; i++){
            for(int j = 0; j < n; j++){
                add[i+j] += (tmp1[i] - '0') * (tmp2[j] - '0');
            }
        }
        int cut = 0, t = 0; // cut是数组索引,t是进位值
        // 当还有未处理的数字或还有进位时继续循环
        StringBuilder ret = new StringBuilder();
        while(cut < add.length || t!=0){
            // 如果还有未处理的数字,加到进位中
            if(cut < add.length){
                t+=add[cut++];
            }
            // 取个位数加入结果
            ret.append((char)(t%10 + '0'));
            t /= 10; // 计算新的进位
        }
        // 处理前导零
        // 当前结果处于逆序状态,所以实际处理的是最高位的零
        // 但要保留最后一个零(即结果本身就是0的情况)
        while(ret.length() > 1 && ret.charAt(ret.length()-1) == '0'){
            ret.deleteCharAt(ret.length()-1);
        }
        return ret.reverse().toString();
    }
}

 

字符串解码

394. 字符串解码 - 力扣(LeetCode)

本题采用双栈策略,通过一个数字栈存储重复次数,一个字符栈处理目标字符串。遍历给定字符串前,需先向字符栈中压入一个空字符串。这是因为后续处理过程中,完成处理的字符串会拼接到栈顶元素上,若栈顶无元素,程序就会报错 。在遍历字符串的过程中,依据当前字符的类型,可分为以下四种情况进行处理:

  1. 当前字符为 “[”:向字符栈中压入一个空字符串,用于拼接该括号内的所有字符。
  2. 当前字符为 “]”:表明括号内的字符已全部就绪,可以进行处理。此时,从数字栈和字符栈分别弹出栈顶元素。数字栈的栈顶元素决定了字符栈栈顶字符串需拼接的次数。完成当前字符串的拼接后,将结果拼接到字符栈的新栈顶元素上。
  3. 当前字符为数字:由于数字可能包含多位,需通过循环读取并将其压入数字栈。
  4. 当前字符为普通字符:直接将其拼接到字符栈的栈顶元素上。若该字符位于括号内,前面步骤已在“[”位置压入空字符串,字符会拼接到该空字符串上;若不在括号内,字符则直接拼接到栈顶已有元素上。

当字符串遍历结束,字符栈的栈顶元素即为完成编码后的字符串。

class Solution {
    public String decodeString(String s) {
        // 使用双栈分别存储数字和字符串
        Deque<Integer> nums = new ArrayDeque<>(); // 数字栈,保存重复次数
        Deque<String> sQue=  new ArrayDeque<>();  // 字符串栈,保存待处理的字符串
        sQue.push(""); // 初始化栈,压入空字符串避免空指针
        for(int i = 0; i < s.length(); i++){
            char ch = s.charAt(i);
            if(ch == '['){
                sQue.push(""); // 遇到左括号:压入新字符串,准备处理括号内的内容
            }else if(ch == ']'){
                // 遇到右括号:1. 弹出栈顶字符串(括号内的内容)
                String s1 = sQue.pop();
                // 2. 弹出重复次数
                Integer t = nums.pop();
                // 3. 构建重复后的字符串
                StringBuilder tmp = new StringBuilder();
                for(int j = 0; j < t; j++){
                    tmp.append(s1);
                }
                // 4. 将结果拼接到上层字符串
                sQue.push(sQue.pop() + tmp.toString());

            } else if(ch >= '0' && ch <= '9'){
                // 处理数字(可能多位):
                int num = 0;
                // 循环读取连续的数字字符
                while(s.charAt(i) >= '0' && s.charAt(i) <='9'){
                    num = num*10 + (s.charAt(i++) - '0');
                }
                i--;  // 回退一步,因为外层循环会i++
                nums.push(num);
            } else {
                // 处理字母:直接拼接到当前栈顶字符串
                sQue.push(sQue.pop() + ch);
            }
        }   
        return sQue.pop();
    }
}

在每个树行中找最大值

515. 在每个树行中找最大值 - 力扣(LeetCode)

本题是对一棵二叉树进行处理,通过层序遍历的方式来解决。层序遍历,就是按照从上到下、从左到右的顺序,一层一层地访问二叉树的节点。在遍历的过程中,需要找出每一层节点值的最大值,并将这些最大值依次加入到返回链表中。
 
特别要注意的是,程序必须能够处理空树的情况。当输入的二叉树为空时,由于树中不存在任何节点,所以返回的链表为空。
 
具体实施过程中,借助队列这一数据结构实现层序遍历。首先,将二叉树的根节点入队。在队列不为空的情况下,获取当前队列的长度,这个长度代表了当前层的节点数量。然后,遍历当前层的所有节点,在遍历过程中,更新当前层的最大值,并将节点的左右子节点(如果存在)加入队列。遍历完当前层后,将求得的最大值加入到返回链表中。如此反复,直至队列为空,完成整棵二叉树的层序遍历,得到符合要求的返回链表。

class Solution {
    public List<Integer> largestValues(TreeNode root) {
        List<Integer> ret = new LinkedList<>(); 
        if(root == null) return ret; // 处理空树情况
        // 使用队列实现BFS层序遍历
        Queue<TreeNode> q = new LinkedList<>();
        q.offer(root); // 根节点入队
        while(!q.isEmpty()){
            // 当前层的节点数量
            int size = q.size();
            // 初始化当前层最大值(设置为最小整数值)
            int max = Integer.MIN_VALUE;
            // 遍历当前层所有节点
            for(int i = 0; i < size; i++){
                TreeNode node = q.poll(); // 取出队首节点
                max = Math.max(max, node.val); // 更新最大值
                //将不为空的子节点加入队列,以便下一次遍历
                if(node.left != null) q.offer(node.left);
                if(node.right != null) q.offer(node.right);
            } 
            // 记录当前层最大值到结果列表
            ret.add(max);
        }
        return ret;
    }
}

数据流的中位数

295. 数据流的中位数 - 力扣(LeetCode)

本题通过构建最大堆和最小堆,解决该题。算法使用两个堆实现:一个最大堆 left ,用于维护数据集中较小的一半元素;一个最小堆 right ,用于维护数据集中较大的一半元素。
 
插入新元素时,需保证两个堆的长度差不超过1。 left 堆的堆顶元素是较小一半元素中的最大值, right 堆的堆顶元素是较大一半元素中的最小值。当两个堆的长度相等时,中位数是两个堆顶元素的平均值;当两个堆的长度不等时,中位数是 left 堆的堆顶元素,因为 left 堆的长度始终大于或等于 right 堆。
 
插入操作的具体逻辑
 
1. 当 left 和 right 堆的长度相等时:

  • 如果插入的元素小于或等于 left 堆的堆顶元素,或 left 堆为空,直接将元素插入 left 堆。因为 left 堆负责存储较小的元素。
  • 如果插入的元素大于 left 堆的堆顶元素,将其插入 right 堆。为了维持 left 堆的长度大于或等于 right 堆的要求,需要将 right 堆的堆顶元素取出,插入到 left 堆中。这样操作后, left 堆依然存储小于或等于中位数的元素, right 堆存储大于中位数的元素。

2. 当 left 和 right 堆的长度不相等时:

  • 如果插入的元素小于 left 堆的堆顶元素,将其插入 left 堆。为了满足既定规则,需将 left 堆的堆顶元素取出,插入到 right 堆中。
  • 如果插入的元素大于或等于 left 堆的堆顶元素,直接将其插入 right 堆。
class MedianFinder {
    //核心思想:用最大堆存较小一半数,最小堆存较大一半数
    Queue<Integer> left; // 最大堆(堆顶是左侧最大值)
    Queue<Integer> right;// 最小堆(堆顶是右侧最小值)
    int m = 0,n = 0;
    public MedianFinder() {
        left = new PriorityQueue<>((a,b)-> b-a); //最大堆
        right = new PriorityQueue<>(); //最小堆
    }
    
    public void addNum(int num) {
        // 情况1:左右堆大小相等(平衡状态)
       if(left.size() == right.size()){
            /* 插入策略:
               如果左堆为空 或 数字<=左堆最大值 则直接插入到左堆
               否则先插入右堆,为了保持平衡再将右堆最小值移到左堆
               保证插入后左堆比右堆多1个元素 */
            if(left.size()==0 || num <= left.peek()){
                left.offer(num);
            } else {
                right.offer(num);
                left.offer(right.poll()); // 平衡操作
            }
       } else {
        // 情况2:左堆比右堆多1个元素(非平衡状态)
            /* 插入策略:
               如果数字<=左堆最大值,为了保持平衡插入左堆后,将左堆最大值移到右堆
               否则直接插入右堆,保证插入后两堆大小相等 */
            if( num <= left.peek()){
                left.offer(num);
                right.offer(left.poll()); // 平衡操作
            } else {
                right.offer(num);
            }
       }
    }
    
    public double findMedian() {
        // 情况1:两堆大小相等则取两个堆顶平均值 左堆多1个元素 ,左堆顶即中位数
        if(left.size() == right.size()){
            return (left.peek() + right.peek()) / 2.0;
        } else {
            return left.peek();
        }
    }
}

被包围的区域

该问题旨在将所有被 `X` 完全包围的 `O` 转换为 `X`,而与边界相连的 `O` 则保持不变。一种可行的方法是对每个 `O` 执行广度优先搜索(BFS)。然而,在BFS遍历过程中,如果将遇到的 `O` 直接修改为 `X`,当此次BFS遍历到边界时,意味着此前修改的所有元素并不符合转换要求,此时就需要将它们改回。因此,需要借助一个数组来记录每次BFS修改过的元素,所以使用正难则反思想

解法一:
与其逐个寻找被包围的 `O`,不如先对所有与边界相连的 `O` 及其连通区域进行标记。如此一来,剩余未被标记的 `O` 即为需要转换的对象,直接将其改为 `X` 即可。

实现步骤

  • 边界'O'入队与标记:对棋盘的四条边进行扫描,将边界上出现的 `O` 加入队列,并同时进行标记。
  • BFS标记相连'O*:以这些边界上的 `O` 作为起点,执行BFS,以此标记所有与之相连的内部 `O`。
  • 转换未标记'O':最后对内部区域进行遍历,将其中未被标记的 `O` 替换为 `X`。

注意事项
1. 全面的边界检查:必须对棋盘的四条边进行完整检查,确保所有边界上的 `O` 都被正确处理。
2. BFS的越界控制:在执行BFS时,要严格确保搜索过程不会越界访问棋盘之外的区域。
3. 仅处理内部区域:在最终处理阶段,只需针对内部区域进行操作,边界部分应保持原状,不做任何改动。 

class Solution {
    // 定义四个方向的位移数组:右、左、上、下
    int[] dx = new int[]{0, 0, -1, 1};
    int[] dy = new int[]{1, -1, 0, 0};
    public void solve(char[][] board) {
        // 获取棋盘的行数和列数
        int m = board.length, n = board[0].length;
        // 创建访问标记数组,记录哪些'O'是与边界相连的
        boolean[][] vis = new boolean[m][n];
        // 使用队列进行BFS遍历
        Queue<int[]> q = new LinkedList<>();
        // 处理第一列和最后一列
        for(int i = 0; i < m; i++) {
            // 第一列的'O'
            if(board[i][0] == 'O') {
                q.offer(new int[]{i, 0});  // 加入队列
                vis[i][0] = true;          // 标记为已访问
            }
            // 最后一列的'O'
            if(board[i][n-1] == 'O') {
                q.offer(new int[]{i, n-1}); // 加入队列
                vis[i][n-1] = true;         // 标记为已访问
            }
        }
        // 处理第一行和最后一行
        for(int i = 0; i < n; i++) {
            // 第一行的'O'
            if(board[0][i] == 'O') {
                q.offer(new int[]{0, i});   // 加入队列
                vis[0][i] = true;           // 标记为已访问
            }
            // 最后一行的'O'
            if(board[m-1][i] == 'O') {
                q.offer(new int[]{m-1, i}); // 加入队列
                vis[m-1][i] = true;         // 标记为已访问
            }
        }
        // 开始BFS遍历所有与边界相连的'O'
        while(!q.isEmpty()) {
            int[] t = q.poll();  // 取出队列中的坐标
            int a = t[0], b = t[1];
            // 检查四个方向
            for(int i = 0; i < 4; i++) {
                int x = a + dx[i];  // 计算新坐标x
                int y = b + dy[i];  // 计算新坐标y
                
                // 检查新坐标是否在棋盘内、未被访问过且是'O'
                if(x >= 0 && x < m && y >= 0 && y < n 
                   && !vis[x][y] && board[x][y] == 'O') {
                    q.offer(new int[]{x, y});  // 加入队列
                    vis[x][y] = true;          // 标记为已访问
                }
            }
        }
        // 遍历棋盘内部区域(不包括边界)
        for(int i = 1; i < m-1; i++) {
            for(int j = 1; j < n-1; j++) {
                // 如果该位置的'O'未被标记(即不与边界相连)
                if(!vis[i][j] && board[i][j] == 'O') {
                    board[i][j] = 'X';  // 将其改为'X'
                }
            }
        }
    }
}

解法二:

采用逆向思维,不是直接寻找被包围的'O',而是先将所有与边界相连的'O'转换成'.处理完之后,遍历整个数组,如果是'O'则直接改成'X',并还原'.'转换成‘O';
执行步骤:

边界处理阶段:扫描四条边界,对每个边界'O'执行BFS
标记阶段:BFS会扩散标记所有相连的'O'为'.'
转换阶段:遍历整个棋盘,将未被标记的'O'转为'X',被标记的'.'恢复为'O'

class Solution {
    // 定义四个方向的位移数组:右、左、上、下
    int[] dx = new int[]{0, 0, -1, 1};
    int[] dy = new int[]{1, -1, 0, 0};
    int m, n;  // 棋盘的行数和列数,定义为成员变量避免重复计算
    public void solve(char[][] board) {
        // 边界检查:如果棋盘为空或大小为0,直接返回
        if (board == null || board.length == 0) return;
        // 初始化棋盘的行数和列数
        m = board.length; n = board[0].length;
        for (int i = 0; i < m; i++) {
            // 检查第一列的每个元素是否为'O'
            if (board[i][0] == 'O') {
                bfs(board, i, 0);  // 执行BFS标记所有相连的'O'
            }
            // 检查最后一列的每个元素是否为'O'
            if (board[i][n-1] == 'O') {
                bfs(board, i, n-1);  // 执行BFS标记所有相连的'O'
            }
        }
        for (int i = 0; i < n; i++) {
            // 检查第一行的每个元素是否为'O'
            if (board[0][i] == 'O') {
                bfs(board, 0, i);  // 执行BFS标记所有相连的'O'
            }
            // 检查最后一行的每个元素是否为'O'
            if (board[m-1][i] == 'O') {
                bfs(board, m-1, i);  // 执行BFS标记所有相连的'O'
            }
        }
        // 3. 遍历整个棋盘,进行最终转换
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 将未被标记的'O'(不与边界相连的)转换为'X'
                if (board[i][j] == 'O') {
                    board[i][j] = 'X'; 
                }
                // 将被标记为'.'的'O'(与边界相连的)恢复为'O'
                if (board[i][j] == '.') {
                    board[i][j] = 'O';
                }
            }
        }
    }
    
    void bfs(char[][] board, int i, int j) {
        // 使用队列实现BFS
        Queue<int[]> q = new LinkedList<>();
        // 将起始点加入队列
        q.offer(new int[]{i, j});
        // 将起始点标记为'.'(表示与边界相连的'O')
        board[i][j] = '.';
        while (!q.isEmpty()) {
            // 取出队列中的当前坐标
            int[] t = q.poll();
            int a = t[0], b = t[1];
            // 检查四个方向
            for (int k = 0; k < 4; k++) {
                // 计算新坐标
                int x = a + dx[k];
                int y = b + dy[k];
                // 检查新坐标是否有效且为未被标记的'O'
                if (x >= 0 && x < m && y >= 0 && y < n && board[x][y] == 'O') {
                    // 将新坐标加入队列
                    q.offer(new int[]{x, y});
                    // 标记为'.'表示与边界相连
                    board[x][y] = '.';
                }
            }
        }
    }
}

为高尔夫比赛砍树

675. 为高尔夫比赛砍树 - 力扣(LeetCode)

该题和迷宫问题比较像,只不过迷宫问题是寻找一个出口(最短路径),而这里需要寻找多条最短路径,所以就可以按照迷宫问题那时候的解法BFS解决该题:
1. 收集树木
第一步,需要对整个森林网格进行全面的遍历,找出所有代表树木的单元格(也就是值大于 1 的位置)。这一步就像是在地图上标记出所有的目标点,为后续的行动做好准备。可以使用一个列表来存储这些树木的信息,列表中的每个元素是一个包含树木位置(行和列坐标)。
2. 排序树木
在收集到所有树木的信息后,对这些树木进行排序。排序的依据是树木的高度,要确保树木按照高度从小到大的顺序排列。这样做的目的是为了保证后续的砍伐行动是按照正确的顺序进行的(题目要求必须要低到高砍树)。

在确定了树木的砍伐顺序后,需要计算从当前位置到下一棵树的最短路径步数。这里和迷宫问题采用思想广度优先搜索(BFS)算法来实现。BFS 是一种非常适合用于寻找最短路径的算法,因为它会逐层扩展搜索范围,确保找到的路径是最短的。

  • 使用队列实现:BFS 算法使用队列来实现。将起始点加入队列,并开始进行搜索。每次从队列中取出一个节点,然后将其相邻的四个方向的节点加入队列,继续进行搜索。
  • 维护访问标记数组:为了避免重复访问同一个单元格,需要维护一个访问标记数组。这个数组的大小与森林网格相同,初始时所有元素都标记为未访问。每当我们访问一个单元格时,就将对应的标记数组元素标记为已访问。
  • 分层处理计算步数:在 BFS 搜索过程中,采用分层处理的方式来计算步数。每一层的节点代表着从起始点出发经过相同步数能够到达的节点。当找到目标树时,当前的层数就是从起始点到目标树的最短路径步数。
  • 遇到目标点立即返回:一旦在搜索过程中遇到目标树,就立即返回当前的步数,因为 BFS 保证了找到的路径是最短的,如果遍历完队列还没有找到则返回-1。

4. 累加步数
在计算出从当前位置到下一棵树的最短路径步数后,将这个步数累加到总步数中。然后,将当前位置更新为下一棵树的位置,继续进行下一轮的搜索,直到所有的树木都被砍伐完。

class Solution {
    // 定义四个方向的位移数组:右、左、下、上
    int[] dx = new int[]{0, 0, 1, -1};
    int[] dy = new int[]{1, -1, 0, 0};
    int m, n; // 森林的行数和列数

    public int cutOffTree(List<List<Integer>> forest) {
        m = forest.size(); 
        n = forest.get(0).size();
        
        // 1. 收集所有需要砍的树(值大于1的位置)
        List<int[]> trees = new ArrayList<>();
        for(int i = 0; i < m; i++) {
            for(int j = 0; j < n; j++) {
                if(forest.get(i).get(j) > 1) {
                    trees.add(new int[]{i, j}); // 保存树的坐标
                }
            }
        }
        
        // 2. 按照树的高度从小到大排序(题目要求按高度顺序砍树)
        Collections.sort(trees, (a, b) -> 
            forest.get(a[0]).get(a[1]) - forest.get(b[0]).get(b[1])
        );

        // 3. 按顺序砍树,计算总步数
        int ret = 0;
        int dx = 0, dy = 0; // 起始位置(从(0,0)开始)
        
        for(int[] tree : trees) {
            int x = tree[0];
            int y = tree[1];
            
            // 计算从当前位置到目标树的最短步数
            int steps = bfs(forest, dx, dy, x, y);
            
            if(steps == -1) { // 如果无法到达该树
                return -1;
            }
            
            ret += steps; // 累加步数
            dx = x; dy = y;    // 更新当前位置为刚砍掉的树的位置
        }    
        return ret;
    }

    int bfs(List<List<Integer>> forest, int bx, int by, int x, int y) {
        // 如果起点就是目标点,步数为0
        if(bx == x && by == y) return 0;
    
        Queue<int[]> queue = new LinkedList<>(); // BFS队列
        boolean[][] vis = new boolean[m][n]; // 访问标记数组
        
        queue.offer(new int[]{bx, by}); // 起点入队
        vis[bx][by] = true;         // 标记起点已访问
        int steps = 0; // 记录步数
        
        while(!queue.isEmpty()) {
            int size = queue.size(); // 当前层的节点数
            steps++; // 每处理一层,步数加1
            
            while(size-- != 0) { // 处理当前层的所有节点
                int[] t = queue.poll();
                int a = t[0], b = t[1];
                
                // 检查四个方向
                for(int i = 0; i < 4; i++) {
                    int newX = a + dx[i];
                    int newY = b + dy[i];
                    
                    // 检查新坐标是否有效  // 未访问过
                    if(newX>=0 && newX<m && newY>=0 && newY<n && forest.get(newX).get(newY)!= 0  && !vis[newX][newY]) { 
                        // 如果到达目标点,返回当前步数
                        if(x == newX && y == newY) {
                            return steps;
                        }
                        // 否则加入队列继续搜索
                        queue.offer(new int[]{newX, newY});
                        vis[newX][newY] = true;
                    }
                }
            }
        }
        // 队列为空仍未找到目标点,返回-1
        return -1;
    }
}

相关文章:

  • leetcode572 另一棵树的子树
  • React 组件样式
  • (已解决)如何安装python离线包及其依赖包 2025最新
  • 计算机操作系统——死锁(详细解释和处理死锁)
  • 编译原理 实验二 词法分析程序自动生成工具实验
  • 解决 Ubuntu 上 Docker 安装与网络问题:从禁用 IPv6 到配置代理
  • 【微知】如何将echo某个数据到文件然后cat出来结合在一起输出?(echo 1 | tee filea; cat fileb | tee fila)
  • 【图像生成之22】CVPR024—SwiftBrush基于变分分数蒸馏的文生图扩散模型
  • LeetCode hot 100—不同路径
  • 软考 系统架构设计师系列知识点之杂项集萃(49)
  • 【力扣hot100题】(093)最长公共子序列
  • 基于 Vue 3 + Express 的网盘资源搜索与转存工具,支持响应式布局,移动端与PC完美适配
  • 关于 Spring Boot 监控方式的详细对比说明及总结表格
  • CAN总线发送方每发送一位,接收方接收一位,但是当在非破坏性仲裁方式失利的情况下是否还能够正确接收数据呢?
  • 【C语言-全局变量】
  • Linux:进程优先级的理解
  • 对话记忆(Conversational Memory)
  • 《汽车电器与电子技术》实验报告
  • HotSpot虚拟机中对象的访问定位机制是怎样的?
  • Python实现贪吃蛇一
  • 成都网站推广公司/广告门
  • qq交流群怎么升级会员/广州 关于进一步优化
  • 如何使用爱站网/武汉搜索推广
  • 怎么做自己的手机网站/开封seo推广
  • 疫情最新数据消息山西/重庆百度推广关键词优化
  • 网站导航固定/电商营销策划方案范文