JAVA数独解题(一):框架搭建

前言

个人兴趣。制作一个可以看到解题方法的数独解题工具。主要的解题方法,就是一直排除候选数字。

环境

   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星难度的数独,然后考虑如何改造与异形数独。

代码地址

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值