回溯法求解N皇后问题及其时间复杂度分析

  • 一、回溯法简介
    • 1. 什么是回溯法?
    • 2. 回溯法的时间复杂度分析
      • 蒙特卡罗方法
      • 蒙特卡罗方法在回溯法求解时间复杂度中的应用
  • 二、回溯法求解N皇后问题
    • 1. 回溯法求解N皇后问题的过程
    • 2. 回溯法求解N皇后问题的时间复杂度
      • 2.1 求解时的效率分析
        • 回溯法进行效率分析的代码
      • 2.2 时间复杂度分析

一、回溯法简介

1. 什么是回溯法?

  相信"迷宫"是许多人儿时的回忆,大家小时候一定都玩过迷宫游戏。我们从不用别人教导,都知道走迷宫的策略是:

  1. 当遇到一个岔路口,会有以下两种情况:

    1. 存在没走过的路。此时可以任意选一条没走过的路深入,只要记住我们所走过的路径即可。倘若下次再来到这个路口,便不再沿着走过的路径继续深入,而是沿着没走过的路径深入下去;
    2. 所有路都已经走过。如果所有岔路口都已经遍历,则回退至上一个最近的岔路口。
  2. 当遇到死胡同,便回退到刚才距离最近的岔路口。
  3. 不断前进并重复该过程,直到找到终点或回退到起点位置。

  其实,这就是回溯法——一个基于深度优先搜索和约束函数的问题求解方法。

2. 回溯法的时间复杂度分析

  众所周知,回溯法的时间复杂度主要取决于如下几点:

  1. 生成每个节点x[k](x[k]表示第k层的当前节点)所用的时间。
  2. 满足约束函数(用约束函数剪去得不到可行解的子树)的x[k]值的个数(第k层有可能生成几个这样的节点)。
  3. 计算约束函数Constraint对节点进行判断的时间。
  4. 计算上界(限界)函数Bound的时间。
  5. 满足约束函数和限界函数的所有节点x[k]的个数。

  当问题和算法一旦确定下来,就只剩下第5点的实际产生的节点数目是根据问题的具体实例而动态生成,不可预知的。
  对于一个具体的问题实例,很难预测出回溯法的行为(先选哪一条岔路,这条岔路是否能很快得到最优解),这时应该怎么办呢?

蒙特卡罗方法

  此时,可以使用蒙特卡罗方法估计回溯法产生的节点数目。
  所谓蒙特卡罗方法,其实就是一种以概率统计理论为指导的,使用随机数(或伪随机数)来解决计算问题的方法,通过对大量随机试验求平均,以求得相近的值。
  蒙特卡罗方法的基本思想是:当求解的问题是某种随机事件出现的概率或数学期望时,通过大量“实验”的方法,以频率估计概率或者得到某些数字特征,将其近似看作问题的解。
  1777年,法国数学家布丰(Buffon)提出使用投针试验的方法求解圆周率π,这被认为是蒙特卡罗方法的起源(扯远了)。

蒙特卡罗方法在回溯法求解时间复杂度中的应用

  我们需要估计的是回溯法实际产生的节点数目,以此计算回溯法的时间复杂度。
  其主要思想是,在解空间树(状态空间树)上动态、随机的产生一条路径,然后沿此路径来估算解空间树中所有满足约束条件的节点总数(这里计算的是最差时间复杂度,假设要走遍所有满足约束条件的节点)。
  多次进行上述实验,对结果求平均值,即可得到回溯法中实际生成的节点数目的估计值。

二、回溯法求解N皇后问题

1. 回溯法求解N皇后问题的过程

  问题背景:8皇后问题是由国际西洋棋棋手马克斯·贝瑟尔于1848年提出的问题,是回溯算法的典型案例。N皇后问题由此推广而来。
  问题描述:在N×N格的国际象棋上摆放N个皇后,使其不能互相攻击,即不能处于同一列或同一行,也不能处在同一斜线上,请问有多少种摆法?

  N皇后问题也是回溯算法的典型案例,这里,我们可以使用递归和循环迭代两种不同的回溯方式编写代码:

//
//  main.cpp
//  BackTrack Solution of N-Queens Problem.
//
//  Created by Kang on 2020/7/2 at NJUPT.
//  Copyright © 2020 Kang. All rights reserved.
//#include <iostream>
#include <cmath>
#include <ctime>
using namespace std;const int maxSize = 10;
int x[maxSize];/**Judge if the Queen can be placed at (k, x[k]), if OK, return true.*/
bool CanPlace(int k){for(int i = 0; i < k; ++i) {if(x[i] == x[k] || abs(i-k) == abs(x[i]-x[k]))return false;}return true;
}void Print(int N){for (int i = 0; i < N; ++i) {for (int j = 0; j < N; ++j) {if(x[i] != j) {printf(" ");} else {printf("▇\n");break;}}}printf("---------\n");
}/**k the current deepth( from 0 to n-1) in the Solution Space Tree*/
int Recursive_BackTrack(int k, int N){int solutionCount = 0;if(k == N){Print(N);return 1;}for (int i = 0; i < N; ++i) {x[k] = i;if(CanPlace(k)){solutionCount += Recursive_BackTrack(k+1, N);}}return solutionCount;
}int Iterated_BackTrack(int N){int solutionCount = 0, k = 0;       // 问题解的个数、当前层数for (int i = 0; i < maxSize; ++i) { // 将所有层中,所选子树(列)的初始值置为-1x[i] = -1;}while (k > -1) {     // 如果已经退回第0行前面,则结束遍历if(k == N){      // 如果已经超过最后一行,则打印路径并返回上一层Print(N);++solutionCount;--k;continue;}if(++x[k] < N){  // 如果还有未访问的子节点,则选择这棵树的下一个子树if(CanPlace(k)){++k;     // 如果当前位置可以摆放,则k自增,进入下一行的循环。} else {// 如果当前位置不能摆放,则k不必动,直接进入下次循环。}} else {         // 子树已经全部搜索,返回上一层x[k] = -1;   // 注意,返回时要将子树进行复位。--k;}}return solutionCount;
}int main(int argc, const char * argv[]) {int N;int count;printf("请输入皇后个数:");scanf("%d", &N);// count = Recursive_BackTrack(0, N);count = Iterated_BackTrack(N);cout<<"共有"<<count<<"种不同的解法。"<<endl;return 0;
}

  笔者在写博客的过程中发现了可以优化的地方,于是赶快去修改了一下。不知道各位读者有没有发现,各位可以仔细对比以下两份不同的代码,答案将在这份代码后面给出。

//
//  optimisedMain.cpp
//  Optimised BackTrack Solution of N-Queens Problem.
//
//  Created by Kang on 2020/7/2 at NJUPT.
//  Copyright © 2020 Kang. All rights reserved.
//#include <iostream>
#include <cmath>
#include <ctime>
using namespace std;const int maxSize = 10;      // 最大能输入的棋盘规模(皇后数量)
int x[maxSize];              // x[k]表示:第k行选择第x[k]列
int flag[maxSize][maxSize];  // flag[k][i]:第k行的第i列如果可选则为1,不可选则为0/**Calculate the kth row's all unalternative column i, and turn flag[i] from 1 to 0.*/
void calcFlag(int k, int N){for (int i = 0; i < N; ++i) {flag[k][i] = 1;}for(int i = 0; i < k; ++i) { // from 0th to (k-1)th rowflag[k][x[i]] = 0;  // 正对下方的列直接Passif(x[i] + abs(i-k) < N)   // 检查右下方:如果当前列号x[i] + 行差abs(i-k) < Nflag[k][x[i] + abs(i-k)] = 0;if (x[i] - abs(i-k) > -1) // 检查左下方:如果当前列号x[i] + 行差abs(i-k) < Nflag[k][x[i] - abs(i-k)] = 0;}
}void Print(int N){for (int i = 0; i < N; ++i) {for (int j = 0; j < N; ++j) {if(x[i] != j) {printf(" ");} else {printf("▇\n");break;}}}printf("---------\n");
}/**k the current deepth( from 0 to n-1) in the Solution Space TreeN the scale of the problemreturn number of solution*/
int Recursive_BackTrack(int k, int N){int solutionCount = 0;if(k == N){Print(N);return 1;}calcFlag(k, N);for (int i = 0; i < N; ++i) {if(flag[k][i]) {x[k] = i;solutionCount += Recursive_BackTrack(k+1, N);}}return solutionCount;
}/**N the scale of the problemreturn number of solution
*/
int Iterated_BackTrack(int N){int solutionCount = 0, k = 0;       // 问题解的个数、当前层数for (int i = 0; i < maxSize; ++i) { // 将所有层中,所选子树(列)的初始值置为-1x[i] = -1;}calcFlag(0, N);while (k > -1) {     // 如果已经退回第0行前面,则结束遍历if(k == N){      // 如果已经超过最后一行,则打印路径并返回上一层Print(N);++solutionCount;--k;continue;}if(++x[k] < N){  // 如果还有未访问的子节点,则选择这棵树的下一个子树if (flag[k][x[k]] == 1) {++k;     // 如果当前列x[k]可以摆放,则k自增,进入下一行的循环。calcFlag(k, N);} else {// 如果当前位置不能摆放,则k不必动,直接进入下次循环。}} else {         // 子树已经全部搜索,返回上一层x[k--] = -1;   // 注意,返回时要将子树进行复位。}}return solutionCount;
}int main(int argc, const char * argv[]) {int N;int count;printf("请输入皇后个数:");scanf("%d", &N);//count = Recursive_BackTrack(0, N);count = Iterated_BackTrack(N);cout<<"共有"<<count<<"种不同的解法。"<<endl;return 0;
}

  Answer:这其实是一种添加辅助数组flag,从而牺牲空间复杂度O(n2),减小时间复杂度n倍的策略。这样,每一个位置判断是否可以摆放,只需要O(1)的时间复杂度,而非前者O(n)的时间复杂度(以下计算时间复杂度时,均采用的是后者的求解方式)。

2. 回溯法求解N皇后问题的时间复杂度

  根据前面所讲到的蒙特卡罗方法,此时可以将其用于求解N皇后的时间复杂度。对于n元组长度的问题实例,其状态空间树中的节点数目常见的有n!(排列树)、an(子集树)和nn(可复选的排列树),则回溯算法的最坏时间复杂度分别为:

  • O(p(n) * n!)
  • O(p(n) * 2n)
  • O(p(n) * nn)

其中,p(n)表示生成一个节点所需的时间复杂度。

  虽然回溯法的最坏时间复杂度非常的大,但是在大多数情况下,通常不需要生成状态空间树中的全部节点,而是通过约束函数限界函数进行剪枝,从而最终只生成状态空间树中很少量的一部分节点,这里,我们就使用N皇后问题来举例,利用蒙特卡罗方法估算一下算法运行中实际生成的节点占状态空间树中总节点的比例

2.1 求解时的效率分析

  在求解时间复杂度之前,先分析一下回溯法的效率。回溯法的运行时间通常取决于状态空间树上实际生成的那部分节点的数目。
  在n皇后问题中,状态空间树(排列树)中总的节点个数为(如图为4皇后问题的完整的解空间树,共有65个节点):

  • 1+n+(n-1)+(n-2)+ ······ +[n-(n-2)]+[n-(n-1)]

而实际生成的节点数的个数怎么计算呢?
不妨对4-皇后问题先演算一下~

计算得到4皇后问题实际生成的节点数为16个,只生成了 (16/65)*100% = 24.6% 的节点。

  但实际上,我们在通常的问题中无法预知要从哪一条岔路开始搜索解(这里很简单我们从0位置开始向后),而且有的时候我们要求出所有符合条件的解,这就要求我们将状态空间树中所有符合剪枝函数的点全部计算进来

  如果要计算实际生成的节点数,那么问题来了:

  1. 当我遇到岔路的时候,会随机走一条路,请问我走了哪条路?
  2. 我也是走一步之后,才知道下一步的有几个岔路或者是否是死胡同。

  这时,蒙特卡罗方法就派上用场了。既然我们不清楚要生成多少节点,那我们不妨进行若干次的试验,然后求平均:每次随机生成一条路径,根据这条路径的情况来草率的推断所有其他路径和这条路一样,从而草率的估计实际生成的节点数量。
  很显然,对于8皇后来说,第一行选第0个位置和选第1个位置,从而造成的第2行所能选择的范围都不一样,所以说是“草率的”估计。
  但是这么草率的估计是否严谨呢?单单试验一次,恐怕并不严谨。但是,如果我们进行大量的试验,然后对这一整批实验数据求平均,从概率论上来说,试验次数越多,结果越准确(例如,投硬币次数越多,正面朝上的频率越稳定于实际的概率0.5)。

  那我们不妨就用实际的栗子,来对8皇后问题进行一个“草率的”估计。这里简单的用(a)图进行举例:

  • 根节点1个(假定为状态空间树的第0行);
  • 第一行可以有8种选择(0, 1, 2, 3, 4, 5, 6, 7),所以状态空间树的第1行有8个节点。此时,假设我们随机选择了当前可行的1号位置;
  • 在第一行的基础上,我们有5种选择(3, 4, 5, 6, 7),这时可以“草率的”假设:对于每一种第一行的可行的选择(8种),我们在第二行都有5种不同的选择,所以状态空间树的第二行我们可以生成8*5个节点。此时,假设我们随机选择了当前可行的3号位置;
  • 在前两行的基础上,我们有4种选择(0, 5, 6, 7),这时可以“草率的”假设:对于每一种前两行的可行排列(8*5=40),我们在第三行都有4种不同的选择,所以状态空间树的第三行有8*5*4个节点。此时,假设我们随机选择了当前可行的0号位置;
  • 在前三行的基础上,我们有3种选择(2, 6, 7),这时可以“草率的”假设:对于每一种前三行的可行排列(8*5*4=160),我们在第四行都有3种不同的选择,所以状态空间树的第4行有8*5*4*3=480个节点。此时,假设我们随机选择了当前可行的2号位置;
  • 在前四行的基础上,我们有2种选择(4, 7),这时可以“草率的”假设:对于每一种前四行的可行排列(8*5*4*3=480),我们在第四行都有3种不同的选择,所以状态空间树的第5行有480*2=480个节点。此时,假设我们随机选择了当前可行的4号位置;
  • 在前五行的基础上,我们已经没有任何选择了,所以我们“草率的”假设,其他所有的路径都和这条路径一样,都无法继续选择了,所以状态空间树从第6行开始不再实际产生节点。

  因此,我们“草率的”估计,此次随机试验,解空间树中实际产生的节点数量有1+8+8*5+8*5*4+8*5*4*3+8*5*4*3*2 = 1649个,占总节点数8!/8!+8!/7!+8!/6!+······+8!/2!+8!/1!+8!/0!=109601的1.55%
  此时,我们是否可以说回溯法一定就实际生成1.55%的节点数呢?答案是否定的。我们需要尽可能的做多次这样的随机试验(例如b、c、d、e等图),然后求出平均值,才是较为准确的实际生成的节点占比。(其他几个图随机的路径也是一样的道理,笔者此处不再赘述。)

回溯法进行效率分析的代码

  此处贴上本人手写的代码一份,可以用于测试实际生成的节点与状态空间树中总结点的占比,有疑问欢迎私信。

//
//  rate.cpp
//  Efficiency Analysis of BackTrack for N-Queens Problem.
//
//  Created by Kang on 2020/7/2 at NJUPT.
//  Copyright © 2020 Kang. All rights reserved.
//#include <iostream>
#include <cmath>
#include <ctime>
using namespace std;const int testCount = 100; // 测试次数
const int MAXN = 20;       // 能测试的皇后最大值
int countPerRow[MAXN];     // 存放每行能选列的个数,便于最终计算结果
int bucket[MAXN];          // 符合约束能选则为1,不符合约束不能选则为0
int x[MAXN];               // 存放生成的随机序列
int N;                     // 本次是N皇后求解// 初始化临时标记是否是可选的列
// 每行的判定开始前,都需要调用,假定该行的每一列都不能选
void initBucket(){for(int i = 0; i < MAXN; ++i) {bucket[i] = 0;}
}// 初始化每行能选的列数,每次试验开始前调用,进行置零
void initCountPerRow(){    for(int i = 0; i < MAXN; ++i) {countPerRow[i] = 0;}
}bool CanPlace(int k) {   // 判断第k行第x[k]列能不能放for (int i = 0; i < k; ++i) {if (x[i] == x[k] || abs(i - k) == abs(x[i] - x[k]))   // 是否下面和斜对角有重合return false;}return true;
}// 返回能够抵达的行数
int RecurBacktrackM(int t) {initBucket();if (t == N) {            // 如果已经进入第N+1行,则找到解return t;} else {int colCount = 0;    // 能摆放几列int randInt, r = 0;  // 随机第几个能摆放的位置int index = 0;       // 摆放的位置的下标for (int i = 0; i < N; ++i) {      // 挨个试探x[t] = i;if (CanPlace(t)) {             // 判断是否能放bucket[i] = 1;colCount++;countPerRow[t]++;          // 第t行实际产生的可行解数量++}}if(colCount > 0) {                 // 如果这一行有可行解randInt = rand()%colCount + 1;while(r < randInt){if(bucket[index] == 1){r++;}index++;}index--;x[t] = index;                 // 得到随机选择的可行解的列号return RecurBacktrackM(t + 1);// 进入下一层} else {                          // 不能放说明是死胡同,直接返回return t;}}
}int main() {srand((unsigned)time(NULL));cout<<"请输入棋盘的大小:";cin>>N;double allRate = 0;for(int number = 0; number < testCount; number++){int row, allNodeCount = 1;int geneNodeCount = 1;   // 蒙特卡洛估计的,实际产生的节点的数量initCountPerRow();row = RecurBacktrackM(0);cout<<"本次实验的随机序列是:";for(int i = 0; i < row; i++){cout<<x[i]<<" ";}cout<<endl;int tmp;cout<<"本次实验的每行节点数分别是:";for(int i = 0; i < row; i++){cout<<countPerRow[i]<<" ";}cout<<endl;for(int i = 0; i < row; i++) {tmp = 1;for(int j = 0; j <= i; j++) {tmp *= countPerRow[j];}geneNodeCount += tmp;}cout<<"本次估计实际产生的节点数是:"<<geneNodeCount<<endl;for(int i = 0; i < N; i++) {tmp = 1;for(int j = 0; j <= i; j++) {tmp *= N-j;}allNodeCount += tmp;}cout<<"    状态空间树中的总节点数是:"<<allNodeCount<<endl;cout<<"    实际生成节点数目与总节点数比值为:"<<((geneNodeCount * 1.0)/allNodeCount)*100<<"%"<<endl;allRate += (geneNodeCount * 1.0)/allNodeCount;cout<<"-----------------------------------------------"<<endl;}cout<<"此"<<testCount<<"次实验,平均实际生成节点数目与总节点数比值为:"<<(allRate/testCount)*100<<"%"<<endl;return 0;
}

运行1000次8皇后问题的试验,效果截图如下:

2.2 时间复杂度分析

  在N皇后问题中,因为每个节点(最下一层除外)都要遍历包括当前层在内的上面每一层的选择,才能得到当前可行的全部直接子孙节点,且绝大多数节点都集中在层数靠下的位置,故可以估计出平均每个节点的生成时间复杂度p(n)都是n。
  例如上例中,根节点生成时要检验它的8个真子孙节点是否可行;对于第一层的每一个节点,也都需要判断其全部的8个真子孙节点是否可行;第二层、第三层······直到倒数第二层,也都需要判断其全部的8个真子孙节点是否可行;最后一层虽比较特殊,无需判断,但最后一层(已经抵达可行解)的节点在N皇后问题(排列树)中和上一层的节点数相同,故与其他节点一视同仁也不影响时间复杂度的计算。
  所以N皇后的时间复杂度为O(n×实际生成的节点数)。

  凑巧赶上小破邮的算法课程,于是写下人生中第一篇正式的CSDN博客,竟然啰嗦了1w多字,希望大家多多点赞支持,有疑问的地方欢迎私信小博同学进行交流奥~

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2x08l67hcieck

回溯法求解N皇后问题及其时间复杂度分析相关推荐

  1. 回溯法求解N皇后问题(Java实现)

    回溯法:也称为试探法,它并不考虑问题规模的大小,而是从问题的最明显的最小规模开始逐步求解出可能的答案,并以此慢慢地扩大问题规模,迭代地逼近最终问题的解.这种迭代类似于穷举并且是试探性的,因为当目前的可 ...

  2. 回溯法求解N皇后问题

    问题描述 给定一个n*n的棋盘,棋盘中有一些位置不能放皇后.现在要向棋盘中放入n个黑皇后和n个白皇后,使任意的两个黑皇后都不在同一行.同一列或同一条对角线上,任意的两个白皇后都不在同一行.同一列或同一 ...

  3. 回溯法——求解N皇后问题

    问题描写叙述 八皇后问题是十九世纪著名数学家高斯于1850年提出的.问题是:在8*8的棋盘上摆放8个皇后.使其不能互相攻击,即随意的两个皇后不能处在允许行.同一列,或允许斜线上. 能够把八皇后问题拓展 ...

  4. n皇后问题python_Python:回溯法解N皇后问题

    这个算法采用的是一维数组,皇后个数即数组长度,数组值即对应行皇后所在的列. 按照每行至上而下,每一行从第一列起尝试放置皇后,每次仅需判断对于已经放置的皇后是否产生冲突.如果某个位置可以放置,则放置皇后 ...

  5. n皇后问题-回溯法求解

    n皇后问题-回溯法求解 1.算法描述 在n×n格的国际象棋上摆放n个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行.同一列或同一斜线上,问有多少种摆法. n皇后是由八皇后问题演变而来的.该问题 ...

  6. 【算法分析】实验 4. 回溯法求解0-1背包等问题

    目录 实验内容 实验目的 实验结果 步骤1:描述与分析 步骤2:策略以及数据结构 步骤3 步骤4 步骤5 步骤6 实验总结 实验内容 本实验要求基于算法设计与分析的一般过程(即待求解问题的描述.算法设 ...

  7. 【回溯法】八皇后问题

    问题描述 在国际象棋棋盘(8×8)(8\times8)(8×8)上放置八个皇后,要求每两个皇后之间不能直接吃掉对方. 皇后可以攻击处于同一行.同一列和同一对角线上的棋子. 思路分析 八皇后问题可以使用 ...

  8. java背包算法回溯法_【算法分析】实验 4. 回溯法求解0-1背包等问题

    [TOC] 实验内容 本实验要求基于算法设计与分析的一般过程(即待求解问题的描述.算法设计.算法描述.算法正确性证明.算法分析.算法实现与测试),通过回溯法的在实际问题求解实践中,加深理解其基本原理和 ...

  9. 算法设计与分析 实验三 回溯法求解地图填色问题

    回溯法求解地图填色问题 一.实验目的与要求 1.实验基本要求: 2.实验亮点: 二.实验内容与方法 三.实验步骤与过程 1.未优化的回溯: (1)算法描述: (2)编程实现 (3)运行并测试: 2.对 ...

最新文章

  1. 使用游戏测试干式EEG传感器的有效性
  2. Spring Boot2.0 JPA 实现分页(简单查询分页、复杂查询分页)
  3. 副主任护师主要英语和计算机吗,有没有晋升副主任护师的
  4. matlab中求积函数,MATLAB软件及高斯勒让德求积公式MATLAB软件及高斯勒让德求积公式.doc...
  5. 微信小程序页面跳转、逻辑层模块化
  6. Tyvj 1176 火焰巨魔的惆怅
  7. 二胎上位之路:html5报表和原生报表的笑尿撕逼
  8. 第13讲++连接查询和子查询
  9. [渝粤教育] 西南科技大学 农业技术经济学 在线考试复习资料
  10. 【pymongodb】去除重复记录
  11. linetv_LINE TV台湾版
  12. adb 静默安装_apk 静默安装
  13. kerberos认证下kafka报错Bootstrap broker host:ip (id: -1 rack: null) disconnected
  14. 五大特色展区抢占C位,2019深圳国际全触与显示展引燃触控显示产业未来
  15. 不限专业和工作年限就能报考的证书有哪些?
  16. XSS 防御方法总结
  17. 牛客练习赛50 F tokitsukaze and Another Protoss and Zerg(分治NTT)
  18. python新年贺卡_写个新年贺卡生成器,各位小伙伴们新年快乐呀~
  19. 什么是大小端?怎样判断?
  20. 百度翻译爬虫(案例练习:POST 请求)

热门文章

  1. 新家未来验房电子报告版本第三代更新,验房师专业电子报告
  2. 编写一个静态方法 lg(), 接受一个整型参数 N,返回不大于 log2N 的最大整数。
  3. form表单的action属性设置相对路径
  4. isNaN、Number.isNaN、isFinite、Number.isFinite
  5. 大话2口袋版不显示服务器,大话西游2新口袋版操作指引及常见问题说明
  6. 判断字符串中哪个字符出现的次数最多?
  7. [大话设计模式C++版] 第17章 在NBA我需要翻译 —— 适配器模式
  8. 世界海洋日 | TcaplusDB号召你一同保护海洋生物多样性
  9. Linux是什么操作系统?Linux和Windows的区别
  10. 如何将过长的链接放到excel里面