算法 搜索

深度优先搜索

广度优先搜索

深搜与广搜的区别

深搜 dfs------回溯------"不撞南墙不回头"

思路

总的来说是不撞南墙不回头,相当于一个人严格按照固定的行为模式。

例如走方格,依次走上下左右,每次走到一个新格子记录自己已经走过的方向,当到达终点或是所有方向走完之后,会重新回到上一格,代表上一格此方向【之前按照哪个方向到达此格的方向】的遍历完成。

形成效果

一个dfs其实是对一个问题的结果集的所有解题情况的遍历【不管这个解题情况是否复合题意】。

dfs 搜索树与回溯

一个dfs其实是对一个问题的结果集的所有解题情况的遍历,在遍历过程中,前面解题情况的选择会影响到后面的解题情况。

例如走迷宫,解题情况其实就是主人公走到哪个格子,他会影响到之后走过的路径。

所以我们其实可以将解题情况进行串联,将前面的选择作为一个图的前驱节点,而后面的结果作为后继节点,那么所有的解题情况的遍历便可以组成一个树,我们叫做搜索树。树上的根节点到 某个叶节点的一条路径就是一条解题过程。

那迷宫来进行距离,我们把主人公当前正在走的点当作前驱,而之后依照行为模式工作后所走到的下一个点作为后继。那么所有所走过的路径的集合便可构成一个树。

在dfs遍历解题结果的过程中,会存储并改变一些解题的状态,由于我们是会对所有情况依次进行遍历,当一种情况遍历完成后,dfs应该回到上一个节点进行行为模式的继续遍历。

例如迷宫,走到死胡同或者终点时,应该回到上一次走过的点,看其他方向能否到达终点。

注意,dfs是进行所有结果的遍历,所以在到达终点后并不会停止,而是会继续回到上一步运行。

而此时回到上一个节点就意味着,之前走过的一些点,在这个时刻应该是没有走过的,所以的话在写代码的时候应该进行一个状态的回溯。在回到上一个节点之前,本节点对于上一个节点应该是没有遍历过的,所以要进行状态改变。

代码模板

人走迷宫,迷宫情况使用0、1表示,1可行,0不可行。先给定起点与终点,求所有路径中最少要走多少格子。

递归实现

#include<stdio.h>
_Bool map[100][100] = { 0 };//1代表可行,0不可行
_Bool visit[100][100] = { 0 };//0未走,1走过
int mov[4][2] = { {0,1},{1,0},{0,-1},{-1,0} };//右下左上
int endx, endy, startx, starty;
int col, row, t;
_Bool can = 0;
int min = 0x3f3f3f3f;//代表正无穷
void dfs(int x,int y, int value) {//如果value没有被设为参数,回溯的时候需要改变value值
	if (value > min)//最优剪枝1
		return;
	if (x<0 || y<0 || x>=row || y>=col)//判断是否出界
		return;
	if (x == endx && y == endy) {//结束条件,找到终点
		can = 1;
		if (value < min)
			min = value;
		return;//回退,防止继续走下去,已经到达终点了,没必要再走了
	}
	if (value >= min)//最优剪枝2,未到达终点,便value=min,那么到达终点时,一定回value>min
		return;
	for (int i = 0;i < 4;i++) {
		int xx = x + mov[i][0], yy = y + mov[i][1];//人物偏移
		if (map[xx][yy] && !visit[xx][yy]) {//判断是否可以进入下一格,可行性剪枝
            visit[xx][yy] = 1;//先标记再访问
		    dfs(xx, yy, value+1);//让人物移动到下一格
		    visit[xx][yy] = 0;//回溯操作,当递归回到这一层的时候,(xx,yy)应该未访问,所以置为0
			//回溯操作是一层递归一层递归进行回溯,每次只会回溯到上一层,所以每次只用将当前位置的下一个操作的数据回溯
		}
		
	}
}
int main() {
	scanf("%d", &t);
	while (t--) {//t轮数据,每一次数据进行操作时,都要保证所有的状态都为最初状态,不被上一次操作所影响
		scanf("%d%d", &col, &row);
		for (int i = 0;i < row;i++)
			for (int j = 0;j < col;j++) {
				scanf("%d", &map[i][j]);
				visit[i][j] = 0;
			}
		scanf("%d%d%d%d", &startx, &starty, &endx, &endy);
		visit[startx][starty] = 1;
		dfs(startx, starty, 0);
		if (can)
			printf("能,min=%d\n", min);
		else
			printf("不能\n");
        _Bool can = 0;
	}
	return 0;
}

栈实现

#include<iostream>
#include<cstdio>
#include<stack>
using namespace std;
struct Point {
	int x, y, value, dir;
	Point(int a,int b,int value):x(a),y(b),value(value),dir(0){}
};
typedef struct Point TYPE;
int main(void) {
	stack<TYPE> Stack;//这个类可以用<>指定里面的内容的类型,类似于一个栈
	bool map[100][100] = { 0 }, visited[100][100] = { 0 };
	int mov[4][2] = { {0,1},{0,-1},{1,0},{-1,0} };
	int m, n;
	cin >> m >> n;
	int i, j;
	for (i = 0;i < m;i++)
		for (j = 0;j < n; j++)
			cin >> map[i][j];
	int startx, starty, endx, endy;
	scanf("%d%d%d%d", &startx, &starty, &endx, &endy);
	Stack.emplace(startx, starty, 0);
	visited[startx][starty] = 1;
	while (!Stack.empty()) {//栈非空进行循环
		TYPE* cur = &Stack.top();
		if (cur->x == endx && cur->y == endy) {//判断是否到达终点
			printf("%d\n", cur->value);
			visited[cur->x][cur->y] = 0;
			Stack.pop();
			continue;//如果到达,弹栈,回到上一次位置,进行下一轮循环
		}
		if (cur->dir <= 3) {//遍历
			int xx = cur->x + mov[cur->dir][0];
			int yy = cur->y + mov[cur->dir][1];
			cur->dir++;//进入一个新的结点的时候,dir=0,当遍历之后要自增

			if (xx < m && yy < n && xx >= 0 && yy >= 0 && !visited[xx][yy] && !map[xx][yy]) {
				Stack.emplace(xx, yy, cur->value + 1);//压栈
				visited[xx][yy] = 1;
			}
		}
		else {//弹栈
			visited[cur->x][cur->y] = 0;
			Stack.pop();
		}
	}
	return 0;
}

与模板差异

  1. 是否回溯
  2. 遍历方向
  3. 结束条件
  4. 结束处理

超时后的解决方法

  1. 换思路:主要换遍历的方式【mov之类的】
  2. 剪枝

普通无回溯 dfs

例题

海战 - 洛谷

该题与走迷宫不同,走迷宫是从一个点为起点,然后一直走下去,每走完一种情况后需要回过头检查是否有其他路可走,需要进行回溯。该题是需要遍历每一个点,若该点是船只的一部分,需要判断该点是否可以与其它点相连构成船只,且每个点只能使用一次【使用过之后就不能再次使用】,走到头后,不需要回过头检查其他的路径,所以不需要回溯。

#include<stdio.h>
int	R, C, S = 0;
char map[1005][1005] = { 0 };
int mov[4][2] = { {-1,0},{1,0},{0,-1},{0,1} };
void dfs(int x, int y) {
	map[x][y] = '.';//因为每只船只能使用一次,所以在使用之后要将'#'标记为'.',后期不可以再使用
	for (int i = 0;i < 4;i++) {
		int xx = x + mov[i][0], yy = y + mov[i][1];
		if (xx >= 0 && xx < R && yy >= 0 && yy < C && map[xx][yy] == '#') {
			dfs(xx, yy);
		}
	}
	return;
}
//判断船的位置是否合法,即在一个正方形中,四个角上如果有三个角有船就是不合理的,那样子会出现一个三角形,不是方形
_Bool d(int i, int j) {
	int c = 0;
	if (map[i][j] == '#')
	    c++;
	if (map[i + 1][j] == '#')
	    c++;
	if (map[i][j + 1] == '#')
	    c++;
	if (map[i + 1][j + 1] == '#')
	    c++;
	if (c == 3)
		return 0;
	return 1;
}
int main() {
	scanf("%d%d", &R, &C);
	for (int i = 0;i < R;i++) {
		scanf("%s", map[i]);
	}
	for (int i = 0;i < R;i++) {
		for (int j = 0;j < C;j++) {
			if (d(i, j) == 0) {
				printf("Bad placement.");
				return 0;
			}
		}
	}
	for (int i = 0;i < R;i++) {
		for (int j = 0;j < C;j++) {
			if (map[i][j] == '#') {
				S++;
				dfs(i, j);
			}
		}
	}
	printf("There are %d ships.", S);

	return 0;
}

剪枝

在搜索树中剪去多余枝干。

另一个结束递归的条件

  1. 可行性剪枝(判断可不可以走,包括有无障碍物和是否走过,......)
  2. 最优剪枝(消耗的能量,若目前未到终点,但是使用的能量已经超过min)
  3. 奇偶性剪枝
  4. 优化搜索顺序(人眼看)
  5. 冗余剪枝
  6. 记忆化搜索
  7. α-β剪枝(博弈算法,可能被人工智能使用)

优化搜索顺序

靠数学功底

可行性剪枝

就像上面的迷宫问题,能否走这个格子就是一个可行性剪枝。这个是根据题意来进行判断,看下一步路线是否可能符合题意,若直接违背则就不用走。

最优剪枝

适用于求最短路径

例题

[USACO2.1] 健康的荷斯坦奶牛 Healthy Holsteins - 洛谷

#include<stdio.h>
int v, g, need[30], food[20][30], ans[20], min = 0x3f3f3f3f, temp[20], len;//v所需要v种营养 g有g种饲料 need每种营养成分需要多少 food二维数组,低维:每种营养的含量,高维:饲料总类 ans答案数组 temp存储现在选择的饲料 len目前选了len种饲料 min做优情况,即选最少的种类便可达到所需的营养
_Bool visited[20];//状态数组,表示是否被选过
_Bool check(int len) {//检查是否达到所需的营养
	int nutrition[30] = { 0 };//每种营养现在的含量
	for (int i = 1;i <= len;i++) {
		for (int l = 1;l <= v;l++) {
			nutrition[l] += food[temp[i]][l];
		}	
	}
	for (int i = 1;i <= v;i++) {
		if (need[i] > nutrition[i])//只要有一种营养成分含量不合格,就返回0
			return 0;
	}
	return 1;
}
void dfs() {//搜索
    //最优剪枝
	if (len > g || len > min)//现在的饲料种类超过最优或超过总的饲料种类就退出递归
		return;
	if (check(len) == 1) {//检验是否达到所需的营养含量,若达到,与最优解进行比较,若所需的种类少于目前的最小的,则代替最小成为新的最少,即新的最优情况
		if (min > len) {
			min = len;
			for (int i = 1;i <= min;i++)
				ans[i] = temp[i];
		}
		return;
	}
	for (int i = temp[len]+1;i <= g;i++) {//遍历,注意遍历的开始是上一次选择下一个【遍历选择每一种饲料,不需要每次都从头开始选】
		if (!visited[i]) {
			visited[i] = 1;
			len++;
			temp[len] = i;
			dfs();
			visited[i] = 0;//回溯
			temp[len] = 0;
			len--;
		}
	}

}
int main() {
	scanf("%d", &v);
	for (int i = 1;i <= v;i++) {
		scanf("%d", &need[i]);
	}
	scanf("%d", &g);
	for (int i = 1;i <= g;i++) {
		for (int j = 1;j <= v;j++) {
			scanf("%d", &food[i][j]);
		}
	}
	dfs();
	printf("%d ", min);
	for (int i = 1;i <= min;i++)
		printf("%d ", ans[i]);
	return 0;
}

奇偶性剪枝

横纵坐标从1开始,横纵坐标相加为奇数赋为0,偶数赋为1

1->1:最近的1走两步,任何一个1走偶数步,0->0也是【同偶异奇】

冗余剪枝

重复的删除去

[USACO2.1] 健康的荷斯坦奶牛 Healthy Holsteins - 洛谷

#include<stdio.h>
int v, g, need[30], food[20][30], ans[20], min = 0x3f3f3f3f, temp[20], len;//v所需要v种营养 g有g种饲料 need每种营养成分需要多少 food二维数组,低维:每种营养的含量,高维:饲料总类 ans答案数组 temp存储现在选择的饲料 len目前选了len种饲料 min做优情况,即选最少的种类便可达到所需的营养
_Bool visited[20];//状态数组,表示是否被选过
_Bool check(int len) {//检查是否达到所需的营养
	int nutrition[30] = { 0 };//每种营养现在的含量
	for (int i = 1;i <= len;i++) {
		for (int l = 1;l <= v;l++) {
			nutrition[l] += food[temp[i]][l];
		}	
	}
	for (int i = 1;i <= v;i++) {
		if (need[i] > nutrition[i])//只要有一种营养成分含量不合格,就返回0
			return 0;
	}
	return 1;
}
void dfs() {//搜索
    //最优剪枝
	if (len > g || len > min)//现在的饲料种类超过最优或超过总的饲料种类就退出递归
		return;
	if (check(len) == 1) {//检验是否达到所需的营养含量,若达到,与最优解进行比较,若所需的种类少于目前的最小的,则代替最小成为新的最少,即新的最优情况
		if (min > len) {
			min = len;
			for (int i = 1;i <= min;i++)
				ans[i] = temp[i];
		}
		return;
	}
	for (int i = temp[len]+1;i <= g;i++) {//遍历,注意遍历的开始是上一次选择下一个【遍历选择每一种饲料,不需要每次都从头开始选】
		if (!visited[i]) {
			visited[i] = 1;
			len++;
			temp[len] = i;
			dfs();
			visited[i] = 0;//回溯
			temp[len] = 0;
			len--;
		}
	}

}
int main() {
	scanf("%d", &v);
	for (int i = 1;i <= v;i++) {
		scanf("%d", &need[i]);
	}
	scanf("%d", &g);
	for (int i = 1;i <= g;i++) {
		for (int j = 1;j <= v;j++) {
			scanf("%d", &food[i][j]);
		}
	}
	dfs();
	printf("%d ", min);
	for (int i = 1;i <= min;i++)
		printf("%d ", ans[i]);
	return 0;
}

记忆化搜索

将之前计算的结果存储在数组中,当计算同样的数据时,可以直接使用之前计算过的答案。

冗余:重复的直接被删除

记忆化搜索:借用之前重复的数据,而不是舍去

(x,y) data[x][y]

dfs(x,y)

if(data[x][y]算过){

return data[x][y];

}

dfs函数返回值类型不是void

使用条件

1.有限个 2.有重复值

例题

int dp[25][25][25];

int dfs(int a, int b, int c) {

if (a <= 0 || b <= 0 || c <= 0)

return 1;

if (a > 20 || b > 20 || c > 20) {

return dfs(20, 20, 20);

}

if (dp[a][b][c]) //先判断该值是否计算过,如果算过,直接调结果,不需要再次计算,直接返回即可

return dp[a][b][c];

if (a < b && b < c) {

dp[a][b][c] = dfs(a, b, c - 1) + dfs(a, b - 1, c - 1) - dfs(a, b - 1, c);

}

else

dp[a][b][c] = dfs(a - 1, b, c) + dfs(a - 1, b - 1, c) + dfs(a - 1, b, c - 1) - dfs(a - 1, b - 1, c - 1);

return dp[a][b][c];

}

例题------全排列

给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。

现在,请你按照字典序将所有的排列方法输出。

输入格式

共一行,包含一个整数 n。

输出格式

按字典序输出所有排列方案,每个方案占一行。

数据范围

1≤n≤7

输入样例

3

输出样例

1 2 3

1 3 2

2 1 3

2 3 1

3 1 2

3 2 1

#include<stdio.h>
int n;
int a[10],state[10]={0};
void dfs(int step){
    if(step==n+1){
        for(int i=1;i<=n;i++)
            printf("%d ",a[i]);
        printf("\n");
        return;
    }
    for(int i=1;i<=n;i++){
        if(state[i]==0){
            a[step]=i;
            state[i]=1;
            dfs(step+1);
            a[step]=0;
            state[i]=0;
        }
    }
    return; 
}
int main(){
    scanf("%d",&n);
    dfs(1);
    return 0;
}

例题------八皇后

[USACO1.5] 八皇后 Checker Challenge - 洛谷

n*n的正方形,i表示行,j表示列,i+j表示其中一条对角线,i-j+n表示另外一条对角线

#include<stdio.h>
int a[15]={0},b[15]={0},c[30]={0},d[30]={0};
int n,sum=0;
void dfs(int i){
    if(i>n){
        if(sum<3){
            for(int k=1;k<=n;k++){
                printf("%d ",a[k]);
            }
            printf("\n");
        }
        sum++;
        return;
    }
    for(int j=1;j<=n;j++){
        if((!b[j])&&(!c[i+j])&&(!d[i-j+n])){
            a[i]=j;//行,第i行是第j个
            b[j]=1;//列
            c[i+j]=1;//对角线1,根据截距给对角线命名
            d[i-j+n]=1;//对角线2
            dfs(i+1);
            b[j]=0;//回溯
            c[i+j]=0;
            d[i-j+n]=0;
        }
    }
}
int main(){
    scanf("%d",&n);
    dfs(1);
    printf("%d",sum);
    return 0;
}

广搜 bfs------一层一层走

一层一层搜索,首先,先把所有第一层的点,即距离为1的点搜完,然后进入下一层,直到搜完。

边权相同时,可以用bfs求最短路。

实现思路

使用队列来实现,每次将要搜索的点放在队列中,刚开始搜索时,队列中只有一个点,然后每次将所有与队头元素相连的且没有访问过的,符合条件的点都放入队列,之后对 队头元素进行处理然后出队。当队列为空则说明搜索停止。

这样就可以保证一层一层遍历,遍历整体深度逐渐增大。

而由这样遍历出来的合法路径,若是两点间权值相同的话,是具有最优性质的。

框架

queue 初始状态

while queue 不空

{

t<--每次取队头

扩展队头t

}

例题------走迷宫

给定一个n*m的二维整数数组,用来表示一个迷宫,数组中只包含0或1,其中0表示可以走的路,1表示不可通过的墙壁。

最初,有一个人位于左上角(1, 1)处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。

请问,该人从左上角移动至右下角(n, m)处,至少需要移动多少次。

数据保证(1, 1)处和(n, m)处的数字为0,且一定至少存在一条通路。

输入格式

第一行包含两个整数n和m。

接下来n行,每行包含m个整数(0或1),表示完整的二维数组迷宫。

输出格式

输出一个整数,表示从左上角移动至右下角的最少移动次数。

数据范围

1≤n,m≤100

输入样例

5 5

0 1 0 0 0

0 1 0 1 0

0 0 0 0 0

0 1 1 1 0

0 0 0 1 0

输出样例

8

//bfs,使用手写队列实现
//输出路径,只需要再开一个数组prev,记录这个点是由那个点扩展出来即可
#include<iostream>
#include<cstring>
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int n, m;
char map[N][N];
int len[N][N];//每个点到起点的距离
PII q[N * N];//实现队列
//PII Prev[N][N];  //输出路径
int bfs() {
    int hh = 0, tt = 0;
    q[0] = { 0,0 };
    memset(len, -1, sizeof len);
    //初始化函数,将某一块内存中的内容全部设置为指定的值,通常为新申请的内存做初始化工作
    //参数1,指针或数组;参数2,赋给参数1的值;参数3,参数1的长度
    len[0][0] = 0;
    int dx[4] = { -1,0,1,0 }, dy[4] = { 0,1,0,-1 };
    while (hh <= tt) {
        auto t = q[hh++];//每次取出队头元素
        for (int i = 0;i < 4;i++) {
            int x = t.first + dx[i], y = t.second + dy[i];
            if (x >= 0 && x < n && y >= 0 && y < m && map[x][y] == '0' && len[x][y] == -1) {
                len[x][y] = len[t.first][t.second] + 1;
                //Prev[x][y] = t;
                q[++tt] = { x,y };//在队尾插入元素
            }
        }
    }
    /*int x = n - 1, y = m - 1;
    while (x || y) {
    cout << x << ' ' << y << endl;
    auto t = Prev[x][y];
    x = t.first, y = t.second;
    }*/
    return len[n - 1][m - 1];
}
int main() {
    cin >> n >> m;
    for (int i = 0;i < n;i++)
        cin >> map[i];
    cout << bfs() << endl;
    return 0;
}

有向图的遍历

宽度优先遍历

一层一层搜索

框架

queue <------ 将起始状态插入队列中,即将1号点插入队列

while (queue 不空){

t 每次取队头元素

拓展 t 所有能到的点 x

if(x 未被遍历){

queue <------x 将 x 入队

d[x]=d[t]+1

}

}

例题------图中点的层次

给定一个n个点m条边的有向图,图中可能存在重边和自环。

所有边的长度都是1,点的编号为1~n。

请你求出1号点到n号点的最短距离,如果从1号点无法走到n号点,输出-1。

输入格式

第一行包含两个整数n和m。

接下来m行,每行包含两个整数a和b,表示存在一条从a走到b的长度为1的边。

输出格式

输出一个整数,表示1号点到n号点的最短距离。

数据范围

1≤n,m≤10^5

输入样例

4 5

1 2

2 3

3 4

1 3

1 4

输出样例

1

代码

#include<iostream>
#include<cstring>
using namespace std;
const int N = 100010;
int n, m;
int h[N], e[N], ne[N], idx;
int d[N], q[N];
void add(int a, int b) {//插入函数
	e[idx] = b;
	ne[idx] = h[a];
	h[a] = idx++;
}
int bfs() {
	int hh = 0, tt = 0;
	q[0] = 1;
	memset(d, -1, sizeof d);//初始化距离,-1代表未被初始化
	d[1] = 0;
	while (hh <= tt) {//判断队列是否为空
		int t = q[hh++];//取队头
		for (int i = h[t];i != -1;i = ne[i]) {//扩展队头
			int j = e[i];
			if (d[j] == -1) {
				d[j] = d[t] + 1;
				q[++tt] = j;
			}
		}
	}
	return d[n];
}
int main() {
	cin >> n >> m;
	memset(h, -1, sizeof h);//初始化表头
	for (int i = 0;i < m;i++) {
		int a, b;
		cin >> a >> b;
		add(a, b);
	}
	cout << bfs() << endl;
}

深度优先遍历

找到一个起点,然后从这个 起点开始,一条路走向黑

邻接表的深度优先遍历

主函数:

for(int i=0;i<n;i++) dfs(i); //枚举起点

dfs:

利用图中结点的编号进行搜索,e存图中结点的编号

int h[N], e[M], ne[M], idx;//h存n个链表的链表头,e存每个结点的值是多少,ne存每个结点的next值

bool st[N];

//树和图的深度优先搜索

void dfs(int u) {//u是当前dfs到的点

st[u] = true;//标记一下,已经被搜过了

for (int i = h[u];i != -1;i = ne[i]) {//遍历u的所有出边

int j = e[i];//链表中该点在图中的编号

if (!st[j])//如果j没有被搜过,那么就进行搜索

dfs(j);

}

例题------树的重心

给定一颗树,树中包含n个结点(编号1~n)和n-1条无向边【无向图】。

请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。

重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。

输入格式

第一行包含整数n,表示树的结点数。

接下来n-1行,每行包含两个整数a和b,表示点a和点b之间存在一条边。

输出格式

输出一个整数m,表示重心的所有的子树中最大的子树的结点数目。

数据范围

1≤n≤10^5

输入样例

9

1 2

1 7

1 4

2 8

2 5

4 3

3 9

4 6

输出样例

4

代码
相关推荐
SpongeG40 分钟前
数据结构第三周做题总结_链表
数据结构·算法·链表
everyStudy41 分钟前
前端五种排序
前端·算法·排序算法
little redcap3 小时前
第十九次CCF计算机软件能力认证-乔乔和牛牛逛超市
数据结构·c++·算法
muyierfly3 小时前
34.贪心算法1
算法·贪心算法
luthane6 小时前
python 实现average mean平均数算法
开发语言·python·算法
静心问道6 小时前
WGAN算法
深度学习·算法·机器学习
杰九6 小时前
【算法题】46. 全排列-力扣(LeetCode)
算法·leetcode·深度优先·剪枝
manba_6 小时前
leetcode-560. 和为 K 的子数组
数据结构·算法·leetcode
liuyang-neu6 小时前
力扣 11.盛最多水的容器
算法·leetcode·职场和发展
忍界英雄6 小时前
LeetCode:2398. 预算内的最多机器人数目 双指针+单调队列,时间复杂度O(n)
算法·leetcode·机器人