回溯算法(Backtracking)
回溯法是一种通过尝试所有可能的解来找到问题解的算法设计方法。它通常通过递归实现,每一步选择一个可能的解,如果解不符合要求,则进行回退,尝试其他可能的解,直到找到满足问题条件的解。它通常应用于组合问题、排列问题、子集问题等。
回溯搜索算法尝试在每次递归时为变量分配一个值,如果没有更多要分配的合法值,则回溯(返回并尝试另一个值)。纯粹的回溯算法可能相当慢,但我们可以通过引导它朝着正确的方向发展来提高它的性能。
我们可以使用 Arc 一致性来加速回溯,这意味着我们只在域中包含每个变量的合法值,因此可供选择的值较少。我们还可以使用最受约束的变量(最小剩余值)启发式方法,首先选择合法值最少的变量。
回溯类型:
虽然回溯可用于解决决策问题、优化问题和枚举问题等问题,但回溯有不同类型的——
- 递归回溯:回溯通常使用递归函数实现,每个递归调用代表一个决策。在少数情况下,使用迭代回溯,使用堆栈和队列来跟踪决策。
- 优化回溯:在一些问题中,回溯不仅用于查找所有可能的解决方案,还用于在各种可能性中找到最佳解决方案。这涉及修剪技术,以消除无法导致最佳解决方案的调用。有时,回溯也可以与动态规划中的记忆和制表相结合。
- 启发式回溯: 这种回溯主要用于人工智能。在穷举探索空间的情况下,启发式方法可用于指导回溯算法找到最佳解决方案。
方法:
首先,我们形成一个状态树,它包含不同的状态来解决给定的问题。然后,我们遍历每个状态,以确定它是否可以成为解决方案或导致解决方案,然后,根据结果,我们可以决定在该迭代中丢弃所有无效的解决方案。
用回溯法解决座位排序
- 有3名学生
- 男孩 1
- 男孩 2
- 女孩 1
- 有 1 排包含三个座位
- 要求
- 所有座位必须满员
- 一个学生一次只能坐在一个座位上
- 找到所有可能的座位安排
座位编号 | 1 | 2 | 3 |
第一排 |
可能的座位安排
有六种可能的安排
座位编号 | 1 | 2 | 3 |
第一排 | 男孩 1 | 男孩 2 | 女孩 1 |
座位编号 | 1 | 2 | 3 |
第一排 | 男孩 1 | 女孩 1 | 男孩 2 |
座位编号 | 1 | 2 | 3 |
第一排 | 男孩 2 | 男孩 1 | 女孩 1 |
座位编号 | 1 | 2 | 3 |
第一排 | 男孩 2 | 女孩 1 | 男孩 1 |
座位编号 | 1 | 2 | 3 |
第一排 | 女孩 1 | 男孩 1 | 男孩 2 |
座位编号 | 1 | 2 | 3 |
第一排 | 女孩 1 | 男孩 2 | 男孩 1 |
示例代码
OPTIONS = {"B1", "B2", "G1"}
# Check if the current state is a valid soluion
def is_valid_state(state):
# The current state is valid is there is a unique student in each seat
return len(state) == 3
# Get list of potential next steps
def get_candidates(state):
# print(list(OPTIONS.difference(set(state))))
return list(OPTIONS.difference(set(state)))
# Recursively, perform a depth-first search to find valid solutions
def search(state, solutions):
# Check is the state is valid
if is_valid_state(state):
# Add a copy of the valid state to list of solutions
solutions.append(state.copy())
print(f"Valid State Found: {state}")
# return # uncomment if you only need to find one valid solution
# Iterate through the candidates that can be used
# to construct the next state
for candidate in get_candidates(state):
# Add candidate to the current state
state.append(candidate)
# Call search function with updated state
search(state, solutions)
# Remove the current candidate from the current state
print(f"backtracking from: {state}")
state.remove(candidate)
# Entry point to the program
# responsible for returning the valid solutions
def solve():
solutions = []
state = []
search(state, solutions)
return solutions
if __name__ == "__main__":
solutions = solve()
print(solutions)
输出
Valid State Found: ['G1', 'B2', 'B1']
backtracking from: ['G1', 'B2', 'B1']
backtracking from: ['G1', 'B2']
Valid State Found: ['G1', 'B1', 'B2']
backtracking from: ['G1', 'B1', 'B2']
backtracking from: ['G1', 'B1']
backtracking from: ['G1']
Valid State Found: ['B2', 'G1', 'B1']
backtracking from: ['B2', 'G1', 'B1']
backtracking from: ['B2', 'G1']
Valid State Found: ['B2', 'B1', 'G1']
backtracking from: ['B2', 'B1', 'G1']
backtracking from: ['B2', 'B1']
backtracking from: ['B2']
Valid State Found: ['B1', 'G1', 'B2']
backtracking from: ['B1', 'G1', 'B2']
backtracking from: ['B1', 'G1']
Valid State Found: ['B1', 'B2', 'G1']
backtracking from: ['B1', 'B2', 'G1']
backtracking from: ['B1', 'B2']
backtracking from: ['B1']
[
['G1', 'B2', 'B1'],
['G1', 'B1', 'B2'],
['B2', 'G1', 'B1'],
['B2', 'B1', 'G1'],
['B1', 'G1', 'B2'],
['B1', 'B2', 'G1']
]
条件变换:女孩不能做中间
OPTIONS = {"B1", "B2", "G1"}
# Check if the current state is a valid soluion
def is_valid_state(state):
# The current state is valid is there is a unique student in each seat
# and the girl is not in the middle seat
return len(state) == 3
# Get list of potential next steps
def get_candidates(state):
# Can only use students that are not already seated
# and the girl cannot be in the middle seat
if len(state) > 1 and state[1] == "G1": return []
return list(OPTIONS.difference(set(state)))
# Recursively, perform a depth-first search to find valid solutions
def search(state, solutions):
# Check is the state is valid
if is_valid_state(state):
# Add a copy of the valid state to list of solutions
solutions.append(state.copy())
print(f"Valid State Found: {state}")
# return # uncomment if you only need to find one valid solution
# Iterate through the candidates that can be used
# to construct the next state
for candidate in get_candidates(state):
# Add candidate to the current state
state.append(candidate)
# Call search function with updated state
search(state, solutions)
# Remove the current candidate from the current state
print(f"backtracking from: {state}")
state.remove(candidate)
# Entry point to the program
# responsible for returning the valid solutions
def solve():
solutions = []
state = []
search(state, solutions)
return solutions
if __name__ == "__main__":
solutions = solve()
print(solutions)
输出
Valid State Found: ['B1', 'B2', 'G1']
backtracking from: ['B1', 'B2', 'G1']
backtracking from: ['B1', 'B2']
backtracking from: ['B1', 'G1']
backtracking from: ['B1']
Valid State Found: ['B2', 'B1', 'G1']
backtracking from: ['B2', 'B1', 'G1']
backtracking from: ['B2', 'B1']
backtracking from: ['B2', 'G1']
backtracking from: ['B2']
Valid State Found: ['G1', 'B1', 'B2']
backtracking from: ['G1', 'B1', 'B2']
backtracking from: ['G1', 'B1']
Valid State Found: ['G1', 'B2', 'B1']
backtracking from: ['G1', 'B2', 'B1']
backtracking from: ['G1', 'B2']
backtracking from: ['G1']
[
['B1', 'B2', 'G1'],
['B2', 'B1', 'G1'],
['G1', 'B1', 'B2'],
['G1', 'B2', 'B1']
]
用回溯法解决数独
此法可用于解决不同大小的 sodoku 谜题。
此代码中包含了两个回溯算法,以及一个优化版本。简单的 sodoku 谜题可以使用第一种算法在合理的时间内解决,而更难的谜题必须使用第二种版本解决。
示例代码
# Import libraries
import copy
# This class represent a sodoku
class Sodoku():
# Create a new sodoku
def __init__(self, state:[], size:int, sub_column_size:int, sub_row_size:int):
# Set values for instance variables
self.state = state
self.size = size
self.sub_column_size = sub_column_size
self.sub_row_size = sub_row_size
self.domains = {}
# Create domains for numbers by using Arc consistency
# Arc consistency: include only consistent numbers in the domain for each cell
self.update_domains()
# Update domains for cells
def update_domains(self):
# Reset domains
self.domains = {}
# Create an array with numbers
numbers = []
# Loop the state (puzzle or grid)
for y in range(self.size):
for x in range(self.size):
# Check if a cell is empty
if (self.state[y][x] == 0):
# Loop all possible numbers
numbers = []
for number in range(1, self.size + 1):
# Check if the number is consistent
if(self.is_consistent(number, y, x) == True):
numbers.append(number)
# Add numbers to a domain
if(len(numbers) > 0):
self.domains[(y, x)] = numbers
# Check if a number can be put in a cell
def is_consistent(self, number:int, row:int, column:int) -> bool:
# Check a row
for x in range(self.size):
# Return false if the number exists in the row
if self.state[row][x] == number:
return False
# Check a column
for y in range(self.size):
# Return false if the number exists in the column
if self.state[y][column] == number:
return False
# Calculate row start and column start
row_start = (row//self.sub_row_size)*self.sub_row_size
col_start = (column//self.sub_column_size)*self.sub_column_size;
# Check sub matrix
for y in range(row_start, row_start+self.sub_row_size):
for x in range(col_start, col_start+self.sub_column_size):
# Return false if the number exists in the submatrix
if self.state[y][x]== number:
return False
# Return true if no conflicts has been found
return True
# Get the first empty cell (backtracking_search_1)
def get_first_empty_cell(self) -> ():
# Loop the state (puzzle or grid)
for y in range(self.size):
for x in range(self.size):
# Check if the cell is empty
if (self.state[y][x] == 0):
return (y, x)
# Return false
return (None, None)
# Get the most constrained cell (backtracking_search_2)
def get_most_constrained_cell(self) -> ():
# No empty cells left, return None
if(len(self.domains) == 0):
return (None, None)
# Sort domains by value count (we want empty cells with most constraints at the top)
keys = sorted(self.domains, key=lambda k: len(self.domains[k]))
# Return the first key in the dictionary
return keys[0]
# Check if the puzzle is solved
def solved(self) -> bool:
# Loop the state (puzzle or grid)
for y in range(self.size):
for x in range(self.size):
# Check if the cell is empty
if (self.state[y][x] == 0):
return False
# Return true
return True
# Solve the puzzle
def backtracking_search_1(self) -> bool:
# Get the first empty cell
y, x = self.get_first_empty_cell()
# Check if the puzzle is solved
if(y == None or x == None):
return True
# Assign a number
for number in range(1, self.size + 1):
# Check if the number is consistent
if(self.is_consistent(number, y, x)):
# Assign the number
self.state[y][x] = number
# Backtracking
if (self.backtracking_search_1() == True):
return True
# Reset assignment
self.state[y][x] = 0
# No number could be assigned, return false
return False
# Solve the puzzle (optimized version)
def backtracking_search_2(self) -> bool:
# Check if the puzzle is solved
if(self.solved() == True):
return True
# Get a an empty cell
y, x = self.get_most_constrained_cell()
# No good cell was found, retry
if (y == None or x == None):
return False
# Get possible numbers in domain
numbers = copy.deepcopy(self.domains.get((y, x)))
# Assign a number
for number in numbers:
# Check if the number is consistent
if(self.is_consistent(number, y, x)):
# Assign the number
self.state[y][x] = number
# Remove the entire domain
del self.domains[(y, x)]
# Backtracking
if (self.backtracking_search_2() == True):
return True
# Reset assignment
self.state[y][x] = 0
# Update domains
self.update_domains()
# No number could be assigned, return false
return False
# Print the current state
def print_state(self):
for y in range(self.size):
print('| ', end='')
if y != 0 and y % self.sub_row_size == 0:
for j in range(self.size):
print(' - ', end='')
if (j + 1) < self.size and (j + 1) % self.sub_column_size == 0:
print(' + ', end='')
print(' |')
print('| ', end='')
for x in range(self.size):
if x != 0 and x % self.sub_column_size == 0:
print(' | ', end='')
digit = str(self.state[y][x]) if len(str(self.state[y][x])) > 1 else ' ' + str(self.state[y][x])
print('{0} '.format(digit), end='')
print(' |')
# The main entry point for this module
def main():
# Small puzzle 81 (9x9 matrix and 3x3 submatrixes)
#data = '4173698.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......'
#data = data.strip().replace('.', '0')
#numbers = [int(i) for i in data]
#size = 9 # 9 columns and 9 rows
#sub_column_size = 3 # 3 columns in each submatrix
#sub_row_size = 3 # 3 rows in each submatrix
# Larger puzzle 144 (12x12 matrix and 4x3 submatrixes)
numbers = [7,0,5,0,4,0,0,1,0,0,3,6,9,6,0,0,7,0,0,0,0,1,4,0,0,2,0,0,0,0,3,6,0,0,0,8,0,0,0,10,8,0,0,9,3,0,0,0,11,0,12,1,0,0,0,0,10,0,5,9,0,0,6,0,0,3,12,0,0,0,0,0,0,0,0,0,0,7,4,0,0,9,0,0,2,12,0,7,0,0,0,0,4,10,0,5,0,0,0,11,5,0,0,2,7,0,0,0,1,0,0,0,3,6,0,0,0,0,8,0,0,11,3,0,0,0,0,5,0,0,9,7,10,5,0,0,2,0,0,7,0,3,0,1]
size = 12 # 12 columns and 12 rows
sub_column_size = 4 # 4 columns in each submatrix
sub_row_size = 3 # 3 rows in each submatrix
# Create the initial state
initial_state = []
row = []
counter = 0
# Loop numbers and append to initial state
for number in numbers:
counter += 1
row.append(number)
if(counter >= size):
initial_state.append(row)
row = []
counter = 0
# Create a sodoku
sodoku = Sodoku(initial_state, size, sub_column_size, sub_row_size)
# Print sodoku
print('Puzzle input:')
sodoku.print_state()
# Solve sodoku with optimized version
sodoku.backtracking_search_2()
# Print sodoku
print('\nPuzzle solution:')
sodoku.print_state()
print()
# Tell python to run main method
if __name__ == "__main__": main()
输出
Puzzle input:
| 7 0 5 0 | 4 0 0 1 | 0 0 3 6 |
| 9 6 0 0 | 7 0 0 0 | 0 1 4 0 |
| 0 2 0 0 | 0 0 3 6 | 0 0 0 8 |
| - - - - + - - - - + - - - - |
| 0 0 0 10 | 8 0 0 9 | 3 0 0 0 |
| 11 0 12 1 | 0 0 0 0 | 10 0 5 9 |
| 0 0 6 0 | 0 3 12 0 | 0 0 0 0 |
| - - - - + - - - - + - - - - |
| 0 0 0 0 | 0 7 4 0 | 0 9 0 0 |
| 2 12 0 7 | 0 0 0 0 | 4 10 0 5 |
| 0 0 0 11 | 5 0 0 2 | 7 0 0 0 |
| - - - - + - - - - + - - - - |
| 1 0 0 0 | 3 6 0 0 | 0 0 8 0 |
| 0 11 3 0 | 0 0 0 5 | 0 0 9 7 |
| 10 5 0 0 | 2 0 0 7 | 0 3 0 1 |
Puzzle solution:
| 7 10 5 8 | 4 12 2 1 | 9 11 3 6 |
| 9 6 11 3 | 7 10 5 8 | 12 1 4 2 |
| 12 2 1 4 | 9 11 3 6 | 5 7 10 8 |
| - - - - + - - - - + - - - - |
| 4 7 2 10 | 8 5 1 9 | 3 12 6 11 |
| 11 3 12 1 | 6 2 7 4 | 10 8 5 9 |
| 8 9 6 5 | 11 3 12 10 | 1 2 7 4 |
| - - - - + - - - - + - - - - |
| 5 1 10 6 | 12 7 4 11 | 8 9 2 3 |
| 2 12 9 7 | 1 8 6 3 | 4 10 11 5 |
| 3 8 4 11 | 5 9 10 2 | 7 6 1 12 |
| - - - - + - - - - + - - - - |
| 1 4 7 2 | 3 6 9 12 | 11 5 8 10 |
| 6 11 3 12 | 10 1 8 5 | 2 4 9 7 |
| 10 5 8 9 | 2 4 11 7 | 6 3 12 1 |
示例代码2
在此代码中,我们定义了一个数独网格,其中填充了一些单元格(0 表示空单元格)。solve_sudoku
函数使用回溯以递归方式解决难题。它找到一个空单元格,尝试不同的数字,并验证每个动作。如果一个数字没有违反数独规则,它将继续递归求解。如果未找到有效号码,则回溯到上一个单元格并尝试使用其他号码。
# Define the Sudoku grid
grid = [
[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]
]
def solve_sudoku(grid):
empty_cell = find_empty_cell(grid)
if not empty_cell:
return True
else:
row, col = empty_cell
for num in range(1, 10):
if is_valid_move(grid, num, (row, col)):
grid[row][col] = num
if solve_sudoku(grid):
return True
grid[row][col] = 0
return False
def find_empty_cell(grid):
for i in range(9):
for j in range(9):
if grid[i][j] == 0:
return (i, j)
return None
def is_valid_move(grid, num, pos):
# Check row
for i in range(9):
if grid[pos[0]][i] == num and pos[1] != i:
return False
# Check column
for i in range(9):
if grid[i][pos[1]] == num and pos[0] != i:
return False
# Check 3 x3 grid
box_x = pos[1] // 3
box_y = pos[0] // 3
for i in range(box_y * 3, box_y * 3 + 3):
for j in range(box_x * 3, box_x * 3 + 3):
if grid[i][j] == num and(i, j) != pos:
return False
return True
# Solve the Sudoku puzzle
solve_sudoku(grid)
# Print the solved Sudoku grid
for row in grid:
print(row)
用回溯法解决车间调度问题
此问题涉及在作业中调度任务,其中每个任务都必须在特定机器中执行。每个任务必须根据作业描述按一定的顺序执行,输出将是每台机器的结束时间。
示例代码
# This class represent a task
class Task:
# Create a new task
def __init__(self, tuple:()):
# Set values for instance variables
self.machine_id, self.processing_time = tuple
# Sort
def __lt__(self, other):
return self.processing_time < other.processing_time
# Print
def __repr__(self):
return ('(Machine: {0}, Time: {1})'.format(self.machine_id, self.processing_time))
# This class represent an assignment
class Assignment:
# Create a new assignment
def __init__(self, job_id:int, task_id:int, start_time:int, end_time:int):
# Set values for instance variables
self.job_id = job_id
self.task_id = task_id
self.start_time = start_time
self.end_time = end_time
# Print
def __repr__(self):
return ('(Job: {0}, Task: {1}, Start: {2}, End: {3})'.format(self.job_id, self.task_id, self.start_time, self.end_time))
# This class represents a schedule
class Schedule:
# Create a new schedule
def __init__(self, jobs:[]):
# Set values for instance variables
self.jobs = jobs
self.tasks = {}
for i in range(len(self.jobs)):
for j in range(len(self.jobs[i])):
self.tasks[(i, j)] = Task(self.jobs[i][j])
self.assignments = {}
# Get the next assignment
def backtracking_search(self) -> bool:
# Prefer tasks with an early end time
best_task_key = None
best_machine_id = None
best_assignment = None
# Loop all tasks
for key, task in self.tasks.items():
# Get task variables
job_id, task_id = key
machine_id = task.machine_id
processing_time = task.processing_time
# Check if the task needs a predecessor, find it if needs it
predecessor = None if task_id > 0 else Assignment(0, 0, 0, 0)
if (task_id > 0):
# Loop assignments
for machine, machine_tasks in self.assignments.items():
# Break out from the loop if a predecessor has been found
if(predecessor != None):
break
# Loop machine tasks
for t in machine_tasks:
# Check if a predecessor exsits
if(t.job_id == job_id and t.task_id == (task_id - 1)):
predecessor = t
break
# Continue if the task needs a predecessor and if it could not be found
if(predecessor == None):
continue
# Get an assignment
assignment = self.assignments.get(machine_id)
# Calculate the end time
end_time = processing_time
if(assignment != None):
end_time += max(predecessor.end_time, assignment[-1].end_time)
else:
end_time += predecessor.end_time
# Check if we should update the best assignment
if(best_assignment == None or end_time < best_assignment.end_time):
best_task_key = key
best_machine_id = machine_id
best_assignment = Assignment(job_id, task_id, end_time - processing_time, end_time)
# Return failure if we can not find an assignment (Problem not solvable)
if(best_assignment == None):
return False
# Add the best assignment
assignment = self.assignments.get(best_machine_id)
if(assignment == None):
self.assignments[best_machine_id] = [best_assignment]
else:
assignment.append(best_assignment)
# Remove the task
del self.tasks[best_task_key]
# Check if we are done
if(len(self.tasks) <= 0):
return True
# Backtrack
self.backtracking_search()
# The main entry point for this module
def main():
# Input data: Task = (machine_id, time)
jobs = [[(0, 3), (1, 2), (2, 2)], # Job 0
[(0, 2), (2, 1), (1, 4)], # Job 1
[(1, 4), (2, 3)]] # Job 2
# Create a schedule
schedule = Schedule(jobs)
# Find a solution
schedule.backtracking_search()
# Print the solution
print('Final solution:')
for key, value in sorted(schedule.assignments.items()):
print(key, value)
print()
# Tell python to run main method
if __name__ == "__main__": main()
输出
Final solution:
0 [(Job: 1, Task: 0, Start: 0, End: 2), (Job: 0, Task: 0, Start: 2, End: 5)]
1 [(Job: 2, Task: 0, Start: 0, End: 4), (Job: 0, Task: 1, Start: 5, End: 7), (Job: 1, Task: 2, Start: 7, End: 11)]
2 [(Job: 1, Task: 1, Start: 2, End: 3), (Job: 2, Task: 1, Start: 4, End: 7), (Job: 0, Task: 2, Start: 7, End: 9)]