前言
这一部分的题目相当相当重要哦,主要用于一些特殊的场合和某些特定题目的优化,大多数是那种表面很难,其实很套路的题目,经常和其他算法结合在一起,但是也不要担心,我感觉这些题目算是最套路的,永远是哪几个场合会用到。
很简单,一个做队列,一个做缓存辅助
入队:队列栈全部放入缓存,将结果放回队列,然后缓存放回栈
出队:直接pop
就是最简单的栈的应用,
如果当前元素是右括号,看看是不是栈不空并且栈顶弹出的是不是对应,不是提前返回false;
如果当前元素是左括号,直接入栈。
最后如果栈空才为真。
class Solution {
public boolean isValid(String s) {
HashMap<Character,Character> map = new HashMap<>();
map.put('(',')');
map.put('[',']');
map.put('{','}');
LinkedList<Character> stack = new LinkedList<>();//注意没有Stack类
for(int i=0;i<s.length();i++){
Character c = s.charAt(i);
if(map.containsKey(c)){
stack.push(c);
}else{
if(stack.isEmpty())
return false;
char top = stack.pop();//栈的方法是push和pop
if(map.get(top)==c)
continue;
else return false;
}
}
return stack.size()==0;
}
}
要用O(1)的复杂度求出栈中的最小元素,但是栈同时也在pop,所以我们不可以用一个变量保存当前栈的最小值,那么怎么办呢?——使用局部最小辅助栈,该辅助栈的栈顶存放当前栈状态的最小值。
主栈进新元素a:
如果辅助栈空,a放入;
a>辅助栈顶,辅助栈不变;
a<=辅助栈顶,辅助栈中将a放入;
主栈弹出元素a:
a>辅助栈顶,此时辅助栈顶是min;
a==辅助栈顶,此时辅助栈顶pop;
class MinStack {
private Stack<Integer> stack;
private Stack<Integer> help;
public MinStack() {
stack = new Stack<>();
help = new Stack<>();
}
public void push(int x) {
stack.push(x);
if(help.isEmpty()||x<=help.peek())
help.push(x);
}
public void pop() {
if(stack.pop().equals(help.peek()))//一定要注意集合中装的是对象,比较的时候用equals方法,==是不可以的
help.pop();
}
public int top() {
return stack.peek();
}
public int getMin() {
return help.peek();
}
}
如题
输入:s = “3[a2[c]]”
输出:“accaccacc”
我们做的时候发现也是需要括号改变顺序的,使用辅助栈。
预先在栈中push一个new StringBuilder("");
如果当前元素是数字,转成数字push入栈;
如果当前元素是[,直接push入栈,并且在栈中push一个new StringBuilder("");
如果当前元素是字母,栈顶的StringBuilder串拼接之;
如果当前元素是],从栈中依次pop出栈顶的StringBuilder串、[、数字,然后该字母串倍增那个数字的次数。然后将倍增后的串追加到栈顶的串后面,回到开始接着遍历;
结束遍历的时候栈顶放着的是结果。
class Solution {
public String decodeString(String s) {
LinkedList stack = new LinkedList();
stack.push(new StringBuilder(""));
for(int i=0;i<s.length();i++){
int digit=0;//
while(s.charAt(i)>='0'&&s.charAt(i)<='9'){
digit=digit*10 +s.charAt(i)-'0';
i++;
}
if(digit!=0)
stack.push(digit);
if(s.charAt(i)=='['){
stack.push('[');
stack.push(new StringBuilder(""));
}
else if(s.charAt(i)==']'){
StringBuilder temp = (StringBuilder)stack.pop();
stack.pop();//pop左括号
Integer count = (Integer)stack.pop();
String ss = temp.toString();
for(int j=0;j<count-1;j++)
temp.append(ss);
StringBuilder sb = (StringBuilder)stack.peek();
sb.append(temp);
}else{
StringBuilder sb = (StringBuilder)stack.peek();
sb.append(s.charAt(i));
}
}
return (String)stack.peek().toString();
}
}
遍历每日温度,维护一个单调栈——栈中保存着递减的下标,为什么要维持这样一个递减的栈呢?因为每次只有变成递增状态的时候能够找到增温点。
如果当日温度<=栈顶温度,也就是说
如果当日温度>栈顶温度,代表栈顶的那天的升温日找到了,栈元素依次出栈,直到把当日温度放在它合适的位置。
class Solution {
public int[] dailyTemperatures(int[] T) {
LinkedList<Integer> stack = new LinkedList<>();
int[] res = new int[T.length];
for(int i=0;i<T.length;i++)
res[i] = 0;
for(int i=0;i<T.length;i++){
if(stack.isEmpty() || T[i] <= T[stack.peek()])
stack.push(i);
else{
while(!stack.isEmpty() && T[stack.peek()] < T[i]){//注意,使用stack.peek的时候确保不空才行
int index = stack.pop();
res[index] = i-index;
}
stack.push(i);
}
}
return res;
}
}
这一题有三种简单解法,每一种都很棒,全部要求掌握。雨水数组是a[]
(1)两遍遍历,用数组保存a[i]的前i个高度中的最大值(可以用动态规划思想)
第二遍遍历,从a最后一个元素向前,每次求出从当前到最后的最大值。此时当前元素左最大值得到,右最大值也得到,得到可积累雨水——最简单通俗容易想,时间复杂度O(n),空间O(n)。
代码如下:
class Solution {
//遍历求左右最大高度
public int trap(int[] height) {
if(height.length==0)
return 0;
int res = 0;
int[] left = new int[height.length];
//第一遍,求每个节点左边最高(含自己)
left[0] = height[0];
for(int i=1;i<height.length;i++){
left[i] = Math.max(left[i-1],height[i]);
}
//第二遍,求右边最高,并且同时比较左右最高,求出可积累雨水量
int rightmx = 0;
for(int i=height.length-1;i>=0;i--){
rightmx = Math.max(rightmx,height[i]);
res += Math.min(left[i],rightmx)-height[i];
}
return res;
}
}
(2)最高效的方法——(双指针),时间O(n),空间O(1)
left指针:从左往右当前下标
right指针:从右向左当前下标
left_max:left指针左边能找的最大值(不含left自己)——这个值一定是当前left的左堤坝
right_max:right指针右边能找到的最大值(不含right)——这个值一定是当前right的优堤坝
对于left而言,左边最大值一定是left_max,右边最大值范围是“大于等于”right_max的,所以,如果当前的left_max < right_max的时候,说明将来右无论怎样移动都不会影响结果,水都会被从右边挡住,左边的left_max一定是短板。当这种情况下,我们可以去除了left指针了,并计算当前left的存水量,left++。
反之,若right_max < left_max,就可以处理右节点了。
class Solution {
public int trap(int[] height) {
if(height.length<=2)
return 0;
int res = 0;
int left = 0,right = height.length-1;
int leftMax = 0,rightMax = 0;
while(left <= right){//注意结束条件,当left==right时,当前节点依然可以和左右最高值计算存雨量
if(leftMax < rightMax){//左指针可计算存储量了
res += Math.max(leftMax-height[left],0);
leftMax = Math.max(leftMax,height[left]);
left += 1;
}else{
res += Math.max(rightMax-height[right],0);
rightMax = Math.max(rightMax,height[right]);
right--;
}
}
return res;
}
}
(3)用单调栈,找到每次发生上升的位置,只有此时才可能出现囤积雨水的现象。
思路:
设置两个栈,一次存放递减高度的下标,一个存放递减的高度(每次囤积雨水时将高度更新为囤积后的高度)
public class Solution{
int trap(int[] height){
int n = height.length;
if(n<=2) return 0;
int res =0;//结果
int curLeftMax=0;//当前节点左边最大的那个高度(不含当前)
LinkedList<Integer> stackHeight = new LinkedList<Integer>();
LinkedList<Integer> stackIndex = new LinkedList<Integer>();
for(int i=0;i<n;i++){
int cur = height[i];
if(cur>=curLeftMax){//如果当前节点是新的制高点
while(!stackHeight.isEmpty()){//更新递减栈,以原来的左边最高为短板,计算雨水
int h = stackHeight.pop();
int index = stackIndex.pop();
if(stackHeight.isEmpty()) break;//最后一个元素,也就是那个原来的左边最高被pop了,可直接退出
res += (index-stackIndex.peek())*(curLeftMax-h);//雨水=长*高,高是和左最高的差,长是递减栈距离自己左边第一个元素的距离。
}
curLeftMax = cur;
stackHeight.push(cur);
stackIndex.push(i);
}else{//当前节点很普通
while(stackHeight.peek()<=cur){
int h = stackHeight.pop();
int index = stackIndex.pop();
res += (index - stackIndex.peek())*(cur-h);
}
stackHeight.push(cur);
stackIndex.push(i);
}
}
return res;
}
}
思路:
首先能想到的是:维持一个有序的数组,每次数据流新加入一个元素的时候,通过二分查找的方式找到<=该元素的位置,右移插入,这种方法太垃圾了;
由于发现要找中位数,左边从小到大,右边从大到小,这样我们存放数据不再用数组,而是两个堆,左边搞一个大根堆Left,右边搞一个小根堆Right,各自存放一半元素。
时间复杂度:查找中位数是O(1),插入元素是O(logN)
空间复杂度:O(N)
class MedianFinder{
Queue<Integer> Left,Right;
public MedianFinder(){
Left = new PriorityQueue<>((x,y)->y-x);//大顶堆,放小的那一半
Right = new PriorityQueue<>();//小顶堆
}
//添加元素
public void add(int x){
if(Left.size() == Right.size()){
Right.add(x);
Left.add(Right.poll());
}else{
Left.add(x);
Right.add(Left.poll());
}
}
//找到中位数
public double findMedian(){
if(Left.size() == Right.size())
return (Left.peek()+Right.peek())/2;
else
return Left.peek();
}
}
方法一:堆选最小,遍历链表
优先队列存放K个链表的当前表头,每次poll()作为结果链表的下一个元素。
时间复杂度:O(KNlogK)KN个元素*堆深度
空间复杂度:O(K)
方法二:依次合并,知道合并完K个,每次用双指针的方法合并
时间复杂度:第一次O(n),第二次O(2N),第三次O(3N)……最后总共O(KKN)
空间复杂度:O(1)只用了双指针
在这里插入代码片
方法三:
分治合并,类似归并,
时间复杂度:第一次K/2组,每小组O(2N),第二次K/4组,每小组O(4N)……,一共O(KNlogK )
空间复杂度:O(logK),K个链表
在这里插入代码片
求第K个大的原素,我们首先本能要知道的是这一题可以用堆排序的方法,毕竟堆排序是一种性能为nlogn的排序算法,性能很棒,找到第k个非常合适。但是我们要知道堆排序不是这种题的最好做法——最好的是快速排序
(1)如何用快排找第k大的元素
先复习一下快排:快排是先从第一个元素出发,交换元素,使得该元素左边都比他小,右边都>=它,这样数组就被划分为左右两部分了,然后左右两部分分别快排。
我们可以利用这种划分+分治的思想找第k大的元素
class Solution {
public int findKthLargest(int[] nums, int k) {
return quicksort(nums,0,nums.length-1,k);
}
private int quicksort(int[] a,int left,int right,int k){
if(left>right)
return -1;
int base=a[left],i=left,j=right;
while(i<j){
while(a[j]>=base && j>i)
j--;
while(a[i]<=base && j>i)//都是带等号的,否则出错
i++;
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
a[left] = a[i];
a[i] = base;
if(i==a.length-k)
return a[i];
else if(i<a.length-k)
return quicksort(a,i+1,right,k);
else
return quicksort(a,left,i-1,k);
}
}
(2)经典做法——堆
这里也有要注意的地方,求第k个最大元素,建立一个大根堆还是小根堆呢?大跟堆——当堆建立好之后,依次pop出去k-1个元素,此时堆顶就是第k最大元素。
当然我们可以调用java的库函数
PriorityQueue<Integer> heap = new PriorityQueue(n,(a,b)->b-a);//大顶堆要写倒过来
但是面试官一般可能让我们自己建立堆,所以还是老老实实自己实现吧。
堆很简单的,从下到上煎堆,从上到下跟下面的孩子比较交换,最大的最为父亲,如果发生交换,交换后得节点接着和孩子比较更新。
时间复杂度O(nlogn),建立堆O(n),删除k个元素O(klogn)因为k
public class Solution{
public int findKthLargest(int[] nums,int k){
return heapsort(nums,k);
}
public int heapsort(int[] a,int k){
int len = a.length;
for(int i = len/2-1;i>=0;i--)//初始化堆
heapajust(a,i,len);
swap(a,0,--len);//找到最大的元素了
if(k==1)
return a[len];
while(len>0){//当未排序长度>0时
heapajust(a,0,len);
swap(a,0,--len);
if(a.length-k==len)
return a[len];
}
return a[0];
}
private void heapajust(int[] a,int index,int len){
int left = index*2+1;
while(left<len){
int larger = left;
if(left+1<len && a[left+1]>a[left])
larger = left+1;
if(a[index]<a[larger]){//更大和和当前节点交换,并且当前节点变成交换后的位置,注意更新左孩子left
swap(a,index,larger);
index = larger;
left = index*2+1;
}else{
break;
}
}
}
private void swap(int[] s,int a,int b){
int temp = s[a];
s[a] = s[b];
s[b] = temp;
}
}
首先用一个HashMap记录每个元素出现的次数,得到一个次数数组,然后需要找到这个次数数组的第k大的元素,注意要求时间复杂度
方法一:hash+快排
quick(a[],left,right),将a从小道大排序,以a[left]为基准元素,将a[]分成两部分,左边都=a[left],得到基准元素的位置
如果该位置
如果该位置=n-k,找到,将它和它右边的元素统统放入结果数组。
时间复杂度:原本的快排是O(nlogn),但是我们现在只需要递归其中一个分支,算法复杂度减低到了O(n);
空间复杂度:O(n)。
class Solution{
public int[] topKeyFrequent(int[] nums ,int k){
//哈希表放着num数组元素出现的次数
Map<Integer,Integer> map = new HashMap<Integer,Integer>();
for(int item:nums)
map.put(item,map.getOrDefault(item,0)+1);
//次数数组,如{[1,3],[2,1],[3,2]}代表元素1出现3次,元素2出现1次,元素3出现2次
List<int[]> list = new ArrayList<int[]>();
for(Map.Entry<Integer,Integer> entry : map.entrySet()){
int key = entry.getKey();
int count = entry.getValue();
list.add(new int[]{key,count});
}
int[] res = new int[k];//结果集
quicksort(list,0,list.size()-1,res,0,k);
return res;
}
//快排求出list的出现次数前k做多的元素,放在res中
public void quicksort(List<int[]>,int left,int right,int[] res,int index,int k){
int base = list.get(left)[1];
int i = left,j = right;
while(i<j){
while(i<j && list.get(j)[1] >= base)
j--;
while(i<j && list.get(i)[1] <= base)
i++;
int[] temp = list.get(i);
list.set(i,list.get(j));
list.set(j,temp);
}
int[] temp = list.get(left);
list.set(left,list.get(i));
list.set(i,temp);
if(i==list.size()-k){
while(i<list.size()){
res[index] = list.get(i)[0];
index++;
i++;
}
}else if(i<list.size()-k){
quicksort(list,i+1,right,res,0,k);
}else{
quicksort(list,left,i-1,res,0,k);
}
}
}
方法二:hash+堆排
就是用堆排序,这一题不能完全按照上面的那种搞一个n元素的大顶堆,全部放进堆里面,最后从堆里面pop出k个元素的方法,因为如果那么做的话时间复杂度会达到nlogn
所以我们建立维护一个k个元素的小顶堆,遍历次数数组,当前元素<=堆顶,不管当前元素;当前元素>堆顶,插入堆。
注意——求最大k个用小顶堆,求最小k个用大顶堆
时间复杂度O(nlogk)
空间复杂度:O(N)
class Solution{
public int[] topKFrequent(int[] nums,int k){
//哈希表放着num数组元素出现的次数
Map<Integer,Integer> map = new HashMap<Integer,Integer>();
for(int item:nums)
map.put(item,map.getOrDefault(item,0)+1);
//最小堆
PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>(){
public int compare(int[] m,int[] n){
return m[1]-n[1];
}
});
//遍历维护这个最小跟堆
for(Map.Entry<Integer,Integer> entry : map){
int key = entry.getKey();
int count = entry.getValue();
if(queue.size()==k){
if(queue.peek()[1] < count){
queue.poll();
queue.offer(new int[]{key,count});
}
}else{
queue.offer(new int[]{key,count});
}
}
//从堆中得到返回值
int[] res = new int[k];
for(int i=0;i<k;i++)
res[i] = queue.poll()[0];
return res;
}
}
题目:给定一个数组和滑动窗口大小为k,求出每次滑动窗口的最大值
思路:
用int[n-k+1] res存放每个滑动窗口的最大值,
用一个队列作为辅助,队列头存放当前窗口的最大值,每次向后滑动窗口的时候,能影响新窗口最大值取值的元素有
我们可以发现,前面窗口的从最大值开始向后面遍历,次最大值、次次最大值、次次次最大值会影响结果。
所以我们可以维护一个单调非严格递减队列,保证队列中的元素是从头到尾的最大值、次最大值、次次最大值……,注意后面出现的元素如果比队列尾部的一些元素小,会覆盖他们变成新的次大元素,他们就没了。
class solution{
public int[] maxSlidingWindow(int[] nums,int k){
//边界
if(nums.length == 0 || k==0)
return new int[0];
//我们写队列最好是用Deque这个API
Deque<Integer> deque = new LinkedList<>();
//先构造第一个窗口
for(int i=0;i<k;i++){
//后面出现的元素如果比队列尾部的一些元素小,会覆盖他们变成新的次大元素
while(!deque.isEmpty() && deque.peekLast() < nums[i])
deque.removeLast();
deque.addLast(nums[i]);
}
res[0] = deque.peekFirst();
//下面是滑动窗口阶段
for(int i=k;i<nums.length; i++){
if(deque.peekFirst() == nums[i-k])
deque.removeFirst();
while(!deque.isEmpty() && deque.peekLast() < nums[i])
deque.removeLast();
deque.addLast(nums[i]);
res[i-k+1] = dequeue.peekFirst();
}
return res;
}
}
要求出队 入队 求最大值的函数都是O(1)复杂度
思路:单调队列
搞一个非递增辅助双端队列max
max的对头始终是当前队列的最大值,
class MaxQueue{
Queue<Integer> queue;
Deque<Integer> max;
public MaxQueue(){
queue = new LinkedList<>();
max = new LinkedList<>();
}
//入队
public void push_back(int value){
queue.add(value);
while(!max.isEmpty() && max.peekLast() < value)
max.removeLast();
max.addFirst(value);
}
//出队
public int pop_front(){
if(!max.isEmpty() && queue.peek().equals(max.peekFirst()))
max.removeFirst();
return queue.size()==0?-1:queue.poll();
}
//最大值
public int max_value(){
return max.size()==0?-1:max.peek();
}
}
冷却n,一个桶子>n+1,桶子里智能放不同的任务,行数由频数最大 的那个元素数量决定。
class Solution{
public int leastInterval(char[] tasks,int n){
int[] counts = new int[26];//存放的是每种任务数量
for(char c : tasks)
counts[c-'A']+=1;
int max = 0;
for(int count : counts) max = Math.max(max,count);
int maxCount = 0; //最后一行还剩的元素数量
for(int count:counts)
if(count == max)
maxCount++;
return Math.max(tasks.length, (n+1)*(max-1)+maxCount);
}
}
求连通分量的个数,比如我们 200和547,其实最好还是用宽搜或者深搜,并查集效率太低了。
这里了解即可。
(1)哈希表+优化
我们考虑枚举数组中的每个数 x,考虑以其为起点,不断尝试匹配 x+1, x+2, ⋯ 是否存在,假设最长匹配到了 x+y,那么以 x 为起点的最长连续序列即为 x, x+1, x+2,⋯,x+y,其长度为 y+1,我们不断枚举并更新答案即可。
其实更高效的方法是用一个哈希表存储数组中的数,这样查看一个数是否存在即能优化至 O(1) 的时间复杂度。
仅仅是这样我们的算法时间复杂度最坏情况下还是会达到 O(n^2),无法满足题目的要求。
但仔细分析这个过程,我们会发现其中执行了很多不必要的枚举,如果已知有一个 x, x+1, x+2⋯,x+y 的连续序列,而我们却重新从 x+1,x+2 或者是 x+y 处开始尝试匹配,那么得到的结果肯定不会优于枚举 x为起点的答案,因此我们在外层循环的时候碰到这种情况跳过即可。
那么怎么判断是否跳过呢?由于我们要枚举的数 x 一定是在数组中不存在前驱数 x-1 的,不然按照上面的分析我们会从 x-1开始尝试匹配,因此我们每次在哈希表中检查是否存在 x-1 即能判断是否需要跳过了。
class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> set = new HashSet<>();
for(int a:nums)
set.add(a);
int maxlen = 0;
for(int a:nums){
if(a==Integer.MIN_VALUE || !set.contains(a-1)){//小心边界
int curlen = 1;
int curnum = a;
while(a != Integer.MAX_VALUE && set.contains(curnum+1)){//小心边界
curnum++;
curlen++;
}
maxlen = Math.max(maxlen,curlen);
}
}
return maxlen;
}
}
方法二:并查集
能够想到使用并查集来解决,关键是将连续的整数视为一个集合,当然最小的集合大小就是1,元素本身。因此在合并时将该数x与x+1合并,(如果x+1也在数组中),并计算集合大小的最大值就好了
class Solution {
//并查集类
class UnionFind{
Map<Integer, Integer> parents;
public UnionFind(int[] arr) {//初始化,map<自己,祖先>
parents = new HashMap<>();
for (int i : arr) {
parents.put(i, i);
}
}
public Integer find(int x) {//查找x的祖宗
if (!parents.containsKey(x)) return null;
int t = parents.get(x);
if(x != t)
parents.put(x, find(t));//如果x的祖先不是自己,更新x的祖先
return parents.get(x);//最后返回x的祖先
}
public boolean union(int x, int y) {//将x和y变成一个集合关系(具有祖先子孙关系)
Integer rootX = find(x), rootY = find(y);
if (rootX == null || rootY == null) return false;
if(rootX.equals(rootY)) return false;
parents.put(rootX, rootY);
return true;
}
}
//-------------------------------------------
//解题
public int longestConsecutive(int[] nums) {
if (nums.length == 0) return 0;
UnionFind u = new UnionFind(nums);
for (int num : nums) {//构造并查集,每次合并num 和 num+1
u.union(num, num + 1);
}
int max = 1;
for (int num : nums) {
max = Math.max(max,u.find(num) - num + 1);
}
return max;
}
}
给你二维数组,找出数组中岛屿数量。
int fa[1000];
int find(int x){//查找x的祖先节点
if(fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
//查找连通分量个数
int findCircleNum(int** M, int MSize, int* MColSize){
for(int i=0;i<MSize;i++) fa[i]=i;
int ans=MSize;//连通分量默认有MSize个
for(int i=0;i<MSize;i++)//遍历行
{
for(int j=0;j<i;j++)//遍历列
{
if(M[i][j]==0)
continue;
if(find(fa[i])!=find(fa[j]))//如果该两个点间不同祖先
{
fa[find(i)]=fa[find(j)];//祖先合并为一家
ans--;//连通分量--
}
}
}
return ans;
}
找到所有被X包围的O,将这些O改为X
class Solution {
// 定义并查集——很简单,并查集都是这么定义的
class UnionFind{
int[] parents;
public UnionFind(int size){
parents = new int[size];
for(int i = 0; i < size; i++)
parents[i] = i;
}
//找x的祖先
public int find(int x){
if(parents[x] == x)
return x;
return parents[x] = find(parents[x]);
}
//合并x和y
public void union(int x, int y){
int px = find(x);
int py = find(y);
if(px == py)
return ;
parents[px] = py;
}
//x和y是否在同一个连通图里
public boolean isConnect(int x, int y){
return find(x) == find(y);
}
}
int[][] d = new int[][]{{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
int m;
int n;
//求解
public void solve(char[][] board) {
m = board.length;
if(m == 0) return;
n = board[0].length;
int size = m * n + 1;
int src = m * n;
UnionFind u = new UnionFind(size);
//遍历这个图,处理并查集
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(board[i][j] == 'X')
continue;
if(i == 0 || i == m-1 || j == 0 || j == n-1)// 是边缘的'O'与超级源点src相连接
u.union(src, i * n + j);
else{//不是边缘的‘O’
for(int k = 0; k < 4; k++){ // 将周围的'O'与(i, j)连接
int x = i + d[k][0];
int y = j + d[k][1];
if(board[x][y] == 'O')
u.union(i * n + j , x * n + y);
}
}
}
}
//最后根据并查集的结果处理图,不和周围的‘O’相联的O都改成“X”
for(int i = 1; i < m-1; i++){
for(int j = 1; j < n-1; j++){
if(board[i][j] == 'O' && !u.isConnect(src, i * n + j)){
board[i][j] = 'X';
}
}
}
}
}