前言

这是一个非常非常简单的消消乐游戏。在一个 M*N 的网格中,一共有种物品。玩家可以列向或者行向得交换两个物品。当列向或者行向存在3 ~ 5个连续的相同物品时就可以消除将这些物品。消除3,4,5个连续的相同物品的得分分别为1,3,10。当物品被消除之后,上方的物品会垂直下落对空缺进行填补。如果填补之后依旧存在3~5个连续且相同的物品,则继续进行消除。如果交换操作无法消除任何物品,则禁止该操作。题目要求在指定的步数内拿到最高的分数。

如上图所示,交换了图中2和3之后,会存在连续的4个2和3个3的情况,然后会一起消除,得分为 1 + 3 = 4分。事实上,再消除完成之后,上方的物品掉落下来会出现连续的3个2和4个4还可以消除。

写个简单的消消乐

基本算法思路及需要解决的问题

基本的算法思路如下:

  1. 交换两个需要交换的点;
  2. 从交换的点出发,分别统计上下左右与之连续且相同的元素的数量,并按照记分规则进行记分。如果交换之后无法消除任何元素,则禁止点的交换。
  3. 消除需要消除的元素,然后将消除的元素上方的元素进行下移操作以填补空缺,顶部落空的位置则标记为无物品;
  4. 进行全盘的检查,对每个点按照步骤2中的方法进行标记记分;
  5. 如果全盘检查发现还存在可以消除的元素,则回到步骤3进行执行,否则结束检查调整。

为了实现用回溯法解决计算消消乐游戏最大得分问题,我们需要先实现一个简单的消消乐游戏。消消乐游戏需要解决的两个主要的问题如下(这里称游戏的网格为棋盘):

  • 检查是否存在连续 3~5个连续且相同物品
  • 计算消除物品之后的得分
  • 消除物品和物品下落之后棋盘调整

问题1: 检查是否存在连续 3~5个连续且相同物品

根据游戏规则,每次操作都是在交换了两个相邻的点,那么我们只要从交换的点开始进行对上下左右的四个方向进行检查。为了方便程序的实现,我们实现了如下四个方向的Enum类,其中的Point类只有rowcol两个属性。

ArrowE.class

public enum ArrowE {UP(1, "^", "up", new Point(-1, 0)),RIGHT(2, ">", "right", new Point(0, 1)),DOWN(3, "v", "down", new Point(1, 0)),LEFT(4, "<", "left", new Point(0, -1));private final int value;private final String arrow;private final String name;private final Point vector;private static final Map<ArrowE, ArrowE> arrowMapReverse;static {arrowMapReverse = new HashMap<ArrowE, ArrowE>();addReverse(ArrowE.UP, ArrowE.DOWN);addReverse(ArrowE.LEFT, ArrowE.RIGHT);}// getters and setters...// 获取相反方向的方法public ArrowE getReverse() {return arrowMapReverse.get(this);}
}

基本的思路很简单,从一个点出发,分别计算向上和下与之相同的物品的总数numUpnumDown,以及分别向左和向右与相同的物品的总数numLeftnumRight。然后按照计算规则计算 numUp + 1 + numDownnumLeft + 1 + numRight 的得分即可,如果得分大于 0,则执行物品消除的操作,否则不计算进行后续操作。

按照上面的逻辑,似乎很简单,而且马上就可以进行消除了,似乎只要消除交换点和numUp numDown numLeft numRight规定的范围就可以了。然而在实际的操作中,会出现更加复杂的场景。如下图:

在正常的交换之后,可以通过上面描述的方式对交换点的行列进行检查,并标记可消除的位置。但是交换之后,由于物品的掉落,会出现非常复杂的场景,如上图中的图三。甚至还可以出现更加复杂的情况,就是出现多个可以消除的不相连的区域。对于这种情况,只能对整个棋盘进行检查判断,找到所有可以消除的区域。

上图中的图三的可消除区域存在2个行和4个列,其中的一些位置会同时作用于一个行和列当中,如[3, 0], [3, 1]等。通过设计一个标记值,标记一个元素参与了行或者是列的消除。标记值为1时,表示该元素会在行中消除,2是为列中消除,3表示同时在行和列中消除(这里的1 + 2 = 3非常的巧妙)。如果一个点参与了行的消除,那么这个点就不能再参与其他行的消除,但可以继续参与其他列的消除。同理,如果这个点参与了列的消除,则这个点就不能再参与其他列的消除,但可以参与其他列的消除。当标记位置的值为3时,则该点无法继续参与其他行或者列的消除。

由于需要消除的元素构成的区域,无法简单的从一个点向上下左右进行拓展,因此需要设计一个二维数组进行标记。上图图3可以得到如下的标记位图。

在进行标记的过程中,可以按照简单版的方式进行记分,也可以直接对标记的二维数组进行行列统计记分。

问题2: 计算消除物品之后的得分

上面已经简单的说明了,这里就不再重复说明。

问题3: 消除物品和物品下落之后棋盘调整

消除操作对于整个过程来说也比较重要,但说简单也不简单,说复杂也不复杂。根据二维标记数组,找到存在需要消除的元素的列,进行列上的下落操作。核心的代码也就只有如下的一行:

chessboard[row][col] = chessboard[row - offset][col];

列消除下落的完整代码如下:

/*** 将第 col 列的,位于(row, col)上方offset距离的点下落位置到从(row, col)开始向上的位置* 这将会从(row, col)开始覆盖* <pre>* 0 0 0  0  0* 0 0 0  1  0* 0 0 0  x  0* 0 0 0  x  0* 0 0 0  *  0* 0 0 0 (*) 0*    v* row = row(*),col=col(*),offset=num(*)*    v* 0 0 0  0  0* 0 0 0  0  0* 0 0 0  0  0* 0 0 0  1  0* 0 0 0  x  0* 0 0 0  x  0* </pre>* @param row 目的位置的行号* @param col 目的位置的列号* @param offset 下降距离*/
private void dropColDownTo(int row, int col, int offset) {if(offset == 0) {return;}while(row - offset >= 0) {// 如果 chessboard[row][col] == 0,则表示 chessboard[row][col] 上方的所有点都是 0chessboard[row][col] = chessboard[row - offset][col];row --;}while(row >= 0 && chessboard[row][col] != 0) {chessboard[row][col] = 0;row --;}
}

完整的对整个棋盘进行调整的实现为如下的一个双重for循环。这里有一个细节需要特别注意,也就是必须对每一列从上到下进行调整。因为如果从下到上进行调整,会改变上方的元素原本的位置。从而导致调整之后调整上方元素的时候没有调整到正确的元素。

/**
* 在指定的范围内,消除标记了的元素。消除方法为:从左到右对每一列进行检测,然后对每一列从上到下进行消除和下落。
* <pre>
*  0  0  0  0
* (1)(1)(1) 0
*  3  3  2  0
*  4 (2)(2)(2)
*
*  0  0  0  0
* (0)(1)(1) 0
*  3  3  2  0
*  4 (2)(2)(2)
*
*  0  0  0  0
* (0)(0)(1) 0
*  3  3  2  0
*  4 (2)(2)(2)
*
*  0  0  0  0
* (0)(0)(1) 0
*  3  3  2  0
*  4 (0)(2)(2)
* </pre>
* @param range 需要调整的范围
* @param marks 标记需要消除的元素
*/
private void eliminateAndDropDownAdjust(DetectionRange range, int[][] marks) {for(int c = range.getColLeft(); c <= range.getColRight(); c ++) {// 对每一列进行调整,从上到下for(int r = range.getRowTop(); r <= range.getRowBottom(); r ++) {// 计算被消除的列长度int originalR = r;while(r <= range.getRowBottom() && marks[r][c] != 0) {// 取消标记marks[r ++][c] = 0;}dropColDownTo(r - 1, c, r - originalR);}}
}

这里其实基本上已经完成了物品的消除和棋盘的调整。但可以注意到,上面的代码中有一个特别的类 DetectionRange。该类如同它的名字一样,为“检测范围”,在统计为需要元素和计算得分之后,并不需要对整个棋盘进行调整,仅需要对受影响的范围进行调整即可。通过对范围的限制,可以减少部分计算。

还有一点小小的优化在上方提及的,就是进行检测的时候全盘检测的时候是从左下角开始,分别向右和上进行检查。这是因为在进行消除之后,顶部的都是没有物品的位置,对其再进行重复的检测没有意义。此外,仅仅检测“右”和“上”两个方向即可覆盖全部检测,无需再进行”左“和”下“的检测,因为“右”和“左”,“上”和“下”是可以等价的。

用回溯法计算消消乐游戏最大得分

整体实现而言,都是很传统的回溯法实现方式。基本的思路如下:

MasScore(xiaoXiaoLeGame, leftStep) = MaxScore(everyPointInChess, xiaoXiaoLeGame, leftStep);
MaxScore(pointInChess, xiaoXiaoLeGame, leftStep) = max(向右交换得分 + MaxScore(xiaoXiaoLeGame.exchange(pointInChess, Right), leftStep - 1),向上交换得分 + MaxScore(xiaoXiaoLeGame.exchange(pointInChess, Up), leftStep - 1));

因为消消乐游戏的代码实现得比较完善,因而实现的代码也没有太多,这里就全部粘贴出来了。

package cn.donespeak.xiaoxiaole;import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;import cn.donespeak.xiaoxiaole.XiaoXiaoLe.OperationForbiddenException;public class AutoPlayer {private XiaoXiaoLe xiaoXiaoLe;private int stepsLeft;private List<TipsPoint> tips;private int maxScore;public AutoPlayer(XiaoXiaoLe xiaoXiaoLe, int stepsLeft) {this.xiaoXiaoLe = xiaoXiaoLe;this.stepsLeft = stepsLeft;this.tips = new LinkedList<TipsPoint>();this.maxScore = 0;}public void start() {maxScore = getMaxScore(xiaoXiaoLe, stepsLeft, tips);}public int getMaxScore() {return maxScore;}public List<TipsPoint> getTips() {return tips;}private int getMaxScore(XiaoXiaoLe xiaoXiaoLe, int stepsLeft, List<TipsPoint> tips) {if(stepsLeft == 0) {return 0;}int maxScore = 0;List<TipsPoint> tipsPicked = new ArrayList<>();for(int r = xiaoXiaoLe.getRowNum() - 1; r >= 0; r --) {for(int c = 0; c < xiaoXiaoLe.getColNum(); c ++) {if(xiaoXiaoLe.getChessboardCell(r, c) == 0) {continue;}List<TipsPoint> tipsRight = new ArrayList<>();List<TipsPoint> tipsUp = new ArrayList<>();XiaoXiaoLe xiaoXiaoLeRight = xiaoXiaoLe.getSnapshot();XiaoXiaoLe xiaoXiaoLeUp = xiaoXiaoLe.getSnapshot();int scoreRight = exchangeRight(xiaoXiaoLeRight, r, c, stepsLeft, tipsRight);int scoreUp = exchangeUp(xiaoXiaoLeUp, r, c, stepsLeft, tipsUp);int maxIncreasement = Math.max(scoreRight, scoreUp);maxScore = Math.max(maxIncreasement, maxScore);if(maxIncreasement == maxScore) {if(maxIncreasement == scoreRight) {if(scoreRight == scoreUp) {// 两个方法得分相同时候,取路径最短的tipsPicked = tipsRight.size() < tipsUp.size()? tipsRight: tipsUp;} else {tipsPicked = tipsRight;}} else if(maxIncreasement == scoreUp) {tipsPicked = tipsUp;}}}}tips.addAll(tipsPicked);return maxScore;}private int exchangeUp(XiaoXiaoLe xiaoXiaoLe, int r, int c, int stepsLeft, List<TipsPoint> tips) {return exchange(xiaoXiaoLe, r, c, stepsLeft, ArrowE.UP, tips);}private int exchangeRight(XiaoXiaoLe xiaoXiaoLe, int r, int c, int stepsLeft, List<TipsPoint> tips) {return exchange(xiaoXiaoLe, r, c, stepsLeft, ArrowE.RIGHT, tips);}private int exchange(XiaoXiaoLe xiaoXiaoLe, int r, int c, int stepsLeft, ArrowE arrow, List<TipsPoint> tips) {int increasement = 0;try {tips.add(new TipsPoint(r, c, arrow));increasement += xiaoXiaoLe.exchangeWith(r, c, arrow);increasement += getMaxScore(xiaoXiaoLe, stepsLeft - 1, tips);// 如果操作允许,也就是没有抛出异常,其该操作得到分数,则添加该操作} catch (OperationForbiddenException e) {// do nothing} finally {if(increasement == 0) {tips.remove(tips.size() - 1);}}return increasement;}
}

上面代码的TipPoint的基本结构如下:

public class TipsPoint extends Point {// 可以考虑加入方向 ArrowEprivate ArrowE arrow;
}

整体而言,实现的难度在于消消乐游戏的实现,而不在于回溯法的是实现。

完整的代码实现见:relaxinginjava/xiaoxiaole @Github

用回溯法计算消消乐游戏最大得分相关推荐

  1. 0913作业(冒泡排序、二分查找法、模拟摇乐游戏)

    1.冒泡排序从小到大打印出(代码如下) package f.java;public class LS {public static void main(String[] args){// TODO A ...

  2. 子集和数问题——回溯法(C++)

    问题描述 已知(w1, w2, -, wn)和M,均为正数.要求找出wi的和数等于M的所有子集. 例如:若n=4,(w1,w2,w3,w4)=(11,13,24,7),M=31,则满足要求的子集是(1 ...

  3. 五大算法思想(三)回溯法及常见例子

    文章目录 一.理论基础 1.1 基本策略 1.2 使用步骤 1.3 经典例子 二.常见例子 2.1 八皇后问题 2.2 装载问题 2.3 批量作业调度问题 2.4 背包问题 一.理论基础   回溯法作 ...

  4. Python实现多维背包问题MKP算法(2)——动态多维背包(回溯法)

    问题描述 MKP多维背包问题,特殊的背包问题,是著名的整数规划问题.要求从多个限制条件中选出满足所有条件的最佳组合,并计算出最优价值. 解题思路 用回溯法计算出每一个属性的所有可能性(不超过限制条件) ...

  5. 回溯法求解消消乐实验

    回溯法求解消消乐问题 实验概述 掌握回溯法设计思想. 掌握消消乐问题的回溯法解法. <开心消消乐>是一款乐元素研发的三消类休闲游戏.游戏中消除的对象为小动物的头像,包括小浣熊.小狐狸.小青 ...

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

    实验代码 + 报告资源: 链接: https://pan.baidu.com/s/1CuuB07rRFh7vGQnGpud_vg 提取码: ccuq 目录 写在前面 实验要求 求解问题的算法原理描述 ...

  7. java开心消消乐代码_Vue实现开心消消乐游戏算法

    摘要:这篇Vue栏目下的"Vue实现开心消消乐游戏算法",介绍的技术点是"开心消消乐.Vue.开心.游戏.算法.实现",希望对大家开发技术学习和问题解决有帮助. ...

  8. c++消消乐游戏课程设计

    1. 系统概述 1.1系统简介 消除类游戏是益智游戏的一种,玩家游戏过程中主要是将一定量相同的游戏元素,如水果.宝石.动物头像.积木麻将牌等,使它们彼此相邻配对消除来获胜.通常是将三个同样的元素配对消 ...

  9. 猜猜乐游戏php源码,C/C++百行代码实现热门游戏消消乐功能的示例代码

    游戏设计 首先我们需要使用第三方框架,这里我使用的是sfml,不会使用sfml在我的上几篇文章当中-扫雷(上)有详细的开发环境搭建介绍 首先准备图片资源 一张背景图片,一张宝石图片 窗口初始化加载图片 ...

最新文章

  1. asp.net2.0如何加密数据库联接字符串
  2. Mdnice 简洁主题
  3. 不使用jQuery对Web API接口POST,PUT,DELETE数据
  4. VTK修炼之道33:边缘检测_Sobel算子
  5. PHP文件函数 记录日志功能
  6. ARM Neon 列子 - Vector Add
  7. 更改windows2003远程最大连接数
  8. 【活动(广州)】MonkeyFest2018 微软最有价值专家讲座
  9. (pytorch-深度学习)SE-ResNet的pytorch实现
  10. android程序表白,几条曲线构建Android表白程序
  11. Flutter实战一Flutter聊天应用(三)
  12. java验证邮件正则
  13. AS3组件之Slider滑块拖动条
  14. 使用Web Deploy进行远程部署
  15. 实现一个监控 IP 的 windows 服务
  16. (四)52周存钱挑战 1.0
  17. 汇报措辞:你懂得如何向领导汇报吗(审阅、审批、审阅、批示、查阅)?
  18. 画以载道:艺术演变的动力与社会思潮的嬗变
  19. hhkb mac设置_键盘界的Iphone,硬件圈中的贵妇,HHKB键盘介绍及在MAC下的优化设置...
  20. 计算机自定义桌面设置在哪里设置,如何在windows10桌面设置自定义图片?查看方法...

热门文章

  1. 需求:结合mousedown、mousemove、mouseup事件让div可以被拖动起来
  2. 连接失败 错误651
  3. 未来的计算机100字的小短文,我的电脑作文100字五篇
  4. drawImage 详解
  5. 大数据助推数字经济时代到来
  6. 推荐:懂程序、不会美术怎么办?
  7. 最新免杀!可过360核晶与Defender(SysWhispers3)
  8. 数字建筑,虚拟世界第一个百万豪宅 | 绿洲 · 虚拟现实专栏
  9. 如何从 MacBook中 移除 AccessibilityMethod
  10. 计算机怎么扩大某个盘的大小,怎么调整电脑磁盘大小