惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
P
Proofpoint News Feed
Hacker News: Ask HN
Hacker News: Ask HN
T
Threatpost
WordPress大学
WordPress大学
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 【当耐特】
Know Your Adversary
Know Your Adversary
P
Palo Alto Networks Blog
S
SegmentFault 最新的问题
月光博客
月光博客
Latest news
Latest news
博客园 - Franky
T
Threat Research - Cisco Blogs
有赞技术团队
有赞技术团队
博客园_首页
T
The Exploit Database - CXSecurity.com
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
C
Cybersecurity and Infrastructure Security Agency CISA
S
Schneier on Security
Simon Willison's Weblog
Simon Willison's Weblog
爱范儿
爱范儿
Security Latest
Security Latest
Scott Helme
Scott Helme
博客园 - 聂微东
T
Tor Project blog
美团技术团队
IT之家
IT之家
Stack Overflow Blog
Stack Overflow Blog
B
Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
C
Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
G
Google Developers Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
AWS News Blog
AWS News Blog
Jina AI
Jina AI
Vercel News
Vercel News
小众软件
小众软件
T
Tenable Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Forbes - Security
Forbes - Security
aimingoo的专栏
aimingoo的专栏
O
OpenAI News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The Last Watchdog
The Last Watchdog
Cloudbric
Cloudbric
AI
AI

Kenvix's Blog

实现带有Nvidia GPU+Rootless Podman+Docker+Systemd+自动驱动注入支持的systemd nspawn容器 | Kenvix's Blog 手动保留常用端口,解决 Windows 端口被 Hyper-V / WinNAT 占用的问题 免Telent/TTL屏蔽运营商新版光猫的远控、TR069和RMS,获取动态随机超级管理员密码并固化权限 | Kenvix's Blog 利用Windows卷影副本(Volume Shadow)找回被覆盖和删除的数据 | Kenvix's Blog 在Windows下实现WireGuard动态DNS解析(DDNS)的正确方法:避免无意义的开销 | Kenvix's Blog OpenWRT/DNSMasq 配置DHCP静态路由主动推送 实现流量直达和旁路由流量零代价分载 | Kenvix's Blog 解决 Windows 打开视频/图片文件夹很慢的问题 | Kenvix's Blog AltA2DP - 向支持Sony LDAC协议的耳机提供Windows下蓝牙LDAC音频编码器支持 | Kenvix's Blog (2024更新)修复黑群晖 DSM7.0 + Btrfs 存储空间/磁盘损毁/堪用 的问题 校园网白嫖思路分享:局域网中转-不花钱、不认证、高速上网 | Kenvix's Blog 在 Windows 上配置网卡多个 VLAN、多个虚拟网卡、实现单线多拨网速叠加(无需驱动支持) | Kenvix's Blog 解决视频彩铃、语音通话自动转视频通话导致打电话自动挂断的问题 | Kenvix's Blog 超低成本廉价考研教程:如何用小于¥500甚至¥300的开销考个研 | Kenvix's Blog 在 Ubuntu 21.10 上启用蓝牙 LDAC/AAC/AptX 高质量音频编码支持 在 VMware Workstation 桥接模式的网卡上让虚拟机使用 VLAN 的正确方法 在 Windows 上设置 NAT 或网络共享的正确方法——避免Wi-Fi热点无法使用 自编译 红米 AC2100 OpenWRT R21.7.26 Linux 内核结构和子系统简介 | Kenvix's Blog Java 快速读取文本 (算法竞赛适用) | Kenvix's Blog 利用 ThreadLocal + Lambda,实现有状态变量的单例模式 | Kenvix's Blog 我的 Windows 10 2004 新增 Bug 解决办法记录 解决 Android Studio 及 IDEA 中 Gradle 错误信息乱码的问题 Kotlin 的那些骚操作 | Kenvix's Blog 在普通的 Gradle Java/Kotlin 项目中使用 BuildConfig 修复国行 MIUI 打开 Google Play 始终提示 DF-DFERH-01 的问题 解决 VSCode 持续调用 WMIC 导致一个 CPU 核心完全被占满的问题 扔鸡蛋问题 | Kenvix's Blog 计算机幻觉从入门到入土 | Kenvix's Blog Java 注解预处理 Annotation Processing & 代码生成 Ubuntu 上通过以太网分享网络连接(NAT) | Kenvix's Blog Windows 选择指定的网卡来开承载网络型热点 | Kenvix's Blog 修复升级 Windows10 版本后所有内置应用闪退+第三方应用参数错误的问题 | Kenvix's Blog 配置用于 Gradle6.x + MySQL 8 的 jOOQ 3.14 代码自动生成 (已更新) 修复 Windows 环境下的程序访问 WSL 中的 MySQL 提示 Access Denied 的问题 修复 WSL 下 PHP+FastCGI 卡死的问题 使用任意磁盘或路径保存 Windows 文件历史记录 | Kenvix's Blog [1.12.2+Mod] MoeCraft :: 自由开放的科技向公益 Mod 服务器 Kenvix's Blog 禁用使用Intel核显的Windows笔记本自动调节亮度功能 | Kenvix's Blog 真正实现Minecraft高级登录(外置登录)的几种方案 | Kenvix's Blog 谈谈神舟的两艘贼船,Z7M-KP7S1 / Z7M-KP7SC USBCopyer: 插上U盘自动按需复制文件 | Kenvix's Blog USBCopyer 回调功能详细说明 | Kenvix's Blog C# 实现自定义"应用程序设置"的配置文件(user.config)存储路径 | Kenvix's Blog Win10 资源管理器为所有格式激活“编辑”按钮并修改文本文件“编辑”按钮的编辑器 | Kenvix's Blog 留言板 | Kenvix's Blog 又一次 Hello world | Kenvix's Blog Java 学习笔记 (仍在更新) | Kenvix's Blog 在Win10 Pro下挂载NFS(网络文件系统) | Kenvix's Blog Nginx 反向代理 Aria2 JSONRPC | Kenvix's Blog (Android6.0~9.0) 清除锁屏密码 | Kenvix's Blog WordPress 更换站点地址后批量修改文章/评论中的旧地址 | Kenvix's Blog 修复一加3/3T因固件过老导致刷入ROM时提示错误7的问题 | Kenvix's Blog 修复Android DM-Verity 警告 | Kenvix's Blog 贴吧云签到 资源索引(下载|文档|插件) | Kenvix's Blog 继续监控!使用树莓派+Motion实现实时视频监控并通过浏览器查看 | Kenvix's Blog 自动获取Pixiv每日排行榜第一张图片(600x600 | 可用于博客背景图) | Kenvix's Blog 使用树莓派实现定时拍照监控并发送邮件到邮箱 | Kenvix's Blog 好压 V2.7 Beta1 绿色版——功能强大,良心的压缩软件 | Kenvix's Blog 任意语言实现读取压缩包注释 | Kenvix's Blog 自己实现QQ群自定义分享(管理员开启了群交易?) | Kenvix's Blog MoeCDN - 加速Gravatar/GoogleAPIs等无法在国内访问的资源 | Kenvix's Blog [Minecraft] WebLogin-连接到你的服务器来检查玩家是否可以登录 | Kenvix's Blog Android卡刷包提示This package is for device: ... this device is ...的解决方案 Kenvix's Blog 给EMLOG评论框加上复选框[√]防止垃圾评论 | Kenvix's Blog 欢迎使用emlog | Kenvix's Blog
状态压缩的动态规划问题:骨牌完全覆盖棋盘问题 | Kenvix's Blog
2020-07-01 · via Kenvix's Blog

这个算法问题对于算法菊苣们来讲不过是小菜一碟。发到博客主要是希望能够对未来拿到这个问题而毫无头绪的读者带来些许帮助。

这篇文章实际上是我的《算法分析与设计》课程的大作业,从 Word 粘贴到博客仅做了一些格式上的修改,因此文章风格会比较离谱(报告文档的风格),敬请谅解。

问题题目

项目需求

本项目旨在解决以下的算法问题:

完全覆盖:有一天acmj在玩一种游戏—-用21或12的骨牌把m*n的棋盘完全覆盖。但他感觉把棋盘完全覆盖有点简单,他想能不能把完全覆盖的种数求出来?由于游戏难度增加他自己已经没法解决了,于是他希望大家能用程序来帮他把问题解决了。

业务要求

解决目标算法问题。

输入有多组数据。每组数据占一行,有两个正整数n(0 < n < 12),m ( 0 < m < 12)。当n,m等于0时输入结束。

输出每组数据输出占一行,输出完全覆盖的种数。

总体设计

解决此算法问题的程序分为算法类、单元测试、主函数三个模块。算法程序使用 Java 编写。

设计内容和要求

算法类:提供解决此算法问题的类。此类中包含解决此问题的全部代码。每一组输入数据对应一个类实例。类的构造方法及其所有成员方法均为私有。对外仅暴露静态方法 solve() 用于解决算法问题。

单元测试:基于 Junit 单元测试框架编写,提供多组测试用例用于测试算法类的正确性。

主函数:可执行程序的入口点,接收用户的输入,输出算法结果。

算法的详细设计思想

本算法问题是一个状态压缩的动态规划问题。把棋盘看成一个 m*n 的矩阵,由于输入规模总是不大于12,并且棋盘的一个格子只有“放”与“不放”两种状态,因此可以用一个整型来表示一列。用一维整型数组表示一个棋盘。用二维数组 status[line][method],以行号、该行的摆放方式为键,可行的摆放方案数(覆盖种数)为值,来存储每种排列方法的覆盖种数的动态规划结果。显然,状态压缩可以使得列的排列方法method直接作为数组的键而无需其他复杂的设计。

定义,对横着放的骨牌,左右两个位置均置为 1,对竖着放的骨牌,上方置 1,下方置 0。从最后一行开始,先假设最后一行的“后”一行全 1(用来过转移条件的判断),求在第 line 行的总方案数。

易证,所有有效的放置办法的第一行必然全为 1。dpSolve() 函数接受两个参数,分别表示需要计算覆盖种数的行数 line、上一行的排列方法lastMethod,返回覆盖种数。函数从本行首列开始,枚举这行的列的所有排列情况。对于一个排列情况,在满足转移条件时递归求覆盖方法的个数。递归时行数递减,并传入枚举的排列方法,若到达第 0 行,当发现整个第 1 行(首行)骨牌全是 1,可知放置办法有效,返回1。否则放置方法无效。最终,每行递归返回的结果求和即为本行、上一行的排列方法对应的覆盖种数。结果保存到二维数组以便反复使用。

转移条件是:

  1. 当前排列情况和之前的那一行的排列方式的“或”结果为全 1:确保不存在上下都是 0 的情况
  2. 当前排列情况和之前的那一行的排列方式的“与”结果为每两个相邻 1 成对或每两个相邻 0 成对:确保不存在孤立 1 和 孤立 0 的情况。

同时满足上述条件时,事实上覆盖了所有有效的排列情况并排除了非法排列。对于最后一行,传入的全 1 可保证不出现孤立1 —— 最后一行不可能再往下竖着放。

当所有排列方法枚举和递归完成时,函数返回的结果即为m*n棋盘的覆盖种数。

源代码

DominoArrangeNumSolver.java

import java.util.Arrays;

/**
 * status[i][S] 表示到达第i行,当前覆盖状态为 S 的方案数
 * 对横着放的骨牌,左右两个位置均置为 1
 * 对竖着放的骨牌,上方置 1,下方置 0
 */
public final class DominoArrangeNumSolver {
    private final long[][] status;
    private final int colNum;

    private DominoArrangeNumSolver(int line, int col) {
        this.colNum = col;
        this.status = new long[line + 1][1 << col];
        Arrays.stream(status).forEach(it -> Arrays.fill(it, -1)); // 全部填充 -1 表示未访问过
    }

    /**
     * 检查排列方法是否存在不成对的孤立 0 或 1
     * @param method 该列的排列方式
     * @return boolean true:没有孤立 0 或 1
     */
    private boolean isMethodPaired(int method) {
        for (int col = 0; col < colNum; col++) { // 查找 1 的下一个是否为 0 来检查是否不成对
            if ((method & (1 << col)) != 0) { // 第 col 列骨牌是 1
                if (col == colNum - 1 || (method & (1 << (col + 1))) == 0) //是最后一列或 col + 1 列是 0
                    return false;

                col++; //跳过下一列,直接进入 col + 2
            }
        }

        return true;
    }

    /**
     * 求在第 line 行的总方案数。
     * 注:函数内的“之前的那一行”指的是函数递归调用前的那一行的排列,意义上为 line + 1 行的排列。
     * @param line 行
     * @param lastMethod 之前的一行的排列(即 line + 1 的排列)
     * @return 方案数
     */
    private long dpSolve(int line, int lastMethod) {
        if (status[line][lastMethod] != -1) // 是否已经计算过目的行的排列
            return status[line][lastMethod];

        // 递归停止条件。易证所有有效的放置办法第一行必然全为 1
        if (line == 0) { //若为第 0 行
            if (lastMethod == ((1 << colNum) - 1)) // 整个第 1 行(首行)骨牌全是 1,也就是放置办法有效。
                return status[line][lastMethod] = 1; // 需要指出,放置时从末行放置,而检查时从首行检查。不可能有首行为 0 的任何有效表示
            else // 其他情况放置的肯定不成立。
                return status[line][lastMethod] = 0;
        }

        status[line][lastMethod] = 0;
        for (int cond = 0; cond < (1 << colNum); cond++) { // 枚举这行的列的所有排列情况
            // 转移条件【同时满足时转移】检查是否存在不合理的排列
            // 1. 当前排列情况和之前的那一行的排列方式的“或”结果为全 1:确保不存在上下都是 0 的情况
            // 2. 当前排列情况和之前的那一行的排列方式的“与”结果为每两个相邻 1 成对或每两个相邻 0 成对:确保不存在孤立 1 和 孤立 0 的情况。
            // 同时满足上述条件时,事实上覆盖了所有有效的排列情况并排除了非法排列
            // 对于最后一行,传入的全 1 可保证不出现孤立1 —— 最后一行不可能再往下竖着放
            if ((cond | lastMethod) == ((1 << colNum) - 1) && isMethodPaired(cond & lastMethod))
                status[line][lastMethod] += dpSolve(line - 1, cond); // 递归求 line - 1 行的放置
        }

        return status[line][lastMethod];
    }

    public static long solve(int line, int col) {
        if (line == 0 || col == 0 || (line % 2 != 0 && col % 2 != 0))
            return 0;

        if (col > line) { // 让较小者成为列,减少情况数。
            int temp = line;
            line = col;
            col = temp;
        }

        // 从最后一行开始,先假设最后一行的“后”一行全 1,用来过转移条件的判断。
        return new DominoArrangeNumSolver(line, col).dpSolve(line, (1 << col) - 1);
    }
}

DominoArrangeNumTest.java

import org.junit.Assert;
import org.junit.Test;

import java.util.Scanner;

public final class DominoArrangeNumTest {
    public static void main(String[] args) {
        try (Scanner scanner = new Scanner(System.in)) {
            int col;
            int line;

            do {
                col = scanner.nextInt();
                line = scanner.nextInt();

                System.out.println(DominoArrangeNumSolver.solve(line, col));
            } while (col != 0 || line != 0);
        }
    }

    @Test
    public void test() {
        Assert.assertEquals(51205, DominoArrangeNumSolver.solve(4, 11));
        Assert.assertEquals(3, DominoArrangeNumSolver.solve(2, 3));
        Assert.assertEquals(2, DominoArrangeNumSolver.solve(2, 2));
        Assert.assertEquals(5, DominoArrangeNumSolver.solve(2, 4));
        Assert.assertEquals(144, DominoArrangeNumSolver.solve(2, 11));
        Assert.assertEquals(0, DominoArrangeNumSolver.solve(1, 3));
        Assert.assertEquals(0, DominoArrangeNumSolver.solve(3, 3));
        Assert.assertEquals(53060477521960000L, DominoArrangeNumSolver.solve(12, 12));
    }
}