JavaScript | 数独游戏核心算法实现

引言

数独(Sudoku)是一款经典的逻辑推理填字游戏,其核心规则简单明了:在一个9×9的网格中,将数字1到9填入每个单元格,使得每一行、每一列以及每一个3×3的宫内都不存在重复数字。数独的魅力在于其看似简单的规则下蕴含着丰富多变的解题策略,从基础的排除法到高级的链式技巧,解题过程如同一次精彩的大脑体操。

对于程序员而言,实现一个数独求解器不仅是很好的算法训练,更是理解约束求解问题的绝佳切入点。本文将系统性地介绍数独核心求解算法的JavaScript实现,从基础技巧到高级技巧,循序渐进地构建一个完整的数独求解器。通过本文的学习,读者将掌握数独求解的核心思路,并具备独立实现更复杂求解器的能力。

数独求解器的实现涉及多个层面的考量。首先是数据结构的选择,我们需要高效地表示网格状态、候选值以及约束关系。其次是算法的设计,从简单的暴力搜索到智能的推理技巧,每种方法都有其适用场景和性能特点。最后是工程实践,包括代码组织、测试策略以及性能优化等方面。本文将围绕这些核心议题展开详细的论述。

数独游戏概述与求解器基础

数独规则详解

标准数独游戏在一个9×9的网格上进行,这个网格被进一步划分为9个3×3的子网格,我们称之为"宫"。游戏开始时,部分单元格已经填入了确定的数字,这些数字被称为"已知数"或"给定数"。玩家的目标是将数字1到9填入所有空单元格,使得以下三个约束条件同时满足:第一,每一行包含数字1到9各一次且不重复;第二,每一列包含数字1到9各一次且不重复;第三,每个宫包含数字1到9各一次且不重复。

JavaScript数据结构设计

在开始实现具体算法之前,我们需要设计合理的数据结构来表示数独状态。数据结构的选择直接影响后续算法的实现难度和执行效率。

javascript 复制代码
// 单元格对象:包含值和候选值列表
function Cell(value) {
    this.value = value;  // 若为0表示未填入
    this.candidates = value === 0 ? [1, 2, 3, 4, 5, 6, 7, 8, 9] : [];
}

// 数独网格:9x9的二维数组
function SudokuGrid(initialValues) {
    this.cells = [];
    this.initGrid(initialValues);
}

SudokuGrid.prototype.initGrid = function(initialValues) {
    this.cells = [];
    for (let row = 0; row < 9; row++) {
        this.cells[row] = [];
        for (let col = 0; col < 9; col++) {
            const value = initialValues ? initialValues[row][col] : 0;
            this.cells[row][col] = new Cell(value);
        }
    }
    if (initialValues) {
        this.updateAllCandidates();
    }
};

// 获取单元格所在的宫的起始行列
SudokuGrid.prototype.getBoxStart = function(row, col) {
    return {
        boxRow: Math.floor(row / 3) * 3,
        boxCol: Math.floor(col / 3) * 3
    };
};

// 获取某行的所有单元格
SudokuGrid.prototype.getRowCells = function(row) {
    return this.cells[row];
};

// 获取某列的所有单元格
SudokuGrid.prototype.getColCells = function(col) {
    const colCells = [];
    for (let row = 0; row < 9; row++) {
        colCells.push(this.cells[row][col]);
    }
    return colCells;
};

// 获取某宫的所有单元格
SudokuGrid.prototype.getBoxCells = function(boxRow, boxCol) {
    const boxCells = [];
    for (let r = boxRow; r < boxRow + 3; r++) {
        for (let c = boxCol; c < boxCol + 3; c++) {
            boxCells.push(this.cells[r][c]);
        }
    }
    return boxCells;
};

// 获取某单元格所在行的其他单元格(排除自身)
SudokuGrid.prototype.getRowPeers = function(row, col) {
    const peers = [];
    for (let c = 0; c < 9; c++) {
        if (c !== col) peers.push(this.cells[row][c]);
    }
    return peers;
};

// 获取某单元格所在列的其他单元格(排除自身)
SudokuGrid.prototype.getColPeers = function(row, col) {
    const peers = [];
    for (let r = 0; r < 9; r++) {
        if (r !== row) peers.push(this.cells[r][col]);
    }
    return peers;
};

// 获取某单元格所在宫的其他单元格(排除自身)
SudokuGrid.prototype.getBoxPeers = function(row, col) {
    const peers = [];
    const { boxRow, boxCol } = this.getBoxStart(row, col);
    for (let r = boxRow; r < boxRow + 3; r++) {
        for (let c = boxCol; c < boxCol + 3; c++) {
            if (r !== row || c !== col) peers.push(this.cells[r][c]);
        }
    }
    return peers;
};

// 获取某单元格的所有相关单元格(同行、同列、同宫)
SudokuGrid.prototype.getAllPeers = function(row, col) {
    const peers = new Set();
    // 添加同行单元格
    for (let c = 0; c < 9; c++) {
        if (c !== col) peers.add(this.cells[row][c]);
    }
    // 添加同列单元格
    for (let r = 0; r < 9; r++) {
        if (r !== row) peers.add(this.cells[r][col]);
    }
    // 添加同宫单元格
    const { boxRow, boxCol } = this.getBoxStart(row, col);
    for (let r = boxRow; r < boxRow + 3; r++) {
        for (let c = boxCol; c < boxCol + 3; c++) {
            if (r !== row || c !== col) peers.add(this.cells[r][c]);
        }
    }
    return Array.from(peers);
};

上述代码定义了数独网格的基本数据结构。Cell对象包含当前值和候选值列表,SudokuGrid对象提供了丰富的访问接口来获取不同区域的单元格。需要特别注意的是,getAllPeers方法使用了Set数据结构来避免重复添加同属多个区域的单元格,这是一个常见的错误点。

javascript 复制代码
// 更新所有单元格的候选值
SudokuGrid.prototype.updateAllCandidates = function() {
    for (let row = 0; row < 9; row++) {
        for (let col = 0; col < 9; col++) {
            const cell = this.cells[row][col];
            if (cell.value === 0) {
                cell.candidates = this.calculateCandidates(row, col);
            }
        }
    }
};

// 计算指定单元格的候选值
SudokuGrid.prototype.calculateCandidates = function(row, col) {
    const usedValues = new Set();
    // 收集同行已使用的值
    for (let c = 0; c < 9; c++) {
        const val = this.cells[row][c].value;
        if (val !== 0) usedValues.add(val);
    }
    // 收集同列已使用的值
    for (let r = 0; r < 9; r++) {
        const val = this.cells[r][col].value;
        if (val !== 0) usedValues.add(val);
    }
    // 收集同宫已使用的值
    const { boxRow, boxCol } = this.getBoxStart(row, col);
    for (let r = boxRow; r < boxRow + 3; r++) {
        for (let c = boxCol; c < boxCol + 3; c++) {
            const val = this.cells[r][c].value;
            if (val !== 0) usedValues.add(val);
        }
    }
    // 返回未被使用的值作为候选值
    const candidates = [];
    for (let num = 1; num <= 9; num++) {
        if (!usedValues.has(num)) {
            candidates.push(num);
        }
    }
    return candidates;
};

// 从某单元格的候选值中移除指定值
SudokuGrid.prototype.removeCandidate = function(row, col, value) {
    const cell = this.cells[row][col];
    const index = cell.candidates.indexOf(value);
    if (index !== -1) {
        cell.candidates.splice(index, 1);
        return true;
    }
    return false;
};

// 从某单元格的所有相关单元格中移除指定候选值
SudokuGrid.prototype.removeCandidateFromPeers = function(row, col, value) {
    const peers = this.getAllPeers(row, col);
    let changed = false;
    for (const peer of peers) {
        if (peer.value === 0 && this.removeCandidateFromPeersCell(peer, value)) {
            changed = true;
        }
    }
    return changed;
};

// 从单元格候选值中移除(内部方法)
SudokuGrid.prototype.removeCandidateFromPeersCell = function(cell, value) {
    const index = cell.candidates.indexOf(value);
    if (index !== -1) {
        cell.candidates.splice(index, 1);
        return true;
    }
    return false;
};

// 设置单元格的值并更新相关候选值
SudokuGrid.prototype.setCellValue = function(row, col, value) {
    const cell = this.cells[row][col];
    if (cell.value !== 0) {
        return false;  // 单元格已有值,不能修改
    }
    cell.value = value;
    cell.candidates = [];
    return this.removeCandidateFromPeers(row, col, value);
};

// 检查数独是否已解决
SudokuGrid.prototype.isSolved = function() {
    for (let row = 0; row < 9; row++) {
        for (let col = 0; col < 9; col++) {
            if (this.cells[row][col].value === 0) {
                return false;
            }
        }
    }
    return this.isValid();
};

// 检查当前状态是否有效
SudokuGrid.prototype.isValid = function() {
    // 检查行
    for (let row = 0; row < 9; row++) {
        const seen = new Set();
        for (let col = 0; col < 9; col++) {
            const val = this.cells[row][col].value;
            if (val !== 0) {
                if (seen.has(val)) return false;
                seen.add(val);
            }
        }
    }
    // 检查列
    for (let col = 0; col < 9; col++) {
        const seen = new Set();
        for (let row = 0; row < 9; row++) {
            const val = this.cells[row][col].value;
            if (val !== 0) {
                if (seen.has(val)) return false;
                seen.add(val);
            }
        }
    }
    // 检查宫
    for (let boxRow = 0; boxRow < 9; boxRow += 3) {
        for (let boxCol = 0; boxCol < 9; boxCol += 3) {
            const seen = new Set();
            for (let r = boxRow; r < boxRow + 3; r++) {
                for (let c = boxCol; c < boxCol + 3; c++) {
                    const val = this.cells[r][c].value;
                    if (val !== 0) {
                        if (seen.has(val)) return false;
                        seen.add(val);
                    }
                }
            }
        }
    }
    return true;
};

这些基础方法为数独求解器提供了必要的状态查询和修改能力。接下来,我们将从最基础的技巧开始,逐步构建完整的求解系统。

基础技巧实现

基础技巧是数独求解的入门级方法,它们直观易懂,实现起来也相对简单。这些技巧能够解决大量简单的数独题目,掌握它们是进一步学习高级技巧的基础。

唯余法(Naked Single)

唯余法是最直观的求解技巧之一。当我们发现某个单元格只能填入唯一一个数字时,就可以直接将该数字填入。这个唯一性可能是因为该单元格所在的行、列、宫已经填入了其他八个数字,也可能是因为其他数字都被排除后只剩下了一个候选值。

唯余法的核心原理是约束传播(Constraint Propagation)。当我们填入一个数字后,这个数字会从其所在行、列、宫的所有其他单元格的候选值中移除。这种级联效应可能导致其他单元格也变成唯余法的情况,从而触发连锁反应。理解这一点对于设计高效的求解器至关重要。

javascript 复制代码
// 唯余法(Naked Single):单元格只有一个候选值时直接填入
SudokuGrid.prototype.applyNakedSingle = function() {
    let filled = 0;
    for (let row = 0; row < 9; row++) {
        for (let col = 0; col < 9; col++) {
            const cell = this.cells[row][col];
            // 只处理未填入且候选值唯一的单元格
            if (cell.value === 0 && cell.candidates.length === 1) {
                const value = cell.candidates[0];
                this.setCellValue(row, col, value);
                filled++;
            }
        }
    }
    return filled;
};

// 检查是否有唯余单元格
SudokuGrid.prototype.hasNakedSingle = function() {
    for (let row = 0; row < 9; row++) {
        for (let col = 0; col < 9; col++) {
            const cell = this.cells[row][col];
            if (cell.value === 0 && cell.candidates.length === 1) {
                return true;
            }
        }
    }
    return false;
}

// 获取所有唯余单元格
SudokuGrid.prototype.getNakedSingles = function() {
    const singles = [];
    for (let row = 0; row < 9; row++) {
        for (let col = 0; col < 9; col++) {
            const cell = this.cells[row][col];
            if (cell.value === 0 && cell.candidates.length === 1) {
                singles.push({ row, col, value: cell.candidates[0] });
            }
        }
    }
    return singles;
};

唯余法的应用通常是求解循环的第一步。每次填入数字后,我们都需要重新检查是否有新的唯余单元格出现。这种迭代过程一直持续到没有新的单元格可以被唯余法填入为止。在实际实现中,我们会将唯余法集成到主求解循环中,作为最基本的推理步骤。

排除法(Hidden Single)

排除法是另一种基础但极为重要的技巧。与唯余法关注单元格自身不同,排除法关注的是数字在某个区域(行、列或宫)中的位置。当我们发现在某个区域内,某个数字只能出现在唯一一个单元格中时,就可以将该数字填入该单元格,即使该单元格还有其他候选值。

排除法的原理可以通过一个简单的例子说明:假设在第一行中,数字5不能出现在其他八个单元格中(因为它们已经被填入其他数字或者与同行其他数字冲突),那么数字5必定在第一行剩下的那个单元格中。这就是排除法的核心思想------通过排除其他可能性来定位唯一可能的单元格。

javascript 复制代码
// 排除法(Hidden Single):数字在某区域只有一个可能位置时填入
SudokuGrid.prototype.applyHiddenSingle = function() {
    let filled = 0;

    // 检查每一行
    for (let row = 0; row < 9; row++) {
        filled += this.applyHiddenSingleInRow(row);
    }

    // 检查每一列
    for (let col = 0; col < 9; col++) {
        filled += this.applyHiddenSingleInCol(col);
    }

    // 检查每一宫
    for (let boxRow = 0; boxRow < 9; boxRow += 3) {
        for (let boxCol = 0; boxCol < 9; boxCol += 3) {
            filled += this.applyHiddenSingleInBox(boxRow, boxCol);
        }
    }

    return filled;
};

// 检查某行中的排除法
SudokuGrid.prototype.applyHiddenSingleInRow = function(row) {
    let filled = 0;
    // 对每个数字1-9分别检查
    for (let num = 1; num <= 9; num++) {
        const positions = [];
        // 找出该行中可能填入num的所有单元格
        for (let col = 0; col < 9; col++) {
            const cell = this.cells[row][col];
            if (cell.value === 0 && cell.candidates.includes(num)) {
                positions.push({ row, col });
            }
        }
        // 如果只有一个可能位置,则填入该数字
        if (positions.length === 1) {
            const { row: r, col } = positions[0];
            if (this.setCellValue(r, col, num)) {
                filled++;
            }
        }
    }
    return filled;
};

// 检查某列中的排除法
SudokuGrid.prototype.applyHiddenSingleInCol = function(col) {
    let filled = 0;
    for (let num = 1; num <= 9; num++) {
        const positions = [];
        for (let row = 0; row < 9; row++) {
            const cell = this.cells[row][col];
            if (cell.value === 0 && cell.candidates.includes(num)) {
                positions.push({ row, col });
            }
        }
        if (positions.length === 1) {
            const { row, col: c } = positions[0];
            if (this.setCellValue(row, c, num)) {
                filled++;
            }
        }
    }
    return filled;
};

// 检查某宫中的排除法
SudokuGrid.prototype.applyHiddenSingleInBox = function(boxRow, boxCol) {
    let filled = 0;
    for (let num = 1; num <= 9; num++) {
        const positions = [];
        for (let r = boxRow; r < boxRow + 3; r++) {
            for (let c = boxCol; c < boxCol + 3; c++) {
                const cell = this.cells[r][c];
                if (cell.value === 0 && cell.candidates.includes(num)) {
                    positions.push({ row: r, col: c });
                }
            }
        }
        if (positions.length === 1) {
            const { row, col } = positions[0];
            if (this.setCellValue(row, col, num)) {
                filled++;
            }
        }
    }
    return filled;
};

排除法的实现需要遍历每个区域,对每个数字进行检查。虽然这看起来计算量较大,但在实际应用中效果很好,因为大部分数独题都可以通过基础技巧解决。对于更复杂的数独题目,我们需要在基础技巧之间建立循环,不断应用直到没有新进展,然后才转向更高级的技巧。

基础技巧整合

将唯余法和排除法整合成一个基础求解循环是很常见的做法。这个循环会持续运行,直到没有单元格可以被填入为止。

javascript 复制代码
// 应用所有基础技巧直到无法继续
SudokuGrid.prototype.applyBasicTechniques = function() {
    let totalFilled = 0;
    let iterations = 0;
    const maxIterations = 81;  // 防止无限循环

    while (iterations < maxIterations) {
        let filled = 0;

        // 先应用唯余法
        filled += this.applyNakedSingle();
        this.updateAllCandidates();

        // 再应用排除法
        filled += this.applyHiddenSingle();
        this.updateAllCandidates();

        if (filled === 0) {
            break;  // 没有新进展,退出循环
        }

        totalFilled += filled;
        iterations++;
    }

    return totalFilled;
};

这个整合方法体现了数独求解的基本模式:通过迭代应用推理技巧来逐步缩小搜索空间。在实际求解器中,我们通常会首先尝试所有不需要猜测的技巧,只有在这些技巧都无法继续时才会考虑回溯搜索。

中级技巧实现

当基础技巧无法继续推进时,我们需要借助更复杂的推理方法。中级技巧考虑的是单元格之间的组合关系,通过分析多个单元格之间的候选值约束来排除不可能的情况。

数对法(Naked Pairs)

数对法是一种观察两个单元格之间关系的技巧。当两个单元格拥有完全相同的两个候选值时,这两个候选值必定分别位于这两个单元格中(虽然我们不知道具体哪个在哪个)。因此,这两个候选值可以安全地从这两个单元格所在行、列、宫的其他单元格的候选值中移除。

例如,假设在第一行中,单元格A1的候选值是{2, 5},单元格A4的候选值也是{2, 5}。这意味着第一行中,如果数字2不在A1那就一定在A4,反之亦然。无论哪种情况,数字2和5都不可能出现在该行其他单元格的候选值中。

javascript 复制代码
// 数对法(Naked Pairs):两个单元格有相同的两个候选值时排除其他候选
SudokuGrid.prototype.applyNakedPairs = function() {
    let removed = 0;

    // 检查每一行
    for (let row = 0; row < 9; row++) {
        removed += this.applyNakedPairsInRow(row);
    }

    // 检查每一列
    for (let col = 0; col < 9; col++) {
        removed += this.applyNakedPairsInCol(col);
    }

    // 检查每一宫
    for (let boxRow = 0; boxRow < 9; boxRow += 3) {
        for (let boxCol = 0; boxCol < 9; boxCol += 3) {
            removed += this.applyNakedPairsInBox(boxRow, boxCol);
        }
    }

    return removed;
};

// 在某行中应用数对法
SudokuGrid.prototype.applyNakedPairsInRow = function(row) {
    return this.applyNakedPairsInCells(this.getRowCells(row), 'row', row, null);
};

// 在某列中应用数对法
SudokuGrid.prototype.applyNakedPairsInCol = function(col) {
    const cells = this.getColCells(col);
    return this.applyNakedPairsInCells(cells, 'col', null, col);
};

// 在某宫中应用数对法
SudokuGrid.prototype.applyNakedPairsInBox = function(boxRow, boxCol) {
    const cells = this.getBoxCells(boxRow, boxCol);
    return this.applyNakedPairsInCells(cells, 'box', boxRow, boxCol);
};

// 数对法的核心实现
SudokuGrid.prototype.applyNakedPairsInCells = function(cells, regionType, regionRow, regionCol) {
    let removed = 0;
    // 找出所有候选值数量为2的单元格
    const pairs = [];
    for (const cell of cells) {
        if (cell.value === 0 && cell.candidates.length === 2) {
            pairs.push(cell);
        }
    }

    // 检查是否有匹配的数对
    for (let i = 0; i < pairs.length; i++) {
        for (let j = i + 1; j < pairs.length; j++) {
            const cell1 = pairs[i];
            const cell2 = pairs[j];
            // 检查是否是完全相同的两个候选值
            if (cell1.candidates[0] === cell2.candidates[0] &&
                cell1.candidates[1] === cell2.candidates[1]) {
                // 从该区域的其他单元格中移除这两个候选值
                const pairValues = [cell1.candidates[0], cell1.candidates[1]];
                for (const cell of cells) {
                    if (cell.value === 0 && cell !== cell1 && cell !== cell2) {
                        for (const val of pairValues) {
                            if (cell.candidates.includes(val)) {
                                this.removeCandidateFromPeersCell(cell, val);
                                removed++;
                            }
                        }
                    }
                }
            }
        }
    }

    return removed;
};

三数组法(Naked Triples)

三数组法是数对法的扩展,但处理起来更加复杂。在三个单元格中,如果它们候选值的并集恰好是三个数字(不一定每个单元格都有这三个数字),那么这三个数字必定分别或部分位于这三个单元格中,因此可以从这三个单元格所在区域的其他单元格中移除这三个候选值。

三数组可以有多种形式:三个单元格分别拥有{1, 2, 3}、{1, 2}、{1, 3},或者{1, 2, 3}、{1, 2}、{2, 3},或者三个都是{1, 2, 3},等等。关键是这三个单元格候选值的并集不超过三个数字。

javascript 复制代码
// 三数组法(Naked Triples):三个单元格候选值并集为三个数字时排除
SudokuGrid.prototype.applyNakedTriples = function() {
    let removed = 0;

    for (let row = 0; row < 9; row++) {
        removed += this.applyNakedTriplesInRow(row);
    }

    for (let col = 0; col < 9; col++) {
        removed += this.applyNakedTriplesInCol(col);
    }

    for (let boxRow = 0; boxRow < 9; boxRow += 3) {
        for (let boxCol = 0; boxCol < 9; boxCol += 3) {
            removed += this.applyNakedTriplesInBox(boxRow, boxCol);
        }
    }

    return removed;
};

// 在某行中应用三数组法
SudokuGrid.prototype.applyNakedTriplesInRow = function(row) {
    return this.applyNakedTriplesInCells(this.getRowCells(row));
};

// 在某列中应用三数组法
SudokuGrid.prototype.applyNakedTriplesInCol = function(col) {
    return this.applyNakedTriplesInCells(this.getColCells(col));
};

// 在某宫中应用三数组法
SudokuGrid.prototype.applyNakedTriplesInBox = function(boxRow, boxCol) {
    return this.applyNakedTriplesInCells(this.getBoxCells(boxRow, boxCol));
};

// 三数组法核心实现
SudokuGrid.prototype.applyNakedTriplesInCells = function(cells) {
    let removed = 0;
    const emptyCells = cells.filter(c => c.value === 0);

    // 找出所有候选值数量不超过3的单元格
    const candidates = emptyCells.filter(c => c.candidates.length >= 2 && c.candidates.length <= 3);

    // 遍历所有三元组组合
    for (let i = 0; i < candidates.length; i++) {
        for (let j = i + 1; j < candidates.length; j++) {
            for (let k = j + 1; k < candidates.length; k++) {
                const cell1 = candidates[i];
                const cell2 = candidates[j];
                const cell3 = candidates[k];

                // 计算三个单元格候选值的并集
                const union = [...new Set([...cell1.candidates, ...cell2.candidates, ...cell3.candidates])];

                // 如果并集恰好是3个数字
                if (union.length === 3) {
                    // 从其他单元格中移除这三个候选值
                    for (const cell of emptyCells) {
                        if (cell !== cell1 && cell !== cell2 && cell !== cell3) {
                            for (const val of union) {
                                if (cell.candidates.includes(val)) {
                                    this.removeCandidateFromPeersCell(cell, val);
                                    removed++;
                                }
                            }
                        }
                    }
                }
            }
        }
        // 也检查两个单元格的情况(候选值各为2和3,并集为3)
        for (let j = i + 1; j < candidates.length; j++) {
            const cell1 = candidates[i];
            const cell2 = candidates[j];
            if (cell1.candidates.length === 2 && cell2.candidates.length === 3) {
                const union = [...new Set([...cell1.candidates, ...cell2.candidates])];
                if (union.length === 3) {
                    for (const cell of emptyCells) {
                        if (cell !== cell1 && cell !== cell2) {
                            for (const val of union) {
                                if (cell.candidates.includes(val)) {
                                    this.removeCandidateFromPeersCell(cell, val);
                                    removed++;
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    return removed;
};

隐性数对(Hidden Pairs)

与数对法不同,显性数对关注的是那些候选值数量较少的单元格。而隐性数对则关注的是候选值较多的单元格。当我们发现在某个区域内,只有两个单元格可能包含某两个特定数字时,这两个单元格的候选值就可以被缩减为只包含这两个数字,其他候选值都可以被排除。

例如,在第一行中,只有单元格A2和A5可能包含数字3,而只有单元格A2和A7可能包含数字5。同时,数字3和5都只可能出现在A2、A5、A7三个单元格中。这意味着A2一定包含3或5,A5只能是3,A7只能是5。但更关键的是,如果A5只能是3,A7只能是5,那么A2的候选值中就可以排除5,保留3。

javascript 复制代码
// 隐性数对(Hidden Pairs):某两个数字只出现在两个单元格时锁定它们
SudokuGrid.prototype.applyHiddenPairs = function() {
    let changed = false;

    // 检查每一行
    for (let row = 0; row < 9; row++) {
        if (this.findHiddenPairsInRow(row)) {
            changed = true;
        }
    }

    // 检查每一列
    for (let col = 0; col < 9; col++) {
        if (this.findHiddenPairsInCol(col)) {
            changed = true;
        }
    }

    // 检查每一宫
    for (let boxRow = 0; boxRow < 9; boxRow += 3) {
        for (let boxCol = 0; boxCol < 9; boxCol += 3) {
            if (this.findHiddenPairsInBox(boxRow, boxCol)) {
                changed = true;
            }
        }
    }

    return changed;
};

// 在某行中寻找隐性数对
SudokuGrid.prototype.findHiddenPairsInRow = function(row) {
    const cells = this.getRowCells(row).filter(c => c.value === 0);
    return this.findHiddenPairsInCellSet(cells);
};

// 在某列中寻找隐性数对
SudokuGrid.prototype.findHiddenPairsInCol = function(col) {
    const cells = this.getColCells(col).filter(c => c.value === 0);
    return this.findHiddenPairsInCellSet(cells);
};

// 在某宫中寻找隐性数对
SudokuGrid.prototype.findHiddenPairsInBox = function(boxRow, boxCol) {
    const cells = this.getBoxCells(boxRow, boxCol).filter(c => c.value === 0);
    return this.findHiddenPairsInCellSet(cells);
};

// 在一组单元格中寻找隐性数对
SudokuGrid.prototype.findHiddenPairsInCellSet = function(cells) {
    let changed = false;

    // 对每对候选数字进行检查
    for (let num1 = 1; num1 <= 9; num1++) {
        for (let num2 = num1 + 1; num2 <= 9; num2++) {
            // 找出可能包含这两个数字的所有单元格
            const possibleCells = cells.filter(cell =>
                cell.candidates.includes(num1) || cell.candidates.includes(num2)
            );

            // 如果恰好只有两个单元格可能包含这两个数字
            if (possibleCells.length === 2) {
                const cell1 = possibleCells[0];
                const cell2 = possibleCells[1];

                // 检查这两个数字是否都只出现在这两个单元格中
                const num1OnlyHere = cells.filter(c => c.candidates.includes(num1)).length === 2;
                const num2OnlyHere = cells.filter(c => c.candidates.includes(num2)).length === 2;

                if (num1OnlyHere && num2OnlyHere) {
                    // 锁定这两个单元格的候选值为这两个数字
                    const oldLen1 = cell1.candidates.length;
                    const oldLen2 = cell2.candidates.length;

                    cell1.candidates = [num1, num2];
                    cell2.candidates = [num1, num2];

                    if (oldLen1 > 2 || oldLen2 > 2) {
                        changed = true;
                    }
                }
            }
        }
    }

    return changed;
};

区块排除法(Box-Line Reduction)

区块排除法是一种利用宫与行、列之间关系的技巧。它有两种形式:指向式(Pointing)和申领式(Claiming)。

指向式发生在当某个数字在宫中的候选位置全部位于同一行或同一列时。这时,这个数字不可能出现在该行或该列的其他宫中,因此可以从这些其他宫的该行或该列的候选位置中移除这个数字。

申领式则相反,当某个数字在行或列中的候选位置全部位于同一个宫中时,这个数字不可能出现在该宫的其他行或列中,因此可以从该宫其他行或列的候选位置中移除这个数字。

javascript 复制代码
// 区块排除法(Box-Line Reduction)
// 包括Pointing(指向)和Claiming(申领)两种形式
SudokuGrid.prototype.applyBoxLineReduction = function() {
    let removed = 0;

    // Pointing:数字在宫内只出现在一行或一列
    removed += this.applyPointingPairs();

    // Claiming:数字在行或列中只出现在一宫
    removed += this.applyClaimingPairs();

    return removed;
};

// Pointing形式:数字在宫内只出现在一行或一列
SudokuGrid.prototype.applyPointingPairs = function() {
    let removed = 0;

    // 检查每个宫
    for (let boxRow = 0; boxRow < 9; boxRow += 3) {
        for (let boxCol = 0; boxCol < 9; boxCol += 3) {
            const boxCells = this.getBoxCells(boxRow, boxCol).filter(c => c.value === 0);

            // 对每个数字检查
            for (let num = 1; num <= 9; num++) {
                // 找出该数字在宫中可能出现的所有行和列
                const rows = new Set();
                const cols = new Set();

                for (const cell of boxCells) {
                    // 需要找到单元格的位置
                    for (let r = boxRow; r < boxRow + 3; r++) {
                        for (let c = boxCol; c < boxCol + 3; c++) {
                            if (this.cells[r][c] === cell && cell.candidates.includes(num)) {
                                rows.add(r);
                                cols.add(c);
                            }
                        }
                    }
                }

                // 如果该数字在宫中只出现在同一行
                if (rows.size === 1) {
                    const targetRow = Array.from(rows)[0];
                    // 从该行其他宫中移除该候选值
                    for (let c = 0; c < 9; c++) {
                        const { boxRow: cellBoxRow, boxCol: cellBoxCol } = this.getBoxStart(targetRow, c);
                        if (cellBoxRow !== boxRow || cellBoxCol !== boxCol) {
                            const cell = this.cells[targetRow][c];
                            if (cell.value === 0 && cell.candidates.includes(num)) {
                                this.removeCandidateFromPeersCell(cell, num);
                                removed++;
                            }
                        }
                    }
                }

                // 如果该数字在宫中只出现在同一列
                if (cols.size === 1) {
                    const targetCol = Array.from(cols)[0];
                    // 从该列其他宫中移除该候选值
                    for (let r = 0; r < 9; r++) {
                        const { boxRow: cellBoxRow, boxCol: cellBoxCol } = this.getBoxStart(r, targetCol);
                        if (cellBoxRow !== boxRow || cellBoxCol !== boxCol) {
                            const cell = this.cells[r][targetCol];
                            if (cell.value === 0 && cell.candidates.includes(num)) {
                                this.removeCandidateFromPeersCell(cell, num);
                                removed++;
                            }
                        }
                    }
                }
            }
        }
    }

    return removed;
};

// Claiming形式:数字在行或列中只出现在一宫
SudokuGrid.prototype.applyClaimingPairs = function() {
    let removed = 0;

    // 检查每一行
    for (let row = 0; row < 9; row++) {
        const rowCells = this.getRowCells(row).filter(c => c.value === 0);

        for (let num = 1; num <= 9; num++) {
            // 找出该数字在该行中的所有可能列
            const possibleCols = [];
            for (let col = 0; col < 9; col++) {
                if (this.cells[row][col].value === 0 &&
                    this.cells[row][col].candidates.includes(num)) {
                    possibleCols.push(col);
                }
            }

            // 检查这些列是否都在同一个宫中
            if (possibleCols.length > 0) {
                const boxes = new Set();
                for (const col of possibleCols) {
                    const { boxRow, boxCol } = this.getBoxStart(row, col);
                    boxes.add(`${boxRow},${boxCol}`);
                }

                // 如果都在同一个宫中
                if (boxes.size === 1) {
                    const [boxRow, boxCol] = Array.from(boxes)[0].split(',').map(Number);
                    // 从该宫其他行中移除该候选值
                    for (let r = boxRow; r < boxRow + 3; r++) {
                        if (r !== row) {
                            for (let c = boxCol; c < boxCol + 3; c++) {
                                const cell = this.cells[r][c];
                                if (cell.value === 0 && cell.candidates.includes(num)) {
                                    this.removeCandidateFromPeersCell(cell, num);
                                    removed++;
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    // 检查每一列
    for (let col = 0; col < 9; col++) {
        const colCells = this.getColCells(col).filter(c => c.value === 0);

        for (let num = 1; num <= 9; num++) {
            const possibleRows = [];
            for (let row = 0; row < 9; row++) {
                if (this.cells[row][col].value === 0 &&
                    this.cells[row][col].candidates.includes(num)) {
                    possibleRows.push(row);
                }
            }

            if (possibleRows.length > 0) {
                const boxes = new Set();
                for (const row of possibleRows) {
                    const { boxRow, boxCol } = this.getBoxStart(row, col);
                    boxes.add(`${boxRow},${boxCol}`);
                }

                if (boxes.size === 1) {
                    const [boxRow, boxCol] = Array.from(boxes)[0].split(',').map(Number);
                    for (let c = boxCol; c < boxCol + 3; c++) {
                        if (c !== col) {
                            for (let r = boxRow; r < boxRow + 3; r++) {
                                const cell = this.cells[r][c];
                                if (cell.value === 0 && cell.candidates.includes(num)) {
                                    this.removeCandidateFromPeersCell(cell, num);
                                    removed++;
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    return removed;
};

中级技巧整合

将所有中级技巧整合成一个统一的方法,可以使主求解循环更加简洁清晰。

javascript 复制代码
// 应用所有中级技巧
SudokuGrid.prototype.applyIntermediateTechniques = function() {
    let totalRemoved = 0;
    let iterations = 0;
    const maxIterations = 20;

    while (iterations < maxIterations) {
        let removed = 0;

        // 应用数对法
        removed += this.applyNakedPairs();

        // 应用三数组法
        removed += this.applyNakedTriples();

        // 应用隐性数对
        if (this.applyHiddenPairs()) {
            this.updateAllCandidates();
            removed++;
        }

        // 应用区块排除法
        removed += this.applyBoxLineReduction();

        // 更新候选值
        this.updateAllCandidates();

        if (removed === 0) {
            break;
        }

        totalRemoved += removed;
        iterations++;
    }

    return totalRemoved;
};

高级技巧实现

当基础技巧和中级技巧都无法继续推进时,我们需要借助更复杂的推理方法。高级技巧通常涉及对多个单元格、多个区域的联合分析,以及链式推理等复杂逻辑。

X-Wing(X翼)

X-Wing是一种基于两行或两列的模式。当某个数字在两行中的候选位置完全相同(即都只在相同的两个列中出现),那么这个数字必定分别位于这两行的对应列中。因此,这个数字可以从这两列的其他行中排除。

X-Wing可以理解为一种"强链"结构。如果我们将每个候选位置看作一条链接,那么X-Wing形成了一个矩形结构,两条对角线上的四个单元格必须包含该数字,而另外两条边上的单元格则不能包含该数字。

javascript 复制代码
// X-Wing(X翼):某数字在两行中只出现在相同两列
SudokuGrid.prototype.applyXWing = function() {
    let removed = 0;

    // 按行检查X-Wing
    removed += this.findXWingInRows();

    // 按列检查X-Wing
    removed += this.findXWingInCols();

    return removed;
};

// 在行中寻找X-Wing
SudokuGrid.prototype.findXWingInRows = function() {
    let removed = 0;

    // 对每个数字检查
    for (let num = 1; num <= 9; num++) {
        // 找出每行中该数字出现的列
        const rowPatterns = [];
        for (let row = 0; row < 9; row++) {
            const cols = [];
            for (let col = 0; col < 9; col++) {
                const cell = this.cells[row][col];
                if (cell.value === 0 && cell.candidates.includes(num)) {
                    cols.push(col);
                }
            }
            if (cols.length > 0) {
                rowPatterns.push({ row, cols });
            }
        }

        // 找出有相同两列模式的两行
        for (let i = 0; i < rowPatterns.length; i++) {
            for (let j = i + 1; j < rowPatterns.length; j++) {
                const pattern1 = rowPatterns[i];
                const pattern2 = rowPatterns[j];

                // 检查是否有相同的列模式(恰好两列)
                if (pattern1.cols.length === 2 &&
                    pattern2.cols.length === 2 &&
                    pattern1.cols[0] === pattern2.cols[0] &&
                    pattern1.cols[1] === pattern2.cols[1]) {

                    const cols = pattern1.cols;
                    // 从这两列的其他行中移除该候选值
                    for (let col of cols) {
                        for (let row = 0; row < 9; row++) {
                            if (row !== pattern1.row && row !== pattern2.row) {
                                const cell = this.cells[row][col];
                                if (cell.value === 0 && cell.candidates.includes(num)) {
                                    this.removeCandidateFromPeersCell(cell, num);
                                    removed++;
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    return removed;
};

// 在列中寻找X-Wing
SudokuGrid.prototype.findXWingInCols = function() {
    let removed = 0;

    for (let num = 1; num <= 9; num++) {
        // 找出每列中该数字出现的行
        const colPatterns = [];
        for (let col = 0; col < 9; col++) {
            const rows = [];
            for (let row = 0; row < 9; row++) {
                const cell = this.cells[row][col];
                if (cell.value === 0 && cell.candidates.includes(num)) {
                    rows.push(row);
                }
            }
            if (rows.length > 0) {
                colPatterns.push({ col, rows });
            }
        }

        // 找出有相同两行模式的两列
        for (let i = 0; i < colPatterns.length; i++) {
            for (let j = i + 1; j < colPatterns.length; j++) {
                const pattern1 = colPatterns[i];
                const pattern2 = colPatterns[j];

                if (pattern1.rows.length === 2 &&
                    pattern2.rows.length === 2 &&
                    pattern1.rows[0] === pattern2.rows[0] &&
                    pattern1.rows[1] === pattern2.rows[1]) {

                    const rows = pattern1.rows;
                    // 从这两行的其他列中移除该候选值
                    for (let row of rows) {
                        for (let col = 0; col < 9; col++) {
                            if (col !== pattern1.col && col !== pattern2.col) {
                                const cell = this.cells[row][col];
                                if (cell.value === 0 && cell.candidates.includes(num)) {
                                    this.removeCandidateFromPeersCell(cell, num);
                                    removed++;
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    return removed;
};

Swordfish(剑鱼)

Swordfish是X-Wing的扩展,涉及三行三列。如果某个数字在三行中都只出现在相同的三个列中(每行至少有两个候选位置在这三列中),那么该数字必定位于这三条行与三条列的交叉点上。我们可以从这些交叉点之外的该三列的其他候选位置中排除该数字。

Swordfish的检测比X-Wing更加复杂,因为它允许每行在候选列中有多个候选位置,只要这些位置都在那三列中即可。

javascript 复制代码
// Swordfish(剑鱼):X-Wing的扩展,涉及三行三列
SudokuGrid.prototype.applySwordfish = function() {
    let removed = 0;

    // 按行检查Swordfish
    removed += this.findSwordfishInRows();

    // 按列检查Swordfish
    removed += this.findSwordfishInCols();

    return removed;
};

// 在行中寻找Swordfish
SudokuGrid.prototype.findSwordfishInRows = function() {
    let removed = 0;

    for (let num = 1; num <= 9; num++) {
        // 找出每行中该数字可能出现的列
        const rowCandidates = [];
        for (let row = 0; row < 9; row++) {
            const cols = [];
            for (let col = 0; col < 9; col++) {
                const cell = this.cells[row][col];
                if (cell.value === 0 && cell.candidates.includes(num)) {
                    cols.push(col);
                }
            }
            if (cols.length > 0) {
                rowCandidates.push({ row, cols });
            }
        }

        // 找出所有可能形成Swordfish的三行组合
        for (let i = 0; i < rowCandidates.length; i++) {
            for (let j = i + 1; j < rowCandidates.length; j++) {
                for (let k = j + 1; k < rowCandidates.length; k++) {
                    const row1 = rowCandidates[i];
                    const row2 = rowCandidates[j];
                    const row3 = rowCandidates[k];

                    // 找出三行候选列的交集
                    const unionCols = [...new Set([...row1.cols, ...row2.cols, ...row3.cols])];

                    // 如果并集恰好是3列
                    if (unionCols.length === 3) {
                        // 检查每行是否至少有2个候选在这三列中
                        const valid = row1.cols.filter(c => unionCols.includes(c)).length >= 2 &&
                                     row2.cols.filter(c => unionCols.includes(c)).length >= 2 &&
                                     row3.cols.filter(c => unionCols.includes(c)).length >= 2;

                        if (valid) {
                            // 从这三列的其他行中移除该候选值
                            for (let col of unionCols) {
                                for (let row = 0; row < 9; row++) {
                                    if (row !== row1.row && row !== row2.row && row !== row3.row) {
                                        const cell = this.cells[row][col];
                                        if (cell.value === 0 && cell.candidates.includes(num)) {
                                            this.removeCandidateFromPeersCell(cell, num);
                                            removed++;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    return removed;
};

// 在列中寻找Swordfish
SudokuGrid.prototype.findSwordfishInCols = function() {
    let removed = 0;

    for (let num = 1; num <= 9; num++) {
        const colCandidates = [];
        for (let col = 0; col < 9; col++) {
            const rows = [];
            for (let row = 0; row < 9; row++) {
                const cell = this.cells[row][col];
                if (cell.value === 0 && cell.candidates.includes(num)) {
                    rows.push(row);
                }
            }
            if (rows.length > 0) {
                colCandidates.push({ col, rows });
            }
        }

        for (let i = 0; i < colCandidates.length; i++) {
            for (let j = i + 1; j < colCandidates.length; j++) {
                for (let k = j + 1; k < colCandidates.length; k++) {
                    const col1 = colCandidates[i];
                    const col2 = colCandidates[j];
                    const col3 = colCandidates[k];

                    const unionRows = [...new Set([...col1.rows, ...col2.rows, ...col3.rows])];

                    if (unionRows.length === 3) {
                        const valid = col1.rows.filter(r => unionRows.includes(r)).length >= 2 &&
                                     col2.rows.filter(r => unionRows.includes(r)).length >= 2 &&
                                     col3.rows.filter(r => unionRows.includes(r)).length >= 2;

                        if (valid) {
                            for (let row of unionRows) {
                                for (let col = 0; col < 9; col++) {
                                    if (col !== col1.col && col !== col2.col && col !== col3.col) {
                                        const cell = this.cells[row][col];
                                        if (cell.value === 0 && cell.candidates.includes(num)) {
                                            this.removeCandidateFromPeersCell(cell, num);
                                            removed++;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    return removed;
};

XY-Wing(XY翼)

XY-Wing是一种三元组模式,涉及三个单元格。假设有三个单元格A、B、C,其中A的候选值是{X, Y},B的候选值是{X, Z},C的候选值是{Y, Z}。这三个单元格形成了一个"Y"形结构。在这种情况下,如果A填入X,则B不能是X(因为A已经是X),所以B必须是Z,进而C不能是Z(因为B已经是Z),所以C必须是Y。反之亦然,无论A填入X还是Y,都意味着B和C不能同时为Z。因此,B和C的交集区域(即同行、同列或同宫)中的单元格不能是Z。

javascript 复制代码
// XY-Wing(XY翼):三个单元格形成Y形链
SudokuGrid.prototype.applyXYWing = function() {
    let removed = 0;

    // 遍历所有单元格作为中心单元格
    for (let row = 0; row < 9; row++) {
        for (let col = 0; col < 9; col++) {
            const pivot = this.cells[row][col];

            // 中心单元格必须有恰好3个候选值
            if (pivot.value === 0 && pivot.candidates.length === 3) {
                const [x, y, z] = pivot.candidates;

                // 找出所有与中心单元格相关的单元格
                const peers = this.getAllPeers(row, col);

                // 找出两个"翼"单元格
                for (const peer1 of peers) {
                    if (peer1.value !== 0) continue;

                    // 翼1必须是{x,y}或{x,z}或{y,z}
                    const c1 = peer1.candidates;
                    if (c1.length !== 2) continue;

                    // 确定翼1的类型
                    let wing1Type = null;
                    if (c1.includes(x) && c1.includes(y)) wing1Type = 'xy';
                    else if (c1.includes(x) && c1.includes(z)) wing1Type = 'xz';
                    else if (c1.includes(y) && c1.includes(z)) wing1Type = 'yz';
                    else continue;

                    // 找出翼1的另一个候选值(排除z)
                    const wing1Other = wing1Type === 'xy' ? z : (wing1Type === 'xz' ? y : x);

                    for (const peer2 of peers) {
                        if (peer2.value !== 0 || peer2 === peer1) continue;

                        const c2 = peer2.candidates;
                        if (c2.length !== 2) continue;

                        // 翼2必须是包含z和wing1Other的单元格
                        if (!c2.includes(z) || !c2.includes(wing1Other)) continue;

                        // 找到翼1和翼2的交集(排除中心)
                        const wing1Cell = peer1;
                        const wing2Cell = peer2;

                        // 从翼1和翼2的交集区域中移除z
                        const wingPeers1 = this.getAllPeersCell(wing1Cell);
                        const wingPeers2 = this.getAllPeersCell(wing2Cell);

                        // 翼1和翼2的交集单元格
                        const commonPeers = wingPeers1.filter(p =>
                            wingPeers2.some(p2 => this.cellsAreEqual(p, p2))
                        );

                        // 从共同相关格中移除z
                        for (const cell of commonPeers) {
                            if (cell.value === 0 && cell.candidates.includes(z)) {
                                this.removeCandidateFromPeersCell(cell, z);
                                removed++;
                            }
                        }
                    }
                }
            }
        }
    }

    return removed;
};

// 获取单元格的所有相关单元格
SudokuGrid.prototype.getAllPeersCell = function(targetCell) {
    const peers = [];
    for (let row = 0; row < 9; row++) {
        for (let col = 0; col < 9; col++) {
            if (this.cells[row][col] === targetCell) {
                return this.getAllPeers(row, col);
            }
        }
    }
    return peers;
};

// 检查两个单元格是否相等
SudokuGrid.prototype.cellsAreEqual = function(cell1, cell2) {
    return cell1 === cell2;
};

回溯法(Backtracking)

当所有推理技巧都无法继续时,我们需要借助搜索来求解。回溯法是解决数独问题的经典方法,其基本思想是选择一个候选值最少的单元格进行尝试,然后递归求解。如果遇到矛盾,就回溯到上一步尝试其他候选值。

javascript 复制代码
// 回溯法求解数独
SudokuGrid.prototype.solve = function() {
    // 首先应用所有推理技巧
    this.applyBasicTechniques();
    this.applyIntermediateTechniques();

    // 如果已经解决,直接返回
    if (this.isSolved()) {
        return true;
    }

    // 检查是否有矛盾
    if (this.hasConflict()) {
        return false;
    }

    // 找到候选值最少的单元格
    const cell = this.findMostConstrainedCell();
    if (!cell) {
        return false;  // 无法找到有效单元格
    }

    const { row, col } = cell;
    const candidates = this.cells[row][col].candidates;

    // 尝试每个候选值
    for (const value of candidates) {
        // 保存当前状态
        const savedGrid = this.clone();

        // 尝试填入值
        this.setCellValue(row, col, value);

        // 递归求解
        if (this.solve()) {
            return true;
        }

        // 回溯:恢复之前的状态
        this.restore(savedGrid);
    }

    return false;
};

// 克隆当前网格状态
SudokuGrid.prototype.clone = function() {
    const newGrid = new SudokuGrid();
    for (let row = 0; row < 9; row++) {
        for (let col = 0; col < 9; col++) {
            const original = this.cells[row][col];
            const clone = newGrid.cells[row][col];
            clone.value = original.value;
            clone.candidates = [...original.candidates];
        }
    }
    return newGrid;
};

// 恢复网格状态
SudokuGrid.prototype.restore = function(savedGrid) {
    for (let row = 0; row < 9; row++) {
        for (let col = 0; col < 9; col++) {
            const original = savedGrid.cells[row][col];
            const current = this.cells[row][col];
            current.value = original.value;
            current.candidates = [...original.candidates];
        }
    }
};

// 找到候选值最少的未填单元格
SudokuGrid.prototype.findMostConstrainedCell = function() {
    let minCandidates = 10;
    let bestCell = null;

    for (let row = 0; row < 9; row++) {
        for (let col = 0; col < 9; col++) {
            const cell = this.cells[row][col];
            if (cell.value === 0 && cell.candidates.length < minCandidates) {
                minCandidates = cell.candidates.length;
                bestCell = { row, col };
            }
        }
    }

    return bestCell;
};

// 检查是否有矛盾(如某个单元格没有候选值)
SudokuGrid.prototype.hasConflict = function() {
    for (let row = 0; row < 9; row++) {
        for (let col = 0; col < 9; col++) {
            const cell = this.cells[row][col];
            if (cell.value === 0 && cell.candidates.length === 0) {
                return true;
            }
        }
    }
    return false;
};

算法框架整合

现在我们已经实现了多种求解技巧,接下来需要将这些技巧整合到一个完整的框架中。良好的框架设计应该考虑技巧的优先级、循环控制、状态检测等多个方面。

完整求解器实现

javascript 复制代码
// 完整的数独求解器类
function SudokuSolver() {
    this.iterations = 0;
    this.techniquesApplied = {};
}

// 初始化求解器统计
SudokuSolver.prototype.reset = function() {
    this.iterations = 0;
    this.techniquesApplied = {
        nakedSingle: 0,
        hiddenSingle: 0,
        nakedPairs: 0,
        nakedTriples: 0,
        hiddenPairs: 0,
        boxLineReduction: 0,
        xWing: 0,
        swordfish: 0,
        xyWing: 0,
        backtracking: 0
    };
};

// 主求解方法
SudokuSolver.prototype.solve = function(initialValues) {
    this.reset();

    const grid = new SudokuGrid(initialValues);

    // 应用推理技巧直到无法继续
    while (!grid.isSolved()) {
        let progress = false;

        // 检查是否有矛盾
        if (grid.hasConflict()) {
            return null;  // 无解
        }

        // 应用基础技巧
        const basicFilled = grid.applyBasicTechniques();
        if (basicFilled > 0) {
            progress = true;
            this.techniquesApplied.nakedSingle += basicFilled;
            continue;
        }

        // 应用中级技巧
        const intermediateRemoved = grid.applyIntermediateTechniques();
        if (intermediateRemoved > 0) {
            progress = true;
            this.techniquesApplied.nakedPairs += intermediateRemoved;
            continue;
        }

        // 应用高级技巧
        const advancedRemoved = grid.applyAdvancedTechniques();
        if (advancedRemoved > 0) {
            progress = true;
            continue;
        }

        // 如果没有进展,检查是否解决
        if (!progress) {
            if (grid.isSolved()) {
                break;
            }
            // 需要回溯
            if (this.tryBacktracking(grid)) {
                this.techniquesApplied.backtracking++;
                break;
            }
            return null;  // 回溯也失败,无解
        }

        this.iterations++;
        if (this.iterations > 1000) {
            return null;  // 防止无限循环
        }
    }

    return grid;
};

// 应用所有高级技巧
SudokuGrid.prototype.applyAdvancedTechniques = function() {
    let totalRemoved = 0;

    // X-Wing
    totalRemoved += this.applyXWing();

    // Swordfish
    totalRemoved += this.applySwordfish();

    // XY-Wing
    totalRemoved += this.applyXYWing();

    this.updateAllCandidates();

    return totalRemoved;
};

// 尝试回溯法
SudokuSolver.prototype.tryBacktracking = function(grid) {
    // 保存当前状态
    const savedGrid = grid.clone();

    // 找到候选值最少的单元格
    const cell = grid.findMostConstrainedCell();
    if (!cell) {
        return false;
    }

    const { row, col } = cell;
    const candidates = [...grid.cells[row][col].candidates];

    // 尝试每个候选值
    for (const value of candidates) {
        // 克隆网格进行尝试
        const testGrid = grid.clone();
        testGrid.setCellValue(row, col, value);

        // 递归尝试求解
        const solver = new SudokuSolver();
        const result = solver.solve(testGrid);

        if (result && result.isSolved()) {
            // 将结果复制回原网格
            for (let r = 0; r < 9; r++) {
                for (let c = 0; c < 9; c++) {
                    grid.cells[r][c].value = result.cells[r][c].value;
                    grid.cells[r][c].candidates = [...result.cells[r][c].candidates];
                }
            }
            return true;
        }
    }

    // 所有尝试都失败
    return false;
};

// 获取求解统计
SudokuSolver.prototype.getStats = function() {
    return {
        iterations: this.iterations,
        techniques: this.techniquesApplied
    };
};

高级技巧整合

将X-Wing、Swordfish、XY-Wing等高级技巧整合到统一的接口中:

javascript 复制代码
// 添加到SudokuGrid.prototype中

// 应用所有高级技巧
SudokuGrid.prototype.applyAdvancedTechniques = function() {
    let totalRemoved = 0;
    let iterations = 0;
    const maxIterations = 10;

    while (iterations < maxIterations) {
        let removed = 0;

        // X-Wing
        removed += this.applyXWing();

        // Swordfish
        removed += this.applySwordfish();

        // XY-Wing
        removed += this.applyXYWing();

        if (removed === 0) {
            break;
        }

        this.updateAllCandidates();
        totalRemoved += removed;
        iterations++;
    }

    return totalRemoved;
};

测试与优化

测试策略

为了确保求解器的正确性,我们需要设计全面的测试用例。测试用例应该涵盖不同难度等级的数独题目。

javascript 复制代码
// 测试用例库
const testCases = {
    // 简单难度
    easy: [
        [
            [5, 3, 0, 0, 7, 0, 0, 0, 0],
            [6, 0, 0, 1, 9, 5, 0, 0, 0],
            [0, 9, 8, 0, 0, 0, 0, 6, 0],
            [8, 0, 0, 0, 6, 0, 0, 0, 3],
            [4, 0, 0, 8, 0, 3, 0, 0, 1],
            [7, 0, 0, 0, 2, 0, 0, 0, 6],
            [0, 6, 0, 0, 0, 0, 2, 8, 0],
            [0, 0, 0, 4, 1, 9, 0, 0, 5],
            [0, 0, 0, 0, 8, 0, 0, 7, 9]
        ]
    ],
    // 中等难度
    medium: [
        [
            [0, 0, 0, 2, 6, 0, 7, 0, 1],
            [6, 8, 0, 0, 7, 0, 0, 9, 0],
            [1, 9, 0, 0, 0, 4, 5, 0, 0],
            [8, 2, 0, 1, 0, 0, 0, 4, 0],
            [0, 0, 4, 6, 0, 2, 9, 0, 0],
            [0, 5, 0, 0, 0, 3, 0, 2, 8],
            [0, 0, 9, 3, 0, 0, 0, 7, 4],
            [0, 4, 0, 0, 5, 0, 0, 3, 6],
            [7, 0, 3, 0, 1, 8, 0, 0, 0]
        ]
    ],
    // 困难难度
    hard: [
        [
            [0, 0, 0, 0, 0, 0, 2, 0, 0],
            [0, 8, 0, 0, 0, 7, 0, 9, 0],
            [6, 0, 2, 0, 0, 0, 5, 0, 0],
            [0, 7, 0, 0, 6, 0, 0, 0, 0],
            [0, 0, 0, 9, 0, 1, 0, 0, 0],
            [0, 0, 0, 0, 2, 0, 0, 4, 0],
            [0, 0, 5, 0, 0, 0, 6, 0, 3],
            [0, 9, 0, 4, 0, 0, 0, 7, 0],
            [0, 0, 6, 0, 0, 0, 0, 0, 0]
        ]
    ],
    // 专家难度
    expert: [
        [
            [0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 3, 0, 8, 5],
            [0, 0, 1, 0, 2, 0, 0, 0, 0],
            [0, 0, 0, 5, 0, 7, 0, 0, 0],
            [0, 0, 4, 0, 0, 0, 1, 0, 0],
            [0, 9, 0, 0, 0, 0, 0, 0, 0],
            [5, 0, 0, 0, 0, 0, 0, 7, 3],
            [0, 0, 2, 0, 1, 0, 0, 0, 0],
            [0, 0, 0, 0, 4, 0, 0, 0, 9]
        ]
    ]
};

// 运行测试
function runTests() {
    const solver = new SudokuSolver();
    const results = {};

    for (const [difficulty, cases] of Object.entries(testCases)) {
        results[difficulty] = [];
        for (const [index, puzzle] of cases.entries()) {
            console.log(`Testing ${difficulty} puzzle ${index + 1}...`);
            const startTime = Date.now();
            const solution = solver.solve(puzzle);
            const elapsed = Date.now() - startTime;

            if (solution) {
                results[difficulty].push({
                    solved: true,
                    time: elapsed,
                    iterations: solver.getStats().iterations
                });
                console.log(`  Solved in ${elapsed}ms`);
            } else {
                results[difficulty].push({
                    solved: false,
                    time: elapsed
                });
                console.log(`  Failed to solve`);
            }
        }
    }

    return results;
}

// 验证解答的正确性
function validateSolution(puzzle, solution) {
    for (let row = 0; row < 9; row++) {
        for (let col = 0; col < 9; col++) {
            // 检查原给定数是否被保留
            if (puzzle[row][col] !== 0) {
                if (solution[row][col] !== puzzle[row][col]) {
                    return false;
                }
            }
        }
    }

    // 检查数独规则
    const grid = new SudokuGrid(solution);
    return grid.isValid();
}

性能优化

在实际应用中,性能优化是一个重要的考量。以下是一些常用的优化策略:

javascript 复制代码
// 性能优化:候选值缓存
class CachedSudokuGrid extends SudokuGrid {
    constructor(initialValues) {
        super(initialValues);
        this.candidateCache = new Map();
    }

    // 获取单元格的缓存键
    getCellCacheKey(row, col) {
        return `${row},${col}`;
    }

    // 使用缓存的候选值计算
    getCandidatesWithCache(row, col) {
        const key = this.getCellCacheKey(row, col);
        const cell = this.cells[row][col];

        if (cell.value !== 0) {
            return [];
        }

        // 如果缓存存在且有效,返回缓存值
        if (this.candidateCache.has(key)) {
            const cached = this.candidateCache.get(key);
            // 检查缓存是否仍然有效
            if (this.isCacheValid(cached, row, col)) {
                return cached.candidates;
            }
        }

        // 重新计算候选值
        const candidates = this.calculateCandidates(row, col);
        this.candidateCache.set(key, {
            candidates: candidates,
            row: row,
            col: col
        });

        return candidates;
    }

    // 检查缓存是否仍然有效
    isCacheValid = function(cached, row, col) {
        // 检查同行、同列、同宫是否有值变化
        // 简化版本:总是重新计算
        return false;
    }

    // 清除缓存
    clearCache() {
        this.candidateCache.clear();
    }
}

// 性能优化:早期退出
SudokuGrid.prototype.tryQuickSolve = function() {
    // 先尝试快速求解(只使用基础技巧)
    const quickGrid = this.clone();
    quickGrid.applyBasicTechniques();
    quickGrid.applyIntermediateTechniques();

    if (quickGrid.isSolved()) {
        return quickGrid;
    }

    // 如果快速求解失败,返回null表示需要完整求解
    return null;
};

结语

通过本文的详细介绍,我们从最基础的唯余法和排除法,到中级的数对法、三数组法、区块排除法,再到高级的X-Wing、Swordfish、XY-Wing等技巧,我们逐一实现了这些算法的JavaScript版本。

数独求解的实现是一个非常好的算法练习,它涉及的知识点包括约束传播、模式识别、图搜索等,这些都是计算机科学中的核心概念。通过深入理解这些概念并付诸实践,不仅可以掌握数独求解的技术,更可以提升解决其他复杂问题的能力。

对于想要进一步深入研究的读者,可以尝试将机器学习方法应用于数独,训练能够智能选择求解策略的模型。

数独的世界丰富多彩,希望本文能够为读者打开这扇门,在算法的海洋中享受探索的乐趣。

相关推荐
qiqsevenqiqiqiqi1 小时前
MT2048三连 暴力→数学推导→O (n) 优化
数据结构·c++·算法
海天鹰1 小时前
EXIF-JS
javascript
码之气三段.1 小时前
十五届山东ccpc省赛补题(update)
数据结构·c++·算法
清汤饺子1 小时前
【译】我的 AI 进阶之路:从怀疑到深度整合
前端·javascript·后端
@菜菜_达1 小时前
Vue生命周期
前端·javascript·vue.js
每天吃饭的羊1 小时前
UMD和IIfe
开发语言·前端·javascript
AI科技星2 小时前
ELN 升级:π 级数自动生成器全域数理架构
大数据·人工智能·python·算法·金融
强盛机器学习~2 小时前
2026年SCI一区新算法-傅里叶变换优化算法(FTO)-公式原理详解与性能测评 Matlab代码免费获取
算法·matlab·进化计算·群体智能·傅里叶变换·元启发式算法
gCode Teacher 格码致知2 小时前
Javascript提高:自定义的占位符替换-由Deepseek产生
开发语言·javascript·ecmascript