알고리즘 소개
이 페이지에서는 5×5 퍼펙트 스도쿠 퍼즐의 난이도를 평가하기 위한 Human-like Sudoku Solver 알고리즘을 소개합니다. 이 알고리즘은 실제 인간이 스도쿠를 푸는 방식을 모방하여 설계되었으며, 논문에서 제시된 난이도 평가 방법론의 핵심입니다.
주요 특징:
- 결정론적 사이클(Deterministic Cycle): 후보가 하나뿐인 셀을 채우는 논리적 추론
- 시행 사이클(Trial Cycle): 후보가 적은 셀부터 백트래킹을 통한 탐색
- 완전 독립 실행형: 외부 파일 의존성 없이 단독 실행 가능
- 상세한 통계 수집: 각 방법별 사용 횟수를 통한 난이도 점수 산출
알고리즘 작동 방식
1. 결정론적 사이클 (Deterministic Cycle)
인간이 논리적으로 확실하게 추론할 수 있는 방법들을 사용합니다:
- Naked Singles (셀 관점): 특정 셀에 들어갈 수 있는 후보 숫자가 하나뿐인 경우
- Hidden Singles (숫자 관점): 특정 행/열/블록에서 어떤 숫자가 들어갈 수 있는 위치가 하나뿐인 경우
2. 시행 사이클 (Trial Cycle)
결정론적 방법으로 더 이상 진행할 수 없을 때, 가장 적은 후보를 가진 셀에서 추측을 시작합니다. 백트래킹을 통해 잘못된 추측을 취소하고 다른 가능성을 탐색합니다.
3. 난이도 평가
알고리즘은 퍼즐을 풀면서 각 방법의 사용 횟수를 기록합니다. 난이도 점수 = 총 숫자 배치 시도 횟수 - 빈 셀의 개수로 계산됩니다. 이 점수가 높을수록 더 어려운 퍼즐입니다.
파이썬 코드
아래는 논문에서 사용된 Human-like Sudoku Solver의 전체 구현 코드입니다. 이 코드는 완전히 독립적으로 실행 가능하며, 5×5 퍼펙트 스도쿠의 특별한 블록 구조를 포함하고 있습니다.
human_like_solver.py
"""
Standalone Human-like Sudoku Solver
- Human-like logical solver algorithm for 5x5 Sudoku puzzles
- Deterministic cycles: Fill cells with only one candidate
- Trial cycles: Backtracking on cells with fewer candidates
- Completely standalone (no external file dependencies)
"""
import copy
import time
import random
# ==================== Utility Functions ====================
def print_puzzle(puzzle_matrix):
"""Print puzzle in a readable format (0 shown as .)"""
for row in puzzle_matrix:
print(" " + " ".join(str(x) if x != 0 else '.' for x in row))
def validate_sudoku_solution(solution, verbose=False):
"""
Validate if a 5x5 Sudoku solution is correct
Args:
solution: 5x5 list [[1,2,3,4,5], [3,4,5,1,2], ...]
verbose: If True, print detailed error messages
Returns:
bool: True if valid, False otherwise
"""
# Block structure definition (unique to 5x5 Sudoku)
block_structure = [
[5,4,2,2,2],
[5,5,1,2,5],
[5,1,1,1,3],
[3,4,1,3,3],
[4,4,4,2,3],
]
# Generate block position mapping
blocks = {}
for r in range(5):
for c in range(5):
block_id = block_structure[r][c]
if block_id not in blocks:
blocks[block_id] = []
blocks[block_id].append((r, c))
# Basic size check
if len(solution) != 5:
if verbose:
print(f"[Error] Invalid number of rows: {len(solution)} (should be 5)")
return False
for i, row in enumerate(solution):
if len(row) != 5:
if verbose:
print(f"[Error] Row {i} has invalid number of columns: {len(row)} (should be 5)")
return False
# 1. Row constraint check
for i, row in enumerate(solution):
if sorted(row) != [1, 2, 3, 4, 5]:
if verbose:
print(f"[Error] Row {i} constraint violation: {row}")
print(f" → Sorted: {sorted(row)} (should be 1,2,3,4,5)")
return False
# 2. Column constraint check
for col in range(5):
column = [solution[row][col] for row in range(5)]
if sorted(column) != [1, 2, 3, 4, 5]:
if verbose:
print(f"[Error] Column {col} constraint violation: {column}")
print(f" → Sorted: {sorted(column)} (should be 1,2,3,4,5)")
return False
# 3. Block constraint check
for block_id, positions in blocks.items():
block_values = [solution[r][c] for r, c in positions]
if sorted(block_values) != [1, 2, 3, 4, 5]:
if verbose:
print(f"[Error] Block {block_id} constraint violation: {block_values}")
print(f" → Positions: {positions}")
print(f" → Sorted: {sorted(block_values)} (should be 1,2,3,4,5)")
return False
return True
# ==================== Main Solver Class ====================
class HumanLikeSolver:
"""Human-like solver for 5x5 Sudoku puzzles"""
def __init__(self):
# Block structure definition
self.block_structure = [
[5,4,2,2,2],
[5,5,1,2,5],
[5,1,1,1,3],
[3,4,1,3,3],
[4,4,4,2,3],
]
# Block position mapping
self.blocks = {}
for r in range(5):
for c in range(5):
block_id = self.block_structure[r][c]
if block_id not in self.blocks:
self.blocks[block_id] = []
self.blocks[block_id].append((r, c))
# Statistics information
self.stats = {
'deterministic_cycles': 0,
'trial_cycles': 0,
'backtrack_count': 0,
'cells_filled_deterministic': 0,
'cells_filled_trial': 0,
'total_time': 0,
# Detailed deterministic statistics
'naked_singles': 0, # Cell perspective (1 candidate)
'hidden_singles': 0, # Number perspective (1 location)
'hidden_singles_by_row': 0, # Number unique position in row
'hidden_singles_by_col': 0, # Number unique position in column
'hidden_singles_by_block': 0, # Number unique position in block
# Cells filled per cycle
'fills_per_deterministic_cycle': [], # Cells filled in each deterministic cycle
'trial_depth_counts': {}, # Trial counts by depth
# Trial cycle detailed statistics
'trial_fills_by_depth': {}, # Numbers placed at each depth {depth: count}
'trial_attempts_by_depth': {}, # Attempts at each depth {depth: count}
'trial_successes_by_depth': {}, # Successful attempts at each depth {depth: count}
'trial_failures_by_depth': {}, # Failed attempts at each depth {depth: count}
'trial_cycles_by_depth': {}, # Cycles at each depth {depth: count}
# Cycle-specific detailed statistics
'cycle_details': [], # Detailed information for each cycle
'initial_deterministic_stats': {}, # First deterministic cycle statistics
'total_fill_attempts': 0, # Total number placement attempts (deterministic+trial)
'cycle_number': 0, # Current cycle number
}
def get_candidates(self, grid, row, col):
"""Calculate candidate numbers for a specific position"""
if grid[row][col] != 0:
return [] # Already filled cell
candidates = set([1, 2, 3, 4, 5])
# 1. Row constraint - remove numbers in the same row
for c in range(5):
if grid[row][c] != 0:
candidates.discard(grid[row][c])
# 2. Column constraint - remove numbers in the same column
for r in range(5):
if grid[r][col] != 0:
candidates.discard(grid[r][col])
# 3. Block constraint - remove numbers in the same block
block_id = self.block_structure[row][col]
block_positions = self.blocks[block_id]
for r, c in block_positions:
if grid[r][c] != 0:
candidates.discard(grid[r][c])
return list(candidates)
def get_all_candidates(self, grid):
"""Calculate candidates for all empty cells"""
candidates_map = {}
for r in range(5):
for c in range(5):
if grid[r][c] == 0:
candidates = self.get_candidates(grid, r, c)
candidates_map[(r, c)] = candidates
return candidates_map
def detect_errors(self, grid, candidates_map):
"""Detect error situations"""
# 1. Are there empty cells with no candidates?
for pos, candidates in candidates_map.items():
if len(candidates) == 0:
return f"Empty cell {pos} has no candidate numbers"
# 2. Are there empty cells remaining but no progress possible?
empty_cells = sum(1 for r in range(5) for c in range(5) if grid[r][c] == 0)
if empty_cells > 0 and len(candidates_map) == 0:
return f"{empty_cells} empty cells remain but no candidates available"
# 3. Are there constraint violations in currently filled parts? (partial validation)
# Row check
for r in range(5):
row_nums = [grid[r][c] for c in range(5) if grid[r][c] != 0]
if len(row_nums) != len(set(row_nums)):
return f"Duplicate numbers in row {r}: {row_nums}"
# Column check
for c in range(5):
col_nums = [grid[r][c] for r in range(5) if grid[r][c] != 0]
if len(col_nums) != len(set(col_nums)):
return f"Duplicate numbers in column {c}: {col_nums}"
# Block check
for block_id, positions in self.blocks.items():
block_nums = [grid[r][c] for r, c in positions if grid[r][c] != 0]
if len(block_nums) != len(set(block_nums)):
return f"Duplicate numbers in block {block_id}: {block_nums}"
return None # No errors
def find_hidden_singles(self, grid):
"""Number perspective constraints: Find positions where each number can only go in one place in a region"""
hidden_fills = []
for num in [1, 2, 3, 4, 5]:
# 1. Row-wise check
for row in range(5):
# Check if number already exists in this row
if num in [grid[row][c] for c in range(5)]:
continue
# Find empty cells where this number can go in this row
possible_cols = []
for col in range(5):
if grid[row][col] == 0: # Empty cell and
candidates = self.get_candidates(grid, row, col)
if num in candidates: # Number can go here
possible_cols.append(col)
# If unique position found, mark as determined
if len(possible_cols) == 1:
col = possible_cols[0]
hidden_fills.append(((row, col), num, 'row'))
# 2. Column-wise check
for col in range(5):
# Check if number already exists in this column
if num in [grid[r][col] for r in range(5)]:
continue
# Find empty cells where this number can go in this column
possible_rows = []
for row in range(5):
if grid[row][col] == 0: # Empty cell and
candidates = self.get_candidates(grid, row, col)
if num in candidates: # Number can go here
possible_rows.append(row)
# If unique position found, mark as determined
if len(possible_rows) == 1:
row = possible_rows[0]
hidden_fills.append(((row, col), num, 'col'))
# 3. Block-wise check
for block_id in self.blocks.keys():
block_positions = self.blocks[block_id]
# Check if number already exists in this block
if num in [grid[r][c] for r, c in block_positions]:
continue
# Find empty cells where this number can go in this block
possible_positions = []
for row, col in block_positions:
if grid[row][col] == 0: # Empty cell and
candidates = self.get_candidates(grid, row, col)
if num in candidates: # Number can go here
possible_positions.append((row, col))
# If unique position found, mark as determined
if len(possible_positions) == 1:
row, col = possible_positions[0]
hidden_fills.append(((row, col), num, 'block'))
return hidden_fills
def deterministic_cycle(self, grid, verbose=False):
"""Deterministic cycle: Fill cells with only one candidate + number perspective constraints"""
self.stats['deterministic_cycles'] += 1
self.stats['cycle_number'] += 1
cycle_fills = 0 # Cells filled in this cycle
# Initialize cycle-specific detailed statistics
cycle_naked_singles = 0
cycle_hidden_singles = 0
if verbose:
print(f"[Deterministic] Deterministic cycle #{self.stats['deterministic_cycles']} (overall cycle #{self.stats['cycle_number']}) started")
while True:
candidates_map = self.get_all_candidates(grid)
# Error check
error = self.detect_errors(grid, candidates_map)
if error:
if verbose:
print(f" [Error] Error detected: {error}")
# Record cycle-specific detailed statistics (error occurred)
cycle_detail = {
'cycle_number': self.stats['cycle_number'],
'cycle_type': 'deterministic',
'naked_singles': cycle_naked_singles,
'hidden_singles': cycle_hidden_singles,
'total_fills': cycle_fills,
'error': error
}
self.stats['cycle_details'].append(cycle_detail)
return False, cycle_fills
# Completion check
if len(candidates_map) == 0:
if verbose:
print(f" [Complete] Puzzle completed!")
# Record cycle-specific detailed statistics
cycle_detail = {
'cycle_number': self.stats['cycle_number'],
'cycle_type': 'deterministic',
'naked_singles': cycle_naked_singles,
'hidden_singles': cycle_hidden_singles,
'total_fills': cycle_fills
}
self.stats['cycle_details'].append(cycle_detail)
# Save separately if first deterministic cycle
if self.stats['deterministic_cycles'] == 1:
self.stats['initial_deterministic_stats'] = {
'naked_singles': cycle_naked_singles,
'hidden_singles': cycle_hidden_singles,
'total_fills': cycle_fills
}
self.stats['fills_per_deterministic_cycle'].append(cycle_fills)
return True, cycle_fills
# 1. Cell perspective: Find cells with only 1 candidate (Naked Singles)
naked_singles = [(pos, candidates[0]) for pos, candidates in candidates_map.items()
if len(candidates) == 1]
# 2. Number perspective: Find unique positions (Hidden Singles)
hidden_singles = self.find_hidden_singles(grid)
# 3. Collect all determinable cells (remove duplicates)
all_fills = {} # (r,c): (num, type, source)
# Add Naked Singles
for (r, c), num in naked_singles:
if (r, c) not in all_fills:
all_fills[(r, c)] = (num, 'naked', 'cell')
# Add Hidden Singles (only if not duplicate)
for (r, c), num, source in hidden_singles:
if (r, c) not in all_fills:
all_fills[(r, c)] = (num, 'hidden', source)
# 4. If no cells can be determined, terminate
if len(all_fills) == 0:
if verbose:
print(f" [Stop] No determinable empty cells ({len(candidates_map)} empty cells remaining)")
# Record cycle-specific detailed statistics
cycle_detail = {
'cycle_number': self.stats['cycle_number'],
'cycle_type': 'deterministic',
'naked_singles': cycle_naked_singles,
'hidden_singles': cycle_hidden_singles,
'total_fills': cycle_fills
}
self.stats['cycle_details'].append(cycle_detail)
# Save separately if first deterministic cycle
if self.stats['deterministic_cycles'] == 1:
self.stats['initial_deterministic_stats'] = {
'naked_singles': cycle_naked_singles,
'hidden_singles': cycle_hidden_singles,
'total_fills': cycle_fills
}
self.stats['fills_per_deterministic_cycle'].append(cycle_fills)
return True, cycle_fills
# 5. Fill determinable cells + record statistics
for (r, c), (num, fill_type, source) in all_fills.items():
grid[r][c] = num
cycle_fills += 1
self.stats['cells_filled_deterministic'] += 1
self.stats['total_fill_attempts'] += 1 # Total number placement attempts
# Record detailed statistics
if fill_type == 'naked':
self.stats['naked_singles'] += 1
cycle_naked_singles += 1 # Cycle-specific statistics
if verbose:
print(f" [Determined] Cell perspective: ({r},{c}) = {num}")
else: # hidden
self.stats['hidden_singles'] += 1
cycle_hidden_singles += 1 # Cycle-specific statistics
if source == 'row':
self.stats['hidden_singles_by_row'] += 1
elif source == 'col':
self.stats['hidden_singles_by_col'] += 1
elif source == 'block':
self.stats['hidden_singles_by_block'] += 1
if verbose:
print(f" [Determined] Number perspective({source}): ({r},{c}) = {num}")
def find_best_trial_cell(self, candidates_map):
"""Find optimal cell for trial (randomly select cell with fewest candidates)"""
if not candidates_map:
return None
# Sort by number of candidates
sorted_cells = sorted(candidates_map.items(), key=lambda x: len(x[1]))
# Minimum number of candidates
min_candidates = len(sorted_cells[0][1])
# Cells with same number of candidates
best_cells = [(pos, candidates) for pos, candidates in sorted_cells
if len(candidates) == min_candidates]
# Random selection (among cells with same candidate count)
return random.choice(best_cells)
def trial_cycle(self, grid, depth=0, verbose=False):
"""Trial cycle: Solve using backtracking"""
self.stats['trial_cycles'] += 1
self.stats['cycle_number'] += 1 # Overall cycle number
# Initialize cycle-specific detailed statistics
cycle_trial_attempts = 0
cycle_deterministic_fills = 0
# Initialize and record depth-specific basic statistics
depth_stats = ['trial_depth_counts', 'trial_fills_by_depth', 'trial_attempts_by_depth',
'trial_successes_by_depth', 'trial_failures_by_depth', 'trial_cycles_by_depth']
for stat_name in depth_stats:
if depth not in self.stats[stat_name]:
self.stats[stat_name][depth] = 0
self.stats['trial_depth_counts'][depth] += 1
self.stats['trial_cycles_by_depth'][depth] += 1
if verbose:
indent = " " * depth
print(f"{indent}[Trial] Trial cycle (depth {depth}, overall cycle #{self.stats['cycle_number']})")
# 1. Run deterministic cycle first
success, filled = self.deterministic_cycle(grid, verbose=False)
cycle_deterministic_fills = filled # Record cells filled in deterministic cycle
if not success:
# Error occurred - backtracking needed
if verbose:
indent = " " * depth
print(f"{indent} [Error] Error occurred in deterministic cycle")
# Record cycle-specific detailed statistics (termination due to error)
cycle_detail = {
'cycle_number': self.stats['cycle_number'],
'cycle_type': 'trial',
'depth': depth,
'deterministic_fills': cycle_deterministic_fills,
'trial_attempts': cycle_trial_attempts,
'status': 'error_in_deterministic'
}
self.stats['cycle_details'].append(cycle_detail)
return False
# 2. Check completion
candidates_map = self.get_all_candidates(grid)
if len(candidates_map) == 0:
# Completed!
# Record cycle-specific detailed statistics (completion)
cycle_detail = {
'cycle_number': self.stats['cycle_number'],
'cycle_type': 'trial',
'depth': depth,
'deterministic_fills': cycle_deterministic_fills,
'trial_attempts': cycle_trial_attempts,
'status': 'completed'
}
self.stats['cycle_details'].append(cycle_detail)
return True
# 3. Select cell to try
trial_cell = self.find_best_trial_cell(candidates_map)
if trial_cell is None:
# Record cycle-specific detailed statistics (no trial cell available)
cycle_detail = {
'cycle_number': self.stats['cycle_number'],
'cycle_type': 'trial',
'depth': depth,
'deterministic_fills': cycle_deterministic_fills,
'trial_attempts': cycle_trial_attempts,
'status': 'no_trial_cell'
}
self.stats['cycle_details'].append(cycle_detail)
return False
(r, c), candidates = trial_cell
if verbose:
indent = " " * depth
print(f"{indent} [Select] Trial position: ({r},{c}), candidates: {candidates}")
# 4. Try candidates in random order
random_candidates = candidates.copy() # Preserve original
random.shuffle(random_candidates) # Shuffle randomly
if verbose:
indent = " " * depth
print(f"{indent} [Random] Random trial order: {random_candidates}")
for num in random_candidates:
# Record attempt count
self.stats['trial_attempts_by_depth'][depth] += 1
cycle_trial_attempts += 1 # Cycle-specific attempt count
self.stats['total_fill_attempts'] += 1 # Total number placement attempts
if verbose:
indent = " " * depth
print(f"{indent} → Trying {num} (attempt #{self.stats['trial_attempts_by_depth'][depth]})")
# Copy grid and place number
grid_copy = copy.deepcopy(grid)
grid_copy[r][c] = num
self.stats['cells_filled_trial'] += 1
self.stats['trial_fills_by_depth'][depth] += 1 # Depth-specific number placement count
# Recursive trial
if self.trial_cycle(grid_copy, depth + 1, verbose):
# Success! Update original grid
self.stats['trial_successes_by_depth'][depth] += 1 # Success count
for row in range(5):
for col in range(5):
grid[row][col] = grid_copy[row][col]
if verbose:
indent = " " * depth
print(f"{indent} [Success] {num} succeeded!")
# Record cycle-specific detailed statistics (success)
cycle_detail = {
'cycle_number': self.stats['cycle_number'],
'cycle_type': 'trial',
'depth': depth,
'deterministic_fills': cycle_deterministic_fills,
'trial_attempts': cycle_trial_attempts,
'status': 'success'
}
self.stats['cycle_details'].append(cycle_detail)
return True
# Failed - try next candidate
self.stats['backtrack_count'] += 1
self.stats['trial_failures_by_depth'][depth] += 1 # Failure count
if verbose:
indent = " " * depth
print(f"{indent} [Failed] {num} failed, backtracking (failure #{self.stats['trial_failures_by_depth'][depth]})")
# All candidates failed
if verbose:
indent = " " * depth
print(f"{indent} [Failed] All candidates failed")
# Record cycle-specific detailed statistics (all candidates failed)
cycle_detail = {
'cycle_number': self.stats['cycle_number'],
'cycle_type': 'trial',
'depth': depth,
'deterministic_fills': cycle_deterministic_fills,
'trial_attempts': cycle_trial_attempts,
'status': 'all_candidates_failed'
}
self.stats['cycle_details'].append(cycle_detail)
return False
def solve(self, puzzle, verbose=False):
"""Main solving function"""
if verbose:
print("[Puzzle] Human-like Solver started")
print("=" * 50)
print("[Initial] Initial puzzle:")
print_puzzle(puzzle)
print()
# Initialize statistics
self.stats = {
'deterministic_cycles': 0,
'trial_cycles': 0,
'backtrack_count': 0,
'cells_filled_deterministic': 0,
'cells_filled_trial': 0,
'total_time': 0,
'naked_singles': 0,
'hidden_singles': 0,
'hidden_singles_by_row': 0,
'hidden_singles_by_col': 0,
'hidden_singles_by_block': 0,
'fills_per_deterministic_cycle': [],
'trial_depth_counts': {},
'trial_fills_by_depth': {},
'trial_attempts_by_depth': {},
'trial_successes_by_depth': {},
'trial_failures_by_depth': {},
'trial_cycles_by_depth': {},
# Cycle-specific detailed statistics
'cycle_details': [], # Detailed information for each cycle
'initial_deterministic_stats': {}, # First deterministic cycle statistics
'total_fill_attempts': 0, # Total number placement attempts (deterministic+trial)
'cycle_number': 0, # Current cycle number
}
start_time = time.time()
# Copy puzzle (preserve original)
grid = copy.deepcopy(puzzle)
# Start solving
success = self.trial_cycle(grid, verbose=verbose)
# Record time
self.stats['total_time'] = time.time() - start_time
if success:
# Verify solution
if validate_sudoku_solution(grid):
if verbose:
print("\n[Success] Solving succeeded!")
print("[Solution] Solution:")
print_puzzle(grid)
self.print_stats(puzzle)
return grid
else:
if verbose:
print("\n[Failed] Solving failed: Constraint violation")
return None
else:
if verbose:
print("\n[Failed] Solving failed: No solution found")
self.print_stats(puzzle)
return None
def print_stats(self, initial_puzzle=None):
"""Print solving statistics"""
print(f"\n[Statistics] Detailed solving statistics:")
print(f"[Methods] Number placement counts:")
print(f" Cell perspective: {self.stats['naked_singles']} times")
print(f" Number perspective: {self.stats['hidden_singles']} times")
print(f" Trial (guessing): {self.stats['cells_filled_trial']} times")
total_attempts = self.stats['naked_singles'] + self.stats['hidden_singles'] + self.stats['cells_filled_trial']
print(f" Total attempts: {total_attempts} times")
# Calculate difficulty score
if initial_puzzle:
# Count initial hints (non-zero cells)
initial_hints = sum(1 for r in range(5) for c in range(5) if initial_puzzle[r][c] != 0)
empty_cells = 25 - initial_hints # Total cells - hints = empty cells
difficulty_score = total_attempts - empty_cells
print(f"\n[Difficulty] Difficulty assessment:")
print(f" Difficulty score: {difficulty_score}")
# ==================== Example Puzzles ====================
# K4 - 18th puzzle (4 hints)
PUZZLE_K4_18 = [
[1, 0, 0, 4, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[2, 3, 0, 0, 0],
[0, 0, 0, 0, 0]
]
# K5 - 16th puzzle (5 hints)
PUZZLE_K5_16 = [
[1, 2, 3, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 2, 0, 0],
[0, 0, 4, 0, 0],
[0, 0, 0, 0, 0]
]
# K6 - 17th puzzle (6 hints)
PUZZLE_K6_17 = [
[1, 2, 3, 0, 5],
[0, 0, 0, 0, 0],
[5, 1, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]
]
def run_puzzle_test(puzzle, puzzle_name, verbose=True):
"""Run individual puzzle test"""
print("=" * 70)
print(f"[Test] {puzzle_name} solving test")
print("=" * 70)
solver = HumanLikeSolver()
result = solver.solve(puzzle, verbose=verbose)
if result:
print(f"\n[Result] {puzzle_name} solving succeeded!")
if validate_sudoku_solution(result):
print("[Validation] Solution is correct.")
else:
print("[Validation] Solution has errors.")
else:
print(f"\n[Result] {puzzle_name} solving failed!")
print("\n")
return result is not None
# ==================== Main Execution ====================
if __name__ == "__main__":
print("[Start] Standalone Human-like Sudoku Solver")
print("=" * 70)
print("5x5 Sudoku block structure:")
print(" 5 4 2 2 2")
print(" 5 5 1 2 5")
print(" 5 1 1 1 3")
print(" 3 4 1 3 3")
print(" 4 4 4 2 3")
print("=" * 70)
# Total success count
total_success = 0
total_puzzles = 3
# K4 18th puzzle test
if run_puzzle_test(PUZZLE_K4_18, "K4-18th puzzle", verbose=True):
total_success += 1
# K5 16th puzzle test
if run_puzzle_test(PUZZLE_K5_16, "K5-16th puzzle", verbose=True):
total_success += 1
# K6 17th puzzle test
if run_puzzle_test(PUZZLE_K6_17, "K6-17th puzzle", verbose=True):
total_success += 1
# Overall result summary
print("=" * 70)
print(f"[Summary] Overall test results")
print("=" * 70)
print(f"Successful puzzles: {total_success}/{total_puzzles}")
print(f"Success rate: {total_success/total_puzzles*100:.1f}%")
print("=" * 70)
print("[Complete] All tests completed!")
위 코드를 복사하여 human_like_solver.py 파일로 저장한 후,
Python 3 환경에서 실행하면 예제 퍼즐들에 대한 알고리즘 동작을 확인할 수 있습니다.
실행 예시
코드를 실행하면 다음과 같은 출력을 볼 수 있습니다:
| 퍼즐 | 힌트 개수 | 셀 관점 (Naked Singles) | 숫자 관점 (Hidden Singles) | 시행착오 (Trial) | 난이도 점수 |
|---|---|---|---|---|---|
| K4-18 | 4 | 8회 | 12회 | 5회 | 4 |
| K5-16 | 5 | 10회 | 10회 | 3회 | 3 |
| K6-17 | 6 | 11회 | 9회 | 1회 | 2 |
난이도 점수가 높을수록 더 많은 추론과 시행착오가 필요한 어려운 퍼즐입니다. 이 점수는 우리가 생성한 5×5 퍼펙트 스도쿠 퍼즐들의 난이도를 객관적으로 평가하는 기준이 됩니다.
이 알고리즘을 통해 우리는 퍼펙트 코드 이론을 기반으로 생성된 5×5 스도쿠 퍼즐의 난이도를 정량적으로 평가하고, 다양한 난이도의 퍼즐을 체계적으로 분류할 수 있었습니다.