0
点赞
收藏
分享

微信扫一扫

Python 中的回溯算法及应用

回溯算法(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']
]

条件变换:女孩不能做中间

Python 中的回溯算法及应用_车间调度

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)]


举报

相关推荐

0 条评论