[iPad Swift Playgrounds] Sudokuアプリ作成【Step6】解析ロジック Locked Candidates 追加
iPad Swift PlaygroundsでSudokuのPlay Boardを作成します。
SwiftUIの使い方をWeb情報などで学習した結果を書きます。
プログラミングすることで同時にSudoku解法の理解を深めます。
【Step6】解析ロジック Locked Candidates 追加
Locked Candidates をコード化して組み込みました。一番複雑でコード量も多くなりました。
Locked Candidatesについて
Sudokuは「3x3のブロック」と「1x9の行」と「9x1の列」の単位で、1から9の数値で埋めるというルールがあります。ブロックと行は1x3の部分で重なり、ブロックと列は3x1の部分で重なって、互いにこの規則の制約を受けます。Locked Candidatesはその制約に基づいて他のセルに入る候補数字を削減します。このルールで候補数字が絞られた後に、Naiked SingleとHidden Singleで答えを調べる必要があります。
制約を見える化
答えだけでは何故そうなったかが分かりにくいので、制約となった候補数字を赤の小さな文字で表示するようにしました。
Locked Candidates の代表的な問題です。3の数字が入る位置はどこでしょうか?
答えは下の図の赤い3の位置です。あたかも右上の3の影響がぐるりと回って近くへ戻ってくるような感じがします。図中の小さな数字3のように制約の原因となった数値とその位置を表示するようにしました。
Step5で追加した問題リストのTest21-011を解くにはLocked Candidatesのロジックが必要となります。今回の解析ロジック追加で導いた下段の回答表示と制約表示です。
ContentView.swift 全コード
長くなったのでGitHubを参照してください。
プログラムコード説明(追加分)
制約条件の数値を表示するための配列を追加しました。
@State private var data_hints: [[String]] = Array(repeating: Array(repeating: "", count: 9), count: 9)
制約条件の数値の文字を小さく、色を赤で表示する為に関数化しました。
.foregroundColor(getCellFontColor(row: row, col: col))
.font(getCellFontSize(row: row, col: col))
getCellText()にヒント表示を追加しました。
Button(action: {
selectCell(row: row, col: col)
}) {
Text(getCellText(row: row, col: col))
.frame(width: cellSize, height: cellSize, alignment: .center)
.border(lineColor)
.foregroundColor(getCellFontColor(row: row, col: col))
.background(getCellColor(row: row, col: col))
.font(getCellFontSize(row: row, col: col))
.fontWeight(data_save[row][col] == 0 ? .regular : .semibold) // 問題は太字で表示
}
private func getCellFontColor(row: Int, col: Int) -> Color {
if answers && data_hints[row][col] != "" {
return Color.red
}
return Color.black
}
private func getCellFontSize(row: Int, col: Int) -> Font {
var size: CGFloat = 35
if answers && data_hints[row][col] != "" {
size = 15
}
return .system(size: size)
}
private func getCellText(row: Int, col: Int) -> String {
// Cellに表示する文字を返す
// 0 の時はスペースを返す
var answer = " "
if data[row][col] != 0 {
answer = String(data[row][col])
} else if answers && data_hints[row][col] != "" { // 回答モードでヒントデータありなら
answer = data_hints[row][col] // ヒントを表示
}
return answer
}
答えを求めるnext()にLocked Candidatesのロジックを追加しました。
最初の部分で制約条件の数値表示を全てクリアした状態で準備しています。
private func next() {
data_hints = Array(repeating: Array(repeating: "", count: 9), count: 9)
var candidates = listCandidates(sudokuMatrix: data)
var answer: [[Int]] = []
answer = getAnswerNakedSingle(candicates_9x9: candidates)
let newAnswer = getAnswerHiddenSingle(candicates_9x9: candidates)
for ans in newAnswer {
answer.append(ans)
}
if answer.count == 0 {
var update = false
repeat {
update = updateLockedCandicates(candicates_9x9: &candidates)
} while update
answer = getAnswerNakedSingle(candicates_9x9: candidates)
let newAnswer = getAnswerHiddenSingle(candicates_9x9: candidates)
for ans in newAnswer {
answer.append(ans)
}
}
nextChoices = answer
}
Locked Candidates を次のように実装してみました。
私の技術レベルではこの様な実装となりました。配列を操作するテクニックは色々ありますし、発想を変えると各部分を数行で書く方法があるかもしれません。煩雑なコードなので説明しきれませんし、コードを読むことはお勧めしません。興味があれば、あなたの発想でコード化をEnjoyしてみてはどうでしょうか。Sudokuを解くより楽しいと私は思います。
private func updateLockedCandicates(candicates_9x9: inout [[[Int]]]) -> Bool {
var update: Bool = false
var candicates_Row: [[[[Int]]]] = []
var candicates_Column: [[[[Int]]]] = []
var candicates_Block: [[[[Int]]]] = []
for _ in 0..<9 {
var temp_Row: [[[Int]]] = []
var temp_Column: [[[Int]]] = []
var temp_Block: [[[Int]]] = []
for _ in 0..<9 {
temp_Row.append([])
temp_Column.append([])
temp_Block.append([])
}
candicates_Row.append(temp_Row)
candicates_Column.append(temp_Column)
candicates_Block.append(temp_Block)
}
// Block Loop
for block in 0..<9 {
let baseRow = Int(block / 3) * 3
let baseCol = Int(block % 3) * 3
var numberLocations: [[[Int]]] = [[], [], [], [], [], [], [], [], []]
for row in baseRow..<baseRow + 3 {
for col in baseCol..<baseCol + 3 {
for n in 0..<numberLocations.count {
if candicates_9x9[row][col].contains(n + 1) {
numberLocations[n].append([row, col])
}
}
}
}
for n in 0..<numberLocations.count {
let times = numberLocations[n].count
if times > 0 {
for location in numberLocations[n] {
candicates_Block[location[0]][location[1]].append([n + 1, times])
}
}
}
// Locked Candidates (row within block)
for row in baseRow..<baseRow + 3 {
for times in 2..<3 + 1 {
var columnLocations: [[Int]] = [[], [], [], [], [], [], [], [], []]
for col in baseCol..<baseCol + 3 {
for i in 0..<candicates_Block[row][col].count {
if candicates_Block[row][col][i][1] == times {
columnLocations[candicates_Block[row][col][i][0] - 1].append(col)
}
}
}
for n in 0..<columnLocations.count {
if columnLocations[n].count == times {
for col in 0..<candicates_9x9[row].count {
if !columnLocations[n].contains(col) {
let str = candicates_9x9[row][col].map { String(describing: $0) }
candicates_9x9[row][col] = candicates_9x9[row][col].filter { $0 != n + 1 }
if str != (candicates_9x9[row][col].map { String(describing: $0) }) {
update = true
}
}
}
if update {
for col in 0..<candicates_9x9[row].count {
if columnLocations[n].contains(col) {
if !data_hints[row][col].contains(String(n + 1)) {
if data_hints[row][col].count > 0 {
data_hints[row][col] += " "
}
data_hints[row][col] += String(n + 1)
}
}
}
}
}
}
}
}
// Locked Candidates (column within block)
for col in baseCol..<baseCol + 3 {
for times in 2..<3 + 1 {
var rowLocations: [[Int]] = [[], [], [], [], [], [], [], [], []];
for row in baseRow..<baseRow + 3 {
for i in 0..<candicates_Block[row][col].count {
if candicates_Block[row][col][i][1] == times {
rowLocations[candicates_Block[row][col][i][0] - 1].append(row)
}
}
}
for n in 0..<rowLocations.count {
if rowLocations[n].count == times {
for row in 0..<candicates_9x9.count {
if !rowLocations[n].contains(row) {
let str = candicates_9x9[row][col].map { String(describing: $0) }
candicates_9x9[row][col] = candicates_9x9[row][col].filter { $0 != n + 1 }
if str != (candicates_9x9[row][col].map { String(describing: $0) }) {
update = true
}
}
}
if update {
for row in 0..<candicates_9x9.count {
if rowLocations[n].contains(row) {
if !data_hints[row][col].contains(String(n + 1)) {
if data_hints[row][col].count > 0 {
data_hints[row][col] += " "
}
data_hints[row][col] += String(n + 1)
}
}
}
}
}
}
}
}
}
// Row Loop
for row in 0..<candicates_9x9.count {
var numberLocations: [[[Int]]] = [[], [], [], [], [], [], [], [], []]
for col in 0..<candicates_9x9[row].count {
for n in 0..<numberLocations.count {
if candicates_9x9[row][col].contains(n + 1) {
numberLocations[n].append([row, col])
}
}
}
for n in 0..<numberLocations.count {
let times = numberLocations[n].count
if times > 0 {
for location in numberLocations[n] {
candicates_Row[location[0]][location[1]].append([n + 1, times])
}
}
}
// Locked Candidates (block within row)
for block in 0..<3 {
let baseRow = Int(row / 3) * 3
let baseCol = block * 3
for times in 2..<3 + 1 {
var columnLocations: [[Int]] = [[], [], [], [], [], [], [], [], []];
for col in baseCol..<baseCol + 3 {
for i in 0..<candicates_Row[row][col].count {
if candicates_Row[row][col][i][1] == times {
columnLocations[candicates_Row[row][col][i][0] - 1].append(col)
}
}
}
for n in 0..<columnLocations.count {
if columnLocations[n].count == times {
for row2 in baseRow..<baseRow + 3 {
for col in baseCol..<baseCol + 3 {
if row2 != row || !columnLocations[n].contains(col) {
let str = candicates_9x9[row2][col].map { String(describing: $0) }
candicates_9x9[row2][col] = candicates_9x9[row2][col].filter { $0 != n + 1 }
if str != (candicates_9x9[row2][col].map { String(describing: $0) }) {
update = true
}
}
}
}
if update {
for row2 in baseRow..<baseRow + 3 {
for col in baseCol..<baseCol + 3 {
if row2 == row && columnLocations[n].contains(col) {
if !data_hints[row][col].contains(String(n + 1)) {
if data_hints[row][col].count > 0 {
data_hints[row][col] += " "
}
data_hints[row][col] += String(n + 1)
}
}
}
}
}
}
}
}
}
}
// Column Loop
for col in 0..<candicates_9x9[0].count {
var numberLocations: [[[Int]]] = [[], [], [], [], [], [], [], [], []]
for row in 0..<candicates_9x9.count {
for n in 0..<numberLocations.count {
if candicates_9x9[row][col].contains(n + 1) {
numberLocations[n].append([row, col])
}
}
}
for n in 0..<numberLocations.count {
let times = numberLocations[n].count
if times > 0 {
for location in numberLocations[n] {
candicates_Column[location[0]][location[1]].append([n + 1, times])
}
}
}
// Locked Candidates (block within Column)
for block in 0..<3 {
let baseRow = block * 3
let baseCol = Int(col / 3) * 3
for times in 2..<3 + 1 {
var rowLocations: [[Int]] = [[], [], [], [], [], [], [], [], []];
for row in baseRow..<baseRow + 3 {
for i in 0..<candicates_Column[row][col].count {
if candicates_Column[row][col][i][1] == times {
rowLocations[candicates_Column[row][col][i][0] - 1].append(row)
}
}
}
for n in 0..<rowLocations.count {
if rowLocations[n].count == times {
for row in baseRow..<baseRow + 3 {
for col2 in baseCol..<baseCol + 3 {
if col2 != col || !rowLocations[n].contains(row) {
let str = candicates_9x9[row][col2].map { String(describing: $0) }
candicates_9x9[row][col2] = candicates_9x9[row][col2].filter { $0 != n + 1 }
if str != (candicates_9x9[row][col2].map { String(describing: $0) }) {
update = true
}
}
}
}
if update {
for row in baseRow..<baseRow + 3 {
for col2 in baseCol..<baseCol + 3 {
if col2 == col && rowLocations[n].contains(row) {
if !data_hints[row][col].contains(String(n + 1)) {
if data_hints[row][col].count > 0 {
data_hints[row][col] += " "
}
data_hints[row][col] += String(n + 1)
}
}
}
}
}
}
}
}
}
}
return update
}
次回予定
Sudoku解析ロジック追加
Naked Pair/Triplet/QuadHidden Pair/Triplet/Quad
予定していたものは作成できてテスト段階にあります。結果の表示にうまくいかないところがあって、公開に至っていません。iPadに大きな配列データをプリント文でデバッグするのはつらいところがあって、同じロジックをJavascriptで確認しようか思案しております。趣味のプログラマーなので気の向くまま、興味の強いテーマを優先させていただきます。
2024年9月28 追記