遍历列举俄罗斯方块的所有形状

以前玩俄罗斯方块的时候,就想过一个问题,为什么俄罗斯方块就这7种形状,还有没有别的形状?自己也在纸上画过,比划来比划去,确实就这几种形状。

继续思考一下,那假如是3个块组合的形状,有几种有效组合形式?

这个似乎不是太难,在纸上比划几下,应该能得出结果。

但是,如果是5个块组合起来,又有多少种有效的组合形式?

这就比较麻烦了,排列组合的可能性大增,不那么容易比划清楚。

从程序员的角度,这其实是一个遍历穷举的过程,所以有了这一篇文章。

首先也不搞太复杂的,先从3个块的组合开始尝试。

3个块的组合,考虑所有可能性,就是在3x3的一个区域里面,任意取点。然后添加一些限制条件,例如:

1,区域内点位不重复;
2,每个点都至少需要有一个相邻点;
3,平移不重复;
4,旋转不重复;

这4个条件是否充足了?

反正我开始时就是这么想的,认为充足了。当然,到后面遇到了问题,才又添加了个限制条件。

为了能方便的判断结果是否正确,将有效结果展示出来,是有必要的。

需要界面展示,这里选择了是要QT,毕竟有可移植性,在windows下开发,以后要移植到linux下也可以。

看一下,基本的布局界面是这样的:

左上角,是那个画出方块组合形状的区域。在遍历过程中,遇到有效的方块组合,就在下面区域中以一半的宽高比例画出来。

这里由于涉及到了方块组合的不同大小的展示,以及还有平移动作,就需要考虑方块的点坐标,不能存储实际的宽高值(例如:0,20,40),而是更抽象的基础比例(例如:0,1,2)。

下面是简要介绍实现过程,具体实现可从文末的下载地址去获取。

先定义方块的类:

class Block
{
public:QPoint leftUp;//左上角的坐标int width;//宽度int height;//高度QColor penColor;//铅笔的颜色QColor brushColor;//画刷的颜色
}

再定义一个方块组合的类:

class BlockGroup
{
public:BlockGroup(int num);int blockNum;//方块的数目Block block[BLOCK_NUM];//方块的数组int flagValid;//有效标志
};

一个穷举遍历的主要过程如下:

        int pos[5];int pointTotal=pow(curBlock.blockNum,2);//一个点可以选择的位置int tryNum = pow(pointTotal,curBlock.blockNum);//每一个点,都有这么多种可能的位置,总的可能点位选择while(tickNum<tryNum){//遍历所有的可能性for(int i=0;i<curBlock.blockNum;i++){//将一个大数,分解为几个部分,每部分给一个方块使用,用作坐标赋值int pointLevel=pow(pointTotal,curBlock.blockNum-1-i);pos[i]=(tickNum/pointLevel)%pointTotal;}//计算,寻找有效的方块组合for(int j=0;j<curBlock.blockNum;j++){curBlock.block[j].leftUp.setX((x+pos[j]/curBlock.blockNum)*w);curBlock.block[j].leftUp.setY((y+pos[j]%curBlock.blockNum)*h);curBlock.block[j].width = w;curBlock.block[j].height = h;curBlock.block[j].penColor = Qt::black;curBlock.block[j].brushColor = Qt::yellow;}if(checkPosValid(&curBlock)!=1){//内部有点位重复,此种组合无效,跳过tickNum+=1;continue;}if(checkAdjacent(&curBlock)!=1){//检测每点都有相邻点,若不是,跳过tickNum+=1;continue;}if(checkConnectivity(&curBlock)!=1){//检测组块内部的连通性,若不是,跳过tickNum+=1;continue;}//规范坐标adjustCoordinates(&curBlock);int flagRepeat=0;for(auto it=blockGroupList.begin();it!=blockGroupList.end();it++){if(checkTranslationRepeatability(&(*it),&curBlock)){//发现是平移重复的flagRepeat=1;break;}if(checkRotaltionalRepeatability(&(*it),&curBlock)){//发现是旋转重复的flagRepeat=1;break;}}if(flagRepeat==1){tickNum+=1;continue;}//新的有效方块组合,存入结果链表中blockGroupList.push_back(curBlock);//写文件writeBlockInfo(&curBlock);//设置标志,通知画出              flagFlush=1;update();break;            }

下面逐个展示循环里面调用的方法。

检查方块组合中的点位是否重复:

//检测块内点位是否有效(不能重复)
int BlockDialog::checkPosValid(BlockGroup *blockInfo){for(int k=0;k<blockInfo->blockNum-1;k++){for(int i=k+1;i<blockInfo->blockNum;i++){if(blockInfo->block[k].leftUp==blockInfo->block[i].leftUp){return 0;}}}return 1;
}

检查不是孤立点位:

//检测两点是否相邻
int BlockDialog::checkNearPoint(QPoint *point1,QPoint *point2,int w,int h){QPoint pointRight=QPoint(w,0);QPoint pointDown=QPoint(0,h);if(*point1+pointRight == *point2){//右邻return 1;}if(*point1 == *point2+pointRight){//左邻return 1;}if(*point1+pointDown == *point2){//下邻return 1;}if(*point1 == *point2+pointDown){//上邻return 1;}return 0;//不相邻
}
//检测有相邻块
int BlockDialog::checkAdjacent(BlockGroup *blockInfo){int isNearNum=0;for(int i=0;i<blockInfo->blockNum;i++){isNearNum=0;for(int k=0;k<blockInfo->blockNum;k++){if(k!=i){if(1==checkNearPoint(&blockInfo->block[k].leftUp, &blockInfo->block[i].leftUp,blockInfo->block[i].width,blockInfo->block[i].height)){isNearNum++;}}}if(isNearNum==0){//没有相邻点return 0;}}return 1;
}
已经检查过不是孤立点了,为什么还要检查点的连通性(checkConnectivity)?

这就是前面说过的那个添加的限制条件。为什么呢?

前面列举了4个限制条件,在应用到3个方块组合时没有遇到问题,但是在穷举4个方块的组合的时候就遇到了问题,例如下面这种方块组合,竟然是满足条件的:

然而,这并不是我期望的形状,我希望所有方块都是相连通的。没有孤立方块,这个条件没有包含所有方块连通的要求,所以,还需要增加一个限制条件:

5,要保证所有方块是连通起来的。

如下代码是检测方法:

//检测块内连通性
int BlockDialog::checkConnectivity(BlockGroup *blockInfo){int isNearNum=0;//将块组各点位信息赋值到节点中Node *nodeArray=new Node[blockInfo->blockNum];for(int i=0;i<blockInfo->blockNum;i++){nodeArray[i].point = blockInfo->block[i].leftUp;}//设置相邻节点for(int i=0;i<blockInfo->blockNum;i++){for(int j=0;j<blockInfo->blockNum;j++){if(i!=j){setNearNode(&nodeArray[i],&nodeArray[j]);}}}//从节点0开始,进行遍历DepthFirstSearch(&nodeArray[0]);//检查是否有未遍历到的结点for(int i=0;i<blockInfo->blockNum;i++){if(1==nodeArray[i].isChecked){isNearNum++;} else {break;//有未走到的点,即组块有未连通部分}}delete[] nodeArray;if(isNearNum==blockInfo->blockNum){//完成遍历,即方块组是连通的return 1;}return 0;
}

这里调用了一个函数 DepthFirstSearch() ,是深度优先搜索算法。

这里涉及到图的连通性,参考这个链接,讲得比较清楚:
    https://blog.csdn.net/weixin_44122303/article/details/122924246

//深度优先搜索--遍历相邻节点
void DepthFirstSearch(Node *node){if(node==NULL){return;}//递归调用,注意避免无限循环!if(node->isChecked==1){//已经检查过的,不要再进行检查了,避免无限循环!return;}//设置检查标志node->isChecked=1;//递归调用,检查相邻点DepthFirstSearch(node->up);DepthFirstSearch(node->down);DepthFirstSearch(node->left);DepthFirstSearch(node->right);return ;
}

这里还有一个规范坐标的函数 adjustCoordinates(&curBlock); 为什么需要它?

也是在遇到问题之后才知道的。

在平移比较方块组合是否是重复的情况时,发现明明是一样的形状,却判断为不重复。

细细分析之后,发现是顺序不同,例如如下情况:

(0,1,2)与(0,2,1)

都是3个直线相邻的方块,但是由于顺序不同,直接按逐点比较的话,属于不同的组合。所以,添加一个规范点位顺序的方法:

//规范坐标
//1,左上平移;
//2,按顺序存放:从左上角开始,先第一排,从左往右;然后第二排
int BlockDialog::adjustCoordinates(BlockGroup *blockInfo){//定位左上角方块的坐标QPoint leftTopPoint1(0,0);QPoint leftTopPoint2=getLeftTopBoundary(blockInfo);//1,对整个方块组进行平移int leftShift = leftTopPoint2.x()-leftTopPoint1.x();int upShift = leftTopPoint2.y()-leftTopPoint1.y();QPoint pointShift(leftShift,upShift);Block blockTmp=Block();list<Block> listBlock={};for(int i=0;i<blockInfo->blockNum;i++){blockTmp.leftUp=blockInfo->block[i].leftUp-pointShift;blockTmp.penColor=blockInfo->block[i].penColor;//进行块组的赋值,注意不能遗漏颜色属性blockTmp.brushColor=blockInfo->block[i].brushColor;listBlock.push_back(blockTmp);}//2,排序 -- 按什么顺序排列的?//重载Block的比较操作符: 顺序定义:从左上角开始,先第一排,从左往右;然后第二排listBlock.sort();int blockPos=0;for(auto it=listBlock.begin();it!=listBlock.end();it++){blockInfo->block[blockPos]=*it;blockPos++;}return 0;
}

这里的排序方法,依赖的是比较操作,先比较点位的纵坐标y,再比较横坐标x:

    bool operator<(const Block& block1) const{if(this->leftUp.y()<block1.leftUp.y()){return true;} else if(this->leftUp.y()==block1.leftUp.y()){if(this->leftUp.x()<block1.leftUp.x()){return true;}}return false;}

再就是平移重复检测:

//检测平移重复性
int BlockDialog::checkTranslationRepeatability(BlockGroup *blockInfo1,BlockGroup *blockInfo2)
{//逐点进行坐标比较for(int i=0;i<blockInfo2->blockNum;i++){if(blockInfo1->block[i].leftUp!=blockInfo2->block[i].leftUp){//坐标不同return 0;}}return 1;//重复
}

最后是旋转重复检查,比平移重复检测复杂一点的是,有3次旋转(90度、180度、270度)。还有一点是旋转后的坐标如何确定,好在我在坐标系上比划了一下,旋转90度,就是(x,y) -> (y,-x),还是比较简单的:

//将方块组进行顺时针旋转90度
int BlockDialog::RotateBlockGroup(BlockGroup *blockInfo){//1,对整个方块组进行顺时针旋转90度Block blockTmp=Block();list<Block> listBlock={};for(int i=0;i<blockInfo->blockNum;i++){//旋转90度,就是(x,y) -> (y,-x)blockTmp.leftUp.setX(blockInfo->block[i].leftUp.y());blockTmp.leftUp.setY(0-blockInfo->block[i].leftUp.x());blockTmp.penColor=blockInfo->block[i].penColor;//进行块组的赋值,注意不能遗漏颜色属性blockTmp.brushColor=blockInfo->block[i].brushColor;listBlock.push_back(blockTmp);}//2,排序 -- 按什么顺序排列的?//重载Block的比较操作符: 顺序定义:从左上角开始,先第一排,从左往右;然后第二排listBlock.sort();//重新赋值int blockPos=0;for(auto it=listBlock.begin();it!=listBlock.end();it++){blockInfo->block[blockPos]=*it;blockPos++;}adjustCoordinates(blockInfo);//调整坐标,避免有负值坐标出现return 0;
}
//检测旋转重复性
int BlockDialog::checkRotaltionalRepeatability(BlockGroup *blockInfo1,BlockGroup *blockInfo2)
{for(int i=0;i<3;i++){//每次旋转90度,检查3次,加之前的一次平移检查,即完成了360度的检查//1,对方块组旋转90度,就是(x,y) -> (y,-x)RotateBlockGroup(blockInfo2);//2,先进行平移检测if(1==checkTranslationRepeatability(blockInfo1,blockInfo2)){return 1;}}return 0;
}

至此,就实现了循环遍历所有方块组合的主体功能。
然后,将相关信息写入文件即可。

完整的代码可在如下链接地址获取:

修改代码中方块组合中方块的个数:

#define BLOCK_NUM 5 //4  //3    方块组内包含的方块个数

可以遍历获取不同个方块的组合类型。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/813840.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网页input框自动填充问题

autocomplete 大部分查询解决办法是设置&#xff0c;autocompleteoff&#xff0c;关于autocomplete的含义&#xff0c;官网参考如下: HTML attribute: autocomplete - HTML: HyperText Markup Language | MDN 在 autocomplete 的文档中说明了 value 为 off 时&#xff0c;浏览…

双子座 Gemini1.5和谷歌的本质

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

【STL】list

目录 1. list的使用 1.1 list的构造 1.2 list iterator的使用 1.3 list capacity 1.4 list element access 1.5 list modifiers 1.6 list的迭代器失效 2. list的模拟实现 3. list与vector的对比 1. list的使用 1.1 list的构造 1.2 list iterator的使用 1. begin与end为…

KVM + GFS 分布式存储

目录 一、案例分析 1.1、案例概述 1.2、案例前置知识点 1&#xff09;Glusterfs 简介 2&#xff09;Glusterfs 特点 1.3、案例环境 1&#xff09;案例环境 2&#xff09;案例需求 3&#xff09;案例实现思路 二、案例实施 2.1、安装部署 KVM 虚拟化平台 1&…

Mac的终端配置

Mac的终端配置 参考教程包管理工具 - Homebrew出现的问题用虚拟环境解决方案&#xff1a;直接将解释器的路径放过去错误方法&#xff1a;用find查找到虚拟环境安装的路径&#xff0c;其链接的是brew安装的python路径 编辑器没有报错&#xff0c;但是运行过程中仍然找不到pandas…

JavaEE初阶Day 7:多线程(5)

目录 Day 7&#xff1a;多线程&#xff08;5&#xff09;1. 死锁2. 死锁场景3. 场景二&#xff1a;两个线程&#xff0c;两把锁4. 场景三&#xff1a;N个线程&#xff0c;M把锁5. 避免死锁问题6. 内存可见性问题 Day 7&#xff1a;多线程&#xff08;5&#xff09; 回顾synchr…

Windows下使用SDKMAN对JDK(Java)进行多版本管理

Windows下使用SDKMAN对JDK&#xff08;Java&#xff09;进行多版本管理 1.背景2.基于msys2工具2.1. msys2简介2.2. 安装与配置2.2. Windows环境变量配置参考2.3 结果确认 3. 基于WSL 1.背景 前端有nvm,python有miniconda,miniforge等&#xff0c;java呢&#xff1f;java在Linu…

前端css笔记(pink老师)

css css书写顺序 自适应屏幕 html { width: 100%; height: 100%; display: table; } body { display: table-cell; } 用了这个方法以后&#xff0c;如果希望页面内的盒子也适应屏幕大小&#xff0c;则使用以下方法&#xff0c;会根据父亲的宽高计算出该盒子的宽高 width:xx%; …

java进阶---反射

获取class对象 Testvoid reflect() throws Exception {Class userClass User.class;System.out.println(userClass.getName()); // 全类名 com.zyw.proxydemo.pojo.UserSystem.out.println(userClass.getSimpleName()); // 简名 UserClass userClass1 Class.forName("co…

SpringBoot通过UUid实现文件上传接口及问题解决

在controller中&#xff0c;添加对应的方法体&#xff1a; PostMapping("/upload")ResponseBodypublic ApiRestResponse upload(HttpServletRequest httpServletRequest, RequestParam("file")MultipartFile file) throws IOException {String fileName f…

工业项目中你连PLM系统都没见过?

什么是 PLM 软件&#xff1f; PLM 软件是用于管理全球供应链中产品或服务全生命周期环节的解决方案。它包括从物料、零部件、产品、文档、规定、工程变更单到质量工作流的数据管理。 PLM 的发展历史 从最初的产品设计管理到如今的数字化转型和智能化生产&#xff0c;PLM 在不断…

tkinter窗口组件Entry

from tkinter import * 创建主窗口 app Tk() 设置窗口大小为1040x2048(手机) app.geometry(“1040x2048”) 设置窗口背景为灰色 app.configure(bg“gray”) 定义一个函数text()&#xff0c;用于处理输入框内容的变化 def text(): # 获取输入框e1的内容 if e1.get() “…

2024年阿里云优惠券领取和使用方法

阿里云优惠代金券领取入口&#xff0c;阿里云服务器优惠代金券、域名代金券&#xff0c;在领券中心可以领取当前最新可用的满减代金券&#xff0c;阿里云百科aliyunbaike.com分享阿里云服务器代金券、领券中心、域名代金券领取、代金券查询及使用方法&#xff1a; 阿里云优惠券…

基于ssm的大学生租房平台的设计与实现(java源码+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于ssm的大学生租房平台。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 大学生租房平台的设计与实现的主…

架构设计参考项目系列主题:新零售SaaS架构:客户管理系统架构设计

什么是客户管理系统? 客户管理系统,也称为CRM(Customer Relationship Management),主要目标是建立、发展和维护好客户关系。 CRM系统围绕客户全生命周期的管理,吸引和留存客户,实现缩短销售周期、降低销售成本、增加销售收入的目的,从而提高企业的盈利能力和竞争力。 …

linux——Bash特性

bash是一个命令解释器&#xff0c;其支持命令行展开&#xff5b;&#xff5d;写法 alias是命令别称&#xff0c;即为命令等同于&#xff0c;使用unalias对应命令可以取消该别称 alias可以对命令进行更改

紫光展锐T610平台_4G安卓核心板方案定制开发

紫光展锐T610核心板配备Android 11操作系统&#xff0c;采用12nm制程工艺。该处理器CPU由2颗基于Cortex-A75架构的大核心和6颗基于Cortex-A55架构的小核心组成&#xff0c;最高主频为1.8GHz。GPU采用的是614.4MHz的Mali G52&#xff0c;可以流畅播放2400*1080分辨率视频&#x…

淘宝批量采集商品详情数据(属性丨详情图丨sku丨价格等)

淘宝批量采集商品详情数据&#xff08;包括属性、详情图、SKU、价格等&#xff09;可以通过以下几种方式实现&#xff1a; 使用淘宝数据抓取工具&#xff1a;这类工具&#xff0c;如某鱼等&#xff0c;能够自动化采集淘宝商品数据&#xff0c;并将其转换成CSV、Excel等格式&am…

【C++ 学习】 priority_queue 优先队列的学习!!

1 queue****的介绍** 队列是一种容器适配器&#xff0c;专门用于在FIFO上下文(先进先出)中操作&#xff0c;其中从容器一端插入元素&#xff0c;另一端提取元素。 队列作为容器适配器实现&#xff0c;容器适配器即将特定容器类封装作为其底层容器类&#xff0c;queue提供一组特…

DVWA -File Upload-通关教程-完结

DVWA -File Upload-通关教程-完结 文章目录 DVWA -File Upload-通关教程-完结页面功能LowMediumHighImpossible 页面功能 此页面的功能为选择某个图片文件点击Upload按钮上传&#xff0c;上传成功后得知文件上传路径为DVWA\hackable\uploads。 Low 源码审计 这段 PHP 代码…