回溯算法

1,677 阅读14分钟

回溯算法

什么是回溯算法

回溯算法(Back Tracking),又称试探法,是一种系统地搜索问题解的方法。之所以叫回溯,是因为在搜索解的过程中,如果发现当前路径已不能满足求解条件时,就“回溯”返回,尝试别的路径。

用回溯算法求解问题的步骤是什么

  1. 定义易于搜索的问题的解空间。
  2. 以深度优先搜索的方式搜索解空间,当搜索到某一步,发现原先的选择不能成为解,就向上退回一步重新选择,直到搜索完整个解空间。

回溯算法的关键是什么

  1. 以什么样的数据结构来代表解空间 解空间要易于以深度优先搜索的方式搜索,且能回退到上一步,最方便的数据结构就是树。

  2. 以什么样的编程方法来实现回溯算法 回溯搜索的过程中常常需要退回上一步重新选择,最方便的编程方式就是递归,因为递归可以记录下上一步的上下文,便于重新选择路径。

八皇后问题

十九世纪著名的数学高斯在1850年提出:在8✖️8的国际象棋上摆放八个皇后(棋子),使其不能相互攻击,即任意两个皇后都不能处于同一行,同一列或同一斜线上。

八皇后问题的解空间

为了简化问题,将棋盘缩小为4。
由于两个皇后不能处于同一行,所以每一行只能放一个皇后,可以放的位置是0,1,2,3。所以解空间就是从空棋盘开始,从上往下放上四个皇后。
树的第一层:
空棋盘
f(4, 0, 0, {}): 棋盘大小是4,准备放皇后的行是0,也就是棋盘的第一行,当前放置了皇后的个数是0, {}空棋盘

flowchart LR
    A["f(4, 0, 0, {})"] 

树的第二层:
在第一行放置棋子,在第一行可以放置的棋子位置是0,1,2,3
f(4, 0, 0, {}): 棋盘大小是4,准备放皇后的行是0,也就是棋盘的第一行,当前放置了皇后的个数是0, {}空棋盘
f(4, 1, 1, {i}): 棋盘大小是4,准备放皇后的行是1,也就是棋盘的第二行,当前放置了皇后的个数是1, {i},第一行皇后放置的位置i

flowchart LR
    A["f(4, 0, 0, {})"] --> |放在第0列|AB["f(4, 1, 1, {0})"]
    A["f(4, 0, 0, {})"] --> |放在第1列|AC["f(4, 1, 1, {1})"]
    A["f(4, 0, 0, {})"] --> |放在第2列|AD["f(4, 1, 1, {2})"]
    A["f(4, 0, 0, {})"] --> |放在第3列|AE["f(4, 1, 1, {3})"]  

树的第三层:
在第二行放置棋子,在第二行可以放置的棋子位置是0,1,2,3
f(4, 0, 0, {}): 棋盘大小是4,准备放皇后的行是0,也就是棋盘的第一行,当前放置了皇后的个数是0, {}空棋盘
f(4, 1, 1, {i}): 棋盘大小是4,准备放皇后的行是1,也就是棋盘的第二行,当前放置了皇后的个数是1, {i}第一行皇后放置的位置i
f(4, 2, 2, {i, j}): 棋盘大小是4,准备放皇后的行是2,也就是棋盘的第三行,当前放置了皇后的个数是2, {i,j}第一行皇后放置的位置i, 第二行皇后放置的位置是j

第二行放置的皇后是可以剪枝的, 比如图中红色虚线对应的节点的第一行放置的皇后与第二行放置的皇后不满足八皇后问题

flowchart LR
    A["f(4, 0, 0, {})"] --> |放在第0列|B["f(4, 1, 1, {0})"]
    A["f(4, 0, 0, {})"] --> |放在第1列|C["f(4, 1, 1, {1})"]
    A["f(4, 0, 0, {})"] --> |放在第2列|D["f(4, 1, 1, {2})"]
    A["f(4, 0, 0, {})"] --> |放在第3列|E["f(4, 1, 1, {3})"]
    
    classDef Pruning fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
    B["f(4, 1, 1, {0})"] --> |放在第0列|F["f(4, 2, 2, {0, 0})"]
    B["f(4, 1, 1, {0})"] --> |放在第1列|G["f(4, 2, 2, {0, 1})"]
    B["f(4, 1, 1, {0})"] --> |放在第2列|H["f(4, 2, 2, {0, 2})"]
    B["f(4, 1, 1, {0})"] --> |放在第3列|I["f(4, 2, 2, {0, 3})"]
    
    class F,G Pruning
    
    C["f(4, 1, 1, {1})"] --> |放在第0列|J["f(4, 2, 2, {1, 0})"]
    C["f(4, 1, 1, {1})"] --> |放在第1列|K["f(4, 2, 2, {1, 1})"]
    C["f(4, 1, 1, {1})"] --> |放在第2列|L["f(4, 2, 2, {1, 2})"]
    C["f(4, 1, 1, {1})"] --> |放在第3列|M["f(4, 2, 2, {1, 3})"]
    
     class J,K,L Pruning
     
    D["f(4, 1, 1, {2})"] --> |放在第0列|N["f(4, 2, 2, {2, 0})"]
    D["f(4, 1, 1, {2})"] --> |放在第1列|O["f(4, 2, 2, {2, 1})"]
    D["f(4, 1, 1, {2})"] --> |放在第2列|P["f(4, 2, 2, {2, 2})"]
    D["f(4, 1, 1, {2})"] --> |放在第3列|Q["f(4, 2, 2, {2, 3})"]
    
     class O,P,Q Pruning
     
    E["f(4, 1, 1, {3})"] --> |放在第0列|R["f(4, 2, 2, {3, 0})"]
    E["f(4, 1, 1, {3})"] --> |放在第1列|S["f(4, 2, 2, {3, 1})"]
    E["f(4, 1, 1, {3})"] --> |放在第2列|T["f(4, 2, 2, {3, 2})"]
    E["f(4, 1, 1, {3})"] --> |放在第3列|U["f(4, 2, 2, {3, 3})"]
     class T,U Pruning

树的第四层:
在第三行放置棋子,在第三行可以放置的棋子位置是0,1,2,3
f(4, 0, 0, {}): 棋盘大小是4,准备放皇后的行是0,也就是棋盘的第一行,当前放置了皇后的个数是0, {}空棋盘
f(4, 1, 1, {i}): 棋盘大小是4,准备放皇后的行是1,也就是棋盘的第二行,当前放置了皇后的个数是1, {i},第一行皇后放置的位置i
f(4, 2, 2, {i, j}): 棋盘大小是4,准备放皇后的行是2,也就是棋盘的第三行,当前放置了皇后的个数是2, {i,j}第一行皇后放置的位置i, 第二行皇后放置的位置是j
f(4, 3, 3, {i, j, k}): 棋盘大小是4,准备放皇后的行是3,也就是棋盘的第四行,当前放置了皇后的个数是3, {i,j,k}第一行皇后放置的位置i, 第二行皇后放置的位置是j,第三行皇后放置的位置是k

第三行放置的皇后是可以剪枝的, 比如图中红色虚线对应的节点的第一行放置的皇后,第二行放置的皇后与第三行放置的皇后不满足八皇后问题

flowchart LR
    A["f(4, 0, 0, {})"] --> |放在第0列|B["f(4, 1, 1, {0})"]
    A["f(4, 0, 0, {})"] --> |放在第1列|C["f(4, 1, 1, {1})"]
    A["f(4, 0, 0, {})"] --> |放在第2列|D["f(4, 1, 1, {2})"]
    A["f(4, 0, 0, {})"] --> |放在第3列|E["f(4, 1, 1, {3})"]
    
    classDef Pruning fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
    B["f(4, 1, 1, {0})"] --> |放在第0列|F["f(4, 2, 2, {0, 0})"]
    B["f(4, 1, 1, {0})"] --> |放在第1列|G["f(4, 2, 2, {0, 1})"]
    B["f(4, 1, 1, {0})"] --> |放在第2列|H["f(4, 2, 2, {0, 2})"]
    B["f(4, 1, 1, {0})"] --> |放在第3列|I["f(4, 2, 2, {0, 3})"]
    
    class F,G Pruning
    
    C["f(4, 1, 1, {1})"] --> |放在第0列|J["f(4, 2, 2, {1, 0})"]
    C["f(4, 1, 1, {1})"] --> |放在第1列|K["f(4, 2, 2, {1, 1})"]
    C["f(4, 1, 1, {1})"] --> |放在第2列|L["f(4, 2, 2, {1, 2})"]
    C["f(4, 1, 1, {1})"] --> |放在第3列|M["f(4, 2, 2, {1, 3})"]
    
     class J,K,L Pruning
     
    D["f(4, 1, 1, {2})"] --> |放在第0列|N["f(4, 2, 2, {2, 0})"]
    D["f(4, 1, 1, {2})"] --> |放在第1列|O["f(4, 2, 2, {2, 1})"]
    D["f(4, 1, 1, {2})"] --> |放在第2列|P["f(4, 2, 2, {2, 2})"]
    D["f(4, 1, 1, {2})"] --> |放在第3列|Q["f(4, 2, 2, {2, 3})"]
    
     class O,P,Q Pruning
     
    E["f(4, 1, 1, {3})"] --> |放在第0列|R["f(4, 2, 2, {3, 0})"]
    E["f(4, 1, 1, {3})"] --> |放在第1列|S["f(4, 2, 2, {3, 1})"]
    E["f(4, 1, 1, {3})"] --> |放在第2列|T["f(4, 2, 2, {3, 2})"]
    E["f(4, 1, 1, {3})"] --> |放在第3列|U["f(4, 2, 2, {3, 3})"]
    
     class T,U Pruning
     
    H["f(4, 2, 2, {0, 2})"] --> |放在第0列|V["f(4, 3, 3, {0, 2, 0})"]
    H["f(4, 2, 2, {0, 2})"] --> |放在第1列|W["f(4, 3, 3, {0, 2, 1})"]
    H["f(4, 2, 2, {0, 2})"] --> |放在第2列|X["f(4, 3, 3, {0, 2, 2})"]
    H["f(4, 2, 2, {0, 2})"] --> |放在第3列|Y["f(4, 3, 3, {0, 2, 3})"]
    
    class V,W,X,Y Pruning
    
    I["f(4, 2, 2, {0, 3})"] --> |放在第0列|Z["f(4, 3, 3, {0, 3, 0})"]
    I["f(4, 2, 2, {0, 3})"] --> |放在第1列|AA["f(4, 3, 3, {0, 3, 1})"]
    I["f(4, 2, 2, {0, 3})"] --> |放在第2列|AB["f(4, 3, 3, {0, 3, 2})"]
    I["f(4, 2, 2, {0, 3})"] --> |放在第3列|AC["f(4, 3, 3, {0, 3, 3})"]
    
    class Z,AB,AC Pruning
    
    M["f(4, 2, 2, {1, 3})"] --> |放在第0列|AD["f(4, 3, 3, {1, 3, 0})"]
    M["f(4, 2, 2, {1, 3})"] --> |放在第1列|AE["f(4, 3, 3, {1, 3, 1})"]
    M["f(4, 2, 2, {1, 3})"] --> |放在第2列|AF["f(4, 3, 3, {1, 3, 2})"]
    M["f(4, 2, 2, {1, 3})"] --> |放在第3列|AG["f(4, 3, 3, {1, 3, 3})"]
    class AE,AF,AG Pruning
    
    N["f(4, 2, 2, {2, 0})"] --> |放在第0列|AH["f(4, 3, 3, {2, 0, 0})"]
    N["f(4, 2, 2, {2, 0})"] --> |放在第1列|AI["f(4, 3, 3, {2, 0, 1})"]
    N["f(4, 2, 2, {2, 0})"] --> |放在第2列|AJ["f(4, 3, 3, {2, 0, 2})"]
    N["f(4, 2, 2, {2, 0})"] --> |放在第3列|AK["f(4, 3, 3, {2, 0, 3})"]  
    class AH,AI,AJ Pruning
    
    R["f(4, 2, 2, {3, 0})"]--> |放在第0列|AL["f(4, 3, 3, {3, 0, 0})"]
    R["f(4, 2, 2, {3, 0})"]--> |放在第1列|AM["f(4, 3, 3, {3, 0, 1})"]
    R["f(4, 2, 2, {3, 0})"]--> |放在第2列|AN["f(4, 3, 3, {3, 0, 2})"]
    R["f(4, 2, 2, {3, 0})"]--> |放在第3列|AO["f(4, 3, 3, {3, 0, 3})"]    
    class AL,AM,AO Pruning
    
    S["f(4, 2, 2, {3, 1})"]--> |放在第0列|AP["f(4, 3, 3, {3, 1, 0})"]
    S["f(4, 2, 2, {3, 1})"]--> |放在第1列|AQ["f(4, 3, 3, {3, 1, 1})"]
    S["f(4, 2, 2, {3, 1})"]--> |放在第2列|AR["f(4, 3, 3, {3, 1, 2})"]
    S["f(4, 2, 2, {3, 1})"]--> |放在第3列|AS["f(4, 3, 3, {3, 1, 3})"] 
    class AP,AQ,AR Pruning

树的第五层:
在第四行放置棋子,在第四行可以放置的棋子位置是0,1,2,3
f(4, 0, 0, {}): 棋盘大小是4,准备放皇后的行是0,也就是棋盘的第一行,当前放置了皇后的个数是0, {}空棋盘
f(4, 1, 1, {i}): 棋盘大小是4,准备放皇后的行是1,也就是棋盘的第二行,当前放置了皇后的个数是1, {i},第一行皇后放置的位置i
f(4, 2, 2, {i, j}): 棋盘大小是4,准备放皇后的行是2,也就是棋盘的第三行,当前放置了皇后的个数是2, {i,j}第一行皇后放置的位置i, 第二行皇后放置的位置是j
f(4, 3, 3, {i, j, k}): 棋盘大小是4,准备放皇后的行是3,也就是棋盘的第四行,当前放置了皇后的个数是3, {i,j,k}第一行皇后放置的位置i, 第二行皇后放置的位置是j,第三行皇后放置的位置是k
f(4, 4, 4, {i, j, k, l}): 棋盘大小是4,准备放皇后的行是4,也就是棋盘的第五行,当前放置了皇后的个数是4, {i,j,k,l}第一行皇后放置的位置i, 第二行皇后放置的位置是j,第三行皇后放置的位置是k, 第四行皇后放置的位置是l

第四行放置的皇后是可以剪枝的, 比如图中红色虚线对应的节点的第一行放置的皇后,第二行放置的皇后,第三行放置的皇后与第四行放置的皇后不满足八皇后问题

flowchart LR
    A["f(4, 0, 0, {})"] --> |放在第0列|B["f(4, 1, 1, {0})"]
    A["f(4, 0, 0, {})"] --> |放在第1列|C["f(4, 1, 1, {1})"]
    A["f(4, 0, 0, {})"] --> |放在第2列|D["f(4, 1, 1, {2})"]
    A["f(4, 0, 0, {})"] --> |放在第3列|E["f(4, 1, 1, {3})"]
    
    classDef Pruning fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
    B["f(4, 1, 1, {0})"] --> |放在第0列|F["f(4, 2, 2, {0, 0})"]
    B["f(4, 1, 1, {0})"] --> |放在第1列|G["f(4, 2, 2, {0, 1})"]
    B["f(4, 1, 1, {0})"] --> |放在第2列|H["f(4, 2, 2, {0, 2})"]
    B["f(4, 1, 1, {0})"] --> |放在第3列|I["f(4, 2, 2, {0, 3})"]
    
    class F,G Pruning
    
    C["f(4, 1, 1, {1})"] --> |放在第0列|J["f(4, 2, 2, {1, 0})"]
    C["f(4, 1, 1, {1})"] --> |放在第1列|K["f(4, 2, 2, {1, 1})"]
    C["f(4, 1, 1, {1})"] --> |放在第2列|L["f(4, 2, 2, {1, 2})"]
    C["f(4, 1, 1, {1})"] --> |放在第3列|M["f(4, 2, 2, {1, 3})"]
    
     class J,K,L Pruning
     
    D["f(4, 1, 1, {2})"] --> |放在第0列|N["f(4, 2, 2, {2, 0})"]
    D["f(4, 1, 1, {2})"] --> |放在第1列|O["f(4, 2, 2, {2, 1})"]
    D["f(4, 1, 1, {2})"] --> |放在第2列|P["f(4, 2, 2, {2, 2})"]
    D["f(4, 1, 1, {2})"] --> |放在第3列|Q["f(4, 2, 2, {2, 3})"]
    
     class O,P,Q Pruning
     
    E["f(4, 1, 1, {3})"] --> |放在第0列|R["f(4, 2, 2, {3, 0})"]
    E["f(4, 1, 1, {3})"] --> |放在第1列|S["f(4, 2, 2, {3, 1})"]
    E["f(4, 1, 1, {3})"] --> |放在第2列|T["f(4, 2, 2, {3, 2})"]
    E["f(4, 1, 1, {3})"] --> |放在第3列|U["f(4, 2, 2, {3, 3})"]
    
     class T,U Pruning
     
    H["f(4, 2, 2, {0, 2})"] --> |放在第0列|V["f(4, 3, 3, {0, 2, 0})"]
    H["f(4, 2, 2, {0, 2})"] --> |放在第1列|W["f(4, 3, 3, {0, 2, 1})"]
    H["f(4, 2, 2, {0, 2})"] --> |放在第2列|X["f(4, 3, 3, {0, 2, 2})"]
    H["f(4, 2, 2, {0, 2})"] --> |放在第3列|Y["f(4, 3, 3, {0, 2, 3})"]
    
    class V,W,X,Y Pruning
    
    I["f(4, 2, 2, {0, 3})"] --> |放在第0列|Z["f(4, 3, 3, {0, 3, 0})"]
    I["f(4, 2, 2, {0, 3})"] --> |放在第1列|AA["f(4, 3, 3, {0, 3, 1})"]
    I["f(4, 2, 2, {0, 3})"] --> |放在第2列|AB["f(4, 3, 3, {0, 3, 2})"]
    I["f(4, 2, 2, {0, 3})"] --> |放在第3列|AC["f(4, 3, 3, {0, 3, 3})"]
    
    class Z,AB,AC Pruning
    
    M["f(4, 2, 2, {1, 3})"] --> |放在第0列|AD["f(4, 3, 3, {1, 3, 0})"]
    M["f(4, 2, 2, {1, 3})"] --> |放在第1列|AE["f(4, 3, 3, {1, 3, 1})"]
    M["f(4, 2, 2, {1, 3})"] --> |放在第2列|AF["f(4, 3, 3, {1, 3, 2})"]
    M["f(4, 2, 2, {1, 3})"] --> |放在第3列|AG["f(4, 3, 3, {1, 3, 3})"]
    
    class AE,AF,AG Pruning
    
    N["f(4, 2, 2, {2, 0})"] --> |放在第0列|AH["f(4, 3, 3, {2, 0, 0})"]
    N["f(4, 2, 2, {2, 0})"] --> |放在第1列|AI["f(4, 3, 3, {2, 0, 1})"]
    N["f(4, 2, 2, {2, 0})"] --> |放在第2列|AJ["f(4, 3, 3, {2, 0, 2})"]
    N["f(4, 2, 2, {2, 0})"] --> |放在第3列|AK["f(4, 3, 3, {2, 0, 3})"]
    
    class AH,AI,AJ Pruning
    
    R["f(4, 2, 2, {3, 0})"]--> |放在第0列|AL["f(4, 3, 3, {3, 0, 0})"]
    R["f(4, 2, 2, {3, 0})"]--> |放在第1列|AM["f(4, 3, 3, {3, 0, 1})"]
    R["f(4, 2, 2, {3, 0})"]--> |放在第2列|AN["f(4, 3, 3, {3, 0, 2})"]
    R["f(4, 2, 2, {3, 0})"]--> |放在第3列|AO["f(4, 3, 3, {3, 0, 3})"]
    
    class AL,AM,AO Pruning
    
    S["f(4, 2, 2, {3, 1})"]--> |放在第0列|AP["f(4, 3, 3, {3, 1, 0})"]
    S["f(4, 2, 2, {3, 1})"]--> |放在第1列|AQ["f(4, 3, 3, {3, 1, 1})"]
    S["f(4, 2, 2, {3, 1})"]--> |放在第2列|AR["f(4, 3, 3, {3, 1, 2})"]
    S["f(4, 2, 2, {3, 1})"]--> |放在第3列|AS["f(4, 3, 3, {3, 1, 3})"]
    
    class AP,AQ,AR,AS Pruning
    
    AA["f(4, 3, 3, {0, 3, 1})"] --> |放在第0列|AT["f(4, 4, 4, {0, 3, 1, 0})"]
    AA["f(4, 3, 3, {0, 3, 1})"] --> |放在第1列|AU["f(4, 4, 4, {0, 3, 1, 1})"]
    AA["f(4, 3, 3, {0, 3, 1})"] --> |放在第2列|AV["f(4, 4, 4, {0, 3, 1, 2})"]
    AA["f(4, 3, 3, {0, 3, 1})"] --> |放在第3列|AW["f(4, 4, 4, {0, 3, 1, 3})"]
    class AT,AU,AV,AW Pruning
    
    AD["f(4, 3, 3, {1, 3, 0})"] --> |放在第0列|AX["f(4, 4, 4, {1, 3, 0, 0})"]
    AD["f(4, 3, 3, {1, 3, 0})"] --> |放在第1列|AY["f(4, 4, 4, {1, 3, 0, 1})"]
    AD["f(4, 3, 3, {1, 3, 0})"] --> |放在第2列|AZ["f(4, 4, 4, {1, 3, 0, 2})"]
    AD["f(4, 3, 3, {1, 3, 0})"] --> |放在第3列|BA["f(4, 4, 4, {1, 3, 0, 3})"]
    class AX,AY,AZ Pruning
    
    AK["f(4, 3, 3, {2, 0, 3})"] --> |放在第0列|BB["f(4, 4, 4, {2, 0, 3, 0})"]
    AK["f(4, 3, 3, {2, 0, 3})"] --> |放在第1列|BC["f(4, 4, 4, {2, 0, 3, 1})"]
    AK["f(4, 3, 3, {2, 0, 3})"] --> |放在第2列|BD["f(4, 4, 4, {2, 0, 3, 2})"]
    AK["f(4, 3, 3, {2, 0, 3})"] --> |放在第3列|BE["f(4, 4, 4, {2, 0, 3, 3})"]
    class BB,BD,BE Pruning
    
    AN["f(4, 3, 3, {3, 0, 2})"] --> |放在第0列|BF["f(4, 4, 4, {3, 0, 2, 0})"]
    AN["f(4, 3, 3, {3, 0, 2})"] --> |放在第0列|BG["f(4, 4, 4, {3, 0, 2, 1})"]
    AN["f(4, 3, 3, {3, 0, 2})"] --> |放在第0列|BH["f(4, 4, 4, {3, 0, 2, 2})"]
    AN["f(4, 3, 3, {3, 0, 2})"] --> |放在第0列|BI["f(4, 4, 4, {3, 0, 2, 3})"]
    class BF,BG,BH,BI Pruning
     

该问题的解就在树的第五层节点上,每个第五层的非剪枝节点就是问题的解。

八皇后问题的代码如何写

利用递归来实现八皇后问题,递归首先找到终止条件,在找划分成子问题的方法。

终止条件:所有的行都放置了皇后,也就是当待放置皇后的行编号与棋盘大小相等时,就是有一个成功的放置皇后的方案。

划分成子问题的方法:在当前行放置皇后后,剩下没有放置皇后的行就减少了,直到没有行需要放置皇后,就结束了。在当前行放置皇后的时候,行对应的列都可以放置皇后,在某一列能否放置皇后,要看已经放置皇后的行在当前列或者左右对角线是否没有放置皇后,如果没有,就可以在这个列放置皇后,然后继续放置下一行。

八皇后问题的完整代码

bool isValid(int size, vector<int> &result, int row, int column) {
    int leftUp = column - 1;
    int rightUp = column + 1;

    // 遍历当前行之上所有放置了皇后的行,看在当前列放置皇后是否合适
    for (int r = row - 1; r >= 0; --r)
    {
        // 上面的行对应的列放有棋子
        if(result[r] == column)
        {
            return false;
        }

        // 上面的行对应的左斜线上的列放有棋子
        if (leftUp >= 0 && result[r] == leftUp)
        {
            return false;
        }

        // 上面的行对应的右斜线上的列放有棋子
        if (rightUp < size && result[r] == rightUp )
        {
            return false;
        }

        
        --leftUp;
        ++rightUp;
    }

    return true;
    
}
void queen(int size, int row, int &count, vector<int> &result, list<vector<int> > &results) {
    // 棋盘大小和准备放皇后的行编号相等,说明皇后都放到棋盘上了
    if (size == row) {
        // 统计可以成功在棋盘上放置皇后的方法的次数
        ++count;
        // 记录成功摆放皇后的方法
        results.push_back(vector<int>(result));
        return;
    }
    // 遍历当前行对应的所有列
    for (size_t column = 0; column < size; ++column)
    {
        // 判断皇后是否能放到当前行对应的列上
        if (isValid(size, result, row, column))
        {
            // 能放置,记录下列编号
            result[row] = column;
            // 放置下一行的皇后
            queen(size, row + 1, count, result, results);
        }
    }

}

旅行者问题

某售货员要到若干城市去推销商品已知各城市之间的路程他要选定一条从驻地1出发经过每个城市一次最后回到驻地的路线使总的路程最小。 image.png

旅行者问题的解空间

旅行者问题是从起始点1出发,经过2,3,4号城市各一次,最后回到起始点,其解空间是这样的:
树的第一层:
1号城市作为起始点,首先经过1号城市,没有经过的城市就是2,3,4。
f(1, 1, {2, 3, 4}): 表示起点是编号1的城市,当前旅行的是编号1的城市,没有旅行过的城市集合是{2,3,4}

flowchart TD
    A["f(1, 1, {2, 3, 4})"]
    

树的第二层:
从1号城市出发,可以先经过2号城市,没用经过的城市是3,4。
f(1, 2, {3, 4}): 表示起点是编号1的城市,当前旅行的是编号2的城市,没有旅行过的城市集合是{3,4}
从1号城市出发,也可以先经过3号城市,没有经过的城市就是2,4。
f(1, 3, {2, 4}): 表示起点是编号1的城市,当前旅行的是编号3的城市,没有旅行过的城市集合是{2,4}
从1号城市出发,也可以先经过4号城市,没有经过的城市就是2,3。
f(1, 4, {2, 3}): 表示起点是编号1的城市,当前旅行的是编号4的城市,没有旅行过的城市集合是{2,3}
...

flowchart TD
    A["f(1, 1, {2, 3, 4})"] --> B["f(1, 2, {3, 4})"]
    A["f(1, 1, {2, 3, 4})"] --> C["f(1, 3, {2, 4})"]
    A["f(1, 1, {2, 3, 4})"] --> D["f(1, 4, {2, 3})"]
    

树的第三层:
如果先经过2号城市,接着可以再经过3号城市,没有经过的城市就是4。
f(1, 3, {4}): 表示起点是编号1的城市,当前旅行的是编号3的城市,没有旅行过的城市集合是{4}
如果先经过2号城市,接着可以再经过4号城市,没有经过的城市就是3。
f(1, 4, {3}): 表示起点是编号1的城市,当前旅行的是编号4的城市,没有旅行过的城市集合是{3}

如果先经过3号城市,接着可以再经过2号城市,没有经过的城市就是4。
f(1, 2, {4}): 表示起点是编号1的城市,当前旅行的是编号2的城市,没有旅行过的城市集合是{4}
如果先经过3号城市,接着可以再经过4号城市,没有经过的城市就是2。
f(1, 4, {2}): 表示起点是编号1的城市,当前旅行的是编号4的城市,没有旅行过的城市集合是{2}

如果先经过4号城市,接着可以再经过2号城市,没有经过的城市就是3。
f(1, 2, {3}): 表示起点是编号1的城市,当前旅行的是编号2的城市,没有旅行过的城市集合是{3}
如果先经过4号城市,接着可以再经过3号城市,没有经过的城市就是2。
f(1, 3, {2}): 表示起点是编号1的城市,当前旅行的是编号3的城市,没有旅行过的城市集合是{2}

flowchart TD
    A["f(1, 1, {2, 3, 4})"] --> B["f(1, 2, {3, 4})"]
    A["f(1, 1, {2, 3, 4})"] --> C["f(1, 3, {2, 4})"]
    A["f(1, 1, {2, 3, 4})"] --> D["f(1, 4, {2, 3})"]
    
    B["f(1, 2, {3, 4})"] --> E["f(1, 3, {4})"]
    B["f(1, 2, {3, 4})"] --> F["f(1, 4, {3})"]
    
    C["f(1, 3, {2, 4})"] --> G["f(1, 2, {4})"]
    C["f(3, {2, 4})"] --> H["f(1, 4, {2})"]
    
    D["f(1, 4, {2, 3})"] --> I["f(1, 2, {3})"]
    D["f(1, 4, {2, 3})"] --> J["f(1, 3, {2})"]
    

树的第四层:
如果先经过3号城市,接着可以再经过4号城市,没有经过的城市就是{}。
f(1, 4, {}): 表示起点是编号1的城市,当前旅行的是编号4的城市,没有旅行过的城市集合是{}

如果先经过4号城市,接着可以再经过3号城市,没有经过的城市就是{}。
f(1, 3, {}): 表示起点是编号1的城市,当前旅行的是编号3的城市,没有旅行过的城市集合是{}

如果先经过2号城市,接着可以再经过4号城市,没有经过的城市就是{}。
f(1, 4, {}): 表示起点是编号1的城市,当前旅行的是编号4的城市,没有旅行过的城市集合是{}

如果先经过4号城市,接着可以再经过2号城市,没有经过的城市就是{}。
f(1, 2, {}): 表示起点是编号1的城市,当前旅行的是编号2的城市,没有旅行过的城市集合是{}

如果先经过2号城市,接着可以再经过3号城市,没有经过的城市就是{}。
f(1, 3, {}): 表示起点是编号1的城市,当前旅行的是编号3的城市,没有旅行过的城市集合是{}

如果先经过3号城市,接着可以再经过2号城市,没有经过的城市就是{}。
f(1, 2, {}): 表示起点是编号1的城市,当前旅行的是编号2的城市,没有旅行过的城市集合是{}

flowchart TD
    A["f(1, 1, {2, 3, 4})"] --> B["f(1, 2, {3, 4})"]
    A["f(1, 1, {2, 3, 4})"] --> C["f(1, 3, {2, 4})"]
    A["f(1, 1, {2, 3, 4})"] --> D["f(1, 4, {2, 3})"]
    
    B["f(1, 2, {3, 4})"] --> E["f(1, 3, {4})"]
    B["f(1, 2, {3, 4})"] --> F["f(1, 4, {3})"]
    
    C["f(1, 3, {2, 4})"] --> G["f(1, 2, {4})"]
    C["f(3, {2, 4})"] --> H["f(1, 4, {2})"]
    
    D["f(1, 4, {2, 3})"] --> I["f(1, 2, {3})"]
    D["f(1, 4, {2, 3})"] --> J["f(1, 3, {2})"]
    
    E["f(1, 3, {4})"] --> K["f(1, 4, {})"]
    F["f(1, 4, {3})"] --> L["f(1, 3, {})"]
    
    G["f(1, 2, {4})"] --> M["f(1, 4, {})"]
    H["f(1, 4, {2})"] --> N["f(1, 2, {})"]
    
    I["f(1, 2, {3})"] --> O["f(1, 3, {})"]
    J["f(1, 3, {2})"] --> P["f(1, 2, {})"]
    

旅行者问题的解就在这个解空间的叶子节点上,因为每到一个叶子结点,所有的城市都经过了一遍,可以回到起点城市。

旅行者问题的代码如何写

算法思路:
算法是要求出最短的路程长度,同时有哪些路径对应最短的路程长度。

利用递归来实现旅行者问题,递归首先找到终止条件,在找划分成子问题的方法。

终止条件:所有的城市都经过了就得到一个路径和路程。这个时候让路程和目前最短路程进行比较,如果比最短路程小,说明它是一个跟接近最终解的解,清理之前记录的路径列表,把这个路径加入到路径列表中。如果跟最短路程相等,说明它是一个可能的解,记录下这个路径到路径列表里面。

划分成子问题的方法:通过当前城市,选择下一个要通过的城市,就可以减少要经过城市的个数,减少问题的规模。在通过了当前城市,选择下一个城市只能从没有经过的城市里面选,选择后将从当前城市到选择城市的距离加到路程中,并将当前城市加到路径数组中,z再将当前城市从未遍历城市中去掉,然后把选择的城市作为当前城市继续递归调用。

旅行者问题的完整代码

struct BacktrackingState {

    int curDistance; // 经过当前城市后路程的长度
    int travelledCityCount; //当前经过的城市的数量
    set<int> notTravelledCitySet; //没有经过城市序号的集合

    BacktrackingState() {
        curDistance = 0;
        travelledCityCount = 0;
    }
};
// cityMap: 从一个城市到另一个城市距离的矩阵,横轴和纵轴都是城市的编号
// srcCityIndex: 起点城市的编号
// curCityIndex: 当前旅行城市的编号
// cityRoute: 记录旅行路径
// cityRoutes: 记录目前求得的所有最短路程对应的旅行路径的列表
// shortestDistance:目前求得的最短距离
void getShortestDistance(vector<int* > &cityMap, int srcCityIndex, int curCityIndex,
                         BacktrackingState backtrackingState, 
                          vector<int> &cityRoute, list<vector<int> > &cityRoutes, 
                         int &shortestDistance) {

    // 当每个城市都旅行过了,就可以回到起点城市
    if (backtrackingState.notTravelledCitySet.empty()) {
        
        // 将回到起点城市的路程长度计算到旅行完所有城市的路程长度得到总的路程长度
        backtrackingState.curDistance += cityMap[curCityIndex][srcCityIndex];

        // 如果这次搜索到的总路程小于之前记录的最小总路程,
        // 清除调之前记录的最短总路程的城市顺序记录集合,
        // 将当前旅行过的城市顺序记录添加到最短总路程的城市顺序记录集合
        if (backtrackingState.curDistance < shortestDistance) {
            shortestDistance = backtrackingState.curDistance;
            cityRoutes.clear();
            cityRoutes.push_back(vector<int>(cityRoute));
        }
        // 如果这次搜索到的总路程等于之前记录的最小总路程,
        // 将当前旅行过的城市顺序记录添加到最短总路程的城市顺序记录集合
        else if (backtrackingState.curDistance == shortestDistance) {
            cityRoutes.push_back(vector<int>(cityRoute));
        }

        return;
    }

    // 遍历未旅行过的城市
    for (set<int>::iterator i = backtrackingState.notTravelledCitySet.begin();
         i != backtrackingState.notTravelledCitySet.end(); ++i)
    {
        BacktrackingState nextState = backtrackingState;
        cityRoute[nextState.travelledCityCount++] = *i; //记录下旅行过的城市
        nextState.notTravelledCitySet.erase(*i);        //移除旅行过的城市
        nextState.curDistance += cityMap[curCityIndex][*i]; // 将从当前城市旅行到该城市的路程添加到总路程
        
        //继续向下搜索
        getShortestDistance(cityMap, srcCityIndex, *i, nextState, cityRoute, cityRoutes, shortestDistance);
        
    }
    
    return;
    
}

参考文献

  1. 回溯算法_百度百科 (baidu.com) 2.《算法图解: 像小说一样有趣的算法入门书》- 作者:[美] Aditya Bhargava

  2. 39 | 回溯算法:从电影《蝴蝶效应》中学习回溯算法的核心思想 (geekbang.org)