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

实验代码 + 报告资源:
链接: https://pan.baidu.com/s/1CuuB07rRFh7vGQnGpud_vg 
提取码: ccuq 

写在前面

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

这学期算法课可谓是最难顶的课程了,又正好是线上上课,提问互动的机会相对较少,老师上课抛砖引玉,实验内容又比较难,我花了大部分的时间在找算法,实现算法,改算法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,可加分。
    在这里插入图片描述

实验要求:

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

求解问题的算法原理描述

消去方块

这个消去方块比较难搞,我是用长度为 3/4/5 的横竖条去覆盖棋盘,看看有没有相等的三联,四联,五联,统计数目为 cnt3, cnt4, cnt5,然后将他们标记成负数,方便之后消除。

覆盖结束后,消除所有负数的方块,然后下落。注意下落后要再次判断,因为可能可以再消。用递归

因为四联,五联包含三联,所以要去重重复的计数。一个五联包含两个四联和三个三联,一个四联包含2个三联。

cnt4-=2*cnt5, cnt3-=(3*cnt5+2*cnt4);		// 消去重复的计数 

回溯法

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

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

在这里插入图片描述
剪枝优化:不一定要对所有的状态都进行递归,而是选取前topk大的状态进行递归,这很容易剪掉一些显然不可能是最大答案的分支,topk的选择可以是1,3,5,7,….

在这里插入图片描述
记忆化:在回溯法的基础上,将答案保存,如果遇到相同状态的棋盘,不必再次求解,直接取保存的答案,节省计算的时间开销

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

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

算法实现的核心伪代码

回溯法
在这里插入图片描述
回溯法+剪枝,只保留前k大的状态
在这里插入图片描述
记忆化递归:
在这里插入图片描述

算法测试结果及效率分析

回溯法:得分测试

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

移动一次:
回溯法最大得分 4
在这里插入图片描述
移动两次:
回溯法最大得分:9
在这里插入图片描述

移动三次
回溯法最大得分:15
在这里插入图片描述

移动四次
回溯法最大得分:17
在这里插入图片描述

移动五次
回溯法最大得分:20
在这里插入图片描述
移动6次
回溯法最大得分:21
在这里插入图片描述
移动7次:
回溯法最大得分:22
在这里插入图片描述
移动8次:
回溯法最大得分:20
在这里插入图片描述
移动9次
回溯法最大得分:15
在这里插入图片描述
移动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)

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

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

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

测试规模:k=6 m=8 n=8 x=1,2,3,4,5,横坐标操作次数,纵坐标得分
在这里插入图片描述
在这里插入图片描述
总结:可以看到,回溯法和记忆回溯法的得分总是最大的,因为回溯法是全局最优解

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

时间与得分 分析:

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

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

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

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

图形演示

由于word无法展示动图,所以老师请关注我的实验答辩及ppt,或者是移步到我的GitHub上查看详细的演示
https://github.com/AKGWSB/grapic-demo-of-XiaoXiaoLe-algorithm
在这里插入图片描述

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

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

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

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

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

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

代码

在这里插入图片描述

消消乐

#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]
  • 1
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这是一个使用 HTML 和 JavaScript 编写的基本的 3x3 随机数字游戏代码: ```html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>随机数字游戏</title> <style> table { border-collapse: collapse; } td { width: 50px; height: 50px; text-align: center; vertical-align: middle; font-size: 24px; font-weight: bold; border: 1px solid black; } td.selected { background-color: yellow; } td.matched { background-color: green; color: white; } </style> </head> <body> <h1>随机数字游戏</h1> <table id="board"> <!-- 游戏板格子 --> </table> <p> <button id="reset">重新开始</button> </p> <script> // 创建一个 3x3 的游戏板 var board = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; // 随机填充每个位置 for (var i = 0; i < 3; i++) { for (var j = 0; j < 3; j++) { board[i][j] = Math.floor(Math.random() * 9) + 1; } } // 创建游戏板格子并添加到 HTML 页面中 var table = document.getElementById("board"); for (var i = 0; i < 3; i++) { var row = document.createElement("tr"); for (var j = 0; j < 3; j++) { var cell = document.createElement("td"); cell.innerText = board[i][j]; cell.addEventListener("click", cellClicked); row.appendChild(cell); } table.appendChild(row); } // 当前选中的格子 var selectedCell = null; // 处理格子的点击事件 function cellClicked(event) { var cell = event.target; if (cell.classList.contains("matched")) { return; // 已经匹配的格子不能再次选择 } if (selectedCell == null) { cell.classList.add("selected"); selectedCell = cell; } else { var x1 = selectedCell.parentNode.rowIndex; var y1 = selectedCell.cellIndex; var x2 = cell.parentNode.rowIndex; var y2 = cell.cellIndex; if (Math.abs(x1 - x2) + Math.abs(y1 - y2) != 1) { alert("您选择的两个位置不相邻,请重新选择!"); } else { // 交换两个位置的数字 var temp = board[x1][y1]; board[x1][y1] = board[x2][y2]; board[x2][y2] = temp; selectedCell.innerText = board[x1][y1]; cell.innerText = board[x2][y2]; checkMatches(); } selectedCell.classList.remove("selected"); selectedCell = null; } } // 检查是否存在相同的三个数字 function checkMatches() { var foundMatch = false; for (var i = 0; i < 3; i++) { if (board[i][0] == board[i][1] && board[i][1] == board[i][2]) { foundMatch = true; markMatched(i, 0, i, 1, i, 2); } if (board[0][i] == board[1][i] && board[1][i] == board[2][i]) { foundMatch = true; markMatched(0, i, 1, i, 2, i); } } if (foundMatch) { alert("恭喜你,游戏结束!"); document.getElementById("reset").disabled = false; } } // 标记匹配的格子 function markMatched(x1, y1, x2, y2, x3, y3) { var cells = [ table.rows[x1].cells[y1], table.rows[x2].cells[y2], table.rows[x3].cells[y3] ]; for (var i = 0; i < 3; i++) { cells[i].classList.add("matched"); } } // 按钮的点击事件 document.getElementById("reset").addEventListener("click", function() { window.location.reload(); }); </script> </body> </html> ``` 这个游戏会随机生成一个 3x3 的游戏板,每个位置填充一个 1~9 的数字。用户可以选择两个相邻的位置交换,如果交换后存在三个相同的数字排成一行或一列,则游戏结束。游戏结束后,用户可以点击重新开始按钮重新开始游戏。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值