问题描述:

旅行商问题(Traveling Salesman Problem,TSP)是旅行商要到若干个城市旅行,各城市之间的费用是已知的,为了节省费用,旅行商决定从所在城市出发,到每个城市旅行一次后返回初始城市,问他应选择什么样的路线才能使所走的总费用最短?此问题可描述如下:设G=(V,E)是一个具有边成本cij的有向图,cij的定义如下,对于所有的i和j,cij>0,若<i,j>不属于E,则cij=∞。令|V|=n,并假设n>1。 G的一条周游路线是包含V中每个结点的一个有向环,周游路线的成本是此路线上所有边的成本和。

问题分析:

旅行商问题要从图G的所有周游路线中求取最小成本的周游路线,而从初始点出发的周游路线一共有(n-1)!条,即等于除初始结点外的n-1个结点的排列数,因此旅行商问题是一个排列问题。排列问题比子集合的选择问题通常要难于求解得多,这是因为n个物体有n!种排列,只有 个子集合(n!>O( ))。通过枚举(n-1)!条周游路线,从中找出一条具有最小成本的周游路线的算法,其计算时间显然为O(n!)。

枚举法思想:程序中采用深度优先策略。(采用隐式和显式两种形式)

枚举算法的特点是算法简单,但运算量大,当问题的规模变大,循环的阶数越大,执行的速度越慢。如果枚举范围太大(一般以不超过两百万次为限),在时间上就难以承受。在解决旅行商问题时,以顶点1为起点和终点,然后求{2…N}的一个全排列,使路程1→{2…N}的一个全排列→1上所有边的权(代价)之和最小。所有可能解由(2,3,4,…,N)的不同排列决定。

核心代码(完整源代码见源代码)

为便于讨论,介绍一些关于解空间树结构的术语。在下面分析回溯法和分支限界法时都直接或间接用到解空间树。在解空间树中的每一个结点确定所求问题的一个问题状态(problem state)。由根结点到其它结点的所有路径则确定了这个问题的状态空间(state space)。解状态(solution states)表示一些问题状态S,对于这些问题状态,由根到S的那条路径确定了这解空间中的一个元组。答案状态(answer states)表示一些解状态S,对于这些解状态而言,由根到S的这条路径确定了这问题的一个解(即,它满足隐式约束条件)。解空间的树结构称为状态空间树(state space tree)。

对于旅行商问题,一旦设想出一种状态空间树,那么就可以先系统地生成问题状态,接着确定这些问题状态中的哪些状态是解状态,最后确定哪些解状态是答案状态,从而将问题解出。为了生成问题状态,采用两种根本不同的方法。如果已生成一个结点而它的所有儿子结点还没有全部生成,则这个结点叫做活结点。当前正在生成其儿子结点的活结点叫E-结点。不再进一步扩展或者其儿子结点已全部生成的生成结点就是死结点。在生成问题状态的两种方法中,都要用一张活结点表。在第一种方法中,当前的E-结点R一旦生成一个新的儿子C,这个儿子结点就变成一个新的E-结点,当完全检测了子树C之后,R结点就再次成为E-结点。这相当与问题状态的深度优先生成。在第二种状态生成方法中,一个E-结点一直保持到死结点为止。这两种方法中,将用限界函数去杀死还没有全部生成其儿子结点的那些活结点。如果旅行商问题要求找出全部解,则要生成所有的答案结点。使用限界函数的深度优先结点生成方法称为回溯法。E-结点一直保持到死为止的状态生成方法称为分支限界法

回溯法思想:

       为了应用回溯法,所要求的解必须能表示成一个n- 元组(x1,…,Xn),其中x1是取自某个有穷集Si。通常,所求解的问题需要求取一个使某一规范函数P(x1,…,Xn)取极大值(或取极小值或满足该规范函数条件)的向量。

假定集合Si的大小是mi,于是就有m=m1m2…Mn个n-元组可能满足函数P。所谓硬性处理是构造这m个n-元组并逐一测试它们是否满足P,从而找出该问题的所有最优解。而回溯法的基本思想是,不断地用修改过的函数Pi(x1,…Xi)(即限界函数)去测试正在构造中的n-元组的部分向量(x1,…,Xi),看其是否可能导致最优解。如果判定(x1,…,Xi)不可能导致最优解,那么就可能要测试的后n-i个元素组成的向量一概略去。因此回溯法作的次数比硬性处理作的测试次数(m次)要少得多。用回溯法求解的旅行商问题,即在枚举法的基础上多了一个约束条件,约束条件可以分为两种类型:显示约束和隐式约束。

核心代码(完整源代码见源代码)

分支限界法思想:本题采用FIFO分支限界法。

如前所述,分支限界法是在生成当前E-结点全部儿子之后再生成其它活结点的儿子,且用限界函数帮助避免生成不包含答案结点子树的状态空间的检索方法。在总的原则下,根据对状态控件树中结点检索的次序的不同又将分支限界设计策路分为数种不同的检索方法。在求解旅行商问题时,程序中采用FIFO检索(First In First Out),它的活结点表采用一张先进先出表(即队列)。可以看出,分支限界法在两个方面加速了算法的搜索速度,一是选择要扩展的节点时,总是选择选择一个最小成本的结点,尽可能早的进入最有可能成为最优解的分支;二是扩展节点的过程中,舍弃导致不可行解或导致非最优解的子结点。

核心代码(完整源代码见源代码)

贪心法思想:

贪心法是一种改进了的分级处理方法。它首先旅行商问题描述,选取一种度量标准。然后按这种度量标准对n个输入城市排序,并按序一次输入一个城市。如果这个输入和当前已构成在这种量度意义下的部分最优解加在一起不能产生一个可行解,则不把这个城市加入到这部分解中。这种能够得到某种量度意义下的最优解的分级处理方法成为谈心方法。

获得最优路径的贪心法应一条边一条边地构造这棵树。根据某种量度来选择将要计入的下一条边。最简单的量度标准是选择使得迄今为止计入的那些边的成本的和有最小增量的那条边。

核心代码(完整源代码见源代码)

源代码:

在程序执行目录下建立data.txt文件,用于存放城市节点信息,格式如下:

5  5

0  7  6  1  3

7  0  3  7  8

6  3  0  12 11

1  7  12 0  2

3  8  11 2  0

第一行表示为5行5列,之后为各个节点的权值;

程序执行前先建立如下头文件,用于存储和表示节点信息:

[cpp] view plaincopy
  1. //------------------------------------- AdjtwGraph.h文件--------------------------------------------------
  2. #ifndef AdjTWGraph_H
  3. #define AdjTWGraph_H
  4. #include <vector>
  5. #include <iostream>
  6. using namespace std;
  7. const int MaxV=100;
  8. struct Edge
  9. {
  10. int dest;
  11. int weight;
  12. Edge * next;
  13. Edge(){}
  14. Edge(int d,int w):dest(d),weight(w),next(NULL){}
  15. };
  16. struct item
  17. {    int data;
  18. Edge * adj;
  19. };
  20. class AdjTWGraph
  21. {
  22. private:
  23. item vertices[MaxV];
  24. int numV,numE;
  25. public :
  26. AdjTWGraph();
  27. ~AdjTWGraph();
  28. int NumV(){return numV;}
  29. int NumE(){return numE;}
  30. int GetValue(const int i);
  31. int GetWeight(const int v1,const int v2);
  32. void InsertV(const int & vertex);
  33. void InsertE(const int v1,const int v2,int weight);
  34. friend ostream& operator<<(ostream& os,  AdjTWGraph & m)
  35. {     for (int i = 0; i < m.numV ; i++)     {
  36. for (int j = 0; j < m.numV; j++)
  37. os << right << m.GetWeight(i,j) << " ";
  38. os << endl;
  39. }
  40. return os;
  41. }
  42. friend istream& operator>>(istream& is, AdjTWGraph & m)
  43. {    int t;
  44. for (int i = 0; i < m.NumV(); i++)
  45. for (int j = 0; j < m.NumV(); j++)
  46. {
  47. is >> t;     m.InsertE(i,j,t);
  48. }
  49. return is;
  50. }
  51. };
  52. AdjTWGraph::AdjTWGraph()
  53. {
  54. for(int i=0;i<MaxV;i++)     vertices[i].adj=NULL;
  55. numV=0;numE=0;
  56. }
  57. AdjTWGraph::~AdjTWGraph()
  58. {
  59. for(int i=0;i<numV;i++)
  60. {
  61. Edge * p=vertices[i].adj,*q;
  62. while(p!=NULL)
  63. {
  64. q=p->next;delete p;p=q;
  65. }
  66. }
  67. }
  68. int AdjTWGraph::GetValue(const int i){    return vertices[i].data;  }
  69. int AdjTWGraph::GetWeight(const int v1,const int v2)
  70. {
  71. Edge *p=vertices[v1].adj;
  72. while(p!=NULL && p->dest<v2) p=p->next;
  73. if(v2!=p->dest)    {    return 0;    }
  74. return p->weight;
  75. }
  76. void AdjTWGraph::InsertV(const int & v) { vertices[numV].data=v; numV++;  }
  77. void AdjTWGraph::InsertE(const int v1,const int v2,int weight)
  78. {
  79. Edge * q=new Edge(v2,weight);
  80. if(vertices[v1].adj==NULL) vertices[v1].adj=q;
  81. else
  82. {
  83. Edge *curr=vertices[v1].adj,*pre=NULL;
  84. while(curr!=NULL && curr->dest<v2)    {    pre=curr;curr=curr->next;    }
  85. if(pre==NULL){    q->next=vertices[v1].adj;vertices[v1].adj=q;        }
  86. else    {    q->next=pre->next;pre->next=q;    }
  87. }
  88. numE++;
  89. }
  90. #endif
  91. //------------------------------------- tsp.cpp文件--------------------------------------------------
  92. #include "AdjtwGraph.h"
  93. #include <fstream>
  94. #include <vector>
  95. #include <algorithm>
  96. #include <ctime>
  97. #include <queue>
  98. using namespace std;
  99. ofstream fout("out.txt");
  100. int N;
  101. AdjTWGraph g;
  102. struct Node
  103. {   int currentIndex;
  104. int level;
  105. Node * previous;
  106. Node(int L = 0, int V = 0, Node *p = NULL):level(L),currentIndex(V), previous(p) {}
  107. };
  108. class TspBase
  109. {
  110. protected:
  111. vector<int> currentPath;
  112. vector<int> bestPath;
  113. int cv;
  114. int bestV;
  115. Node * root;
  116. int SumV();
  117. void EnumImplicit(int k);
  118. void BackTrackImplicit(int k);
  119. void EnumExplicit(Node * r);
  120. void BackTrackExplicit(Node * r);
  121. void FIFOBB();
  122. bool Valid(Node *p,int v)  //
  123. {    bool flag = true;
  124. for(Node *r = p; r->level > 0 && V; r = r->previous)  flag = r->currentIndex !=v;
  125. return flag;
  126. }
  127. void StoreX(Node * p) //
  128. {for(Node *r = p; r->level >0 ; r = r->previous )
  129. {    currentPath[r->level-1] = r->currentIndex;    }
  130. }
  131. void Print();
  132. public:
  133. TspBase(){currentPath.resize(N);    bestPath.resize(N);    }
  134. ~TspBase(){currentPath.resize(0);bestPath.resize(0);}
  135. void TspEnumImplicit();
  136. void TspBackTrackImplicit();
  137. void TspEnumExplicit();
  138. void TspBackTrackExplicit();
  139. void TspBB();
  140. void TspGreedy();
  141. void DataClear(bool flag)
  142. {   currentPath.resize(N);        bestPath.resize(N);
  143. if(flag)        { Node * p=root,*q;
  144. while(p!=NULL) {q=p->previous; delete p; p=q;}
  145. }
  146. }
  147. };
  148. void TspBase::TspEnumImplicit()  //         枚举隐式
  149. {    fout<<"TspEnumImplicit ..."<<endl;
  150. cv=0; bestV=10000;
  151. for(int i=0;i<N;i++)    currentPath[i]=i;
  152. EnumImplicit(1);
  153. Print();
  154. }
  155. void TspBase::EnumImplicit(int k)
  156. {    if(k == N)
  157. {    if((cv + g.GetWeight(currentPath[N-1],0)) < bestV)
  158. {
  159. bestV = cv + g.GetWeight(currentPath[N-1],0);
  160. for(int i = 0; i < N; i++)
  161. bestPath[i] = currentPath[i];
  162. }
  163. }
  164. else
  165. for(int j = k; j < N; j++)
  166. {    swap(currentPath[k],currentPath[j]);
  167. cv += g.GetWeight(currentPath[k-1],currentPath[k]);
  168. EnumImplicit(k+1);
  169. cv -= g.GetWeight(currentPath[k-1],currentPath[k]);
  170. swap(currentPath[k],currentPath[j]);
  171. }
  172. }
  173. void TspBase::TspEnumExplicit()    //  枚举显式
  174. {   fout<<"TspEnumExplicit  ..."<<endl;
  175. cv=0;     bestV=10000;
  176. for(int i=0;i<N;i++)     currentPath[i]=i;
  177. root=new Node(0,-1,NULL);
  178. EnumExplicit(root);
  179. Print();
  180. }
  181. void TspBase::EnumExplicit(Node * r)
  182. {    if(r->level == N)
  183. {    StoreX(r);    cv = SumV();
  184. if(cv  < bestV)
  185. {    bestV = cv  ;
  186. for(int i = 0; i < N; i++)
  187. bestPath[i] = currentPath[i];
  188. }
  189. }
  190. else
  191. for(int i = 0; i < N; i ++)
  192. { if(Valid(r,i))
  193. {  Node *q = new Node(r->level+1,i,r);    EnumExplicit(q);    }
  194. }
  195. }
  196. void TspBase::TspBackTrackImplicit()     //回溯隐式
  197. {    fout<<"TspBackTrackImplicit ..."<<endl;
  198. cv=0;  bestV=10000;
  199. for(int i=0;i<N;i++)    currentPath[i]=i;
  200. BackTrackImplicit(1);
  201. Print();
  202. }
  203. void TspBase::BackTrackImplicit(int k)
  204. {    if(k == N)
  205. {    if((cv + g.GetWeight(currentPath[N-1],0)) < bestV)
  206. {
  207. bestV = cv + g.GetWeight(currentPath[N-1],0);
  208. for(int i = 0; i < N; i++)
  209. bestPath[i] = currentPath[i];
  210. }
  211. }
  212. else
  213. for(int j = k; j < N; j++)
  214. { if((cv + g.GetWeight(currentPath[k-1],currentPath[j])) < bestV)
  215. {    swap(currentPath[k],currentPath[j]);
  216. cv += g.GetWeight(currentPath[k-1],currentPath[k]);
  217. BackTrackImplicit(k+1);
  218. cv -= g.GetWeight(currentPath[k-1],currentPath[k]);
  219. swap(currentPath[k],currentPath[j]);
  220. }
  221. }
  222. }
  223. void TspBase::TspBackTrackExplicit()      // 回溯显式
  224. {    fout<<"TspBackTrackExplicit  ..."<<endl;
  225. cv=0;     bestV=10000;
  226. for(int i=0;i<N;i++)     currentPath[i]=i;
  227. root=new Node(0,-1,NULL);
  228. BackTrackExplicit(root);
  229. Print();
  230. }
  231. void TspBase::BackTrackExplicit(Node * r)
  232. {    int w=0;  //初值
  233. if(r->level == N)
  234. {    StoreX(r);
  235. cv = SumV();
  236. if(cv  < bestV)
  237. {   bestV = cv  ;
  238. for(int i = 0; i < N; i++)        bestPath[i] = currentPath[i];
  239. }
  240. }
  241. else
  242. for(int i = 0; i < N; i ++)
  243. {  if(Valid(r,i))
  244. {    Node *q = new Node(r->level+1,i,r);
  245. w += g.GetWeight(q->currentIndex,i);
  246. if(w < bestV)       BackTrackExplicit(q);
  247. w -= g.GetWeight(q->currentIndex,i);
  248. }
  249. }
  250. }
  251. void TspBase::Print() //
  252. {       fout<<"the shortest path is  ";
  253. for(unsigned i = 0; i < N; i++)
  254. fout<<bestPath[i] + 1<<"--";
  255. fout<<"1"<<endl;
  256. fout<<"minimum distance is  "<<bestV<<endl;
  257. }
  258. void TspBase::TspBB()       // 分支限界法
  259. {        fout<<"TspBB(FIFOBB)  ........"<<endl;
  260. cv = 0;        bestV = 100000;
  261. for(unsigned i = 0; i < N; i++)    currentPath[i] = i;
  262. root=new Node(0,-1,NULL);
  263. FIFOBB();
  264. Print();
  265. }
  266. void TspBase::FIFOBB()
  267. { queue<Node*> q;   Node *r;
  268. q.push(root);
  269. int w=0;  //初值
  270. while(!q.empty())
  271. {      r = q.front();      q.pop();
  272. if(r->level == N)
  273. { StoreX(r);
  274. cv = SumV();
  275. if(cv  < bestV)
  276. {   bestV = cv  ;
  277. for(int i = 0; i < N; i++)     bestPath[i] = currentPath[i];
  278. }
  279. }
  280. else
  281. for(int i = 0; i < N; i ++)
  282. {    if(Valid(r,i))
  283. {   Node *s = new Node(r->level+1,i,r);
  284. w += g.GetWeight(s->currentIndex,i);
  285. if(w < bestV)       q.push(s);
  286. w -=  g.GetWeight(s->currentIndex,i);
  287. }
  288. }
  289. }
  290. }
  291. int TspBase::SumV()           //用于FIFOBB
  292. {    int s = 0;
  293. for(int i = 0; i < N; i++)
  294. s += g.GetWeight(currentPath[i],currentPath[(i + 1)%N]);
  295. return s;
  296. }
  297. void TspBase::TspGreedy()  //TSP贪心算法
  298. {     fout<<"TspGreedy ........"<<endl;
  299. bestV = 0;
  300. vector<int> NEAR(N); //
  301. NEAR[0] = -1;
  302. for (int i = 1; i < N; i++)
  303. NEAR[i] = 0;
  304. bestPath[0] = 1;
  305. int t;
  306. for (int s = 1; s < N; s++)
  307. {
  308. int j = 1;
  309. while (j < N && NEAR[j] < 0) /
  310. j++;
  311. int K = j;
  312. for (int k = j + 1; k < N; k++)
  313. if (NEAR[k] >= 0 &&  g.GetWeight(k,NEAR[k]) < g.GetWeight(j,NEAR[j]))
  314. j = k;
  315. bestPath[s] = j + 1;
  316. bestV +=g.GetWeight(j,NEAR[j]);
  317. NEAR[j] = -1;
  318. for (k = K; k < N; k++) //调整NEAR值
  319. if (NEAR[k] >= 0)
  320. NEAR[k] = j;
  321. t = j;
  322. }
  323. bestV += g.GetWeight(t,0);
  324. fout<<"the shortest path is  ";
  325. for(unsigned w = 0; w < N; w++)
  326. fout<<bestPath[w] <<"--";
  327. fout<<"1"<<endl;
  328. fout<<"minimum distance is  "<<bestV<<endl;
  329. }
  330. int main(int argc, char* argv[])
  331. {   int m,n;
  332. ifstream fin("data.txt");
  333. if(fin.bad()) return 1;
  334. fin >> m >> n;
  335. N = n;
  336. for(int i=0;i<N;i++)  g.InsertV(i);
  337. fin >> g;
  338. TspBase it;
  339. it.TspEnumImplicit();    it.DataClear(false);
  340. it.TspBackTrackImplicit();    it.DataClear(false);
  341. it.TspEnumExplicit();    it.DataClear(true);
  342. it.TspBackTrackExplicit();    it.DataClear(true);
  343. it.TspBB();    it.DataClear(true);
  344. it.TspGreedy();    it.DataClear(false);
  345. return 0;
  346. }

执行结果:
the shortest path is  1--3--2--5--4--1
minimum distance is  20

旅行商问题的n种解法相关推荐

  1. usaco Ordered Fractions 顺序的分数(两种解法)

    这题很好玩,这里有两种解法. 第一种我自己写的,先找到所有的既约真分数,然后写了一个cmp函数进行排序最后输出.我写的时候还在想这章不是搜索吗这跟搜索关系不大吧,难道是怕我们思维定式化故意出的题不是搜 ...

  2. 青蛙跳台阶c语言递归函数,青蛙跳台阶问题的四种解法

    http://raychase.iteye.com/blog/1337359 题目:一只青蛙一次可以跳1级台阶,也可以跳2级.求该青蛙跳上一个n级的台阶总共有多少种跳法. 这道题还被ITEye放在了博 ...

  3. opengl正方形绕点旋转_一题十五种解法够不够? 旋转,构造,四点共圆乐不停...

    平移,旋转,轴对称是我们初中学习的"几何三大变换".在我们初中阶段学习的几何知识中占据着核心的地位,特别是旋转,那更是核心中的核心(河南中考22题年年考). 如何更好的理解旋转,如 ...

  4. 约瑟夫环问题的两种解法(详解)

    约瑟夫环问题的两种解法(详解) 题目: Josephus有过的故事:39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓.于是决定了自杀方式,41个人排成一个圆 ...

  5. 牛客--追债之旅 两种解法

    文章目录 第一种 第二种: 一共两种解法,所以即便你不会最短路,也可以做,甚至爆搜+剪枝的时间和空间消耗小于最短路做法. 第一种 题意: 小明现在要追讨一笔债务,已知有n座城市,每个城市都有编号,城市 ...

  6. java binarytreenode_LeetCode算法题-Binary Tree Paths(Java实现-3种解法)

    这是悦乐书的第199次更新,第206篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第62题(顺位题号是257).给定二叉树,返回所有根到叶路径.例如: 输入: 1 / \ ...

  7. 关于leetcode第K个最大元素的几种解法

    对于这一题我使用了最大堆,快速排序,归并排序几种解法来做这一题,速度最快的是归并排序 使用定值的最小堆每次更新数组最后剩下前k个最大元素,而且堆顶就是我们要的第K个元素. 堆排序: import he ...

  8. [简单题]自定义取余(三种解法)C++实现

    题目链接: 点击打开原题链接 题目意思,就是标题意思. 第一种解法:(加法迭代)用加法来模拟这个(17行代码) int mod256WithoutMod(int number) {if (number ...

  9. 推荐系统炼丹笔记:Embedding在内存问题上的一种解法

    作 者:一元 公众号:炼丹笔记 很多朋友都会发现,修改embedding的大小能对深度模型带来非常大的影响,往往越大的embedding能带来更佳的效果,但是却因为Embedding内存的问题,没法直 ...

最新文章

  1. ERP与EWM集成配置-ERP端组织架构(二)
  2. 论文笔记:Weighted Graph Cuts without Eigenvectors:A Multilevel Approach
  3. 【数据结构与算法】之线性表的应用和操作
  4. vs中工具箱代表的意思_“日”除了代表太阳,其实还有这种意思,特别是出现在这些词语中的“日”...
  5. 百度网盘Linux版放出deb包客户端:新增支持Ubuntu 18.04 LTS
  6. java url 协议_Java自定义URL协议
  7. (转)趋势因子:利用投资期内所有信息的获利方法
  8. python爬虫下一页_Python爬虫怎么获取下一页的URL和网页内容?
  9. matlab weibpdf函数,MATLAB常用函数
  10. html5 斗鱼 苹果,斗鱼ios端——手游直播开播指导
  11. 软件供应链安全威胁:从“奥创纪元”到“无限战争”
  12. windows系统设置定时开关机的方法
  13. XML 大于号 小于号 处理
  14. php实现智能音箱播放内容,智能音箱应用分析 智能音箱五大功能介绍
  15. Android文字实现跑马灯效果——两种方法实现
  16. python简历项目经验在哪里找_Linux运维工程师简历项目经验
  17. qq绑定outlook邮箱服务器,Outlook2013怎么绑定QQ邮箱
  18. 美元汇率【贪心算法练习题】
  19. Windows脚本:打开浏览器访问任意网址
  20. 「水」滔天巨浪---牛客小白

热门文章

  1. linux终止mysql进程_Ubuntu Linux下定时监测MySQL进程终止时自动重启的方法
  2. shell中正则表达式详解_Linux中的正则表达式
  3. android自定义弹出对话框,使用FlyDialog实现自定义Android弹窗对话框
  4. 工业级PoE交换机的分类知识详解
  5. 选购安防交换机时需要注意哪些误区?
  6. 【渝粤教育】国家开放大学2018年秋季 0508-21T影视特技及后期合成 参考试题
  7. 【渝粤题库】陕西师范大学209010 现代教育战略 作业 (专升本)
  8. 御龙在天手游怎么不显示服务器了,御龙在天手游进不去怎么办 闪退原因及解决办法...
  9. 基于VS的连连看小游戏
  10. 访问GitHub超慢的解决办法