基础算法训练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++){//如果元素存在则取出下标和当前下标进行运算 判断是否<=keyif(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];  // 计算新坐标xint 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) {// 使用队列实现BFSQueue<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) {// 如果起点就是目标点,步数为0if(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++; // 每处理一层,步数加1while(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;}}}}// 队列为空仍未找到目标点,返回-1return -1;}
}

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

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

相关文章

从单机版到超级APP:MCP如何解锁AI的超能力

MCP&#xff1a;AI界的“万能充电宝”——让AI从此告别“语言不通”的尴尬&#xff01; 开篇&#xff1a;AI咖啡馆的尴尬日常 想象一下这样的场景&#xff1a; 一位AI助手在咖啡馆里手忙脚乱——它想帮用户点杯咖啡&#xff0c;但需要先写代码调用天气API&#xff08;“今天下…

Grafana将弃用AngularJS-我们该如何迁移

AngularJS 弃用时间线 AngularJS 支持已在 Grafana 9 中正式弃用。在 2024 年 5 月发布的 Grafana 11 中&#xff0c;所有 Grafana Cloud 和自托管安装默认关闭该功能。到 Grafana 12 版本时&#xff0c;将完全移除对 AngularJS 的支持&#xff0c;包括配置参数开关 angular_s…

Qt之opengl定点数据添加更多属性

将颜色数据加入到定点数据中去 shader中代码 api中的代码 #include "sunopengl.h"#include <QTime>sunOpengl::sunOpengl(QWidget *parent) { } unsigned int VBO,VAO; float vertices[]{0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f,0.5f, -0.5f, 0.0f, 0.0f, 1.0f…

【Flink运行时架构】作业提交流程

本文介绍在单作业模式下Flink提交作业的具体流程&#xff0c;如下图所示。 客户端将作业提交给YARN的RM&#xff1b;YARN的RM启动Flink JobManager&#xff0c;并将作业提交给JobMaster&#xff1b;JobMaster向Flink内置的RM请求slots&#xff1b;Flink内置的RM向YARN RM请求…

AI大模型技术之RAG、模型微调、知识蒸馏

AI大模型技术之RAG、模型微调、知识蒸馏 检索增强生成&#xff08;RAG&#xff09;技术原理垂直领域应用场景使用的局限性 模型微调&#xff08;Fine-tuning&#xff09;技术原理垂直领域应用场景使用的局限性 知识蒸馏&#xff08;Distillation&#xff09;技术原理垂直领域应…

深入浅出:信号灯与系统V信号灯的实现与应用

深入浅出&#xff1a;信号灯与系统V信号灯的实现与应用 信号灯&#xff08;Semaphore&#xff09;是一种同步机制&#xff0c;用于控制对共享资源的访问。在多线程或多进程环境下&#xff0c;信号灯能够帮助协调多个执行单元对共享资源的访问&#xff0c;确保数据一致性与程序…

消防设施操作员岗位注意事项有哪些?

消防设施操作员主要负责消防设施的操作、维护和管理等工作&#xff0c;其岗位注意事项涉及操作规范、设备维护、应急处理等多个关键领域&#xff0c;以下是具体内容&#xff1a; 操作规范方面 熟悉设备原理&#xff1a;要全面了解各类消防设施的工作原理、性能参数和操作方法…

SQL:Relationship(关系)

目录 &#x1f517; 什么是 Relationship&#xff1f; 三种基本关系类型&#xff08;基于实体间的关系&#xff09;&#xff1a; 1. 一对一&#xff08;One-to-One&#xff09; 2. 一对多&#xff08;One-to-Many&#xff09; 3. 多对多&#xff08;Many-to-Many&#xf…

php伪协议

PHP 伪协议&#xff08;PHP Stream Wrapper&#xff09; PHP 的伪协议&#xff08;Protocol Wrapper&#xff09;是一种机制&#xff0c;允许开发者通过统一的文件访问函数&#xff08;如 file_get_contents、fopen、include 等&#xff09;访问不同类型的数据源&#xff0c;包…

当DRAM邂逅SSD:新型“DRAM+”存储技术来了!

在当今快速发展的科技领域&#xff0c;数据存储的需求日益增长&#xff0c;对存储设备的性能和可靠性提出了更高的要求。传统DRAM以其高速度著称&#xff0c;但其易失性限制了应用范围&#xff1b;而固态硬盘SSD虽然提供非易失性存储&#xff0c;但在速度上远不及DRAM。 为了解…

org.apache.spark.SparkException: Kryo serialization failed: Buffer overflow...

Spark异常&#xff1a;Kryo serialization failed: Buffer overflow. 1、问题描述 SparkSQL任务报错如下&#xff1a; org.apache.spark.SparkException: Kryo serialization failed: Buffer overflow. Available: 0, required: xxx. To avoid this, increase spark.kryoseri…

编译原理 实验二 词法分析程序自动生成工具实验

文章目录 实验环境的准备实验实验预备知识分析案例所要做的任务实战 实验环境的准备 安装flex 安装MinGW MinGW Installation Manager页面 apply changes 下载比较耗时 只看到了一个文件&#xff0c;复制过去 配置环境变量 使用gcc -v检验是否安装完成 实验 实验预备知识…

BERT - 直接调用transformers.BertModel, BertTokenizerAPI不进行任何微调

本节代码将使用 transformers 库加载预训练的BERT模型和分词器&#xff08;Tokenizer&#xff09;&#xff0c;并处理文本输入。 1. 加载预训练模型和分词器 from transformers import BertTokenizer, BertModelmodel_path "/Users/azen/Desktop/llm/models/bert-base-…

Python 质数筛选:从入门到优化的 5 种方法

质数&#xff08;Prime Number&#xff09;是指只能被 1 和自身整除的自然数&#xff0c;如 2、3、5、7 等。在算法题、密码学或数学计算中&#xff0c;高效生成质数至关重要。 Python 提供了多种方法来实现质数筛选&#xff0c;但不同方法的效率差异巨大。本文从 最基础的方法…

C#MQTT协议服务器与客户端通讯实现(客户端包含断开重连模块)

C#MQTT协议服务器与客户端通讯实现 1 DLL版本2 服务器3 客户端 1 DLL版本 MQTTnet.DLL版本-2.7.5.0 基于比较老的项目中应用的DLL&#xff0c;其他更高版本变化可能较大&#xff0c;谨慎参考。 2 服务器 开启服务器 关闭服务器 绑定事件【客户端连接服务器事件】 绑定事件【客户…

【连载3】基础智能体的进展与挑战综述

基础智能体的进展与挑战综述 从类脑智能到具备可进化性、协作性和安全性的系统 【翻译团队】刘军(liujunbupt.edu.cn) 钱雨欣玥 冯梓哲 李正博 李冠谕 朱宇晗 张霄天 孙大壮 黄若溪 2. 认知 人类认知是一种复杂的信息处理系统&#xff0c;它通过多个专门的神经回路协调运行…

Python语言介绍

Python 是一种高级、通用、解释型的编程语言&#xff0c;由 Guido van Rossum 于 1991 年首次发布。其设计哲学强调代码的可读性和简洁性。 Python通过简洁的语法和强大的生态系统&#xff0c;成为当今最受欢迎的编程语言之一。 一、核心特点 Python 是一种解释型、面向对象、…

什么是回表?哪些数据库存在回表?

目录 一、什么是回表1. 回表的核心流程2. 示例说明3. 回表的性能问题4. 总结 二、哪些数据库会有回表1. MySQL&#xff08;InnoDB&#xff09;2. Oracle3. 其他数据库&#xff08;如 SQL Server、PostgreSQL&#xff09;4. 总结 三、非聚集索引与聚集索引的区别及产生原因1. 聚…

ssh 免密登录服务器(vscode +ssh 免密登录)

每次打开vscode连接服务器都需要输入密码&#xff0c;特别繁琐。 然后自己在网上翻阅了一下教程&#xff0c;发现说的内容比较啰嗦&#xff0c;而且个人感觉非常有误导性倾向。 因此自己直接干脆写一个简便易懂的教程算了。 &#xff08;以经过本人亲测&#xff0c;真实可靠&am…

基于低空经济的无人机操控与维护实训室解决方案

一、低空经济时代下的无人机人才需求 1.1 低空经济发展趋势与政策机遇 在当前经济与科技飞速发展的大背景下&#xff0c;低空经济作为国家战略性新兴产业&#xff0c;正以迅猛之势崛起&#xff0c;展现出无限的潜力与活力。其应用场景极为广泛&#xff0c;涵盖了物流、安防、…