한국어 English

Human-like Sudoku Solver Algorithm

5×5 퍼펙트 스도쿠의 난이도 평가를 위한 인간 모방 알고리즘

메인으로 돌아가기

알고리즘 소개

이 페이지에서는 5×5 퍼펙트 스도쿠 퍼즐의 난이도를 평가하기 위한 Human-like Sudoku Solver 알고리즘을 소개합니다. 이 알고리즘은 실제 인간이 스도쿠를 푸는 방식을 모방하여 설계되었으며, 논문에서 제시된 난이도 평가 방법론의 핵심입니다.

주요 특징:

  • 결정론적 사이클(Deterministic Cycle): 후보가 하나뿐인 셀을 채우는 논리적 추론
  • 시행 사이클(Trial Cycle): 후보가 적은 셀부터 백트래킹을 통한 탐색
  • 완전 독립 실행형: 외부 파일 의존성 없이 단독 실행 가능
  • 상세한 통계 수집: 각 방법별 사용 횟수를 통한 난이도 점수 산출

알고리즘 작동 방식

1. 결정론적 사이클 (Deterministic Cycle)

인간이 논리적으로 확실하게 추론할 수 있는 방법들을 사용합니다:

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 스도쿠 퍼즐의 난이도를 정량적으로 평가하고, 다양한 난이도의 퍼즐을 체계적으로 분류할 수 있었습니다.