小孩的暑假作业有一项就是数独练习,刚好记得以前搞团队培训时写过一个简单的算法,当时的思路是按每个小九宫格顺序生成,依稀记得平均每局生成时间以秒记,采用的方法是每个小九宫格顺序生成,不成立则回归,今天又重写一遍,采用新的思路,直接按九行九列的方式生成,一样是不成立回归,不但计算复杂度要远小于前次方案(仅在行列计算方面稍微比前次复杂一些),但执行效率提高了三个量级,目前测试是在平均10ms内,有机会找到老的代码也发来比较一下。
后面有时间逐步扩展为游戏。
直接上代码:
shudu.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>数独</title>
<script language="javascript" src="shudu.js"></script>
</head>
<style>
body{
font-size: 30px;
}
table{
border:1px solid #ddd;
border-collapse: collapse;
}
td{
border:1px solid #ddd;
border-collapse: collapse;
}
.subtable td{
width: 50px;
height: 50px;
vertical-align: middle;
text-align: center;
}
</style>
<body>
<script>
getGame();
startGame();
document.write(tb);
</script>
</body>
</html>
shudu.js:
//数独
var game=[];
var tb="";
var back=0;
var backcnt=0;
var stime=new Date().getTime();
function getGame(){
game=[];
back=0;
backcnt=0;
stime=new Date().getTime();
createGame();
var etime=new Date().getTime();
console.log('开始时间:'+stime+',结束时间:'+etime+' 耗时:'+(etime-stime)+"毫秒");
}
function createGame(){
var i,j;
for(i=0;i<9;i++){
game[i]=[0,0,0,0,0,0,0,0,0];
for(j=0;j<9;j++){
//获取可以随机的数字列表
tmpnums=getTmpnums(i,j);
if (tmpnums.length==0){
//不成立,回归
back++;
backcnt++;
//onsole.log('不成立回归:'+i+":"+j);
if (back<10){//10次内回归单个九宫格的当前行
j=j=parseInt(j/3)*3-1;
}else if (back<20){//20次内回归当前整行
j=-1;
}else{//大于20次回归到上一整行
i-=2;
if (i<-1) i=-1;
j=10;
back=0;
}
}else {
idx = parseInt(Math.random() * tmpnums.length);
game[i][j] = tmpnums[idx];
}
}
}
console.log(game);
console.log('总回归次数:'+backcnt);
}
function getTmpnums(ci,rj) {
var i, j;
var tmp = [1, 2, 3, 4, 5, 6, 7, 8, 9];
//本九宫格内有的排除
for (i = parseInt(ci / 3) * 3; i < parseInt(ci / 3) * 3 + 3; i++) {
for (j = parseInt(rj / 3) * 3; j < parseInt(rj / 3) * 3 + 3; j++) {
if (game.length > i && game[i][j] != 0) {
tmp[game[i][j] - 1] = 0;
}
}
}
//本行内有的排除
for (j = 0; j < rj; j++) tmp[game[ci][j] - 1] = 0;
//本列内有的排除
for (i = 0; i < ci; i++) tmp[game[i][rj] - 1] = 0;
var rlt = [];
for (i = 0; i < 9; i++) {
if (tmp[i] != 0) {
rlt[rlt.length] = tmp[i];
}
}
return rlt;
}
//输出表格
function startGame(){
var i,j;
getGame();
for(i=0;i<9;i++){
if (i==0) tb+="<table>";
if (i % 3==0) tb+="<tr><td>";
for(j=0;j<9;j++){
if (j==0) tb+="<table class='subtable'>";
if (j%3==0) tb+="<tr><td>";
tb+=game[parseInt(i/3)*3+parseInt(j/3)][i%3*3+j%3];
if (j%3==2)
tb+="</td></tr>";
else
tb+="</td><td>";
if (j==8) tb+="</table>";
}
if (i%3==2)
tb+="</td></tr>";
else
tb+="</td><td>"
if (i==8) tb+="</table>";
}
}
ps:写完后随便查了一下其他人的文章,又测试了一下,发现极偶然的情况下,此算法生成时间会达到秒级别,测试中最高达到14秒,显然这种情况下是不实用的,解决方法是在回归判定中再加一级回退整三行,极致一点就再加一级重新生成,可以完全解决偶发性效率过低的问题;
另:关于生成局是否仅有唯一解的问题,如图:
只需要在判断是否回归的时候再加一个前面的生成结果与当前小九宫与其横竖排的其它小九宫格的横纵数值是否有颠倒的情况即可,判断方案也比较简单,用一个中间数组缓存每个小九宫格,判断是否需要回归的时候,将当前生成数值所在小九宫的行和列分别生成最多3种换序值(12 23 13换序【注意考虑不足三行或三列时简化】)与同行和同列的小九宫格进行比较,如果发现有相同位置(由于已经倒序过,所以判断相同位置)就回归(梯次回归条件不用变),这样的比较速度也非常快,几乎不会影响到生成局效率,虽然没试过;
不过,话说回来,也可以在挖空的时候将这些非唯一结果人为保留一个也可以实现游戏唯一解,只是这样一样,不太好控制难度,另外,对于游戏来说,是否唯一解并不重要,只是在算法层面较真的话,要这么做,举个例子,微软的扫雷就是纯随机的,极大机率出现必死局和猜测局的;
难度控制:不同的难度级别随机每个小九宫的挖坑个数范围即可,比如简单(1~4),复杂(2~5)困难(3~6)专家(4~7)
困了,去睡觉去了!