目录
库存管理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数组
双重循环模拟竖式乘法
- 外层循环遍历第一个数的每一位
- 内层循环遍历第二个数的每一位
- 将每一位相乘的结果累加到数组的对应位置(i+j)
- 例如:计算123×456时,3×6的结果放在add[0]()这就是要翻转的原因,翻转之后可以直接从前向后一次遍历从低位到高位),2×5的结果放在add[1]等
处理进位
- 循环处理直到所有数字处理完毕且没有进位
- 每次取出当前位的值加上进位值
- 取个位数加入结果字符串
- 计算新的进位值
- 例如:某位计算得到15,则保留5,进位1
处理前导零
- 由于结果是逆序的,实际处理的是最高位的零
- 删除多余的前导零,但要保留最后一个零(即结果本身就是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)
本题采用双栈策略,通过一个数字栈存储重复次数,一个字符栈处理目标字符串。遍历给定字符串前,需先向字符栈中压入一个空字符串。这是因为后续处理过程中,完成处理的字符串会拼接到栈顶元素上,若栈顶无元素,程序就会报错 。在遍历字符串的过程中,依据当前字符的类型,可分为以下四种情况进行处理:
- 当前字符为 “[”:向字符栈中压入一个空字符串,用于拼接该括号内的所有字符。
- 当前字符为 “]”:表明括号内的字符已全部就绪,可以进行处理。此时,从数字栈和字符栈分别弹出栈顶元素。数字栈的栈顶元素决定了字符栈栈顶字符串需拼接的次数。完成当前字符串的拼接后,将结果拼接到字符栈的新栈顶元素上。
- 当前字符为数字:由于数字可能包含多位,需通过循环读取并将其压入数字栈。
- 当前字符为普通字符:直接将其拼接到字符栈的栈顶元素上。若该字符位于括号内,前面步骤已在“[”位置压入空字符串,字符会拼接到该空字符串上;若不在括号内,字符则直接拼接到栈顶已有元素上。
当字符串遍历结束,字符栈的栈顶元素即为完成编码后的字符串。
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;}
}