概述
https://blog.csdn.net/raphealguo/article/details/7523411
https://blog.csdn.net/qq_41681241/article/details/81432634
https://blog.csdn.net/createprogram/article/details/86744931(算法竞赛,牛)
https://blog.csdn.net/ldx19980108/article/details/76324307(DFS及实例)
https://blog.csdn.net/jiange702/article/details/81365005(模板)
广度优先搜索(BFS)
广度优先搜索(也称宽度优先搜索,缩写BFS,以下采用广度来描述)是连通图的一种遍历策略。因为它的思想是从一个顶点V0开始,辐射状地优先遍历其周围较广的区域,故得名。
一般可以用它做什么呢?一个最直观经典的例子就是走迷宫,我们从起点开始,找出到终点的最短路程,很多最短路径算法就是基于广度优先的思想成立的。
基本步骤:
1.从图中某个顶点v0出发,首先访问v0;
2.依次访问v0的各个未被访问的邻接点;
3.依次从上述邻接点出发,访问它们的各个未被访问的邻接点。
4.若此时图中仍有未被访问的结点,则另选图中的一个未被访问的顶点作为起始点。重复广度优先搜索过程,直到图中的所有节点均被访问过。
基本代码结构:
通常用队列(先进先出,FIFO)实现
初始化队列Q.
Q={起点s};
标记s为己访问;
while (Q非空) {
取Q队首元素u; u出队;
if (u == 目标状态) {…}
所有与u相邻且未被访问的点进入队列;
标记与u相邻的点为已访问;
}
//通常用队列queue实现,或者有些时候用数组模拟队列
void bfs()
{
初始化队列q
q.push(起点);
标记上起点;
while(!q.empty())
{
取队首元素u;
q.pop();//队首元素出队
for(int i=0;i<可以走的方向数;i++)
{
if(下一步满足边界内,未访问等条件)
{
q.push();//该点入队
标记上该点;
...
}
}
}
}
DFS/BFS是竞赛中最常见的基础算法。虽然题目多种多样,但无外乎就是套用上文的程序片段,最主要的还是结合习题多练习达到熟能生巧。
这里呢,我想多讲一点。上面的BFS是使用C++库里封装的队列的,这里额外写一个不使用封装队列的方法,就是自己使用一个数组来模拟操作,见下方代码:
#include<bits/stdc++.h>
using namespace std;
int a[105][105],vis[105],n,m;
//a是邻接矩阵 vis是标记 点是否被访问过
void bfs(int k){ //k是当前点的名字
int q[105];
int f,r,i,j;//r表示当前BFS路过的点是第r个点
q[1]=k;
vis[k]=1;
f=1;r=1;
while(f<=r){
i=q[f];
for(j=1;j<=m;j++){
if(a[i][j]>0&&!vis[j]){ //邻接矩阵中a[i][j]>0 表示 i和j连通
r++;
q[r]=j;
vis[j]=1;
}
}
f++;
}
for(i=1;i<=r;i++) cout<<q[i]<<" ";//输出当前BFS层的点的序号
}
int main(){
int h,v1,v2;
cin>>m;//点的数量
cin>>n;//边的数量
memset(a,0,sizeof(a));
memset(vis,0,sizeof(vis));
for(int i=1;i<=n;i++){
cin>>v1>>v2>>h;//每条边的 起点 终点 边长
a[v1][v2]=a[v2][v1]=h;//无向图正反对接
}
for(int i=1;i<=m;i++)if(!vis[i])bfs(i);
return 0;
}
有向图:
广度优先搜索遍历图的过程是以a为起点,由近至远,依次访问和a有路径相通且路径长度为1,2…的顶点,一般用数据结构中的队列来解决比较方便。
用途:求最短路径或最优方案
深度优先搜索
基本步骤:
1.从图中某个顶点v0出发,首先访问v0;
2.访问结点v0的第一个邻接点,以这个邻接点vt作为一个新节点,访问vt所有邻接点。直到以vt出发的所有节点都被访问到,回溯到v0的下一个未被访问过的邻接点,以这个邻结点为新节点,重复上述步骤。直到图中所有与v0相通的所有节点都被访问到。
3.若此时图中仍有未被访问的结点,则另选图中的一个未被访问的顶点作为起始点。重复深度优先搜索过程,直到图中的所有节点均被访问过。
基本代码结构:
void dfs(int t)//t代表目前dfs的深度
{
if(满足输出条件||走不下去了)
{
输出解;
return;
}
else
{
for(int i=1;i<=尝试方法数;i++)
if(满足进一步搜索条件)
{
为进一步搜索所需要的状态打上标记;
dfs(t+1);
恢复到打标记前的状态;//也就是说的{回溯一步}
}
}
}
//另一个模板
int check(参数)
{
if(满足条件)
return 1;
return 0;
}
void dfs(int step)
{
判断边界
{
相应操作
}
尝试每一种可能
{
满足check条件
标记
继续下一步dfs(step+1)
恢复初始状态(回溯的时候要用到)
}
}
总结一下,用递归法来实现DFS,比较好理解,就一直往下找,知道走不通后在回来尝试其它的地方。一个DFS一般要判断边界,check来判断是否符合相应条件,vis或者book来记录是否已经被用过,递归进行下一步操作。有的时候我们要将标记过的点恢复原来的状态,有时候则不必要恢复(油田问题),要结合具体的问题来分析。
恢复标记相当于回溯的思想。
回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
一个小建议:DFS理解起来不是很难,但是只理解不练习是没有用的。一定要找一些经典的题目多加练习,只有这样才能加深自己的理解,掌握的也更快。算法题可能难度越来越大,但是也不能放弃,自己先学再练,对自己的思维和编程能力也会有一定的提升。
两者总结
一般来说,广搜常用于找单一的最短路线,或者是规模小的路径搜索,它的特点是”搜到就是最优解”, 而深搜用于找多个解或者是”步数已知(好比3步就必需达到前提)”的标题,它的空间效率高,然则找到的不必定是最优解,必需记实并完成全数搜索,故一般情况下,深搜需要很是高效的剪枝(优化)
像搜索最短路径这些的很显著是用广搜,因为广搜的特征就是一层一层往下搜的,保证当前搜到的都是最优解,当然,最短路径只是一方面的操作,像什么起码状态转换也是可以操作的。
深搜就是优先搜索一棵子树,然后是另一棵,它和广搜对比,有着内存需要相对较少的所长,八皇后标题就是典范楷模的操作,这类标题很显著是不能用广搜往解决的。或者像图论里面的找圈的算法,数的前序中序后序遍历等,都是深搜。
深搜的实现近似于栈,
广搜则是操作了队列,边进队,边出队。
优缺点:BFS:对于解决最短或最少问题特别有效,而且寻找深度小,但缺点是内存耗费量大(需要开大量的数组单元用来存储状态:hash优化)。
DFS:对于解决遍历和求所有问题有效,对于问题搜索深度小的时候处理速度迅速,然而在深度很大的情况下效率不高
不管是BFS还是DFS,它们虽然好用,但由于时间和空间的局限性,以至于它们只能解决数据量小的问题。
题型归类
坐标类型搜索 :这种类型的搜索题目通常来说简单的比较简单,复杂的通常在边界的处理和情况的讨论方面会比较复杂,分析这类问题,我们首先要抓住题目的意思,看具体是怎么建立坐标系(特别重要),然后仔细分析到搜索的每一个阶段是如何通过条件转移到下一个阶段的。确定每一次递归(对于DFS)的回溯和深入条件,对于BFS,要注意每一次入队的条件同时注意判重。要牢牢把握目标状态是一个什么状态,在什么时候结束搜索。还有,DFS过程的参数如何设定,是带参数还是不带参数,带的话各个参数一定要保证能完全的表示一个状态,不会出现一个状态对应多个参数,而这一点对于BFS来说就稍简单些,只需要多设置些变量就可以了。
数值类型搜索:这种类型的搜索就需要仔细分析分析了,一般来说采用DFS,而且它的终止条件一般都是很明显的,难就难在对于过程的把握,过程的把握类似于坐标类型的搜索(判重、深入、枚举),注意这种类型的搜索通常还要用到剪枝优化,对于那些明显不符合要求的特殊状态我们一定要在之前就去掉它,否则它会像滚雪球一样越滚越大,浪费我们的时间 。
这次解题感觉很多地方并没有说清,若大神有疑问或发现一些bug,还请指正!!!
实例1 迷宫问题(BFS)
定义一个二维数组:
int maze[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,
};
它表示一个迷宫,其中的1表示墙壁,0表示可以走的路,只能横着走或竖着走,不能斜着走,要求编程序找出从左上角到右下角的最短路线。Sample Input
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0Sample Output
(0, 0)
(1, 0)
(2, 0)
(2, 1)
(2, 2)
(2, 3)
(2, 4)
(3, 4)
(4, 4)
思路:BFS的第一步就是要识别图的节点跟边
1.识别出节点跟边
节点就是某种状态,边就是节点与节点间的某种规则。
对应于《迷宫问题》,可以这么认为,节点就是迷宫路上的每一个格子(非墙),走迷宫的时候,格子间的关系是什么呢?按照题目意思,我们只能横竖走,因此我们可以这样看,格子与它横竖方向上的格子是有连通关系的,只要这个格子跟另一个格子是连通的,那么两个格子节点间就有一条边。
如果说本题再修改成斜方向也可以走的话,那么就是格子跟周围8个格子都可以连通,于是一个节点就会有8条边(除了边界的节点)。
2.初始条件
起点Vs为(0,0),终点Vd为(4,4)
灰色节点集合Q={} (队列)
初始化所有节点为白色节点
3.步骤
图中标号即为搜索过程中的顺序,这个搜索顺序是按照上图的层次关系来的,例如节点(0,0)在第1层,节点(1,0)在第2层,节点(2,0)在第3层,节点(2,1)和节点(3,0)在第3层。
我们的搜索顺序就是第一层->第二层->第三层->第N层这样子。
我们假设终点在第N层,因此我们搜索到的路径长度肯定是N,而且这个N一定是所求最短的。
我们用简单的反证法来证明:假设终点在第N层上边出现过,例如第M层,M<N,那么我们在搜索的过程中,肯定是先搜索到第M层的,此时搜索到第M层的时候发现终点出现过了,那么最短路径应该是M,而不是N了。
所以根据广度优先搜索的话,搜索到终点时,该路径一定是最短的。(自己更改)
typedef struct Node{
int x , y ;//坐标
struct node * next ;
} Node;
/**
* 广度优先搜索
* @param Vs 起点
* @param Vd 终点
*/
bool BFS(Node& Vs, Node& Vd){
queue<Node> Q;
Node Vn, Vw;
int i;
//用于标记颜色当visit[i][j]==true时,说明节点访问过,也就是黑色
bool visit[MAXL][MAXL];
//四个方向
int dir[][2] = {
{0, 1}, {1, 0},
{0, -1}, {-1, 0}
};
//初始状态将起点放进队列Q
Q.push(Vs);
visit[Vs.x][Vs.y] = true;//设置节点已经访问过了!
while (!Q.empty()){//队列不为空,继续搜索!
//取出队列的头Vn
Vn = Q.front();
Q.pop();
for(i = 0; i < 4; ++i){
Vw = Node(Vn.x+dir[i][0], Vn.y+dir[i][1]);//计算相邻节点
if (Vw == Vd){//找到终点了!
//把路径记录,这里没给出解法
return true;//返回
}
if (isValid(Vw) && !visit[Vw.x][Vw.y]){
//Vw是一个合法的节点并且为白色节点
Q.push(Vw);//加入队列Q
visit[Vw.x][Vw.y] = true;//设置节点颜色
}
}
}
return false;//无解
}
实例2 不规则棋盘问题
在一个给定形状的棋盘(形状可能是不规则的)上面摆放棋子,棋子没有区别。要求摆放时任意的两个棋子不能放在棋盘中的同一行或者同一列,请编程求解对于给定形状和大小的棋盘,摆放k个棋子的所有可行的摆放方案C。
Input
输入含有多组测试数据。
每组数据的第一行是两个正整数,n k,用一个空格隔开,表示了将在一个n*n的矩阵内描述棋盘,以及摆放棋子的数目。 n <= 8 , k <= n
当为-1 -1时表示输入结束。
随后的n行描述了棋盘的形状:每行有n个字符,其中 # 表示棋盘区域, . 表示空白区域(数据保证不出现多余的空白行或者空白列)。
Output
对于每一组数据,给出一行输出,输出摆放的方案数目C (数据保证C<2^31)。
Sample Input2 1
#.
.#
4 4
...#
..#.
.#..
#...
-1 -1
1
2
3
4
5
6
7
8
9
Sample Output2
1
实例3 油田问题
问题:GeoSurvComp地质调查公司负责探测地下石油储藏。 GeoSurvComp现在在一块矩形区域探测石油,并把这个大区域分成了很多小块。他们通过专业设备,来分析每个小块中是否蕴藏石油。如果这些蕴藏石油的小方格相邻,那么他们被认为是同一油藏的一部分。在这块矩形区域,可能有很多油藏。你的任务是确定有多少不同的油藏。
input: 输入可能有多个矩形区域(即可能有多组测试)。每个矩形区域的起始行包含m和n,表示行和列的数量,1<=n,m<=100,如果m =0表示输入的结束,接下来是n行,每行m个字符。每个字符对应一个小方格,并且要么是’*’,代表没有油,要么是’@’,表示有油。
output: 对于每一个矩形区域,输出油藏的数量。两个小方格是相邻的,当且仅当他们水平或者垂直或者对角线相邻(即8个方向)。
* * * * @
* @ @ * @
* @ * * @
@ @ @ * @
@ @ * * @
方法一:用DFS解决
#include<cstdio>
#include<cstring>
const int maxn=105;
char pic[maxn][maxn];
int m,n,idx[maxn][maxn];
void dfs(int r,int c,int id){
if(r<0||r>=m||c<0||c>=n) return;
if(idx[r][c]>0||pic[r][c]!='@') return;
idx[r][c]=id;
for(int dr=-1;dr<=1;dr++)
for(int dc=-1;dc<=1;dc++)
if(dr!=0||dc!=0)dfs(r+dr,c+dc,id);
}
int main(){
while(scanf("%d%d",&m,&n)==2&&m&&n){
for(int i=0;i<m;i++) scanf("%s",pic[i]);
memset(idx,0,sizeof(idx));
int cnt=0;
for(int i=0;i<m;i++)
for(int j=0;j<n;j++)
if(idx[i][j]==0&&pic[i][j]=='@') dfs(i,j,++cnt);
printf("%dn",cnt);
}
return 0;
}
方法二:BFS方法:
#include<bits/stdc++.h>
using namespace std;
const int maxn=105;
int m,n;
int vis[maxn][maxn]; //vis表示该点是否搜索过,且是否有油田,只会赋值一次
char s[maxn][maxn]; //输入参数
int cnt=0; //油田数
int dir[8][2]={{0,1},{1,-1},{-1,-1},{-1,0},{0,-1},{-1,1},{1,0},{1,1}};
typedef struct Node{
int x,y;
}node;
//广度遍历:实际上只有该点为油田时才进入,将周围所有油田标记上
//1.搜索当前点(x,y)周围8个点(不包括自己)是否有油田,有就用vis标记上
//2.想水波一样,扩散周围8个点的周围8个点,是否有油田,有则标记上
//3.得到的结果就是:该点周围上所有油田vis标记上
void bfs(int x,int y){
node p,t;
queue<node> q;
p.x=x;
p.y=y;
q.push(p);
while(!q.empty()){
p=q.front();
q.pop();
for(int i=0;i<8;i++){
t.x=p.x+dir[i][0]; //x变化
t.y=p.y+dir[i][1]; //y变化
if(t.x<0||t.x>=n||t.x<0||t.y>=m){ //不超过边界
continue;
}
if(!vis[t.x][t.y]&&s[t.x][t.y]=='@'){ //没有搜索过且有油井,去标记,动态规划
vis[t.x][t.y]=1; //标记上,动态规划
q.push(t); //有油田的相邻node入队,8个方向,类似水波扩散
}
}
}
}
int main()
{
while(scanf("%d %d",&n,&m)&&(n+m)){
memset(vis,0,sizeof vis);
cnt=0;
for(int i=0;i<n;i++){
scanf("%s",s[i]);
}
//此处的for循环也很关键,依次遍历所有点,
//1.如果没被标记过且有油田,标记上,进入bfs找到所有周围油田并标记,油田数加1
//2.没被标记,但没有油田,跳过
//3.如果有标记,表示该处为油田,但是属于其他油田块,不用进入寻找,油田数不增加
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(!vis[i][j]&&s[i][j]=='@'){
vis[i][j]=1;
cnt++;
bfs(i,j);
}
}
}
printf("%dn",cnt);
}
return 0;
实例4 岛屿的数量(简化型油田问题)
https://leetcode-cn.com/problems/number-of-islands/
给定一个由
'1'
(陆地)和'0'
(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。示例 1:
输入: 11110 11010 11000 00000 输出: 1
示例 2:
输入: 11000 11000 00100 00011 输出: 3
分享x,y的方法:
x,y的一些表示法:
1.乘法加法
queue<int> q{{i * n + j}};
while (!q.empty()) {
int t = q.front(); q.pop();
for (int k = 0; k < 4; ++k) {
int x = t / n + dirX[k], y = t % n + dirY[k];
q.push(x * n + y);
2.新建结构体
typedef struct Node{
int x,y;
}node;
3.用stl pair
std::pair <int,int> node(x,y); // default constructor
int x =node.first + dirX[k];
int y = node.second + dirY[k];
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
//忘记判断空了!!!!鲁棒性
if(grid.empty() || grid[0].empty())
return 0;
int row = grid.size();
int col = grid[0].size();
int num = 0;
//queue<pair> que; pari 还是需要定义
queue<pair<int,int>> que;
vector<vector<int>> dir {{0,1},{0,-1},{1,0},{-1,0}};//运用初始化列表来初始化的
//动态规划
//vector<vector<int>> vis(row,vector<int>(col,0));自动初始化为0
vector<vector<int>> vis(row,vector<int>(col));
//第一步,遍历所有点,如果没被标记过且有岛屿,标记该位,并用BST检测4个方向的,如果也有标记上,结束循环后,油田数加1
//第二步,标记上的跳过,表示已有岛屿块
//第三步没有油田的跳过
for(int i = 0; i < row;i++)
{
for(int j = 0;j<col;j++)
{
if(vis[i][j] != 1 &&grid[i][j] == '1')//""是string,‘’是char
{
vis[i][j] = 1;//标记上
//接下来是BST
pair<int,int> node(i,j);
pair<int,int> temp;
que.push(node);
while(!que.empty()){
node = que.front();
que.pop();
//接下来扩散
for(int k = 0;k<4;k++)
{
temp.first = node.first + dir[k][0];
temp.second = node.second + dir[k][1];
//边界条件先判断!忘记了!!!
//if(temp.first<0 || temp.first>row||temp.second<0||temp.second>col)//bug,等于边界值时越界了!!!!
if(temp.first<0 || temp.first>=row||temp.second<0||temp.second>=col)
continue;
//判断周围的有没有被标记,如果没有被标记:判断是否有油田,有就标记上,并放入队列;否则跳过
if(vis[temp.first][temp.second] !=1 &&grid[temp.first][temp.second] == '1')
{
vis[temp.first][temp.second] = 1;//标记上
que.push(temp);//放入队列
}
}
//BST结束
}
//岛屿加1
num++;
}
}
}
return num;
}
};
实例 5 朋友圈(本质是 Number of Connected Components in an Undirected Grap)
班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。
给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
示例 1:
输入: [[1,1,0], [1,1,0], [0,0,1]] 输出: 2 说明:已知学生0和学生1互为朋友,他们在一个朋友圈。 第2个学生自己在一个朋友圈。所以返回2。
DFS和BFS可解,不过有更优的
//并查集的经典题目
class Solution {
public:
void bfs(int i,vector<vector<int> >& M,vector<bool>& vec)
{
queue<int> que;
que.push(i);
while(!que.empty())
{
int i = que.front();
que.pop();
for(int j = 0;j<M[0].size();j++)
{
if(!vec[j]&&M[i][j] == 1)
{
que.push(j);
vec[j] = true;
}
}
}
}
void dfs(int i, vector<vector<int> >& M,vector<bool>& vec)
{
for(int j = 0;j<M[0].size();j++)
{
if(!vec[j] && M[i][j] == 1)
{
vec[j] = true;
dfs(j,M,vec);
}
}
}
//此题与油田问题的区别在于,x和y是同一组人
//思想关键在于,遍历第一层,再进入该层遍历他所关联的所有层!!!
//step1,遍历每个人的朋友圈,如果该人被遍历过,则跳过(遍历x)
//step2,进入该人的朋友圈,找到所有他的朋友并标记(遍历y)
int findCircleNum(vector<vector<int>>& M) {
//鲁棒性,如果为null时返回0
if(M.empty() || M[0].empty())
return 0;
int row = M.size();
int col = M[0].size();
int num = 0;
//新建一个vector来存是否遍历过
vector<bool> vis (row, false);
for(int i = 0;i < row;i++)
{
if(vis[i])
continue;
num++;
vis[i] = true;
bfs(i,M,vis);
}
return num;
}
};
分别是:
no.1 去掉第一层循环的vis[i] = true
no.2 DFS
no.3 BFS
下面这种解法叫联合查找 Union Find(也叫并查集),也是一种很经典的解题思路,在之前的两道道题 Graph Valid Tree 和 Number of Connected Components in an Undirected Graph 中也有过应用,核心思想是初始时给每一个对象都赋上不同的标签,然后对于属于同一类的对象,在 root 中查找其标签,如果不同,那么将其中一个对象的标签赋值给另一个对象,注意 root 数组中的数字跟数字的坐标是有很大关系的,root 存的是属于同一组的另一个对象的坐标,这样通过 getRoot 函数可以使同一个组的对象返回相同的值,
可参考前面发布的文章:【算法】并查集详解
几秒前 | 通过 | 20 ms | 13.3 MB | Cpp |
参见代码如下:
class DisjointSet {
public:
DisjointSet(int n) {
for (int i = 0; i < n; i++) {
_id.push_back(i);
_size.push_back(1);
}
_count = n;
}
//查询元素 p 属于哪个集合时返回 id[i]
int find(int p) {
while (p != _id[p]) {
_id[p] = _id[_id[p]];
p = _id[p];
}
return p;
}
//合并时,若两个元素属于同一个集合,则直接返回
void union_(int p, int q) {
int i = find(p);
int j = find(q);
if (i == j) return;
if (_size[i] < _size[j]) {
_id[i] = j;
_size[j] += _size[i];
}
else {
_id[j] = i;
_size[i] += _size[j];
}
_count--;
}
int count(){
return _count;
}
private:
//设置表示集合数组id[i],初始时每个元素构成一个单元素的集合
//编号为 i 的元素属于集合 i
vector<int>_id;
vector<int>_size;
int _count;
};
class Solution {
public:
int findCircleNum(vector<vector<int>>& M) {
DisjointSet disjoint_set(M.size());
for(int i=0; i<M.size(); i++){
for(int j=i+1; j<M.size(); j++){
if(M[i][j]){
disjoint_set.union_(i,j);
}
}
}
return disjoint_set.count();
}
};
最后
以上就是悲凉马里奥为你收集整理的【算法】广度优先搜索(BFS)和深度优先搜索(DFS)的全部内容,希望文章能够帮你解决【算法】广度优先搜索(BFS)和深度优先搜索(DFS)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复