生成可解的双巧克力谜题:高效的数据结构与算法实践

生成可解的双巧克力谜题:高效的数据结构与算法实践

本文深入探讨了如何为“双巧克力”(Double-Choco)谜题游戏设计一套高效的自动生成可解谜题的系统。我们将介绍核心的数据结构——一个增强型二维单元格网格,并详细阐述基于广度优先搜索(BFS)的区域识别算法。在此基础上,文章将构建一个迭代式的谜题生成框架,该框架通过智能绘制边界线、验证形成的区块形状及其内部白灰区域的匹配性,并结合回溯机制,确保生成谜题的有效性和可解性。

1. 双巧克力谜题概述与生成挑战

“双巧克力”(double-choco)是一款由nikoli杂志推出的逻辑谜题游戏。其核心玩法是在一个由白色和灰色单元格组成的二维网格上,通过绘制实线将网格划分为若干个独立的“区块”。每个区块必须满足以下严格条件:

包含一对区域:每个区块内必须包含一个白色单元格区域和一个灰色单元格区域。形状与大小匹配:这对白色和灰色区域必须具有相同的形状和大小。允许其中一个区域相对于另一个进行旋转或镜像变换。可选数字线索:某些区域可能包含一个数字,表示该颜色在该区块内的单元格数量。

谜题生成的核心挑战在于如何自动生成一个既符合所有规则,又能保证其“可解性”的网格。这不仅仅是随机填充,更是一个约束满足问题。我们需要一种机制来:

有效地表示谜题板的状态。识别和验证形成的区块。迭代地构建谜题,确保每一步都朝着一个有效且可解的最终状态迈进。

2. 核心数据结构:增强型二维单元格网格

为了高效地表示谜题板并支持后续的算法操作,我们推荐使用一个二维数组来存储 cell(单元格)对象。每个 cell 对象应包含以下关键属性:

let cell = {    x: Number,             // 单元格的X坐标    y: Number,             // 单元格的Y坐标    color: "white" | "gray" | null, // 单元格的颜色(生成过程中确定,初始可为null)    number: null | Number, // 可选的数字线索    // 边界线标识:true表示该方向存在一条实线,false表示与相邻单元格连通    hasTopLine: boolean,   // 上方是否有线    hasBottomLine: boolean, // 下方是否有线    hasLeftLine: boolean,  // 左方是否有线    hasRightLine: boolean, // 右方是否有线    isTakenByBlock: boolean, // 该单元格是否已被分配给一个最终的、有效的区块    blockId: null | Number   // 该单元格所属区块的唯一标识符};// 谜题板表示为一个二维数组let board = Array(rows).fill(0).map(() => Array(cols).fill(0).map(() => ({    // 初始化属性...    hasTopLine: false, hasBottomLine: false, hasLeftLine: false, hasRightLine: false,    isTakenByBlock: false, blockId: null, color: null, number: null})));

数据结构说明:

x, y: 便于引用和导航。color, number: 谜题的最终呈现信息,在生成过程中逐步确定。hasTopLine等边界属性:这是表示“线”的关键。true意味着在该方向上绘制了一条实线,从而分隔了单元格;false意味着没有线,单元格与相邻的同方向单元格连通。这种表示方式直接支持后续的区域识别算法。isTakenByBlock: 用于在谜题生成过程中跟踪哪些单元格已经被成功分配到有效的区块中。blockId: 标识单元格所属的区块,方便后续对整个区块进行操作和验证。

3. 区块识别算法:基于广度优先搜索 (BFS)

在谜题生成过程中,我们需要频繁地识别由当前边界线所围成的连通区域(即潜在的“区块”)。这可以通过标准的广度优先搜索(BFS)或深度优先搜索(DFS)来实现。这里我们采用BFS,因为它通常更适合于避免递归深度限制,并且易于理解。

该函数从一个起始单元格开始,根据hasLine属性(即!hasLine表示可连通),遍历所有相邻的连通单元格,并将它们收集到一个列表中。

/** * 识别并返回从指定起始点开始的所有连通单元格组成的区域。 * 连通性由单元格的边界线属性决定(hasLine为false表示连通)。 * * @param {number} startX - 起始单元格的X坐标。 * @param {number} startY - 起始单元格的Y坐标。 * @param {Array<Array>} board - 谜题板的二维单元格数组。 * @param {Array<Array>} visited - 用于本次BFS的访问状态数组,防止重复访问。 * @returns {Array} 包含所有连通单元格的数组。 */function identifyConnectedRegion(startX, startY, board, visited) {    let regionCells = [];    let queue = [{x: startX, y: startY}];    visited[startY][startX] = true; // 标记起始单元格已访问    const rows = board.length;    const cols = board[0].length;    while (queue.length > 0) {        let {x, y} = queue.shift();        let currentCell = board[y][x];        regionCells.push(currentCell);        // 检查四个方向的邻居,如果无边界线且未访问,则加入队列        // 向上        if (y > 0 && !currentCell.hasTopLine && !visited[y - 1][x]) {            visited[y - 1][x] = true;            queue.push({x: x, y: y - 1});        }        // 向下        if (y  0 && !currentCell.hasLeftLine && !visited[y][x - 1]) {            visited[y][x - 1] = true;            queue.push({x: x - 1, y: y});        }        // 向右        if (x < cols - 1 && !currentCell.hasRightLine && !visited[y][x + 1]) {            visited[x + 1][y] = true; // Bug fix: x and y swapped in original thought            queue.push({x: x + 1, y: y});        }    }    return regionCells;}

注意事项:

visited数组在每次调用identifyConnectedRegion时都应重新初始化,以确保只识别当前调用所对应的单一连通区域。此函数仅识别区域,不进行规则验证。规则验证将在更高层的生成算法中进行。在实际应用中,需要处理边界条件,例如y > 0、x

4. 谜题生成算法:迭代与回溯

谜题生成过程是一个迭代的、可能需要回溯的搜索过程。其核心思想是:从一个空白板开始,逐步“绘制”线条来定义区块,同时确保每个定义的区块都符合“双巧克力”的规则,直到整个板面被填满。

以下是结合了上述数据结构和区块识别算法的生成框架:

function generateDoubleChocoPuzzle(rows, cols) {    // 1. 初始化谜题板    let board = Array(rows).fill(0).map((_, y) => Array(cols).fill(0).map((_, x) => ({        x, y,        color: null, number: null,        hasTopLine: false, hasBottomLine: false, hasLeftLine: false, hasRightLine: false,        isTakenByBlock: false, blockId: null    })));    let currentBlockId = 1;    /**     * 递归函数,尝试填充谜题板。     * @param {Array<Array>} currentBoard - 当前谜题板状态。     * @returns {boolean} 如果成功生成谜题,返回true;否则返回false。     */    function solve(currentBoard) {        // 查找下一个未被填充的单元格作为新区块的起点        let startCell = null;        for (let y = 0; y < rows; y++) {            for (let x = 0; x  Array(cols).fill(false));            let potentialBlockCells = identifyConnectedRegion(startCell.x, startCell.y, currentBoard, visitedForRegion);            // 6. 验证潜在区块的合法性            if (isValidDoubleChocoBlock(potentialBlockCells, currentBoard)) {                // 7. 如果合法,标记单元格为已占用,并分配blockId,设置颜色/数字                assignBlockProperties(potentialBlockCells, currentBlockId++, currentBoard);                // 8. 递归调用,尝试填充剩余部分                if (solve(currentBoard)) {                    return true; // 成功                }                // 9. 回溯:如果递归失败,撤销当前区块的分配和边界线                revertBlockProperties(potentialBlockCells, currentBoard);                revertBoundaryConfiguration(currentBoard, config); // 撤销线                currentBlockId--;            } else {                // 如果当前边界配置形成的区块不合法,也需要撤销边界配置                revertBoundaryConfiguration(currentBoard, config);            }        }        return false; // 所有尝试都失败,回溯    }    // --- 辅助函数 (需要具体实现) ---    // 伪函数:生成可能的边界配置    // 这是最复杂的部分,决定了谜题的形状和难度。    // 可以尝试生成不同大小和形状的区块,并确保它们不侵犯已有的区块。    function generateBoundaryOptions(startCell, board) {        // 返回一个数组,每个元素代表一种边界线的绘制方案        // 例如:[{x1,y1,x2,y2,direction}, ...] 或更抽象的形状定义        // 这是一个启发式搜索和形状匹配的问题,超出了本教程的详细代码范畴。        // 简单来说,可以尝试从startCell向外扩展,随机选择路径,直到形成一个封闭区域。        // 每次扩展,都需要判断是否会侵犯已有的isTakenByBlock单元格。        return [/* 各种边界配置 */];    }    // 伪函数:应用边界配置到谜题板    function applyBoundaryConfiguration(board, config) {        // 根据config设置board中相关单元格的hasTopLine/hasBottomLine/hasLeftLine/hasRightLine为true    }    // 伪函数:撤销边界配置    function revertBoundaryConfiguration(board, config) {        // 将applyBoundaryConfiguration设置的线撤销(hasLine设回false)    }

以上就是生成可解的双巧克力谜题:高效的数据结构与算法实践的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1513714.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年12月20日 07:55:56
下一篇 2025年12月20日 07:56:09

相关推荐

  • JavaScript 中动态创建和管理对象实例的策略

    本文旨在探讨在JavaScript中如何根据数组中的值动态创建类的多个实例。我们将介绍两种主流且推荐的策略:将实例存储在数组中,可以使用for…of循环或更简洁的Array.prototype.map方法;或者将实例存储在一个对象(哈希映射)中,以便通过键名直接访问。文章将提供详细的代码…

    好文分享 2025年12月20日
    000
  • 使用Flexbox实现可切换布局的响应式文本框排列

    本教程详细介绍了如何利用CSS Flexbox和JavaScript实现一个动态布局系统,允许用户通过切换按钮在垂直和水平方向上改变容器的排列方式,同时智能地调整内部文本框的布局。文章将展示如何通过修改HTML结构、优化CSS样式和编写JavaScript逻辑,实现容器在列/行方向切换时,文本框能自…

    2025年12月20日
    000
  • 浏览器渲染和事件循环之间有什么关系?

    事件循环是浏览器保持响应和更新界面的核心机制,它通过不断检查调用栈和任务队列,在主线程空闲时执行宏任务或微任务;2. 浏览器渲染(包括布局、绘制)也发生在同一主线程上,因此长时间js执行会阻塞渲染;3. 事件循环在每次清空调用栈和微任务队列后,会给予浏览器机会进行渲染更新,从而协调用户交互与页面刷新…

    2025年12月20日 好文分享
    000
  • Angular 响应式表单错误处理与 Material UI 组件样式集成指南

    本教程详细探讨了 Angular 响应式表单中跨字段验证(如密码确认)的正确实现方法,重点解决 mat-error 未按预期显示的问题,并介绍了如何通过自定义验证器在 FormGroup 层面进行有效验证。同时,文章也针对 Angular Material 组件样式不生效的常见问题提供了解决方案,强…

    2025年12月20日
    000
  • js怎么实现ajax请求

    实现ajax请求的核心方法有xmlhttprequest和fetch api两种。1. xmlhttprequest适用于需要兼容旧浏览器的场景,支持超时设置、进度监听和请求中止,但基于事件回调的写法较繁琐,易导致回调地狱;2. fetch api是现代标准,基于promise,语法简洁、可读性强,…

    2025年12月20日
    000
  • js如何实现数据缓存

    选择缓存方式需根据数据生命周期和重要性权衡,内存缓存适合高频访问、临时性数据,localstorage适合需持久化的非敏感小量数据,sessionstorage适合单次会话的临时状态;2. 实现带过期时间的缓存核心是在存储时记录时间戳,读取时校验是否过期,可通过封装类在内存或localstorage…

    2025年12月20日
    000
  • js如何实现原型链的条件继承

    javascript原型链本身不支持“条件继承”,因为原型链是静态的委托机制,无法在查找过程中动态判断分支;所谓“条件继承”实际是在对象创建时通过外部逻辑动态决定其原型链结构,而非原型链自身具备条件判断能力。1. 使用工厂函数结合object.create()可根据参数选择不同原型创建对象,实现动态…

    2025年12月20日 好文分享
    000
  • js怎么实现复制到剪贴板

    现代javascript中推荐使用navigator.clipboard.writetext()实现复制,它基于promise、更安全且用户体验更好;2. document.execcommand(‘copy’)常因非用户触发、无选中文本、浏览器兼容性差或安全策略而失败,且已…

    2025年12月20日
    000
  • js 如何使用takeWhile从开头获取满足条件的元素

    javascript原生数组没有takewhile方法,1. 因为其设计哲学倾向于保留最基础的操作,而takewhile属于特定函数式编程场景下的非核心功能;2. 社区已通过lodash、rxjs等库提供了更专业、健壮的实现,使语言核心能保持精简;3. takewhile适用于需连续性判断的场景,如…

    2025年12月20日
    000
  • javascript如何复制一个数组

    在javascript中复制数组不能直接用等号赋值,因为数组是引用类型,直接赋值只会复制内存地址,导致新旧数组相互影响。1. 使用展开运算符 […originalarray] 是最简洁现代的浅拷贝方法;2. array.from(originalarray) 和 slice() 也能实现…

    2025年12月20日 好文分享
    000
  • javascript数组怎么填充连续数字

    最直接的方法是使用循环填充连续数字,但更优雅的javascript方式包括array.from和扩展运算符结合keys()。1. 循环法:通过for循环手动push元素,兼容性好且性能稳定;2. array.from法:利用array.from({length}, (_, i) => star…

    2025年12月20日 好文分享
    000
  • js怎么获取元素的位置信息

    获取元素位置最推荐使用element.getboundingclientrect(),因为它提供元素相对于视口的精确位置和尺寸,适用于视口检测、滚动交互等场景;2. offsettop和offsetleft用于获取元素相对于其offsetparent的偏移,适合在定位容器内进行相对布局计算;3. 元…

    2025年12月20日
    000
  • js如何获取原型链上的元属性

    获取javascript对象原型链上的元属性需通过遍历原型链并提取各层级自有属性的描述符;2. 使用object.getprototypeof逐层向上遍历直至null;3. 利用reflect.ownkeys获取当前对象所有自有属性名(含symbol和非枚举属性);4. 通过object.getow…

    2025年12月20日 好文分享
    000
  • js怎样实现打印功能

    实现javascript打印功能的核心是调用window.print()方法,它会触发浏览器的打印对话框,允许用户选择打印机并设置选项,默认打印整个页面;1. 要自定义打印内容,可通过css媒体查询@media print设置打印样式,如使用.no-print类隐藏不需打印的元素;2. 也可通过ja…

    2025年12月20日 好文分享
    000
  • js 怎样用entries获取数组键值对的迭代器

    entries()方法返回一个迭代器对象,用于遍历数组的索引和值组成的键值对,1. 调用arr.entries()返回迭代器而非数组,需通过for…of或next()方法访问;2. 每次next()调用返回包含value(键值对)和done(是否结束)属性的对象;3. 实际应用包括同时获…

    2025年12月20日
    000
  • 使用Flexbox实现Web布局动态切换与内部元素智能重排

    本文详细阐述了如何利用HTML结构、CSS Flexbox和JavaScript,实现一个容器(如div)在垂直和水平布局之间的动态切换,并同步调整其内部嵌套元素(如文本输入框)的排列方式。通过精巧的结构设计和JavaScript对CSS属性的动态控制,确保在不同布局模式下,内部元素能自适应地垂直堆…

    2025年12月20日
    000
  • js怎么实现图片懒加载

    图片懒加载的核心是延迟加载非视口内的图片,提升页面加载速度和用户体验;2. 推荐使用 intersectionobserver api 实现,通过将图片真实地址存于 data-src 属性,在元素即将进入视口时再赋值给 src 加载;3. 设置 rootmargin 可提前加载图片,避免内容突然“蹦…

    2025年12月20日
    000
  • CSS技巧:解决悬停效果下图片被遮挡或裁剪的问题

    针对卡片悬停效果中图片被遮挡或裁剪的常见问题,本教程将深入解析其根本原因,即CSS的overflow: hidden、z-index和定位上下文。通过调整HTML结构、合理运用position: absolute和z-index,并结合pointer-events属性,确保图片在任何交互状态下都能保…

    2025年12月20日 好文分享
    000
  • CSS技巧:在复杂悬停效果中确保图像始终可见

    本教程探讨如何在包含悬停效果的CSS卡片布局中,确保图像始终显示在最顶层而不被裁剪或遮挡。通过调整HTML结构,利用CSS的position和z-index属性,以及引入pointer-events,我们将解决图像被overflow: hidden和扩展叠加层遮盖的问题,实现复杂的视觉交互效果。 在…

    2025年12月20日 好文分享
    000
  • JavaScript中基于顺序的连续重复数据分组技巧

    本教程详细讲解如何在JavaScript中对数组中的对象进行“按序”分组,即根据对象某个属性的连续重复性进行分组。我们将利用Array.prototype.reduce()方法,通过比较当前元素与前一个元素的属性值,智能地创建新的子数组或将元素添加到现有子数组中,从而高效地实现非典型的数据去重与分组…

    2025年12月20日
    000

发表回复

登录后才能评论
关注微信