JAVA数独解题(四):数对法

314 阅读6分钟

说明

数对:两个单元格,两个数字。两个数字在当前区域(行、列、宫)中,只能在这两个单元格内出现,则形成数对。 作用:如果形成数对,则能够确定这两个单元格只能填写这两个数字,其他数字不能出现,都可以剔除,并且同区域内,其他单元格不能填写这两个数字。

图片

在这里插入图片描述 如上图所示,第1宫内,有两个空白格只能填入8和9,则这两个单元格与89形成数对,其余单元格内不能填入89。则可以排除。 并且在所处的列中,也能排除。

再看一个隐藏比较深入的,如下图 在这里插入图片描述 上图在第3宫中,4和6只能填写在这两个单元格,故形成46数对,排除本身单元格内的其他数字。所以8只能出现在第3行第9列的位置。

算法代码

CalcEnum

在 绑定算法 中添加 属性

package com.suduku.calc.enums;

import com.suduku.calc.*;
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.HashMap;
import java.util.Map;

/**
 * 功能描述: 算法枚举 <br/>
 *
 */
@Getter
@AllArgsConstructor
public enum CalcEnum {

    /***/
    ONLY_NUM(OnlyNumCalc.class, "唯余法", "唯一余数法(当前单元格中,候选数字只有一个)"),
    ONLY_BOX(OnlyBoxCalc.class, "摒除法", "摒除法(数字在所在行或列或宫中,只有一个空格能够填写,则确定是唯一数字)"),
    GRID_XY(GridXYCalc.class, "单宫行列法", "单宫行列法()"),
    SU_DUI(SuDuiCalc.class, "数对法", "数对法(一个区域内,两个数字只能在两个单元格内,则形成数对,其他值不能填入)"),
    ;

    private static final Map<Class<? extends AbstractCalc>, CalcEnum> CE_MAP = new HashMap<>(CalcEnum.values().length);

    static {
        for(CalcEnum ce : CalcEnum.values()) {
            CE_MAP.put(ce.getClazz(), ce);
        }
    }

    /**
     * 功能描述: 通过类,获取枚举 <br/>
     *
     * @param clazz 类
     * @return "com.suduku.calc.enums.CalcEnum"
     */
    public static CalcEnum indexOf(Class<? extends AbstractCalc> clazz) {
        return CE_MAP.get(clazz);
    }

    private Class<? extends AbstractCalc> clazz;
    private String name;
    private String msg;

}

SuDuiCalc

package com.suduku.calc;

import com.suduku.entity.Box;
import com.suduku.util.SudoUtil;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 数对法(一个区域内,两个数字只能在两个单元格内,则形成数对,其他值不能填入) <br/>
 *
 * 测试数据:DataConstant.XING_02_23
 */
public class SuDuiCalc extends AbstractCalc {

    @Override
    Box solve() {
        // 判断宫内数对
        Box box = twoNumBox(getGMap());
        if(box != null) {
            return box;
        }
        // 判断行内数对
        box = twoNumBox(getXMap());
        if(box != null) {
            return box;
        }
        // 判断列内数对
        box = twoNumBox(getYMap());
        if(box != null) {
            return box;
        }
        return null;
    }

    /**
     * 功能描述: 查找区域内的数对 <br/>
     *
     * @param areaMap 区域集合
     * @return "com.suduku.entity.Box"
     */
    private Box twoNumBox(Map<Integer, List<Box>> areaMap) {
        // 遍历所有区域
        Set<Map.Entry<Integer, List<Box>>> entries = areaMap.entrySet();
        for (Map.Entry<Integer, List<Box>> entry : entries) {
            List<Box> list = entry.getValue();
            // 查找区域内候选字是两个数字,并且候选数字相同,且只有两个单元格的情况,形成数对
            List<Box> nmList = SudoUtil.findSuDuiBoxList(list);
            if(nmList != null && nmList.size() == 2 && SudoUtil.isEqTwoList(nmList.get(0).getCList(), nmList.get(1).getCList())) {
                // 形成数对,则可以排除区域其他宫内的情况
                // 获取数对的下标
                List<Integer> suDuiIndexs = nmList.stream().map(Box::getI).collect(Collectors.toList());
                List<Box> clearBoxList = list.stream().filter(b -> b.isBlank() && !suDuiIndexs.contains(b.getI()) && (b.getCList().contains(nmList.get(0).getCList().get(0)) || b.getCList().contains(nmList.get(0).getCList().get(1)))).collect(Collectors.toList());
                for (Box box : clearBoxList) {
                    // 是空白格,不是数对单元格,包含数对数字
                    getListener().sendMsg("确定数对为【%d, %d】,移除数对区域其他单元格数对数字。\n", nmList.get(0).getCList().get(0), nmList.get(0).getCList().get(1));
                    box.getCList().removeAll(nmList.get(0).getCList());
                    return box;
                }
            }
            // 遍历 1 ~ 9 个数字,在候选值中是否只有两个空白格
            for(Integer n : Box.INIT_LIST) {
                List<Box> nList = list.stream()
                        .filter(b -> b.isBlank() && b.getCList().contains(n))
                        .collect(Collectors.toList());
                // 如果在该区域内,只有两个单元格
                if(nList.size() == 2) {
                    // 遍历下一个数字,并且数字大于第一个数字(减少次数)
                    for(Integer m : Box.INIT_LIST) {
                        if(m > n) {
                            List<Box> mList = list.stream().filter(b -> b.isBlank() && b.getCList().contains(m)).collect(Collectors.toList());
                            // 如果也是只有两个单元格,并且与第一个数字的两个单元格相等。则形成数对
                            if(mList.size() == 2 && SudoUtil.isEqBoxList(nList, mList)) {
                                // 形成数对,开始剔除数据
                                List<Integer> suDuiList = SudoUtil.getList(n, m);
                                // 1、剔除本身两个单元格
                                for(Box box : nList) {
                                    if(box.getCList().size() > 2) {
                                        getListener().sendMsg("确定数对为【%d, %d】,移除数对单元格其余数字。\n", n, m);
                                        box.setCList(suDuiList);
                                        return box;
                                    }
                                }
                                // 2、剔除区域内的其他单元格
                                // 获取数对的下标
                                List<Integer> suDuiIndexs = nList.stream().map(Box::getI).collect(Collectors.toList());
                                // 遍历当前区域
                                for (Box box : list) {
                                    // 是空白格,不是数对单元格,包含数对数字
                                    if(box.isBlank() && !suDuiIndexs.contains(box.getI()) && (box.getCList().contains(n) || box.getCList().contains(m))) {
                                        getListener().sendMsg("确定数对为【%d, %d】,移除数对区域其他单元格数对数字。\n", n, m);
                                        box.getCList().removeAll(suDuiList);
                                        return box;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return null;
    }

}

SudoUtil

    /**
     * 功能描述: 发现数对 <br/>
     *
     * @param towCLists 待发现的数对
     * @return "java.util.List<com.suduku.entity.Box>"
     */
    public static List<Box> findSuDuiBoxList(List<Box> towCLists) {
        for(Box b1 : towCLists) {
            for(Box b2 : towCLists) {
                if(b1.isBlank() && b2.isBlank() && b1.getI() != b2.getI()) {
                    if(b1.getCList().size() == 2 && isEqTwoList(b1.getCList(), b2.getCList())) {
                        return getList(b1, b2);
                    }
                }
            }
        }
        return null;
    }

输出结果

尝试【	数对法	】
确定数对为【4, 6】移除数对单元格其余数字。
确认位置【行:1,列:8】	值为:【】	候选值为:【4,6】
 0 (12       ) 0 (12789    ) 0 (89       ) |  0 (179      ) 0 (579      ) 0 (12579    ) |  3 (         ){0}(46       ) 0 (246789   ) | 
 5 (         ) 0 (12379    ) 4 (         ) |  6 (         ) 8 (         ) 0 (12379    ) |  0 (129      ) 0 (1279     ) 0 (279      ) | 
 6 (         ) 0 (123789   ) 0 (89       ) |  0 (1379     ) 0 (79       ) 4 (         ) |  5 (         ) 0 (1279     ) 0 (2789     ) | 

 0 (79       ) 6 (         ) 1 (         ) |  2 (         ) 3 (         ) 8 (         ) |  0 (49       ) 5 (         ) 0 (479      ) | 
 8 (         ) 5 (         ) 2 (         ) |  0 (79       ) 4 (         ) 0 (79       ) |  6 (         ) 3 (         ) 1 (         ) | 
 0 (79       ) 4 (         ) 3 (         ) |  5 (         ) 1 (         ) 6 (         ) |  8 (         ) 0 (279      ) 0 (279      ) | 

 0 (12       ) 0 (1289     ) 0 (5689     ) |  0 (134789   ) 0 (5679     ) 0 (13579    ) |  0 (1249     ) 0 (12469    ) 0 (23469    ) | 
 3 (         ) 0 (1289     ) 7 (         ) |  0 (1489     ) 0 (69       ) 0 (19       ) |  0 (1249     ) 0 (12469    ) 5 (         ) | 
 4 (         ) 0 (19       ) 0 (569      ) |  0 (139      ) 2 (         ) 0 (1359     ) |  7 (         ) 8 (         ) 0 (369      ) | 

尝试【	数对法	】
确定数对为【4, 6】移除数对单元格其余数字。
确认位置【行:1,列:9】	值为:【】	候选值为:【4,6】
 0 (12       ) 0 (12789    ) 0 (89       ) |  0 (179      ) 0 (579      ) 0 (12579    ) |  3 (         ) 0 (46       ){0}(46       ) | 
 5 (         ) 0 (12379    ) 4 (         ) |  6 (         ) 8 (         ) 0 (12379    ) |  0 (129      ) 0 (1279     ) 0 (279      ) | 
 6 (         ) 0 (123789   ) 0 (89       ) |  0 (1379     ) 0 (79       ) 4 (         ) |  5 (         ) 0 (1279     ) 0 (2789     ) | 

 0 (79       ) 6 (         ) 1 (         ) |  2 (         ) 3 (         ) 8 (         ) |  0 (49       ) 5 (         ) 0 (479      ) | 
 8 (         ) 5 (         ) 2 (         ) |  0 (79       ) 4 (         ) 0 (79       ) |  6 (         ) 3 (         ) 1 (         ) | 
 0 (79       ) 4 (         ) 3 (         ) |  5 (         ) 1 (         ) 6 (         ) |  8 (         ) 0 (279      ) 0 (279      ) | 

 0 (12       ) 0 (1289     ) 0 (5689     ) |  0 (134789   ) 0 (5679     ) 0 (13579    ) |  0 (1249     ) 0 (12469    ) 0 (23469    ) | 
 3 (         ) 0 (1289     ) 7 (         ) |  0 (1489     ) 0 (69       ) 0 (19       ) |  0 (1249     ) 0 (12469    ) 5 (         ) | 
 4 (         ) 0 (19       ) 0 (569      ) |  0 (139      ) 2 (         ) 0 (1359     ) |  7 (         ) 8 (         ) 0 (369      ) | 

最终结果

 1 (         ) 7 (         ) 8 (         ) |  9 (         ) 5 (         ) 2 (         ) |  3 (         ) 4 (         ) 6 (         ) | 
 5 (         ) 2 (         ) 4 (         ) |  6 (         ) 8 (         ) 3 (         ) |  1 (         ) 9 (         ) 7 (         ) | 
 6 (         ) 3 (         ) 9 (         ) |  1 (         ) 7 (         ) 4 (         ) |  5 (         ) 2 (         ) 8 (         ) | 

 7 (         ) 6 (         ) 1 (         ) |  2 (         ) 3 (         ) 8 (         ) |  9 (         ) 5 (         ) 4 (         ) | 
 8 (         ) 5 (         ) 2 (         ) |  7 (         ) 4 (         ) 9 (         ) |  6 (         ) 3 (         ) 1 (         ) | 
 9 (         ) 4 (         ) 3 (         ) |  5 (         ) 1 (         ) 6 (         ) |  8 (         ) 7 (         ) 2 (         ) | 

 2 (         ) 9 (         ) 5 (         ) |  8 (         ) 6 (         ) 7 (         ) |  4 (         ) 1 (         ) 3 (         ) | 
 3 (         ) 8 (         ) 7 (         ) |  4 (         ) 9 (         ) 1 (         ) |  2 (         ){6}(         ) 5 (         ) | 
 4 (         ) 1 (         ) 6 (         ) |  3 (         ) 2 (         ) 5 (         ) |  7 (         ) 8 (         ) 9 (         ) | 

============数独解题完成,尝试次数为:82============

总结

数对属于数独解法中的中级用法,熟练掌握可以快速解决大部分数独。在Sudo Cool 这个app的 2星难度,只需要掌握数对,就能全部通关。

代码详情

代码地址