欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

消消乐实验回溯法(深大算法实验3)报告+代码

程序员文章站 2024-03-22 17:06:40
...
所有实验的资源
链接: https://pan.baidu.com/s/1lukZRM3Rsd1la35EyyJcvg
提取码: iv72

写在前面

期末终于算法课快要完结了。

这学期算法课可谓是最难顶的课程了,又正好是线上上课,提问互动的机会相对较少,老师上课抛砖引玉,实验内容又比较难,我花了大部分的时间在找算法,实现算法,改算法bug上。

我也参考过很多往届师兄的报告,但是大多都比较抽象晦涩,而且没有代码只讲方法,比较难以理解具体实现的细节。

所以我打算记录一下自己的报告+代码,前人coding后人copying ,希望让大家少走弯路。。。

注意:不要直接copy代码,这是冲塔行为!查重系统鲨疯辣。

实验要求

消消乐问题,如果有连续三个同类方块,那么消去得1分,4个4分,5个10分,K为方块的种类,M,N为棋盘尺寸,X为可以移动的次数

  1. 给定K, M, N编写代码计算通过一步操作(X=1)可得的最大得分。
    例如,K=4, M=8, N=4
    测试数据
3 3 4 3
3 2 3 3
2 4 3 4
1 3 4 3
3 3 1 1
3 4 3 3
1 4 4 3
1 2 3 2
  1. 在1的基础上利用回溯算法,找出X交换步骤之后的最大得分。

  2. 对于数值较大的K、M、N、X,在允许近似最优解的情况下,对2中实现的算法进行优化剪枝。并与内容2中最终结果和执行速度进行比较。

  3. 如果能实现可视化输出计算结果(包括回溯过程),如图2,可加分。
    消消乐实验回溯法(深大算法实验3)报告+代码

实验要求:

  1. 对不同K,M, N, X的问题依次求解,演示你的求解结果,请提供你机器上能求解的问题最大规模。
  2. 在blackboard提交电子版实验报告。源代码和PPT作为实验报告附件上传。
  3. 在实验完成之后,将进行一次PPT介绍。
  4. 在实验报告中要求详细说明”实验内容1”和“实验内容2”的实现思想。
  5. 讨论”实验内容1”和“实验内容2”问题复杂度和K, M, N, X的关系。

求解问题的算法原理描述

对于棋盘中的每一个方块,我们都可以做四个操作:与 上 下 左 右 交换

对于每个操作,都可能产生方块的消除,进而产生新的棋盘状态,而这些新状态又可以继续操作,进而可以通过回溯法遍历每个状态来查看所有操作的可能的得分,找到最大的得分

消消乐实验回溯法(深大算法实验3)报告+代码
剪枝优化:不一定要对所有的状态都进行递归,而是选取前topk大的状态进行递归,这很容易剪掉一些显然不可能是最大答案的分支,topk的选择可以是1,3,5,7,….

消消乐实验回溯法(深大算法实验3)报告+代码
记忆化:在回溯法的基础上,将答案保存,如果遇到相同状态的棋盘,不必再次求解,直接取保存的答案,节省计算的时间开销

在代码中,通过交换两个方块并且尝试消除,得到消除之后的棋盘,再次尝试消除,这个过程通过递归直到无法消除(因为消除之后可能又有三联的情况出现)

在记忆化保存状态的时候,将棋盘转换为字符串,再计算棋盘的哈希,做哈希map映射来实现记忆化递归

算法实现的核心伪代码

回溯法
消消乐实验回溯法(深大算法实验3)报告+代码
回溯法+剪枝,只保留前k大的状态
消消乐实验回溯法(深大算法实验3)报告+代码
记忆化递归:
消消乐实验回溯法(深大算法实验3)报告+代码

算法测试结果及效率分析

回溯法:得分测试

最大得分测试:样例:老师提供的样例 k=4 n=8 m=4 x=1,2,3,4,5,6,7,8,9

移动一次:
回溯法最大得分 4
消消乐实验回溯法(深大算法实验3)报告+代码
移动两次:
回溯法最大得分:9
消消乐实验回溯法(深大算法实验3)报告+代码

移动三次
回溯法最大得分:15
消消乐实验回溯法(深大算法实验3)报告+代码

移动四次
回溯法最大得分:17
消消乐实验回溯法(深大算法实验3)报告+代码

移动五次
回溯法最大得分:20
消消乐实验回溯法(深大算法实验3)报告+代码
移动6次
回溯法最大得分:21
消消乐实验回溯法(深大算法实验3)报告+代码
移动7次:
回溯法最大得分:22
消消乐实验回溯法(深大算法实验3)报告+代码
移动8次:
回溯法最大得分:20
消消乐实验回溯法(深大算法实验3)报告+代码
移动9次
回溯法最大得分:15
消消乐实验回溯法(深大算法实验3)报告+代码
移动10次
无 法 做 到 移 动 十 次

回溯法:最大规模

因为数据是随机生成的关系,时间并不好测量,具有很大的随机性,如下例子
K=8 m=8 n=8 x=7 用时79.054 s
K=8 m=9 n=9 x=7 用时12.284 s
但是这个规模K=8 m=9 n=9 x=5的随机样例,经过大量测试,能在15s左右解完
可以近似认为这就是求解问题的最大规模了

回溯法:时间复杂度

每一个棋盘都有mn个方块,每个方块有4种可能的移动方式,一共4mn种新状态,而消去方块的复杂度是O(mn),对每种状态都要消去方块,操作的次数是x意味着递归的深度是x,时间复杂度O((m*n*m*n)x) = O((m*n)2x)

时间测试

测试规模:k=6 m=8 n=8 x=1,2,3,4,5,横坐标操作次数,纵坐标时间(s)

消消乐实验回溯法(深大算法实验3)报告+代码
消消乐实验回溯法(深大算法实验3)报告+代码
因为回溯法用时太长,导致图表结果不够直观,我们去掉回溯法的用时(如下图表)
消消乐实验回溯法(深大算法实验3)报告+代码
消消乐实验回溯法(深大算法实验3)报告+代码
总结:可以看到回溯法用时最长,记忆化有效缩短了时间,而剪枝过后时间大大短于回溯法,其中只保留最大的前1个分支,即topk=1的剪枝回溯法(其实退化为贪心法了)耗时最短,只保留最大的前7个分支,即topk=7的剪枝回溯法,用时最长,且所有剪枝回溯法的用时都与topk成正相关,topk越大,保留分支越多,用时越长,这也符合我们的认知

回溯法:不同参数规模的影响
消消乐实验回溯法(深大算法实验3)报告+代码
消消乐实验回溯法(深大算法实验3)报告+代码
可以看到随着k(方块种类)的增加,时间消耗逐渐减小,因为大的k提供了更少的消除可能性
消消乐实验回溯法(深大算法实验3)报告+代码
消消乐实验回溯法(深大算法实验3)报告+代码
可以看到随着m(行数)的增加,时间开销逐渐增大,因为更大的m,使得每一个棋盘的情况变多,使得搜索的分支数增加
消消乐实验回溯法(深大算法实验3)报告+代码
消消乐实验回溯法(深大算法实验3)报告+代码
可以看到随着n(列数)的增加,时间开销逐渐增大,因为更大的n,使得每一个棋盘的情况变多,使得搜索的分支数增加,但是与行数m对比,n对时间开销的改变没有m那么大,因为m更多的决定了消去的可能(消去方块后,沿着行方向掉落)

回溯法与剪枝回溯法:得分测试

测试规模:k=6 m=8 n=8 x=1,2,3,4,5,横坐标操作次数,纵坐标得分
消消乐实验回溯法(深大算法实验3)报告+代码
消消乐实验回溯法(深大算法实验3)报告+代码
总结:可以看到,回溯法和记忆回溯法的得分总是最大的,因为回溯法是全局最优解

而剪枝回溯法,随着保留的分支数目的增加,即随着topk的增加,得分逐渐逼近回溯法的得分(上界),这表明保留的分支越多,越容易逼近全局最优解,这也符合我们的认知

时间与得分 分析:

可以看到保留7或5个分支的剪枝回溯法最能够逼近最优解,而且耗时相当少,而且在可以接受的程度

而保留3个分支的剪枝回溯法在得分上稍显劣势,但是运行速度比5或7的剪枝快很多

而保留1个分支(即贪心法)是最快的,但是得出的得分却很低,因为一个具有丰富技巧的消消乐选手往往需要通过舍弃局部最优,来为后续的消去拼凑更优的解,这和贪心的“无后效性”矛盾,自然不能用贪心法得出最优解

带记忆化的回溯法可以有效缩短时间,但是在面对大规模问题,仍然具有相当的时间复杂度

图形演示

由于word无法展示动图,所以老师请关注我的实验答辩及ppt,或者是移步到我的GitHub上查看详细的演示
https://github.com/AKGWSB/grapic-demo-of-XiaoXiaoLe-algorithm
消消乐实验回溯法(深大算法实验3)报告+代码

对求解这个问题的经验总结

记忆化的回溯法能够有效减少时间开销,而且节省的时间开销随着规模的扩大而增加,但是在小规模问题时,因为查询和保存操作需要一定的时间,所以会略慢于回溯法,而查询和插入的过程可以通过更好的哈希函数来解决

消去方块后可能形成新的可以消去的方块,这点在计算得分的时候及其容易出错,消去方块一定要保证消到无可在消,这个过程可以用递归进行

因为回溯法是递归的,一旦有错误满盘皆错,所以在递归之前,应该改好一次回溯法的bug,否则调试起来非常困难

回溯法一定要通过小规模样例,打印回溯过程,以及结果,以确保正确性

因为for循环ij从小到大,坐标系是 ↓→ 而不是↑→,在debug的时候要注意

代码

消消乐实验回溯法(深大算法实验3)报告+代码

消消乐

#include <bits/stdc++.h>

using namespace std;

int k, m, n, ans=0;
vector<vector<int>> path, p;

int iabs(int x){return (x>=0)?(x):(-x);}

/*
function  : print 打印矩阵
param mat : 要打印的矩阵的引用
return    : ---- 
*/ 
void print(vector<vector<int>>& mat)
{
	for(int i=0; i<m; i++)
	{
		for(int j=0; j<n; j++) cout<<mat[i][j]<<" ";
		cout<<endl;
	}
}

/*
function : 交换两个数字 
param n1 : 第一个数字的引用 
param n2 : 第二个数字的引用
return   : ---- 
*/
void bswap(int& n1, int& n2)
{
	n1 ^= n2;
	n2 ^= n1;
	n1 ^= n2;
}

/*
function  : erase 消去方块并且使剩余方块下落
param mat : 方块数组的引用
return    : 消去所有可能的方块后最大得分
explain   : 遍历棋盘以计算是否出现3连号或者以上 复杂度为O(m*n) 
		  : 因为消去而产生的新的相邻3连也会被计算(返回时尾递归)
*/
int erase(vector<vector<int>>& mat)
{
	int cnt3=0, cnt4=0, cnt5=0;
	// 计算3,4,5的连续块个数
	for(int i=0; i<m; i++)
	{
		for(int j=0; j<n; j++)
		{
			if(i+2<m && mat[i][j]!=0)	// 长度为3,纵块 
			{
				if(iabs(mat[i][j])==iabs(mat[i+1][j])&&
				   iabs(mat[i+1][j])==iabs(mat[i+2][j])) 
					mat[i][j]=mat[i+1][j]=mat[i+2][j]=-iabs(mat[i][j]), cnt3++;
			}
			if(j+2<n && mat[i][j]!=0)	// 长度为3,横块 
			{
				if(iabs(mat[i][j])==iabs(mat[i][j+1])&&
				   iabs(mat[i][j+1])==iabs(mat[i][j+2])) 
					mat[i][j]=mat[i][j+1]=mat[i][j+2]=-iabs(mat[i][j]), cnt3++;
			}
			if(i+3<m && mat[i][j]!=0)	// 长度为4,纵块 
			{
				if(iabs(mat[i][j])==iabs(mat[i+1][j])&&
				   iabs(mat[i+1][j])==iabs(mat[i+2][j])&&
				   iabs(mat[i+2][j])==iabs(mat[i+3][j])) 
					mat[i][j]=mat[i+1][j]=mat[i+2][j]=mat[i+3][j]=-iabs(mat[i][j]), cnt4++;
			}
			if(j+3<n && mat[i][j]!=0)	// 长度为4,横块 
			{
				if(iabs(mat[i][j])==iabs(mat[i][j+1])&&
				   iabs(mat[i][j+1])==iabs(mat[i][j+2])&&
				   iabs(mat[i][j+2])==iabs(mat[i][j+3])) 
					mat[i][j]=mat[i][j+1]=mat[i][j+2]=mat[i][j+3]=-iabs(mat[i][j]), cnt4++;
			}
			if(i+4<m && mat[i][j]!=0)	// 长度为5,纵块 
			{
				if(iabs(mat[i][j])==iabs(mat[i+1][j])&&
				   iabs(mat[i+1][j])==iabs(mat[i+2][j])&&
				   iabs(mat[i+2][j])==iabs(mat[i+3][j])&&
				   iabs(mat[i+3][j])==iabs(mat[i+4][j])) 
					mat[i][j]=mat[i+1][j]=mat[i+2][j]=mat[i+3][j]=mat[i+4][j]=-iabs(mat[i][j]), cnt5++;
			}
			if(j+4<n && mat[i][j]!=0)	// 长度为5,横块 
			{
				if(iabs(mat[i][j])==iabs(mat[i][j+1])&&
				   iabs(mat[i][j+1])==iabs(mat[i][j+2])&&
				   iabs(mat[i][j+2])==iabs(mat[i][j+3])&&
				   iabs(mat[i][j+3])==iabs(mat[i][j+4])) 
					mat[i][j]=mat[i][j+1]=mat[i][j+2]=mat[i][j+3]=mat[i][j+4]=-iabs(mat[i][j]), cnt5++;
			}
		}
	}
	// 方块下落 
	for(int j=0; j<n; j++)
	{
		int st=m-1, ed=0, dst=0, delta=0;
		while(st>=0 && mat[st][j]>0) st--;
		for(ed=st; ed>=0&&mat[ed][j]<=0; ed--){}
		delta = st-ed;
		for(int i=ed; i>=0; i--) 
			mat[i+delta][j]=mat[i][j], mat[i][j]=0;
	}
	for(int i=0; i<m; i++)
		for(int j=0; j<n; j++) mat[i][j]=max(mat[i][j], 0);
	cnt4-=2*cnt5, cnt3-=(3*cnt5+2*cnt4);		// 消去重复的计数 
	if(cnt3 + cnt4*4 + cnt5*10 == 0) return 0;	// 是否需要消去新的连号 
	return cnt3 + cnt4*4 + cnt5*10 + erase(mat);
}

/*
function   : dfs 深度优先搜索,搜最大得分的消去方式
param mat  : 方块数组的引用
param step : 执行交换的次数
param sum  : 当前递归树节点得分值 
return     : ----
explain    : 最优答案(全局变量)会在递归达到最深时被更新 
*/
void dfs(vector<vector<int>>& mat, int step, int sum)
{
	if(step==0)
	{
		if(sum > ans)
			ans=sum, path=p;
		return;
	}
	vector<vector<int>> a;
	for(int i=0; i<m; i++)
	{
		for(int j=0; j<n; j++)
		{
			if(i+1<m && mat[i][j] && mat[i+1][j])
			{
				a=mat, bswap(a[i][j], a[i+1][j]);
				int res = erase(a);
				if(res>0) 
				{
					p.push_back(vector<int>{i,j,i+1,j});
					dfs(a, step-1, sum+res);
					p.pop_back();
				}
			}
			if(j+1<n && mat[i][j] && mat[i][j+1])
			{
				a=mat, bswap(a[i][j], a[i][j+1]);
				int res = erase(a);
				if(res>0) 
				{
					p.push_back(vector<int>{i,j,i,j+1});
					dfs(a, step-1, sum+res);
					p.pop_back();	
				}
			}
		}
	}
}

/*
function cmp : 比较函数,存储状态时对状态按照得分降序排序时使用
param v1     : 第1个状态的引用 
param v2     : 第2个状态的引用 
return 		 : 比较结果 
explain      : 状态保存为一维数组[得分,x1,y1,x2,y2] xy为交换的方块的坐标 
*/
bool cmp(const vector<int>& v1, const vector<int>& v2)
{
	return v1[0]>v2[0];
}

/*
function   : 贪心法+dfs:每次选择得分前 k 大的分支 
param mat  : 方块数组的引用
param step : 执行交换的次数
param tpk  : 选择前 tpk 大的状态进行递归 
return     : ----
explain    : 不能保证得到最优解 
*/
void dfs_topk(vector<vector<int>>& mat, int step, int sum, int tpk)
{
	if(step==0)
	{
		if(sum > ans)
			ans=sum, path=p;
		return;
	}
	vector<vector<int>> a, seq;
	int maxv=0,x1=0,y1=0,x2=0,y2=0,res;
	for(int i=0; i<m; i++)
	{
		for(int j=0; j<n; j++)
		{
			if(i+1<m && mat[i][j] && mat[i+1][j])
			{
				a=mat, bswap(a[i][j], a[i+1][j]);
				res = erase(a);
				if(res > maxv) seq.push_back(vector<int>{res,i,j,i+1,j});
			}
			if(j+1<n && mat[i][j] && mat[i][j+1])
			{
				a=mat, bswap(a[i][j], a[i][j+1]);
				res = erase(a);
				if(res > maxv) seq.push_back(vector<int>{res,i,j,i,j+1});
			}
		}
	}
	sort(seq.begin(), seq.end(), cmp);
	for(int i=0; i<min((int)seq.size(),tpk); i++)
	{
		a=mat, bswap(a[seq[i][1]][seq[i][2]], a[seq[i][3]][seq[i][4]]);
		res = erase(a);
		p.push_back(vector<int>(seq[i].begin()+1, seq[i].end()));
		dfs_topk(a, step-1, sum+res, tpk);
		p.pop_back();
	}
}

/*
function   : mat2str 矩阵转为字符串,为下面hashmap查表做key用 
param v    : 棋盘矩阵 
param step : 当前走的步数
return     : 转换后的字符串,因为STL有提供string的hash函数,方便做key查表 
*/
string mat2str(vector<vector<int>>& v, int step)
{
	string str = "";
	for(int i=0; i<v.size(); i++)
		for(int j=0; j<v[0].size(); j++) str += (char)(v[i][j]+'0');
	str += (char)(step+'0');
	return str;
}

/* hashmap 哈希表保存棋盘状态与得分,key=棋盘 value=得分 */
unordered_map<string, int> hashmap;

/*
function   : dfs + mem 记忆化的深度优先搜索,也是动态规划的一种实现
param mat  : 方块数组的引用
param step : 执行交换的次数
return     : 当前棋盘能够获得的最大价值 
explain    : 采用记忆化的策略,计算答案之前先查看这个棋盘是否之前被计算过 
		   : 得到的是全局最优解 
*/
int dfs_mem(vector<vector<int>>& mat, int step)
{
	if(step==0) return 0;
	// 递归前先查表,有则直接返回 
	vector<vector<int>> a; 
	string key = mat2str(mat, step);
	if(hashmap[key] != 0) return hashmap[key]; 
	// 穷举状态开始递归 
	int maxv=0, val=0;
	for(int i=0; i<m; i++)
	{
		for(int j=0; j<n; j++)
		{
			if(i+1<m && mat[i][j] && mat[i+1][j])
			{
				a=mat, bswap(a[i][j], a[i+1][j]);
				int res = erase(a);
				if(res>0) 
				{
					p.push_back(vector<int>{i,j,i+1,j});
					val = dfs_mem(a, step-1);
					maxv = max(maxv, val+res);
					p.pop_back();
				}
			}
			if(j+1<n && mat[i][j] && mat[i][j+1])
			{
				a=mat, bswap(a[i][j], a[i][j+1]);
				int res = erase(a);
				if(res>0) 
				{
					p.push_back(vector<int>{i,j,i,j+1});
					val = dfs_mem(a, step-1);
					maxv = max(maxv, val+res);
					p.pop_back();	
				}
			}
		}
	}
	// 保存这一次的结果 
	hashmap[key] = maxv;
	return maxv;
}

/*
function   : printPath 打印一次dfs之后获得的路径以及答案 
param name : 方法名称  
param src  : 路径的目标棋盘矩阵 引用
return     : ----
*/
void printPath(string name, vector<vector<int>> src)
{
	cout<<endl<<"  >>> "<<name<<"过程如下: <<<"<<endl<<endl;
	print(src);
	for(int k=0; k<path.size(); k++)
	{
		cout<<"对上面 ↑棋盘进行交换 "; 
		cout<<"("<<path[k][0]<<","<<path[k][1]<<") <--> ";
		cout<<"("<<path[k][2]<<","<<path[k][3]<<") "<<"得到下面的棋盘 ↓:"<<endl;
		cout<<"棋盘状态:"<<endl;
		bswap(src[path[k][0]][path[k][1]], src[path[k][2]][path[k][3]]);
		erase(src);
		print(src);
	}
	cout<<name<<"最大得分是: "<<ans<<endl;
}

int main()
{	
	#define k_types 6
	#define row 7
	#define col 7 
	#define x_step 1
	#define select_top_k 3
	#define sample 20
	k=k_types, m=row, n=col;
	
	clock_t start, end;
	
	/*
	// 去掉这部分的注释可以实现:实验要求1 手动输入数据,计算不同x,不同算法得分  
	vector<vector<int>> mat(m), m1, m2;
	for(int i=0; i<m; i++) mat[i].resize(n);
	for(int i=0; i<m; i++)
		for(int j=0; j<n; j++) cin>>mat[i][j];
	
	// 回溯法 
	ans=0, m1=mat;
	start = clock();
	dfs(m1, x_step, 0);
	end = clock();
	printPath("回溯法", m1);
	cout<<"回溯法用时: "<<(double)(end-start)/CLOCKS_PER_SEC<<" s"<<endl;
	
	// 回溯+贪心,选择前 select_top_k 大的分支进行dfs 
	ans=0, m2=mat;
	start = clock();
	dfs_topk(m2, x_step, 0, select_top_k);
	end = clock();
	printPath("回溯+贪心法", m2);
	cout<<"回溯+贪心法用时: "<<(double)(end-start)/CLOCKS_PER_SEC<<" s"<<endl;
	*/
	
	/*
	// 去掉这部分的注释可以实现:实验要求2 随机生成数据,测试极限规模 
	default_random_engine rd(time(NULL));
	uniform_int_distribution<int> dist(1, k_types);
	
	vector<vector<int>> mat(m), m1, m2;
	for(int i=0; i<m; i++) mat[i].resize(n);
	while(1)
	{
		for(int i=0; i<m; i++)
			for(int j=0; j<n; j++) mat[i][j]=dist(rd);
		if(erase(mat)==0) break;
	}
	cout<<"数据生成完毕, 执行步数为 "<<x_step<<endl;
	
	ans=0, m1=mat;
	start = clock();
	dfs(m1, x_step, 0);
	end = clock();
	printPath("回溯法", m1);
	cout<<"回溯法用时: "<<(double)(end-start)/CLOCKS_PER_SEC<<" s"<<endl;
	*/
	
	// 实验要求3 随机生成数据,测试时间 
	double t1=0,t2=0,t3=0,t4=0,t5=0,t6=0,s1=0,s2=0,s3=0,s4=0,s5=0,s6=0;
	default_random_engine rd(time(NULL));
	uniform_int_distribution<int> dist(1, k_types);
	
	int tt = sample;
	while(tt--)
	{
		vector<vector<int>> mat(m), m1;
		for(int i=0; i<m; i++) mat[i].resize(n);
		while(1)
		{
			for(int i=0; i<m; i++)
				for(int j=0; j<n; j++) mat[i][j]=dist(rd);
			if(erase(mat)==0) break;
		}
		cout<<"数据生成完毕 "<<sample-tt<<endl;
		
		// 回溯法 
		ans=0, m1=mat;
		start = clock();
		dfs(m1, x_step, 0);
		end = clock();
		t1 += (double)(end-start)/CLOCKS_PER_SEC;
		s1 += ans; 
		
		// 每次递归最大的前 1 个分支 
		ans=0, m1=mat;
		start = clock();
		//dfs_topk(m1, x_step, 0, 1);
		end = clock();
		t2 += (double)(end-start)/CLOCKS_PER_SEC;
		s2 += ans; 
		
		// 每次递归最大的前 3 个分支
		ans=0, m1=mat;
		start = clock();
		//dfs_topk(m1, x_step, 0, 3);
		end = clock();
		t3 += (double)(end-start)/CLOCKS_PER_SEC;
		s3 += ans; 
		
		// 每次递归最大的前 5 个分支
		ans=0, m1=mat;
		start = clock();
		//dfs_topk(m1, x_step, 0, 5);
		end = clock();
		t4 += (double)(end-start)/CLOCKS_PER_SEC;
		s4 += ans; 
		
		// 每次递归最大的前 7 个分支
		ans=0, m1=mat;
		start = clock();
		//dfs_topk(m1, x_step, 0, 7);
		end = clock();
		t5 += (double)(end-start)/CLOCKS_PER_SEC;
		s5 += ans; 
		
		// 记忆化
		ans=0, m1=mat;
		hashmap.clear();	// 清除记忆 
		start = clock();
		//ans = dfs_mem(m1, x_step);
		end = clock();
		t6 += (double)(end-start)/CLOCKS_PER_SEC;
		s6 += ans; 
	} 

	
	cout<<"步数为 "<<x_step<<endl;
	cout<<"回溯:  平均用时 "<<t1/sample<<" s, 平均得分 "<<s1/sample<<endl;
	cout<<"topk=1 平均用时 "<<t2/sample<<" s, 平均得分 "<<s2/sample<<endl;
	cout<<"topk=3 平均用时 "<<t3/sample<<" s, 平均得分 "<<s3/sample<<endl;
	cout<<"topk=5 平均用时 "<<t4/sample<<" s, 平均得分 "<<s4/sample<<endl;
	cout<<"topk=7 平均用时 "<<t5/sample<<" s, 平均得分 "<<s5/sample<<endl;
	cout<<"记忆化 平均用时 "<<t6/sample<<" s, 平均得分 "<<s6/sample<<endl;
	
	cout<<t1/sample<<endl;
	cout<<t2/sample<<endl;
	cout<<t3/sample<<endl;
	cout<<t4/sample<<endl;
	cout<<t5/sample<<endl;
	cout<<t6/sample<<endl<<endl;
	
	cout<<s1/sample<<endl;
	cout<<s2/sample<<endl;
	cout<<s3/sample<<endl;
	cout<<s4/sample<<endl;
	cout<<s5/sample<<endl;
	cout<<s6/sample<<endl<<endl;
	
	return 0;
}

/*
样例1 
3 3 4 3
3 2 3 3
2 4 3 4
1 3 4 3
3 3 1 1
3 4 3 3
1 4 4 3
1 2 3 2

自定义样例 
3 3 4 3 1 3 4 3
3 2 3 3 1 2 3 2
2 4 3 4 3 3 1 1
1 3 4 3 2 4 3 4
3 3 1 1 3 3 1 1
3 4 3 3 2 4 3 4
1 4 4 3 1 2 3 2
1 2 3 2 4 3 2 1

// 测试消除结果 样例 debug用 
3 4 3 3
3 2 4 3
2 3 3 4
1 4 4 3
3 3 1 1
4 4 1 3
1 3 1 3
1 1 1 1

0 0 0 0
0 3 4 3
0 2 3 3
3 4 3 4
3 3 4 3
2 4 3 3
1 4 4 3
1 2 3 2

0 3 4 3
0 2 3 3
0 4 3 4
3 3 4 3
3 1 1 1
2 4 3 3
1 4 4 3
1 2 3 2

*/

图形演示

import matplotlib.pyplot as plt
import matplotlib.animation as animation
import copy

'''
Author   :  RuoLongLi 2018171028 @SZU @CSSE 
time     : 2020/4/28
function : A graphic demo of 'xiao xiao le' game's backtracking(dfs) algorithm 
input    : a m*n mat, mat[i][j] represents the color of blocks (i, j) 
output   : a graphic demo (include backtracking process)
作    者 :  李若龙 2018171028 深大计软
时    间 : 2020 年 4 月 28 号
功    能 : 消消乐实验回溯法图形演示
输    入 : m*n的矩阵,mat[i][j] 表示 (i,j) 位置的颜色
输    出 : 图形颜色结果(包含回溯过程)
'''

'''
sample input: 
样例输入:
3 3 4 3
3 2 3 3
2 4 3 4
1 3 4 3
3 3 1 1
3 4 3 3
1 4 4 3
1 2 3 2
'''

colors = ['white', 'red', 'blue', 'yellow', 'black']
k,m,n,x = 4,8,4,3
artists = []

'''
function  : getCir 得到所有圆的artist对象
param mat : 棋盘数组
return    : 所有圆的artist对象(list[])
'''
def getCir(mat):
    fr = []
    for i in range(m):
        for j in range(n):
            fr += plt.plot(j*10+40,80-i*10, "o", color=colors[mat[i][j]], markersize=19)
    return fr

# 绝对值
def iabs(x):
    if x<0:
        return -x
    return x

# 打印矩阵
def printm(mat):
    for i in range(m):
        for j in range(n):
            print(mat[i][j],' ', end='')
        print()
    print()

# 根据坐标在frame帧上绘制矩形
def getRectangle(frame, x1, y1, x2, y2, c):
    frame += plt.plot([x1, x2], [y1, y1], color=c)
    frame += plt.plot([x1, x2], [y2, y2], color=c)
    frame += plt.plot([x1, x1], [y1, y2], color=c)
    frame += plt.plot([x2, x2], [y1, y2], color=c)
    return frame

'''
function  : erase 消去方块并且使方块下落
param mat : 棋盘数组的引用
return    : 消去后增加的得分
'''
def erase(mat):
    cnt3,cnt4,cnt5 = 0,0,0
    # 计算3,4,5的连续块个数
    for i in range(m):
        for j in range(n):
            if i+2<m and mat[i][j]!=0 :
                if (iabs(mat[i][j])==iabs(mat[i+1][j]) and
                    iabs(mat[i+1][j])==iabs(mat[i+2][j])):
                    mat[i][j]=-iabs(mat[i][j])
                    mat[i+1][j]=-iabs(mat[i][j])
                    mat[i+2][j]=-iabs(mat[i][j])
                    cnt3 += 1
            if j+2<n and mat[i][j]!=0 :
                if (iabs(mat[i][j])==iabs(mat[i][j+1]) and
                    iabs(mat[i][j+1])==iabs(mat[i][j+2])) :
                    mat[i][j]=-iabs(mat[i][j])
                    mat[i][j+1]=-iabs(mat[i][j])
                    mat[i][j+2]=-iabs(mat[i][j])
                    cnt3 += 1
            if i+3<m and mat[i][j]!=0 :
                if (iabs(mat[i][j])==iabs(mat[i+1][j]) and
                    iabs(mat[i+1][j])==iabs(mat[i+2][j]) and
                    iabs(mat[i+2][j])==iabs(mat[i+3][j])):
                    mat[i][j]=-iabs(mat[i][j])
                    mat[i+1][j]=-iabs(mat[i][j])
                    mat[i+2][j]=-iabs(mat[i][j])
                    mat[i+3][j]=-iabs(mat[i][j])
                    cnt4 += 1
            if j+3<n and mat[i][j]!=0:
                if (iabs(mat[i][j])==iabs(mat[i][j+1]) and
                    iabs(mat[i][j+1])==iabs(mat[i][j+2]) and
                    iabs(mat[i][j+2])==iabs(mat[i][j+3])) :
                    mat[i][j]=-iabs(mat[i][j])
                    mat[i][j+1]=-iabs(mat[i][j])
                    mat[i][j+2]=-iabs(mat[i][j])
                    mat[i][j+3]=-iabs(mat[i][j])
                    cnt4 += 1
            if i+4<m and mat[i][j]!=0:
                if (iabs(mat[i][j])==iabs(mat[i+1][j]) and
                    iabs(mat[i+1][j])==iabs(mat[i+2][j]) and
                    iabs(mat[i+2][j])==iabs(mat[i+3][j]) and
                    iabs(mat[i+3][j])==iabs(mat[i+4][j])):
                    mat[i][j]=-iabs(mat[i][j])
                    mat[i+1][j]=-iabs(mat[i][j])
                    mat[i+2][j]=-iabs(mat[i][j])
                    mat[i+3][j]=-iabs(mat[i][j])
                    mat[i+4][j]=-iabs(mat[i][j])
                    cnt5 += 1
            if j+4<n and mat[i][j] != 0:
                if (iabs(mat[i][j])==iabs(mat[i][j+1]) and
                    iabs(mat[i][j+1])==iabs(mat[i][j+2]) and
                    iabs(mat[i][j+2])==iabs(mat[i][j+3]) and
                    iabs(mat[i][j+3])==iabs(mat[i][j+4])):
                    mat[i][j]=-iabs(mat[i][j])
                    mat[i][j+1]=-iabs(mat[i][j])
                    mat[i][j+2]=-iabs(mat[i][j])
                    mat[i][j+3]=-iabs(mat[i][j])
                    mat[i][j+4]=-iabs(mat[i][j])
                    cnt5 += 1
    # 方块下落
    for j in range(n):
        st = m-1
        while st>=0 and mat[st][j]>0 :
            st -= 1
        ed = st
        while ed>=0 and mat[ed][j]<=0 :
            ed -= 1
        delta = st-ed
        for i in range(ed,-1,-1):
            mat[i+delta][j] = mat[i][j]
            mat[i][j] = 0
    for i in range(m):
        for j in range(n):
            if mat[i][j]<0 :
                mat[i][j] = 0
    # 消去重复的计数
    cnt4 -= 2*cnt5
    cnt3 -= (3*cnt5 + 2*cnt4)
    if cnt3+cnt4*4+cnt5*10 == 0 :
        return 0
    return cnt3 + cnt4*4 + cnt5*10 + erase(mat)

'''
function   : dfs_tpk
param mat  : 消消乐棋盘矩阵 
param step : 当前递归树的深度
param sum  : 当前得分和
param tpk  : 选取前 tpk 大的分支进行递归
return     : ----
'''
def dfs_tpk(mat, step, sum, tpk):
    if step==0 :
        return
    seq = []
    # 消去检测,获得所有分支
    for i in range(m):
        for j in range(n):
            if i+1<m and mat[i][j]>0 and mat[i+1][j]>0 :
                a = copy.deepcopy(mat)
                a[i][j],a[i+1][j] = a[i+1][j],a[i][j]
                res = erase(a)
                if res>0 :
                    seq.append([res,i,j,i+1,j])
            if j+1<n and mat[i][j]>0 and mat[i][j+1]>0 :
                a = copy.deepcopy(mat)
                a[i][j], a[i][j+1] = a[i][j+1], a[i][j]
                res = erase(a)
                if res>0 :
                    seq.append([res,i,j,i,j+1])
    # 按照得分排序
    for i in range(seq.__len__()):
        for j in range(seq.__len__()-1):
            if seq[j][0] < seq[j+1][0]:
                seq[j],seq[j+1] = seq[j+1],seq[j]
    # 递归tpk大的分支
    for i in range(min(seq.__len__(), tpk)):
        a = copy.deepcopy(mat)
        artists.append(getCir(a))
        # 打印矩形框同时打印方块们
        x1,y1,x2,y2 = seq[i][1],seq[i][2],seq[i][3],seq[i][4]
        x1,x2 = min(x1,x2), max(x1,x2)
        y1,y2 = min(y1,y2), max(y1,y2)
        y1-=0.5
        y2+=0.5
        x1-=0.5
        x2+=0.5
        artists.append(getRectangle(getCir(a),y1*10+40,80-x1*10,y2*10+40,80-x2*10, 'red'))
        a[seq[i][1]][seq[i][2]], a[seq[i][3]][seq[i][4]] = a[seq[i][3]][seq[i][4]], a[seq[i][1]][seq[i][2]]
        artists.append(getRectangle(getCir(a),y1*10+40,80-x1*10,y2*10+40,80-x2*10, 'blue'))
        # 消除方块,递归
        res = erase(a)
        dfs_tpk(a, step-1, sum+res, tpk)
        # 回溯打印
        artists.append(getCir(a))

if __name__ == '__main__':

    mat = []
    for i in range(m):
        mat.append(list(map(int,input().split(" "))))

    '''
    for i in range(m):
        for j in range(n):
            print(mat[i][j], end='')
        print()
    '''

    fig = plt.figure()
    plt.xlim(-10, 140)
    plt.ylim(-10, 100)
    mngr = plt.get_current_fig_manager()  # 获取当前figure manager
    mngr.window.wm_geometry("+380+50")  # 调整窗口在屏幕上弹出的位置

    dfs_tpk(mat, 3, 0, 3)

    ain = animation.ArtistAnimation(fig=fig, artists=artists, interval=1000)
    plt.show()
    ain.save('demo.gif', fps=2)