数独算法

package test;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.TreeSet;

/**
 * 数独算法
 * @author Mr li
 *
 */
public class Sudokus {
	//将数组分成九个宫,记录每个宫的横纵初始横纵坐标
	Map<Integer,Integer> row = new HashMap<Integer,Integer>();
	Map<Integer,Integer> line = new HashMap<Integer,Integer>();
	//数独数组长度
	static int le = 9;
	//一个坐标允许填的所有值
	static int aRow[] = {1,2,3,4,5,6,7,8,9};
	//初始化数组
	static int sudoku[][] = new int[le][le];
	//获取数独数组中所有为0的位置的坐标,并且存入一个map中
	TreeSet<String> zeroCoord = new TreeSet<String>();

	//thirdSolution方法需要
	static int testSudoku[][] = new int[le][le];
	static int isOk = 1;

	/**
	 * 位解:排除法,根据该坐标获取该坐标行,列,宫的所有数字,然后排除掉,如果该此时剩余唯一一个值,表示这个值为数独解
	 * @param sudoku 传入的数独数组
	 * @param zeroCoord 数组中为0的坐标,即需要解的所有坐标集
	 * @return
	 */
	public int firstSolution(int[][] sudoku,TreeSet<String> zeroCoord){
		System.out.println("firstSolution---start");
		//获取“0”的个数
		int zeroNum = zeroCoord.size();
		//转为数组再进行处理
		Object[] zCoord = zeroCoord.toArray();
		for(int i=0;i<zCoord.length;i++){
			String[] rlArr = zCoord[i].toString().split(",");
			int ro = Integer.parseInt(rlArr[0]);
			int li = Integer.parseInt(rlArr[1]);
			TreeSet<Integer> numCanSet = new TreeSet<Integer>();
			numCanSet = this.getNumCanSet(sudoku,ro,li);//根据坐标获取该坐标可填入的数组集
			if(numCanSet.size() == 1){
				sudoku[ro][li] = numCanSet.first();
				zeroCoord.remove(ro+","+li);//如果该坐标获取到解,则移除该坐标
				System.out.println("坐标:("+ro+","+li+") 值:"+numCanSet.first()+"剩余"+zeroCoord.size());//记录每次解值的坐标以及该坐标的值
			}else if(numCanSet.size() == 0){
				return -1;//出现悖论位
			}
		}
		System.out.println("firstSolution---end");
		if(zeroCoord.size() == zeroNum){
			return 0;//本次过程没有获得一个解
		}
		return 1;
	}
	/**
	 * 宫解:首先遍历一个宫内的九个坐标,并且获取每个坐标可填入的所有值,然后判断在本宫内,哪个值只出现了一次,表示该值只可以填入该坐标
	 * @param sudoku 传入的数独数组
	 * @param zeroCoord 数组中为0的坐标,即需要解的所有坐标集
	 * @return
	 */
	public int secondSolution(int[][] sudoku,TreeSet<String> zeroCoord){
		System.out.println("secondSolution---start");
		//获取“0”的个数
		int zeroNum = zeroCoord.size();
		for(int i=0;i<le;i++){
			//存入每个宫里面的1到9还没有填入的数字,并且存入,每个数字在本宫内可填入几次,如果是一次,表示获取到一个解
			Map<Integer,Integer> canInNum = new HashMap<Integer,Integer>();
			//存入每个宫里面的1到9还没有填入的数字,并且存入,遍历之下,每个数字可填入的最新坐标,然而我们依然只关心只有一个值的解,那么其坐标只存入一次
			Map<Integer,String> newCoord = new HashMap<Integer,String>();
			//获取宫的初始坐标
			int rowF = row.get(i);
			int lineF = line.get(i);
			//循环遍历宫内九个坐标		
			for(int m=0;m<3;m++){
				for(int n=0;n<3;n++){
					//每个坐标可填入的值的集
					TreeSet<Integer> setCan = new TreeSet<Integer>();
					int num = sudoku[rowF+m][lineF+n];
					if(num == 0){
						setCan = this.getNumCanSet(sudoku,rowF+m,lineF+n);
						if(setCan.size() == 0){
							return -1;//出现悖论位
						}
						for(int a : setCan){
							try {
								canInNum.put(a, canInNum.get(a)+1);
							} catch (Exception e) {
								canInNum.put(a, 1);
							}
							newCoord.put(a, (rowF+m)+","+(lineF+n));
						}
					}
				}
			}
			//遍历canInNum,获取只出现一次的值,以及该值的坐标
			for(int key : canInNum.keySet()){
				if(canInNum.get(key) == 1){
					String coord = newCoord.get(key);
					String[] coordArr = coord.split(",");
					int ro = Integer.parseInt(coordArr[0]);
					int le = Integer.parseInt(coordArr[1]);
					//将值填入数组中,同时将zeroNum减1
					sudoku[ro][le] = key;
					zeroCoord.remove(ro+","+le);
					System.out.println("坐标:("+ro+","+le+") 值:"+key+"剩余"+zeroCoord.size());
				}
			}
		}
		System.out.println("secondSolution---end");
		if(zeroCoord.size() == zeroNum){
			return 0;//本次过程没有获得一个解
		}
		return 1;
	}
	/**
	 * 双值位测解:获取某个只可以填入两个值的坐标,然后分别将此值填入其中,并继续解此数独,如果其中一个值填入数独中,出现错误,则可以确定另外一个值为正确值
	 * @param sudoku 传入的数独数组
	 * @param zeroCoord 数组中为0的坐标,即需要解的所有坐标集
	 * @return
	 */
	public int thirdSolution(int[][] sudoku,TreeSet<String> zeroCoord){
		System.out.println("thirdSolution---start");
		//获取“0”的个数
		int zeroNum = zeroCoord.size();
		Object[] zCoord = zeroCoord.toArray();
		for(int i=0;i<zCoord.length;i++){
			String[] rlArr = zCoord[i].toString().split(",");
			int ro = Integer.parseInt(rlArr[0]);
			int li = Integer.parseInt(rlArr[1]);
			TreeSet<Integer> numCanSet = new TreeSet<Integer>();
			numCanSet = getNumCanSet(sudoku,ro,li);//根据坐标获取该坐标可填入的数组集
			if(numCanSet.size() == 2){
				//建立一个set集,存放正确的坐标值
				TreeSet<Integer> trueSet = new TreeSet<Integer>();
				for(int q=0;q<2;q++){
					TreeSet<String> testZeroCoord = new TreeSet<String>();
					//测试数组建立
					int testSudoku[][] = new  int[sudoku.length][sudoku.length];
					for(int m=0;m<sudoku.length;m++){
						for(int n=0;n<sudoku[m].length;n++){
							testSudoku[m][n] = sudoku[m][n];
						}
					}
					int a = (int) numCanSet.toArray()[q];
					testSudoku[ro][li] = a;
					//调用测试方法,如果为1表示改值可填入,当然,如果最后两值判断都正确,则忽略该坐标,继续下一坐标
					testZeroCoord.addAll(zeroCoord);
					testZeroCoord.remove(ro+","+li);
					int r = this.isOk(testSudoku,testZeroCoord);
					if(r == 1){
						trueSet.add(a);
					}
					if(r == -1){
						return -1;
					}
				}
				//集长度为1,表示获取到正确解值
				if(trueSet.size() == 1){
					int a = trueSet.first();
					sudoku[ro][li] = a;
					zeroCoord.remove(ro+","+li);
					System.out.println("成功"+numCanSet.size()+"值位("+ro+","+li+"),值为:"+a+",剩余"+zeroCoord.size());
				}else{
					System.out.println("失败"+numCanSet.size()+"值位("+ro+","+li+"),值集为:"+numCanSet);
				}
			}

		}
		System.out.println("thirdSolution---end");
		if(zeroCoord.size() == zeroNum){
			return 0;
		}
		return 1;
	} 
	/**
	 * 获取该坐标可输入值集合,并返回
	 * @param ro 行坐标
	 * @param li 列坐标
	 * @return
	 */
	public TreeSet<Integer> getNumCanSet(int[][] sudoku,int ro,int li){
		TreeSet<Integer> setNo = new TreeSet<Integer>();
		TreeSet<Integer> setCan = new TreeSet<Integer>();
		//行数组
		int row[] = sudoku[ro];
		for(int i=0;i<le;i++){
			if(row[i] != 0){
				setNo.add(row[i]);	
			}
		}
		//列处理
		for(int j=0;j<le;j++){
			if(sudoku[j][li] != 0){
				setNo.add(sudoku[j][li]);	
			}
		}
		//宫处理
		int r = ro/3*3;//宫的初始行坐标
		int l= li/3*3;//宫的初始列坐标
		for(int m=0;m<3;m++){
			for(int n=0;n<3;n++){
				if(sudoku[r+m][l+n] != 0){
					setNo.add(sudoku[r+m][l+n]);
				}
			}
		}
		Object[] judgeArr = setNo.toArray();
		for(int a : aRow){
			boolean isA = true;
			for(Object b : judgeArr){
				if((int) b == a){
					isA = false;
					break;
				};
			}
			if(isA){
				setCan.add(a);
			}
		}
		return setCan;
	}
	/**
	 * 宫的横纵初始坐标
	 */
	public void rLInitialCoord(){
		//该循环表示九个宫的初始坐标都会分别存入row与line中,i对应的是一到九宫
		for(int i=0;i<le;i++){
			if(i<3){
				row.put(i, 0);
				line.put(i, i*3);
			}else if(i<6){
				row.put(i, 3);
				line.put(i, i%3*3);
			}else{
				row.put(i, 6);
				line.put(i, i%6*3);
			}
		}
	}
	/**
	 * 获取输入的数独,空值位用0表示,纯数字输入
	 */
	public void getInSudoku(){
		@SuppressWarnings("resource")
		Scanner sc = new Scanner(System.in);
		String sudokuIn = sc.next();
		if(sudokuIn.length() != 81){//判断输入的数组是否正确
			System.out.println("请输入正确的数组");
		}else{
			for(int i=0;i<le;i++){//遍历纯数字字符串,并且插入数独数组中
				for(int j=0;j<le;j++){
					int n = Integer.parseInt(sudokuIn.substring(le*i+j,le*i+j+1));
					sudoku[i][j] = n;
				}
			}
		}
	}
	/**
	 * 获取数独数组中所有为0的位置的坐标,并且存入一个map中,初始化zeroNum的值
	 */
	public void getAllZeroNumCoord(){
		for(int i=0;i<le;i++){
			for(int j=0;j<le;j++){
				if(sudoku[i][j] == 0){
					zeroCoord.add(i+","+j);
				}
			}
		}
	}
	/**
	 * 打印数独结果
	 */
	public void printSudoku(int[][] sudoku){
		for(int i=0;i<le;i++){
			int pr[] = sudoku[i];
			System.out.println(Arrays.toString(pr));
		}
	}
	/**
	 * 递归函数
	 */
	public void recursion(){
		int isSuccess = this.callBack();
		if(isSuccess == 1){
			this.recursion();
		}else{
			return;
		}
	}
	/**
	 * 递归函数中调用,返回1表示数独解成功,0表示还未完全解出,-1表示数独解失败
	 * @return
	 */
	public int callBack(){
		//为0的时候表示已经获取全部解,打印最终解
		if(zeroCoord.size() == 0){
			System.out.println("成功:");
			this.printSudoku(sudoku);
			return 0;
		}
		if(this.firstSolution(sudoku, zeroCoord) == 1){
			return 1;
		}else{
			if(this.secondSolution(sudoku, zeroCoord) == 1){
				return 1;
			}else{
				int thr = this.thirdSolution(sudoku, zeroCoord);
				if( thr == 1){
					return 1;
				}else if(thr == -1){
					System.out.println("成功:");
					this.printSudoku(testSudoku);
					return 0;
				}else{
					System.out.println("失败:");
					this.printSudoku(sudoku);
					return -1;
				}
			}
		}
	}
	/**
	 * 判断数独是否出现悖论位
	 * 0出现,1无错,-1填入测试值直接完成解数独
	 * @return
	 */
	public int isOk(int[][] sudoku,TreeSet<String> zeroCoord) {
		//将测试数据的初始化
		for(int m=0;m<le;m++){
			for(int n=0;n<le;n++){
				testSudoku[m][n] = sudoku[m][n];
			}
		}
		isOk = 1;
		this.testRecursion(zeroCoord);
		return isOk;
	}
	/**
	 * 递归函数
	 */
	public void testRecursion(TreeSet<String> testZeroCoord){
		int isSuccess = this.testCallBack(testZeroCoord);
		if(isSuccess == 1){
			this.testRecursion(testZeroCoord);
		}else{
			return;
		}
	}
	/**
	 * 递归函数中调用,返回1表示数独解成功,0表示还未完全解出,-1表示数独解失败
	 * @return
	 */
	public int testCallBack(TreeSet<String> testZeroCoord){
		//为0的时候表示已经获取全部解,打印最终解
		if(testZeroCoord.size() == 0){
			isOk = -1;
			return -1;
		}else{
			int fir = this.firstSolution(testSudoku, testZeroCoord);
			if(fir == 1){
				return 1;
			}else{
				if(fir == 0){
					int sec = this.secondSolution(testSudoku, testZeroCoord);
					if(sec == 1){
						return 1;
					}else{
						if(sec == -1){
							isOk = 0;
							return 0;
						}
					}
				}else{
					isOk = 0;
					return 0;
				}
			}
		}
		return 0;
	}
	/**
	 * main方法测试
	 * @param args
	 */
	public static void main(String[] args) {
		
		Sudokus sd = new Sudokus();
		//获取输入的数独数组
		sd.getInSudoku();
		//初始化数独需要填入的总个数以及其坐标
		sd.getAllZeroNumCoord();
		//宫的横纵初始坐标
		sd.rLInitialCoord();
		//递归函数解数独开始
		sd.recursion();
		System.exit(0);
	}
}

直接复制到java类中运行即可

键盘输入例子:001000600059002000400006002000870010200090007040053000800500006000100790004000500

如上,将每一行的数字全部输入,空值的位置用0表示

主要逻辑:

1、核心:三个主要解值方法,个人命名为位解,宫解,双值位测解,层级递进,第一个只有前一个方法失效,才调用下一个,每次解完如果成功,都需从第一个方法重新开始解。

    (1)位解:普通数独玩家逻辑,找出当前坐标位,行列宫内的所有数字,然后排出掉不可以填的,剩下的就是可以填的,如果可以填的数字个数刚好为1,成功获得一个解

    (2)宫解:以宫为单位,数独9*9八十一格,分为九个宫,宫解则是将该宫内的所有空值的坐标位可以填入的值全部取出来,然后判断在该宫内,是否存在某个数字,只出现一次,如此,即使在这个坐标位可以填入多个值,但是解依然是这个只出现一次的值

    (3)双值位测解:当位解与宫解都无法继续解出任何一个值的时候,调用此方法,找出某个只有两个可填值的坐标位,然后将两个值分别填入该坐标,进行位解与宫解,如果最后结果出现一个数字填入出现错误位,则另外一个为正确解,如果填入测试值的时候,数独直接全部解出,则将测试数组打印出来,结束程序

2、由于测试量不大,算法可能存在问题,单该算法应该可以解决大部分的数独,如果你发现任何问题,请告诉我,谢谢!

转载于:https://my.oschina.net/yueshengwujie/blog/688308

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
数独游戏可以通过编写算法来自动解决。Python是一种流行的编程语言,常用于编写算法。下面是一个简单的数独算法代码示例,用Python编写: ``` def solve_sudoku(board): find = find_empty(board) if not find: return True else: row, col = find for i in range(1,10): if valid(board, i, (row, col)): board[row][col] = i if solve_sudoku(board): return True board[row][col] = 0 return False def valid(board, num, pos): # Check row for i in range(len(board[0])): if board[pos[0]][i] == num and pos[1] != i: return False # Check column for i in range(len(board)): if board[i][pos[1]] == num and pos[0] != i: return False # Check 3x3 box box_x = pos[1] // 3 box_y = pos[0] // 3 for i in range(box_y*3, box_y*3 + 3): for j in range(box_x * 3, box_x*3 + 3): if board[i][j] == num and (i,j) != pos: return False return True def print_board(board): for i in range(len(board)): if i % 3 == 0 and i != 0: print("- - - - - - - - - - - - -") for j in range(len(board[0])): if j % 3 == 0 and j != 0: print(" | ", end="") if j == 8: print(board[i][j]) else: print(str(board[i][j]) + " ", end="") def find_empty(board): for i in range(len(board)): for j in range(len(board[0])): if board[i][j] == 0: return (i, j) return None board = [ [0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0] ] # Update the board with valid numbers board = [ [0,2,3,0,0,0,0,8,0], [0,0,0,7,0,0,0,0,1], [6,0,0,0,0,2,0,0,0], [0,3,0,0,0,0,5,0,7], [0,0,9,0,2,0,6,3,0], [0,7,0,0,0,0,9,4,2], [7,0,0,0,0,9,0,0,0], [0,0,0,0,0,3,0,0,0], [2,0,1,0,0,0,0,6,0] ] print_board(board) solve_sudoku(board) print("\nSolved Board:") print_board(board) ``` 这个算法的实现递归调用`find_empty`函数来查找空的格子,然后使用`valid`函数判断这个格子是否可以填写数字,如果可以则填写数字并通过递归调用进行下一步验证,如果无法填写,则返回上一个步骤继续寻找其他可行解。这个算法非常直接简单,适合初学者练习编写算法。 需要注意的是,这个算法并不一定可以解决所有的数独问题,有一些特定类型的问题可能无法解决。因此,如果您需要解决复杂的数独问题,请参考更复杂、更完整的算法实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值