C++高级数据结构算法 | 回溯算法(Back Tracking Method)

知识共享许可协议 版权声明:署名,允许他人基于本文进行创作,且必须基于与原先许可协议相同的许可协议分发本文 (Creative Commons


回溯法有“通用的解题法”之称。用它可以系统地搜索一个问题的所有解或任一解。回溯法是一个既带有系统性又带有跳跃性的搜索算法。它在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任一结点时,先判断该结点是否包含问题的解。如果肯定不包含,则跳过对以该结点为根的子树的搜索,逐层向其祖先结点回溯。否则,进入该子树,继续按深度优先策略搜索。回溯法求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索遍才结束。回溯法求问题的一个解时,只要搜索到问题的一个解就可结束。这种以深度优先方式系统搜索问题解的算法称为回溯法,它适用于解组合数较大的问题。


回溯法的算法解析

问题的解空间

用回溯法解问题时,应明确定义问题的解空间。问题的解空间至少应包含问题的一个(最优)解。例如,对于有 n n 种可选择物品的背包问题,其解空间由长度为 n n 0 1 0-1 向量组成。该解空间包含对变量的所有可能的赋值。当 n = 3 n=3 时,其解空间是 ( 0 , 0 , 0 ) , ( 0 , 1 , 0 ) , ( 0 , 0 , 1 ) , ( 1 , 0 , 0 ) , ( 0 , 1 , 1 ) , ( 1 , 0 , 1 ) , ( 1 , 1 , 0 ) , ( 1 , 1 , 1 ) {(0,0,0),(0,1,0),(0,0,1),(1,0,0),(0,1,1),(1,0,1),(1,1,0),(1,1,1)}

定义了问题的解空间后,还应将解空间很好地组织起来,使得能用回溯法方便地搜索整个解空间。通常将解空间组织成树或图的形式。

例如,对于 n = 3 n=3 时的背包问题,可用一棵完全二叉树表示其解空间,如下图所示。

解空间树的第 i i 层到第 i + 1 i + 1 层边上的标号给出了变量的值。从树根到叶的任一路径表示解空间中的一个元素。例如,从根结点到结点 H H 的路径相应于解空间中的元素 1 , 1 , 1 (1,1,1)


回溯法的基本思想

确定了解空间的组织结构后,回溯法从开始结点(根结点)出发,以深度优先方式搜索整个解空间。这个开始结点成为活结点,同时也成为当前的扩展结点。在当前的扩展结点处,搜索向纵深方向移至一个新结点。这个新结点就成为新的活结点,并成为当前扩展结点。如果在当前的扩展结点处不能再向纵深方向移动,则当前扩展结点就成为死结点。此时,应往回移动(回溯)至最近的一个活结点处,并使这个活结点成为当前的扩展结点回溯法以这种工作方式递归地在解空间中搜索,直至找到所要求的解或解空间中已无活结点时为止

回溯法搜索解空间树时,通常采用两种策略避免无效搜索,提高回溯法的搜索效率。其一是用约束函数在扩展结点处剪去不满足约束的子树;其二是用限界函数剪去得不到最优解的子树。这两类函数统称为剪枝函数

例如,解0-1背包问题的回溯法用剪枝函数剪去导致不可行解的子树。在解旅行商问题的回溯法中,如果从根结点到当前扩展结点处的部分周游路线的费用已超过当前找到的最好的周游路线费用,则可以断定以该结点为根的子树中不含最优解,因此可将该子树剪去。

综上所述,用回溯法解题通常包含以下3个步骤:

  • 针对所给问题,定义问题的解空间
  • 确定易于搜索的解空间结构
  • 以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索

用回溯法解题的一个显著特征是在搜索过程中动态产生问题的解空间在任何时刻,算法只保存从根结点到当前扩展结点的路径。如果解空间树中从根结点到叶结点的最长路径的长度为 h ( n ) h(n) ,则回溯法所需的计算空间通常为 O ( h ( n ) ) O(h(n)) 。而显式地存储整个解空间则需要 O ( 2 h ( n ) ) O(2^{h(n)}) O ( h ( n ) ! ) O(h(n)!) 内存空间。


递归回溯

回溯法对解空间作深度优先搜索,因此在一般情况下可用递归函数来实现回溯法如下(伪代码描述):

/* 形参t表示递归深度,即当前扩展结点在解空间树的深度 */
void Backtrack(int t)
{
	/* 叶子节点,输出结果,x是可行解 */
	if(t > n) Output(x);
	else
		/* 当前节点的所有子节点 */
		for i = 1 to k 
		{
			/* 每个子节点的值赋值给x */
			x[t] = value[i]; 
			/* 满足约束条件和限界条件 */ 
			if(Constrain(t) && Bound(t))
				Backtrace(t + 1); /* 递归下一层 */
		}
			
}

迭代回溯

采用树的非递归深度优先遍历算法,也可将回溯法表示成为一个非递归的迭代过程如下(伪代码描述):

void Backtrace()
{
	int t = 1;
	while(t > 0)
	{
		/* 当前节点的存在子节点 */
		if (ExistSubNode(t))
		{
			/* 遍历当前节点的所有子节点 */
			for i = 0 to k
			{
				/* 每个子节点的值赋值给x */
				x[t] = value[i];
				/* 满足约束条件和限界条件 */
				if(Constrain(t) && Bound(t)) // 
				{
				 	/* solution表示在当前扩展结点处是否已得到问题的可行解 */
					if(Solution(t))
						Output(t);
				}
				else
					t++; /* 没有得到解,继续向下搜索 */
			}	
		}
		else
			t--; /* 不存在子节点,返回上一层 */
	}
}

子集树与排列树

当所给的问题是从 n n 个元素的集合 S S 中找出满足某种性质的子集时,相应的解空间树称为子集树。例如 n n 个物品的 01 01 背包问题所相应的解空间树就是一棵子集树。这类子集树通常有 2 n 2^n 个叶结点,其结点总个数为 2 n 1 2^n - 1 。遍历子集树的任何算法均需 O ( 2 n ) O(2^n) 的计算时间。

用回溯法搜索子集树的一般算法可描述如下:

void Backtrace(int t)
{
	if(t > n) Output(x);
	else
		for(int i = 0; i <= 1; i++)
			x[t] = i;
			if(Constrain(t) && Bound(t))
				Backtrace(t + 1); 
}

当所给的问题是确定 n n 个元素满足某种性质的排列时,相应的解空间树称为排列树。排列树通常有 n ! n! 个叶结点。因此遍历排列树需要 O ( n ! ) O(n!) 的计算时间。旅行商问题的解空间树就是一棵排列树,如下图所示:

用回溯法搜索排列树的一般算法可描述如下:

void Backtrace(int t)
{
	if(t > n) Output(x);
	else
		for(int i = t; i <= n; ++i)
		{
			Swap(x[t], x[i]);
			if(Constrain(t) && Bound(t))
				Backtrace(t + 1); 
			Swap(x[t], x[i]);
		}
}

经典问题分析

子集树算法框架

/* 子集树 */

#include<iostream>
using namespace std;

void func(int arr[], int i, int length, int brr[])
{
	if (i == length)
	{
		for (int j = 0; j < length; j++)
		{
			if (brr[j] == 1)
			{
				cout << arr[j] << " ";
			}
		}
		cout << endl;
	}
	else
	{
		/* 左子树:元素标志为1 */
		brr[i] = 1;
		func(arr, i + 1, length, brr);
		
		/* 右子树:元素标志为0 */
		brr[i] = 0;
		func(arr, i + 1, length, brr);

	}
}

int main()
{
	int arr[4] = { 1,2,3,4 };
	int brr[4] = { 0 };
	func(arr, 0, 4, brr);
}

整数选择问题

问题描述:

一组整数序列,选择其中的一部分整数,让选择的整数和序列中剩下的整数的和的差值最小。

#include<iostream>
using namespace std;

#define N 10

int arr[N] = { 12, 3, 45, 6, 78, 9, 43, 21, 62, 31 };

// 辅助数组
int brr[N] = { 0 };

// 存储标志位,标志最终的结果集
int res[N] = { 0 };

// 序列中剩余数字的和
int arrSum = 0;

// 当前选择序列的和
//int sum = 0;

// 存储当前的最小差值
unsigned int min = 0xFFFFFFFF;

void func(int i)
{
	if (i == N)
	{
		int sum = 0;
		for (int j = 0; j < N; ++j)
		{
			if (brr[j] == 1)
			{
				// 求当前选择的序列的和
				sum += arr[j];
			}
		}

		//int diff = abs(sum - (arrSum - sum));
		int diff = abs(sum - arrSum);
		
		/* 当前的差值比记录的最小差值还要小,进行更新 */
		if (diff < min)
		{
			min = diff;
			for (int k = 0; k < N; k++)
			{
				res[k] = brr[k];
			}
		}
	}
	else
	{
		/* 左子树中剩余的元素和,arrSum减去选择的元素 */
		arrSum -= arr[i];
		brr[i] = 1;
		func(i + 1);
		
		/* 右子树中的元素不被选择,arrSum加上该元素 */
		arrSum += arr[i];

		brr[i] = 0;
		func(i + 1);
	}
}

int main()
{
	for (int i = 0; i < N; i++)
	{
		arrSum += arr[i];
	}

	func(0);

	for (int i = 0; i < N; i++)
	{
		if (res[i] == 1)
		{
			cout << arr[i] << " ";

		}
	}
	cout << endl;
	cout << "min:" << min << endl;

	return 0;
}

题目变形:

一组2n个整数序列,选择其中n个整数,和序列中剩下的n个整数的和的差值最小

#include<iostream>
using namespace std;

#define N 14
int arr[N] = { 12, 3, 45, 6, 78, 9, 43, 21, 62, 31,23,34,12,56 };

// 辅助数组
int brr[N] = { 0 };

// 存储标志的结果集
int res[N] = { 0 };

// 数组中剩余序列的和
int arrSum = 0;

// 已选择序列的和
int sum = 0;

// 当前已经选择的数字个数
int curNum = 0;

// 存储当前的最小差值
unsigned int min = 0xFFFFFFFF;

void func(int i)
{
	if (i == N)
	{
		/**
		 * 当前选择的数字个树不是n,无需判断,此序列已经不满足
		 * 题意,直接返回即可。
		 */
		if (curNum != N / 2)
		{
			return;
		}

		unsigned int diff = abs(sum - arrSum);

		if (diff < min)
		{
			min = diff;
			for (int k = 0; k < N; k++)
			{
				res[k] = brr[k];
			}
		}
	}
	else
	{
		// 剪枝操作
		if (curNum < N / 2)
		{
			curNum++;
			sum += arr[i];
			arrSum -= arr[i];
			brr[i] = 1;
			func(i + 1);

			curNum--;
			sum -= arr[i];
			arrSum += arr[i];
			brr[i] = 0;
			func(i + 1);
		}
	}
}

int main()
{
	for (int i = 0; i < N; i++)
	{
		arrSum += arr[i];
	}

	func(0);

	for (int i = 0; i < N; i++)
	{
		if (res[i] == 1)
		{
			cout << arr[i] << " ";

		}
	}
	cout << endl;
	cout << "min:" << min << endl;

	return 0;
}

0-1背包问题

问题描述:

假设有n个物品,它们的重量分别是W1, W2, W3… Wn,它们的价值分别是V1, V2, V3… Vn,有一个背包,其容量限制是C,那么怎么样装入物品,能使背包的价值最大化。

#include<iostream>
using namespace std;

// 物品的价值
int v[5] = { 12, 4, 60, 8, 13 };

// 物品的重量
int w[5] = { 8, 6, 9, 4, 7 };

// 背包容量
int c = 25;

// 辅助数组
int brr[5] = { 0 };

// 标志结果集
int res[5] = { 0 };

// 当前选择的价值总和
int choiceV = 0;

// 当前选择的重量总和
int choiceW = 0;

// 当前最优价值
int bestv = INT_MIN;

// 右子树中剩余元素的和
int arrSum = 0;

int lenV = sizeof(v) / sizeof(v[0]);
int lenW = sizeof(w) / sizeof(w[0]);

void func(int i)
{
	if (i == lenV)
	{
		cout << "choice v = " << choiceV << endl;
		if (choiceV > bestv)
		{
			bestv = choiceV;
			for (int j = 0; j < lenV; j++)
			{
				res[j] = brr[j];
			}
		}
	}
	else
	{
		arrSum -= v[i];
		// 判断左孩子结点是不是可行结点
		if (choiceW + w[i] <= c)
		{
			choiceW += w[i];
			choiceV += v[i];
			brr[i] = 1;
			func(i + 1);
			choiceW -= w[i];
			choiceV -= v[i];
		}

		/**
		 * 剪枝函数,保证到达叶结点时候的choiceV都优于当前的最优解bestv
		 */
		if (choiceV + arrSum > bestv)
		{
			brr[i] = 0;
			func(i + 1);
		}
		arrSum += v[i];
	}
}

int main()
{
	for (size_t i = 0; i < lenV; i++)
	{
		arrSum += v[i];
	}

	func(0);
	cout << bestv << endl;
	int len = sizeof(res) / sizeof(res[0]);
	for (int i = 0; i < len; i++)
	{
		if (res[i] == 1)
		{
			cout << v[i] << " ";
		}

	}
	cout << endl;

	return 0;
}

整数求和问题

题目描述:

从一组整数数组中选择n个元素,让其和等于指定的值

#include<iostream>
using namespace std;

int arr[9] = { 12,45,8,91,36,79,83,52,31 };

// 辅助数组
int brr[4] = { 0 };
int res[4] = { 0 };

int length = sizeof(arr) / sizeof(arr[0]);

// 数组中剩余的元素和
int arrSum = 0;

// 已选择的数字的和
int choiceSum = 0;

// 指定的和
int sum = 136;

void func(int i)
{
	if (i == length)
	{
		if (choiceSum == sum)
		{
			for (int j = 0; j < length; j++)
			{
				if (brr[j] == 1)
				{
					cout << arr[j] << " ";
				}
			}
			cout << endl;
		}		
	}
	else
	{
		arrSum -= arr[i];
		if (choiceSum + arr[i] <= sum)
		{
			choiceSum += arr[i];
			brr[i] = 1;
			func(i + 1);
			choiceSum -= arr[i];
		}
		
		// 剪枝函数 - 已选择的与右子树中剩余元素的和小于sum,减去右子树
		if (choiceSum + arrSum >= sum)
		{
			brr[i] = 0;
			func(i + 1);
		}
		arrSum += arr[i];
	}
}


int main()
{
	for (int i = 0; i < length; i++)
	{
		arrSum += arr[i];
	}

	func(0);
}

装载问题

题目描述:

有一批共n个集装箱要装上2艘载重量分别为c1和c2的轮船,其中集装箱i的重量为wi,且集装箱重量总和(w1+w2+…+wn)<c1+c2。试找出一种合理的装载方案将这n个集装箱装上这2艘船。

问题分析:

如果该装载问题有解,装载策略为:
(1) 首先将第一艘船尽可能装满(等价于特殊的0-1背包问题);
(2) 然后将剩余的集装箱都装上第二艘轮船;

#define N 7
// 集装箱的重量
int w[N] = { 7,8,5,9,4,6,3 };

// 辅助数组
int brr[N] = { 0 };

// 第一艘船的载重量
int c1 = 22;

// 第二艘船的载重量
int c2 = 20;

// 当前选择的集装箱重量和
int cw = 0;

int r = 0;

// 当前的最优载重
int bestw = 0;

// 结果集
int res[N] = { 0 };

void func(int  i)
{
	if (i == N)
	{	
		if (cw > bestw)
		{
			bestw = cw;
			for (int j = 0; j < N; j++)
			{
				res[j] = brr[j];
			}
			cout << endl;
		}
	}
	else
	{
		r -= w[i];
		if (cw + w[i] <= c1)
		{
			cw += w[i];
			brr[i] = 1;
			func(i + 1);
			cw -= w[i];
		}
		
		// 剪枝函数,保证到达叶结点时候的cw都优于当前的最优解bestw
		if (r + cw > bestw)
		{
			brr[i] = 0;
			func(i + 1);
		}
		r += w[i];
	}
}

int main()
{
	for (int val : w)
	{
		r += val;
	}

	func(0);
	cout << "c1:" << bestw << endl;
	for (int j = 0; j < N; j++)
	{
		if (res[j] == 1)
		{
			cout << w[j] << " ";
		}
	}
	cout << endl;

	
	
	int sum = 0;
	for (int j = 0; j < N; j++)
	{
		if (res[j] == 0)
		{
			sum += w[j];
			cout << w[j] << " ";
		}
	}
	cout << endl;

	cout << "c2:" << sum << endl;
	return 0;
}

排列树算法框架

/* 排列树 */
void swap(int* arr, int i, int j)
{
	int tmp = arr[i];
	arr[i] = arr[j];
	arr[j] = tmp;
}

void backtrace(int* arr, int i, int length)
{
	if (i == length)
	{
		for (int j = 0; j < length; j++)
		{
			cout << arr[j] << " ";
		}
		cout << endl;
	}
	else
	{
		for (int j = i; j < length; j++)
		{
			swap(arr, i, j);
			backtrace(arr, i + 1, length);
			swap(arr, i, j);
		}
	}
}

int main()
{
	int arr[3] = { 1, 2, 3 };
	int length = sizeof(arr) / sizeof(arr[0]);
	backtrace(arr, 0, length);
}

八皇后问题

问题描述:

按照国际象棋规则,皇后可以攻击与之处在同一行,或者同一列,或者同一斜线上的其它棋子。现在有n个皇后放置在nxn格的棋盘上,如何摆放n个皇后而使它们都不能互相吃子?有多少种摆法?

/* 八皇后问题 */

/* 记录排列总数 */
int countNum = 0;

void swap(int* arr, int i, int j)
{
	int tmp = arr[i];
	arr[i] = arr[j];
	arr[j] = tmp;
}

/**
 * 判断是否满足限制条件
 * 我们用i表示行数、arr[i]表示相应的列数
 * 从第0行开始到第i行,这里不会出现同行(i==j),同列(arr[i] == arr[j])的情况
 * 我们只需要判断是否同斜线的棋子即可(现在的和之前的进行判断)
 */
bool isLine(int* arr, int i)
{
	for (int j = 0; j < i; j++)
	{
		if ((abs(arr[i] - arr[j]) == (i - j))
		{
			return false;
		}
	}
	return true;
}


void backtrace(int* arr, int i, int length)
{
	if (i == length)
	{
		countNum++;
		for (int j = 0; j < length; j++)
		{
			cout << arr[j] << " ";
		}
		cout << endl;
	}
	else
	{
		for (int j = i; j < length; j++)
		{
			swap(arr, i, j);
			if (isLine(arr, i))
			{
				backtrace(arr, i + 1, length);
			}
			
			swap(arr, i, j);
		}
	}
}

int main()
{
	int arr[8] = { 1, 2, 3, 4, 5 ,6, 7, 8 };
	int length = sizeof(arr) / sizeof(arr[0]);
	backtrace(arr, 0, length);
	cout << "countNum = " << countNum;
}

八皇后问题的排列结果如下,共计92种情况:

[0,1] [1,5] [2,8] [3,6] [4,3] [5,7] [6,2] [7,4]
[0,1] [1,6] [2,8] [3,3] [4,7] [5,4] [6,2] [7,5]
[0,1] [1,7] [2,4] [3,6] [4,8] [5,2] [6,5] [7,3]
[0,1] [1,7] [2,5] [3,8] [4,2] [5,4] [6,6] [7,3]
[0,2] [1,4] [2,6] [3,8] [4,3] [5,1] [6,7] [7,5]
[0,2] [1,5] [2,7] [3,4] [4,1] [5,8] [6,6] [7,3]
[0,2] [1,5] [2,7] [3,1] [4,3] [5,8] [6,6] [7,4]
[0,2] [1,6] [2,1] [3,7] [4,4] [5,8] [6,3] [7,5]
[0,2] [1,6] [2,8] [3,3] [4,1] [5,4] [6,7] [7,5]
[0,2] [1,7] [2,3] [3,6] [4,8] [5,5] [6,1] [7,4]
[0,2] [1,7] [2,5] [3,8] [4,1] [5,4] [6,6] [7,3]
[0,2] [1,8] [2,6] [3,1] [4,3] [5,5] [6,7] [7,4]
[0,3] [1,1] [2,7] [3,5] [4,8] [5,2] [6,4] [7,6]
[0,3] [1,5] [2,2] [3,8] [4,1] [5,7] [6,4] [7,6]
[0,3] [1,5] [2,2] [3,8] [4,6] [5,4] [6,7] [7,1]
[0,3] [1,5] [2,7] [3,1] [4,4] [5,2] [6,8] [7,6]
[0,3] [1,5] [2,8] [3,4] [4,1] [5,7] [6,2] [7,6]
[0,3] [1,6] [2,4] [3,1] [4,8] [5,5] [6,7] [7,2]
[0,3] [1,6] [2,4] [3,2] [4,8] [5,5] [6,7] [7,1]
[0,3] [1,6] [2,2] [3,5] [4,8] [5,1] [6,7] [7,4]
[0,3] [1,6] [2,2] [3,7] [4,5] [5,1] [6,8] [7,4]
[0,3] [1,6] [2,2] [3,7] [4,1] [5,4] [6,8] [7,5]
[0,3] [1,6] [2,8] [3,2] [4,4] [5,1] [6,7] [7,5]
[0,3] [1,6] [2,8] [3,1] [4,5] [5,7] [6,2] [7,4]
[0,3] [1,6] [2,8] [3,1] [4,4] [5,7] [6,5] [7,2]
[0,3] [1,7] [2,2] [3,8] [4,5] [5,1] [6,4] [7,6]
[0,3] [1,7] [2,2] [3,8] [4,6] [5,4] [6,1] [7,5]
[0,3] [1,8] [2,4] [3,7] [4,1] [5,6] [6,2] [7,5]
[0,4] [1,2] [2,5] [3,8] [4,6] [5,1] [6,3] [7,7]
[0,4] [1,2] [2,7] [3,5] [4,1] [5,8] [6,6] [7,3]
[0,4] [1,2] [2,7] [3,3] [4,6] [5,8] [6,1] [7,5]
[0,4] [1,2] [2,7] [3,3] [4,6] [5,8] [6,5] [7,1]
[0,4] [1,2] [2,8] [3,5] [4,7] [5,1] [6,3] [7,6]
[0,4] [1,2] [2,8] [3,6] [4,1] [5,3] [6,5] [7,7]
[0,4] [1,1] [2,5] [3,8] [4,6] [5,3] [6,7] [7,2]
[0,4] [1,1] [2,5] [3,8] [4,2] [5,7] [6,3] [7,6]
[0,4] [1,6] [2,1] [3,5] [4,2] [5,8] [6,3] [7,7]
[0,4] [1,6] [2,8] [3,2] [4,7] [5,1] [6,3] [7,5]
[0,4] [1,6] [2,8] [3,3] [4,1] [5,7] [6,5] [7,2]
[0,4] [1,7] [2,3] [3,8] [4,2] [5,5] [6,1] [7,6]
[0,4] [1,7] [2,1] [3,8] [4,5] [5,2] [6,6] [7,3]
[0,4] [1,7] [2,5] [3,3] [4,1] [5,6] [6,8] [7,2]
[0,4] [1,7] [2,5] [3,2] [4,6] [5,1] [6,3] [7,8]
[0,4] [1,8] [2,1] [3,3] [4,6] [5,2] [6,7] [7,5]
[0,4] [1,8] [2,1] [3,5] [4,7] [5,2] [6,6] [7,3]
[0,4] [1,8] [2,5] [3,3] [4,1] [5,7] [6,2] [7,6]
[0,5] [1,2] [2,4] [3,6] [4,8] [5,3] [6,1] [7,7]
[0,5] [1,2] [2,4] [3,7] [4,3] [5,8] [6,6] [7,1]
[0,5] [1,2] [2,6] [3,1] [4,7] [5,4] [6,8] [7,3]
[0,5] [1,2] [2,8] [3,1] [4,4] [5,7] [6,3] [7,6]
[0,5] [1,3] [2,1] [3,6] [4,8] [5,2] [6,4] [7,7]
[0,5] [1,3] [2,1] [3,7] [4,2] [5,8] [6,6] [7,4]
[0,5] [1,3] [2,8] [3,4] [4,7] [5,1] [6,6] [7,2]
[0,5] [1,1] [2,4] [3,6] [4,8] [5,2] [6,7] [7,3]
[0,5] [1,1] [2,8] [3,4] [4,2] [5,7] [6,3] [7,6]
[0,5] [1,1] [2,8] [3,6] [4,3] [5,7] [6,2] [7,4]
[0,5] [1,7] [2,4] [3,1] [4,3] [5,8] [6,6] [7,2]
[0,5] [1,7] [2,1] [3,4] [4,2] [5,8] [6,6] [7,3]
[0,5] [1,7] [2,1] [3,3] [4,8] [5,6] [6,4] [7,2]
[0,5] [1,7] [2,2] [3,4] [4,8] [5,1] [6,3] [7,6]
[0,5] [1,7] [2,2] [3,6] [4,3] [5,1] [6,4] [7,8]
[0,5] [1,7] [2,2] [3,6] [4,3] [5,1] [6,8] [7,4]
[0,5] [1,8] [2,4] [3,1] [4,3] [5,6] [6,2] [7,7]
[0,5] [1,8] [2,4] [3,1] [4,7] [5,2] [6,6] [7,3]
[0,6] [1,2] [2,7] [3,1] [4,4] [5,8] [6,5] [7,3]
[0,6] [1,2] [2,7] [3,1] [4,3] [5,5] [6,8] [7,4]
[0,6] [1,3] [2,5] [3,7] [4,1] [5,4] [6,2] [7,8]
[0,6] [1,3] [2,5] [3,8] [4,1] [5,4] [6,2] [7,7]
[0,6] [1,3] [2,1] [3,7] [4,5] [5,8] [6,2] [7,4]
[0,6] [1,3] [2,1] [3,8] [4,5] [5,2] [6,4] [7,7]
[0,6] [1,3] [2,1] [3,8] [4,4] [5,2] [6,7] [7,5]
[0,6] [1,3] [2,7] [3,4] [4,1] [5,8] [6,2] [7,5]
[0,6] [1,3] [2,7] [3,2] [4,4] [5,8] [6,1] [7,5]
[0,6] [1,3] [2,7] [3,2] [4,8] [5,5] [6,1] [7,4]
[0,6] [1,4] [2,2] [3,8] [4,5] [5,7] [6,1] [7,3]
[0,6] [1,4] [2,1] [3,5] [4,8] [5,2] [6,7] [7,3]
[0,6] [1,4] [2,7] [3,1] [4,3] [5,5] [6,2] [7,8]
[0,6] [1,4] [2,7] [3,1] [4,8] [5,2] [6,5] [7,3]
[0,6] [1,1] [2,5] [3,2] [4,8] [5,3] [6,7] [7,4]
[0,6] [1,8] [2,2] [3,4] [4,1] [5,7] [6,5] [7,3]
[0,7] [1,2] [2,4] [3,1] [4,8] [5,5] [6,3] [7,6]
[0,7] [1,2] [2,6] [3,3] [4,1] [5,4] [6,8] [7,5]
[0,7] [1,3] [2,1] [3,6] [4,8] [5,5] [6,2] [7,4]
[0,7] [1,3] [2,8] [3,2] [4,5] [5,1] [6,6] [7,4]
[0,7] [1,4] [2,2] [3,5] [4,8] [5,1] [6,3] [7,6]
[0,7] [1,4] [2,2] [3,8] [4,6] [5,1] [6,3] [7,5]
[0,7] [1,5] [2,3] [3,1] [4,6] [5,8] [6,2] [7,4]
[0,7] [1,1] [2,3] [3,8] [4,6] [5,4] [6,2] [7,5]
[0,8] [1,2] [2,4] [3,1] [4,7] [5,5] [6,3] [7,6]
[0,8] [1,2] [2,5] [3,3] [4,1] [5,7] [6,4] [7,6]
[0,8] [1,3] [2,1] [3,6] [4,2] [5,5] [6,7] [7,4]
[0,8] [1,4] [2,1] [3,3] [4,6] [5,2] [6,7] [7,5]

countNum = 92

猜你喜欢

转载自blog.csdn.net/ZYZMZM_/article/details/92020557