前言
个人兴趣。制作一个可以看到解题方法的数独解题工具。主要的解题方法,就是一直排除候选数字。
环境
JDK1.8 Lombok插件(可以不需要,懒的写get、set)
创建工程
创建一个名为 Sudoku 的 maven 工程,并添加 lombok 依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
实体类
在数独中,有两个实体类,一个是最基本的单元格 Box,一个是由 9 * 9 共 81 个单元格组成的九宫格数独 Sudo。
Box
最基本的单元格 属性:值,下标,横坐标,纵坐标,所在宫,候选值列表,是否初始数字 行为:是否空白格,是否数字格,设置数字并清空候选值列表
package com.suduku.entity;
import lombok.Data;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Data
public class Box {
public static final List<Integer> INIT_LIST = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
/** 值(0 ~ 9 ;其中 0 代表未填写) */
private Integer v;
/** 一维数组下标 */
private int i;
/** 二维数组行下标 */
private int x;
/** 二维数组列下标 */
private int y;
/** 所在宫 */
private int g;
/** 是否初始化数字 */
private boolean initNum;
/** 候选数字列表 */
private List<Integer> cList;
public Box(char c, int i) {
this.v = c - 48;
this.i = i;
this.x = this.i / 9;
this.y = this.i % 9;
this.g = this.x / 3 + this.y / 3 + this.x / 3 * 2;
this.initNum = this.v != 0;
this.cList = new ArrayList<>(this.v == 0 ? INIT_LIST : Collections.emptyList());
}
/**
* 功能描述: 是否空白 <br/>
* 0表示待填写
* @return "boolean"
*/
public boolean isBlank() {
return v == 0;
}
/**
* 功能描述: 是否数字 <br/>
*
* @return "boolean"
*/
public boolean isNumber() {
return v != 0;
}
/**
* 功能描述: 设置值,并清理候选数字 <br/>
*
* @param v 确定的值
*/
public void setVAndClear(Integer v) {
if(v != 0) {
this.v = v;
this.cList.clear();
}
}
}
Sudo
由 81 个 Box 单元格组成的 九宫格 属性:单元格列表,行组,列组,宫组,监听器 行为:初始化单元格列表,初始化监听器,初始化行组、列组、宫组,初始清理候选值列表,刷新其余单元格,判断是否完成数独
package com.suduku.entity;
import com.suduku.listener.SudoListener;
import com.suduku.listener.impl.SudoPrintImpl;
import com.suduku.util.SudoUtil;
import lombok.Getter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 数独九宫 <br/>
*
* @author chena
*/
public class Sudo {
/** 单元格列表 */
@Getter
private final List<Box> boxList;
/** 行集合 */
@Getter
private Map<Integer, List<Box>> xMap;
/** 列集合 */
@Getter
private Map<Integer, List<Box>> yMap;
/** 宫集合 */
@Getter
private Map<Integer, List<Box>> gMap;
/** 监听器 */
@Getter
private final SudoListener listener;
/** 默认监听 */
private static final SudoListener defaultListener = new SudoPrintImpl();
public Sudo(List<Box> boxList) {
this(boxList, defaultListener);
}
public Sudo(List<Box> boxList, SudoListener listener) {
this.boxList = boxList;
if(listener == null) {
this.listener = defaultListener;
} else {
this.listener = listener;
}
initMap();
initCList();
}
/**
* 功能描述: 首次初始化候选数字列表,按照行,列,宫,清除已经出现的数字 <br/>
*
*/
private void initCList() {
for(Box b : this.boxList) {
if(b.isBlank()) {
List<Integer> cList = b.getCList();
List<Integer> xList = SudoUtil.getNotZero(this.xMap.get(b.getX()));
List<Integer> yList = SudoUtil.getNotZero(this.yMap.get(b.getY()));
List<Integer> gList = SudoUtil.getNotZero(this.gMap.get(b.getG()));
cList.removeAll(xList);
cList.removeAll(yList);
cList.removeAll(gList);
b.setCList(cList);
}
}
}
/**
* 功能描述: 初始化x,y,g三个区域 <br/>
*
*/
private void initMap() {
this.xMap = this.boxList.stream().collect(Collectors.groupingBy(Box::getX));
this.yMap = this.boxList.stream().collect(Collectors.groupingBy(Box::getY));
this.gMap = this.boxList.stream().collect(Collectors.groupingBy(Box::getG));
}
/**
* 功能描述: 刷新其余单元格 <br/>
*
* @param box 单元格
*/
public void refreshOtherBox(Box box) {
if(box != null && box.isNumber()) {
for(Box b : this.boxList) {
if(b.getI() != box.getI() && (b.getX() == box.getX() || b.getY() == box.getY() || b.getG() == box.getG())) {
b.getCList().remove(box.getV());
}
}
}
}
/**
* 功能描述: 是否完成数独 <br/>
*
* @return "boolean"
*/
public boolean isFinish() {
return this.boxList.stream().noneMatch(Box::isBlank);
}
}
监听器
SudoListener
作用是在计算过程中,或者改变的时候,做记录的埋点。再通过不同的监听实现,达到不同的效果。
package com.suduku.listener;
import com.suduku.calc.AbstractCalc;
import com.suduku.entity.Box;
import com.suduku.calc.enums.CalcEnum;
import java.util.List;
/**
* 监听接口 <br/>
*
* @author chena
*/
public interface SudoListener {
/**
* 功能描述: 发送提示信息 <br/>
*
* @param msg 消息内容
*/
void sendMsg(String msg, Object ...args);
/**
* 功能描述: 改变内容 <br/>
*
* @param list 数独列表
* @param b 单元格
*/
void change(List<Box> list, Box b);
/**
* 功能描述: 改变 <br/>
*
* @param ac 算法
* @param list 数独列表
* @param b 单元格
*/
void change(AbstractCalc ac, List<Box> list, Box b);
}
SudoPrintImpl
打印监听实现,默认实现的监听器,打印埋点信息。
package com.suduku.listener.impl;
import com.suduku.calc.AbstractCalc;
import com.suduku.entity.Box;
import com.suduku.listener.SudoListener;
import com.suduku.util.SudoUtil;
import java.util.List;
import java.util.stream.Collectors;
/**
* 数独输出实现监听 <br/>
*
* @author chena
*/
public class SudoPrintImpl implements SudoListener {
@Override
public void sendMsg(String msg, Object ...args) {
System.out.printf((msg) + "%n", args);
}
@Override
public void change(List<Box> list, Box b) {
}
@Override
public void change(AbstractCalc ac, List<Box> list, Box b) {
sendMsg("使用【\t%s\t】\t确认位置【行:%d,列:%d】\t值为:【%d】\t候选值为:【%s】",
ac.getCalcEnum().getName(), b.getX() + 1, b.getY() + 1, b.getV(),
b.getCList().stream().map(String::valueOf).collect(Collectors.joining(",")));
SudoUtil.print(list, b);
}
}
核心类
核心类一共分成两部分。一部分是算法组,由一个算法基类和不同算法实现组成。另一部分是处理者,用于调用算法,并且做相关记录的事情。
SudoHandler
作为处理者的工作内容为:接收数组字符串并转成标准单元格列表,构造出数独实体,设置监听,注册算法,最后进行推算与记录工作。
package com.suduku.handle;
import com.suduku.entity.Box;
import com.suduku.entity.Sudo;
import com.suduku.calc.enums.CalcEnum;
import com.suduku.calc.register.SudoRegister;
import com.suduku.listener.SudoListener;
import com.suduku.util.SudoUtil;
import java.util.List;
/**
* 数独处理者 <br/>
*
* @author chena
*/
public class SudoHandler {
/** 数独数据 */
private final Sudo sudo;
/** 统计次数 */
private int count;
/** 监听 */
private final SudoListener listener;
public SudoHandler(String str) {
this(str, null);
}
public SudoHandler(String str, SudoListener listener) {
// 校验 str 是否满足数独
SudoUtil.isSudoCheck(str);
// 转换成 List<Box>
List<Box> boxList = SudoUtil.toBoxList(str);
// 创建 Sudo
this.sudo = new Sudo(boxList, listener);
this.listener = this.sudo.getListener();
// 注册算法
SudoCalcRegister.register(sudo);
}
/**
* 开始推算 <br/>
*/
public void calculate() {
SudoUtil.print(this.sudo.getBoxList());
// 从注册表中,获取解题方法,一个一个尝试,如果其中一个有改变值,则从头开始继续尝试。
boolean isChange = false;
for(CalcEnum ce : CalcEnum.values()) {
isChange = SudoCalcRegister.get(ce).calculate();
count++;
if(isChange) {
break;
}
}
if(isChange) {
if(!this.sudo.isFinish()) {
// 重复执行
calculate();
} else {
// 解题完成
this.listener.sendMsg("============数独解题完成,尝试次数为:%d============", count);
}
} else {
// 无解
this.listener.sendMsg("************************数独解题失败,一共尝试了:%d次************************", count);
}
}
}
算法组
算法组由一个算法基类、算法枚举、注册表、算法实现,等部分组成。
AbstractCalc
算法基类,实现算法的公共抽象功能,使各个算法实现专注完成本职工作。
package com.suduku.calc;
import com.suduku.entity.Box;
import com.suduku.entity.Sudo;
import com.suduku.calc.enums.CalcEnum;
import lombok.Data;
/**
* 基础解题方法 <br/>
*
* @author chena
*/
@Data
public abstract class AbstractCalc {
private Sudo sudo;
/**
* 功能描述: 实例化 <br/>
*
* @param clazz AbstractCalc子类
* @return "com.suduku.calc.AbstractCalc"
*/
public static AbstractCalc getInstance(Class<? extends AbstractCalc> clazz) {
try {
return clazz.newInstance();
} catch (Exception e) {
throw new RuntimeException("实例化异常:" + e.getMessage());
}
}
/**
* 功能描述: 计算 <br/>
*
* @return "com.suduku.enums.CalcResultEnum"
*/
public boolean calculate() {
// 解题
Box box = solve();
if(box != null) {
// 刷新
this.sudo.refreshOtherBox(box);
// 发送改变监听
this.sudo.getListener().change(this, this.sudo.getBoxList(), box);
if(box.isBlank()) {
// 如果没有得到结果,则重复执行
calculate();
}
return true;
}
return false;
}
/**
* 功能描述: 解题方法 <br/>
*
* @return "com.suduku.entity.Box"
*/
abstract Box solve();
/**
* 功能描述: 算法枚举 <br/>
*
* @return "com.suduku.calc.enums.CalcEnum"
*/
public abstract CalcEnum getCalcEnum();
}
CalcEnum
算法枚举,与算法实现相互绑定。后续添加一个算法,只需要新建一个算法类,并且在该枚举中添加一个枚举值。
package com.suduku.calc.enums;
import com.suduku.calc.AbstractCalc;
import com.suduku.calc.OnlyNumCalc;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 功能描述: 算法枚举,需要与算法相互绑定 <br/>
*
*/
@Getter
@AllArgsConstructor
public enum CalcEnum {
/***/
ONLY_NUL(OnlyNumCalc.class, "唯余法", "唯一余数法(当前单元格中,候选数字只有一个)"),
;
private Class<? extends AbstractCalc> clazz;
private String name;
private String msg;
}
OnlyNumCalc
【唯余法】的算法实现,如果候选值中只有一个数字时,该数字必定为该单元格的值。本系列后续文章,基本上都是实现不同解题的算法
package com.suduku.calc;
import com.suduku.entity.Box;
import com.suduku.calc.enums.CalcEnum;
/**
* 唯一余数法(当前单元格中,候选数字只有一个) <br/>
*/
public class OnlyNumCalc extends AbstractCalc {
@Override
Box solve() {
for(Box box : getSudo().getBoxList()) {
// 如果是空白格,并且候选数字只有一个,则确定为
if(box.isBlank() && box.getCList().size() == 1) {
box.setVAndClear(box.getCList().get(0));
return box;
}
}
return null;
}
@Override
public CalcEnum getCalcEnum() {
return CalcEnum.ONLY_NUL;
}
}
SudoCalcRegister
算法注册表:使用Map存储不同算法。同时拥有通过算法枚举获取不同算法的方法。
package com.suduku.calc.register;
import com.suduku.calc.AbstractCalc;
import com.suduku.entity.Sudo;
import com.suduku.calc.enums.CalcEnum;
import java.util.HashMap;
import java.util.Map;
/**
* 注册表,用于注册解题方法 <br/>
*
* @author chena
*/
public class SudoCalcRegister {
/** 算法注册表 */
public static Map<CalcEnum, AbstractCalc> CALC_MAP = new HashMap<>(CalcEnum.values().length);
/**
* 功能描述: 开始注册 <br/>
*
*/
public static void register(Sudo sudo) {
for(CalcEnum ce : CalcEnum.values()) {
sudo.getListener().sendMsg("开始注册:" + ce.getName());
AbstractCalc ac = AbstractCalc.getInstance(ce.getClazz());
ac.setSudo(sudo);
CALC_MAP.put(ce, ac);
}
sudo.getListener().sendMsg("算法注册完成!");
}
/**
* 功能描述: 获取对应的算法 <br/>
*
* @param ce 算法枚举
* @return "com.suduku.calc.AbstractCalc"
*/
public static AbstractCalc get(CalcEnum ce) {
return CALC_MAP.get(ce);
}
}
其他类
DataConstant
数独数据常量,用于存储需要解题,或者有意思的数独数据。
package com.suduku.constant;
/**
* 数据常量 <br/>
*
* @author chena
*/
public class DataConstant {
/** 一星难度 */
public static final String XING_01_01 = "006002951980500007005039204029160040840203060600078090008050416367901020504020000";
}
SudoUtil
工具类,对数独操作的静态方法。
package com.suduku.util;
import com.suduku.entity.Box;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 工具类 <br/>
*
* @author chena
*/
public class SudoUtil {
/**
* 功能描述: 校验是否数独 <br/>
*
* @param str 字符串
*/
public static void isSudoCheck(String str) {
if(str.length() != 81) {
throw new RuntimeException("数独长度不为81位");
}
if(!str.matches("^[0-9]*$")) {
throw new RuntimeException("包含非数字字符,请检查");
}
// TODO 添加其他校验
}
/**
* 功能描述: 转成成单元格列表 <br/>
*
* @param str 字符串
* @return "java.util.List<com.suduku.entity.Box>"
*/
public static List<Box> toBoxList(String str) {
char[] cs = str.toCharArray();
List<Box> list = new ArrayList<>(cs.length);
for(int i = 0; i < cs.length; i++) {
list.add(new Box(cs[i], i));
}
return list;
}
/**
* 功能描述: 校验数独是否完成 <br/>
*
* @param boxList 数独列表
* @return "boolean"
*/
public static boolean isFinish(List<Box> boxList) {
long count = boxList.stream().filter(Box::isBlank).count();
// 如果需要,可以添加其他校验
return count == 0;
}
/**
* 功能描述: 获取指定区域内,不为0的数字列表 <br/>
*
* @param areaList 指定区域
* @return "java.util.List<java.lang.Integer>"
*/
public static List<Integer> getNotZero(List<Box> areaList) {
return areaList.stream().filter(Box::isNumber).map(Box::getV).collect(Collectors.toList());
}
/**
* 功能描述: 输出数独列表 <br/>
*
* @param boxList 数独
*/
public static void print(List<Box> boxList) {
print(boxList, null);
}
/**
* 功能描述: 输出数独列表 <br/>
*
* @param boxList 数独
* @param box 单元格
*/
public static void print(List<Box> boxList, Box box) {
for(Box b : boxList) {
// 输出填写内容
if(box != null && b.getI() == box.getI()) {
System.out.print("{" + b.getV() + "}");
} else {
System.out.print(" " + b.getV() + " ");
}
// 输出待填区域
System.out.print("(" + padCList(b) + ")");
// 输出宫-列分隔符
if((b.getI() + 1) % 3 == 0) {
System.out.print(" | ");
}
// 输出行换行
if((b.getI() + 1) % 9 == 0) {
System.out.println();
}
// 输出宫-行分隔符
if((b.getI() + 1) % 27 == 0) {
System.out.println();
}
}
}
/**
* 功能描述: 补全单元格待选数字到9为,如果不够,则填充空格 <br/>
*
* @param box 单元格
* @return "java.lang.String"
*/
public static String padCList(Box box) {
return padAfter(box.getCList().stream().map(String::valueOf).collect(Collectors.joining("")), ' ', 9);
}
/**
* 功能描述: 在字符串后面不全指定字符和长度 <br/>
*
* @param str 字符串
* @param c 字符
* @param length 长度
* @return "java.lang.String"
*/
private static String padAfter(String str, char c, int length) {
if(str == null || "".equals(str)) {
return repear(c, length);
}
if(str.length() <= length) {
return str + repear(c, length - str.length());
}
return str.substring(0, length);
}
/**
* 功能描述: 重复填充字符 <br/>
*
* @param c 填充字符
* @param count 填充长度
* @return "java.lang.String"
*/
private static String repear(char c, int count) {
if(count <= 0) {
return "";
}
char[] result = new char[count];
for(int i = 0; i < count; i++) {
result[i] = c;
}
return new String(result);
}
}
SudoMain
主函数入口
package com.suduku;
import com.suduku.constant.DataConstant;
import com.suduku.handle.SudoHandler;
/**
* 主入口 <br/>
*
* @author chena
*/
public class SudoMain {
public static void main(String[] args) {
SudoHandler sudoHandler = new SudoHandler(DataConstant.XING_01_01);
sudoHandler.calculate();
}
}
总结
以上为本次搭建的全部内容。可以解出部分1星难度的数独。本文使用的数独案例取自 Sudo Cool app和 每天3道奥数题 补充:先完成全部数独算法的实现,能够解5星难度的数独,然后考虑如何改造与异形数独。