算法设计与分析


回溯法

计算机学院    张腾

tengzhang@hust.edu.cn

课程大纲

算法设计与分析分治法动态规划贪心法回溯法分支限界法迭代改进n-皇后问题哈密顿回路子集和数问题

回溯法

有些问题要求在相对于输入规模呈指数增长的域中

  • 寻找特定元素
  • 最优化某个目标函数

穷举检查所有的元素?时间复杂度:指数级

回溯法:对穷举法的改进带剪枝的搜索

基本思路:将解表示成元组的形式,依次构建其分量,当构建完某个分量后发现之后的分量无论怎么构建都不可能得到一个解,就提早回头 (剪枝),谨防一条道走到黑

n-皇后问题

在 n × n 的棋盘上放置 n 个皇后,使得任意两个皇后都不能互相攻击:不在同一行、同一列和同一条斜角线上

1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
1 👑 1 👑
2 👑 2 👑
3 👑 3 👑
4 👑 4 👑
5 👑 5 👑
6 👑 6 👑
7 👑 7 👑
8 👑 8 👑
4-皇后问题初试

g n1 ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ n2 👑 ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ n1->n2 n3 ‌‌ 👑 ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ n1->n3 n21 ‌‌ 👑 ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ n1->n21 n22 ‌‌ 👑 ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ n1->n22 n5 n2->n5 n6 👑 ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ 👑 ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ n2->n6 n7 👑 ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ 👑 ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ n2->n7 n8 n3->n8 n10 n3->n10 n11 ‌‌ 👑 ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ 👑 ‌‌ ‌‌ ‌‌ ‌‌ n3->n11 n13 n6->n13 n15 n6->n15 n17 👑 ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ 👑 ‌‌ 👑 ‌‌ ‌‌ ‌‌ n7->n17 n18 n7->n18 n20 ‌‌ 👑 ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ 👑 👑 ‌‌ ‌‌ ‌‌ n11->n20 n24 n11->n24 n28 n17->n28 n32 ‌‌ 👑 ‌‌ ‌‌ ‌‌ ‌‌ ‌‌ 👑 👑 ‌‌ 👑 ‌‌ n20->n32

n-皇后问题

将皇后编号为$1, \ldots, n$,不失一般性,约定皇后$i$放在第$i$

解的表示$n$元组$(x_1, \ldots, x_n)$,其中$x_i$为皇后$i$的列号

显式约束条件$x_i \in \{1, \ldots, n\}$

解空间:所有可能的$n$元组,共有$n^n$

隐式约束条件:没有两个$x_i$相同,没有两个皇后在同一条斜角线上即对任意$i \ne j$$|x_i - x_j| \ne |i - j|$

由隐式约束条件知解只可能是$\{1, \ldots, n\}$的置换,最多有$n!$

n-皇后问题

1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
1 👑 1 👑
2 👑 2 👑
3 👑 3 👑
4 👑 4 👑
5 👑 5 👑
6 👑 6 👑
7 👑 7 👑
8 👑 8 👑

两个解分别是$(4, 6, 8, 2, 7, 1, 3, 5)$$(3, 5, 2, 8, 1, 7, 4, 6)$

注意右棋盘是左棋盘逆时针旋转 90 度得到

解空间的组织

用树结构组织解空间,形成状态空间树

要求:每个解由树中某个结点表示

遍历状态空间树即可检查所有解,不遗漏不重复

  • 深度优先搜索 (depth first search, DFS)
  • 宽度优先搜索 (breath first search, BFS)
  • 深度检索 (D-Search, BFS + 栈)

实际实现时,可将遍历整棵树改成生长出整棵树

4-皇后 状态空间树

g 1 1 2 2 1->2 1 18 18 1->18 2 34 34 1->34 3 50 50 1->50 4 3 3 2->3 2 8 8 2->8 3 13 13 2->13 4 19 19 18->19 1 24 24 18->24 3 29 29 18->29 4 35 35 34->35 1 40 40 34->40 2 45 45 34->45 4 51 51 50->51 1 56 56 50->56 2 61 61 50->61 3 4 4 3->4 3 6 6 3->6 4 9 9 8->9 2 11 11 8->11 4 14 14 13->14 2 16 16 13->16 3 5 5 4->5 4 7 7 6->7 3 10 10 9->10 4 12 12 11->12 2 15 15 14->15 3 17 17 16->17 2 20 20 19->20 3 22 22 19->22 4 25 25 24->25 1 27 27 24->27 4 30 30 29->30 1 32 32 29->32 3 21 21 20->21 4 23 23 22->23 3 26 26 25->26 4 28 28 27->28 1 31 31 30->31 3 33 33 32->33 1 36 36 35->36 2 38 38 35->38 4 41 41 40->41 1 43 43 40->43 4 46 46 45->46 1 48 48 45->48 2 37 37 36->37 4 39 39 38->39 2 42 42 41->42 4 44 44 43->44 1 47 47 46->47 2 49 49 48->49 1 52 52 51->52 2 54 54 51->54 3 57 57 56->57 1 59 59 56->59 3 62 62 61->62 1 64 64 61->64 2 53 53 52->53 3 55 55 54->55 2 58 58 57->58 3 60 60 59->60 1 63 63 62->63 2 65 65 64->65 1

  • 每个结点对应一个棋盘状态,根结点为初始状态:空棋盘
  • $i-1$层到第$i$层结点的边上数字$j$表示将皇后$i$放到第$j$
  • $24$个叶结点对应$4! = 24$个解,其到根结点的路径为该解对应的元组
状态空间树 剪枝

限界函数:对应隐式约束,用来杀死不满足的结点,即剪枝

状态空间树生长时的三类结点

  • 活结点:自己已经生成,但子结点还没全部生成并且有待生成
  • E-结点:当前正在生成其子结点的活结点
  • 死结点:被限界函数杀死或者其子结点已全部生成的结点

状态空间树生长的两种策略:

  • 回溯深度优先,E-结点 R 每生成一个新的儿子 C 时,C 就变成了新的 E-结点,当完全检测了子树 C 之后,R 结点再次成为 E-结点
  • 分支限界宽度优先,在生成当前 E-结点的全部子结点后再生成其它活结点的子结点
4-皇后问题 回溯求解

解的表示$4$元组$(x_1, \ldots, x_4)$,其中$x_i$为皇后$i$的列号

限界函数:设$(x_1, \ldots, x_{i-1})$是根结点到当前 E-结点的路径,那么$x_i$满足限界函数当且仅当其使得$(x_1, \ldots, x_{i-1}, x_i)$表示没有两个皇后可相互攻击的棋盘状态

初始状态:根结点 1,空棋盘

结点生成:依次摆放各个皇后

4-皇后问题 回溯求解

1 2 3 4 1 2 3 4 1 2 3 4
1 1 👑 1 👑
2 2 2 👑
3 3 3
4 4 4

g n1 1 n2 1 n3 2 n2->n3 1 n4 1 n5 2 n4->n5 1 n6 3 n5->n6 2

结点 3 的两个皇后在同一个斜角线

杀死结点 3,返回结点 2 继续扩展

4-皇后问题 回溯求解

1 2 3 4 1 2 3 4 1 2 3 4
1 👑 1 👑 1 👑
2 👑 2 👑 2 👑
3 3 👑 3 👑
4 4 4

g n1 1 n2 2 n1->n2 1 n3 3 n2->n3 2 n4 8 n2->n4 3 1 1 2 2 n5 1 n6 2 n5->n6 1 n7 3 n6->n7 2 n8 8 n6->n8 3 n9 9 n8->n9 2 3 3 4 4 n10 1 n11 2 n10->n11 1 n12 3 n11->n12 2 n13 8 n11->n13 3 n14 9 n13->n14 2 n15 11 n13->n15 4

4-皇后问题 回溯求解

1 2 3 4 1 2 3 4 1 2 3 4
1 👑 1 👑 1 👑
2 👑 2 👑 2 👑
3 3 👑 3 👑
4 4 4 👑

g n10 1 n11 2 n10->n11 1 n12 3 n11->n12 2 n13 8 n11->n13 3 n16 13 n11->n16 4 n14 9 n13->n14 2 n15 11 n13->n15 4 n1 1 n2 2 n1->n2 1 n3 3 n2->n3 2 n4 8 n2->n4 3 n7 13 n2->n7 4 n5 9 n4->n5 2 n6 11 n4->n6 4 n8 14 n7->n8 2 1 1 2 2 1->2 1 3 3 2->3 2 8 8 2->8 3 13 13 2->13 4 9 9 8->9 2 11 11 8->11 4 14 14 13->14 2 15 15 14->15 3

4-皇后问题 回溯求解

1 2 3 4 1 2 3 4 1 2 3 4
1 👑 1 👑 1 👑
2 👑 2 2 👑
3 👑 3 3
4 4 4

g n10 1 n11 2 n10->n11 1 n12 3 n11->n12 2 n13 8 n11->n13 3 n16 13 n11->n16 4 n14 9 n13->n14 2 n15 11 n13->n15 4 n17 14 n16->n17 2 n19 16 n16->n19 3 n18 15 n17->n18 3 n1 1 n2 2 n1->n2 1 n20 18 n1->n20 2 n3 3 n2->n3 2 n4 8 n2->n4 3 n7 13 n2->n7 4 n5 9 n4->n5 2 n6 11 n4->n6 4 n8 14 n7->n8 2 n0 16 n7->n0 3 n9 15 n8->n9 3 1 1 2 2 1->2 1 18 18 1->18 2 3 3 2->3 2 8 8 2->8 3 13 13 2->13 4 9 9 8->9 2 11 11 8->11 4 14 14 13->14 2 16 16 13->16 3 15 15 14->15 3 19 19 18->19 1

4-皇后问题 回溯求解

1 2 3 4 1 2 3 4 1 2 3 4
1 👑 1 👑 1 👑
2 👑 2 👑 2 👑
3 3 3 👑
4 4 4

g n10 1 n11 2 n10->n11 1 n21 18 n10->n21 2 n12 3 n11->n12 2 n13 8 n11->n13 3 n16 13 n11->n16 4 n14 9 n13->n14 2 n15 11 n13->n15 4 n17 14 n16->n17 2 n19 16 n16->n19 3 n18 15 n17->n18 3 n22 19 n21->n22 1 n23 24 n21->n23 3 n1 1 n2 2 n1->n2 1 n20 18 n1->n20 2 n3 3 n2->n3 2 n4 8 n2->n4 3 n7 13 n2->n7 4 n5 9 n4->n5 2 n6 11 n4->n6 4 n8 14 n7->n8 2 n0 16 n7->n0 3 n9 15 n8->n9 3 n24 19 n20->n24 1 n25 24 n20->n25 3 n26 29 n20->n26 4 1 1 2 2 1->2 1 18 18 1->18 2 3 3 2->3 2 8 8 2->8 3 13 13 2->13 4 9 9 8->9 2 11 11 8->11 4 14 14 13->14 2 16 16 13->16 3 15 15 14->15 3 19 19 18->19 1 24 24 18->24 3 29 29 18->29 4 30 30 29->30 1

4-皇后问题 回溯求解

1 2 3 4
1 👑
2 👑
3 👑
4 👑

g 1 1 2 2 1->2 1 18 18 1->18 2 3 3 2->3 2 8 8 2->8 3 13 13 2->13 4 9 9 8->9 2 11 11 8->11 4 14 14 13->14 2 16 16 13->16 3 15 15 14->15 3 19 19 18->19 1 24 24 18->24 3 29 29 18->29 4 30 30 29->30 1 31 31 30->31 3

结点 31 是答案结点,解$(2, 4, 1, 3)$

另一个解$(3,1,4,2)$可从结点 31 继续回溯得到,也可翻转得到

回溯法的一般描述

初始状态是第一个活结点,也是 E-结点

如果能从 E-结点移动到一个新结点,那么新结点将变成活结点和新的 E-结点,旧的 E-结点仍是一个活结点,但不是 E-结点了

如果不能移动到一个新结点,当前的 E-结点就“死”了,返回到上一个活结点 (回溯),该活结点重新变成 E-结点

当找到了答案结点或者遍历了所有活结点后,搜索过程结束

回溯法的一般框架

def backtrack(n):
    k = 1
    while k > 0:
        if x_{k-1}的子结点中还有没检验过的x_k满足限界函数:
            if x_1, ..., x_k是一条抵达答案结点的路径
                print(x_1, ..., x_k)
            k = k + 1  # 考虑下一个结点
        else:
            k = k - 1  # 回溯到先前的结点

递归版,初始调用backtrack_ref(1),自带回退更自然

def backtrack_ref(k):
    for x_k in 还有没检验过的x_{k-1}的子结点 and 满足限界函数:
        if x_1, ..., x_k是一条抵达答案结点的路径
            print(x_1, ..., x_k)
        backtrack_ref(k+1)
n-皇后问题 回溯实现

def not_attack(queen, row, col):  # 不会被攻击吗?
    for i in range(row):  # 遍历已在棋盘上的皇后
        # 在同一列 或 在同一个斜角线
        if queen[i] == col or abs(row - i) == abs(queen[i] - col):
            return False
    return True


def nqueen(queen):
    count = row = 0
    queen[row] = 0                   # 先将皇后1放在第1列
    while row >= 0:                  # 全部结点都检测完后会回溯到-1行
        if queen[row] < n:           # 当前行未检测到最后一列
            if not_attack(queen, row, queen[row]):  # 当前位置不会被攻击
                if row == n-1:       # 已到最后一行
                    count += 1       # 解计数+1
                    print(queen)     # 输出
                    row -= 1         # 返回上一行
                    queen[row] += 1  # 上一行的皇后右移一格
                else:
                    row += 1         # 考虑下一行的皇后右移一格
                    queen[row] = 0   # 将下一行的皇后放在第1列
            else:                    # 当前位置不合法,探测当前行的下一个位置
                queen[row] += 1
        else:                        # 当前行已检测到最后一列
            row -= 1                 # 返回上一行
            queen[row] += 1          # 上一行的皇后右移一格
    return count


def nqueen_rec(queen, row):
    for col in range(n):                 # 遍历n个可放位置
        if not_attack(queen, row, col):  # 若(row, col)位置不会被攻击
            queen[row] = col
            if row == n - 1:             # 若是最后一个皇后 输出
                print(queen)
            else:
                nqueen_rec(queen, row + 1)


n = 4
queen = [None for i in range(n)]
print(nqueen(queen))

queen = [None for i in range(n)]
nqueen_rec(queen, 0)
--------------------------------
[1, 3, 0, 2]
[2, 0, 3, 1]
2
[1, 3, 0, 2]
[2, 0, 3, 1]
哈密顿回路

输入:无向图,初始点

输出:从给定初始点出发,恰好经过每个顶点一次的回路

g a a b b a->b c c a->c d d a->d b->c c->d e e c->e d->e

例 1:从点 a 出发的 2 条哈密顿回路

  • a -> b -> c -> e -> d -> a
  • a -> d -> e -> c -> b -> a
哈密顿回路

输入:无向图,初始点

输出:从给定初始点出发,恰好经过每个顶点一次的回路

g a a b b a->b c c a->c d d a->d b->c f f b->f c->d e e c->e d->e e->f

例 2:从点 c 出发的 6 条哈密顿回路

  • c -> a -> b -> f -> e -> d -> c
  • c -> a -> d -> e -> f -> b -> c
  • c -> b -> f -> e -> d -> a -> c
  • c -> d -> a -> b -> f -> e -> c
  • c -> d -> e -> f -> b -> a -> c
  • c -> e -> f -> b -> a -> d -> c
状态空间树

g a a b b a->b c c a->c d d a->d b->c c->d e e c->e d->e

g a a b b a->b c c a->c d d a->d e e a->e c2 c b->c2 d2 d b->d2 e2 e b->e2 b2 b c->b2 d5 d c->d5 e5 e c->e5 b7 b d->b7 c3 c d->c3 e8 e d->e8 d3 d c2->d3 e3 e c2->e3 e4 e d3->e4 d4 d e3->d4 a2 a e4->a2 a3 a d4->a3 d6 d b2->d6 e6 e b2->e6 b3 b d5->b3 e7 e d5->e7 b6 b e5->b6 d7 d e5->d7 b4 b e7->b4 b5 b d7->b5 b8 b c3->b8 e9 e c3->e9 b10 b e8->b10 c4 c e8->c4 e10 e b8->e10 b9 b e9->b9 b11 b c4->b11 a4 a b11->a4

  • a -> b -> c -> e -> d -> a
  • a -> d -> e -> c -> b -> a
哈密顿回路 回溯实现

def hamilton(vertex):
    if vertex == start and len(cycle) == len(g) + 1:  # 找到回路
        print(cycle)
        return

    for v in g[vertex]:
        if not visited[v]:
            visited[v] = True
            cycle.append(v)
            hamilton(v)
            visited[v] = False
            cycle.pop()


g = {                           # a-----b
    'a': ['b', 'c', 'd'],       # |\   / 
    'b': ['a', 'c'],            # | \ /   
    'c': ['a', 'b', 'd', 'e'],  # |  c     
    'd': ['a', 'c', 'e'],       # | / \   
    'e': ['c', 'd'],            # |/   \ 
}                               # d-----e
visited = {v: False for v in g}
start = 'a'
cycle = [start]
hamilton(start)
#-------------------------------
# ['a', 'b', 'c', 'e', 'd', 'a']
# ['a', 'd', 'e', 'c', 'b', 'a']

g = {                           # a-----b
    'a': ['b', 'c', 'd'],       # |\   / \
    'b': ['a', 'c', 'f'],       # | \ /   \
    'c': ['a', 'b', 'd', 'e'],  # |  c     f
    'd': ['a', 'c', 'e'],       # | / \   /
    'e': ['c', 'd', 'f'],       # |/   \ /
    'f': ['b', 'e'],            # d-----e
}
visited = {v: False for v in g}
start = 'c'
cycle = [start]
hamilton(start)
#------------------------------------
# ['c', 'a', 'b', 'f', 'e', 'd', 'c']
# ['c', 'a', 'd', 'e', 'f', 'b', 'c']
# ['c', 'b', 'f', 'e', 'd', 'a', 'c']
# ['c', 'd', 'a', 'b', 'f', 'e', 'c']
# ['c', 'd', 'e', 'f', 'b', 'a', 'c']
# ['c', 'e', 'f', 'b', 'a', 'd', 'c']

g = { # 全连通图可以输出全排列                           
    0: [1, 2, 3, 4],      
    1: [0, 2, 3, 4],            
    2: [0, 1, 3, 4],  
    3: [0, 1, 2, 4],  
    4: [0, 1, 2, 3],    
}                              
visited = {v: False for v in g}
start = 0
cycle = [start]
hamilton(start)
#-------------------------------
# [0, 1, 2, 3, 4, 0]
# [0, 1, 2, 4, 3, 0]
# [0, 1, 3, 2, 4, 0]
# [0, 1, 3, 4, 2, 0]
# [0, 1, 4, 2, 3, 0]
# [0, 1, 4, 3, 2, 0]
# [0, 2, 1, 3, 4, 0]
# [0, 2, 1, 4, 3, 0]
# [0, 2, 3, 1, 4, 0]
# [0, 2, 3, 4, 1, 0]
# [0, 2, 4, 1, 3, 0]
# [0, 2, 4, 3, 1, 0]
# [0, 3, 1, 2, 4, 0]
# [0, 3, 1, 4, 2, 0]
# [0, 3, 2, 1, 4, 0]
# [0, 3, 2, 4, 1, 0]
# [0, 3, 4, 1, 2, 0]
# [0, 3, 4, 2, 1, 0]
# [0, 4, 1, 2, 3, 0]
# [0, 4, 1, 3, 2, 0]
# [0, 4, 2, 1, 3, 0]
# [0, 4, 2, 3, 1, 0]
# [0, 4, 3, 1, 2, 0]
# [0, 4, 3, 2, 1, 0]
子集和数问题

输入:$n$个正数的集合$W = \{ w_1, w_2, \ldots, w_n \}$和正数$M$

输出:$W$中的和数等于$M$的所有子集

例 1:

  • $n=4$$(w_1, w_2, w_3, w_4) = (7, 11, 13, 24)$$M = 31$
  • 子集:$(7, 11, 13)$$(7, 24)$

例 2:

  • $n=6$$(w_1, w_2, w_3, w_4, w_5, w_6) = (5, 10, 12, 13, 15, 18)$$M = 30$
  • 子集:$(5, 10, 15)$$(5, 12, 13)$$(12,18)$
状态空间树

  • 左图:定长$4$元组$(x_1, x_2, x_3, x_4)$$x_i \in \{1, 0\}$表示是否选$w_i$,16 个叶结点对应 16 个可能的解,其到根结点的路径为该解对应的元组
  • 右图:变长递增元组,16 个结点对应 16 个可能的解,其到根结点的路径为该解对应的元组,例如结点 9 对应的$(1,4)$表示选择$w_1$$w_4$

g 1 1 2 2 1->2 1 17 17 1->17 0 3 3 2->3 1 10 10 2->10 0 4 4 3->4 1 7 7 3->7 0 5 5 4->5 1 6 6 4->6 0 8 8 7->8 1 9 9 7->9 0 11 11 10->11 1 14 14 10->14 0 12 12 11->12 1 13 13 11->13 0 15 15 14->15 1 16 16 14->16 0 18 18 17->18 1 25 25 17->25 0 19 19 18->19 1 22 22 18->22 0 20 20 19->20 1 21 21 19->21 0 23 23 22->23 1 24 24 22->24 0 26 26 25->26 1 29 29 25->29 0 27 27 26->27 1 28 28 26->28 0 30 30 29->30 1 31 31 29->31 0 n100 n100 n1 1 n2 2 n1->n2 1 n10 10 n1->n10 2 n14 14 n1->n14 3 n16 16 n1->n16 4 n3 3 n2->n3 2 n7 7 n2->n7 3 n9 9 n2->n9 4 n4 4 n3->n4 3 n6 6 n3->n6 4 n5 5 n4->n5 4 n8 8 n7->n8 4 n11 11 n10->n11 3 n13 13 n10->n13 4 n12 12 n11->n12 4 n15 15 n14->n15 4

限界函数

对每个$w_k$做出选择后,以下两种情形可以剪枝:

  • 已选数之和 + 剩余所有数之和 < 目标值
  • 已选数之和 + 剩余最小数 > 目标值

$w_1, w_2, \ldots, w_n$已按升序排列,则有

$$ \begin{align*} \quad \sum_{i=1}^k w_i x_i + \sum_{i=k+1}^n w_i \ge M, \quad \sum_{i=1}^k w_i x_i + w_{k+1} \le M \end{align*} $$

上述两个不等式均满足才继续考虑$w_{k+1}$,否则回溯

子集和数 定长元组

$n=6$$(w_1, w_2, w_3, w_4, w_5, w_6) = (5, 10, 12, 13, 15, 18)$$M = 30$

g (1, 0, 73) (1, 0, 73) (2, 5, 68) (2, 5, 68) (1, 0, 73)->(2, 5, 68) 1 (2, 0, 68) (2, 0, 68) (1, 0, 73)->(2, 0, 68) 0 (3, 15, 58) (3, 15, 58) (2, 5, 68)->(3, 15, 58) 1 (3, 5, 58) (3, 5, 58) (2, 5, 68)->(3, 5, 58) 0 (3, 10, 58) (3, 10, 58) (2, 0, 68)->(3, 10, 58) 1 (3, 0, 58) (3, 0, 58) (2, 0, 68)->(3, 0, 58) 0 (4, 15, 46) (4, 15, 46) (3, 15, 58)->(4, 15, 46) 0 (4, 17, 46) (4, 17, 46) (3, 5, 58)->(4, 17, 46) 1 (4, 5, 46) (4, 5, 46) (3, 5, 58)->(4, 5, 46) 0 (5, 15, 33) (5, 15, 33) (4, 15, 46)->(5, 15, 33) 0 (1, 1, 0, 0, 1) (1, 1, 0, 0, 1) (5, 15, 33)->(1, 1, 0, 0, 1) 1 (1, 0, 1, 1) (1, 0, 1, 1) (4, 17, 46)->(1, 0, 1, 1) 1 (5, 5, 33) (5, 5, 33) (4, 5, 46)->(5, 5, 33) 0 (4, 10, 46) (4, 10, 46) (3, 10, 58)->(4, 10, 46) 0 (4, 12, 46) (4, 12, 46) (3, 0, 58)->(4, 12, 46) 1 (4, 0, 46) (4, 0, 46) (3, 0, 58)->(4, 0, 46) 0 (5, 10, 33) (5, 10, 33) (4, 10, 46)->(5, 10, 33) 0 (5, 12, 33) (5, 12, 33) (4, 12, 46)->(5, 12, 33) 0 (5, 13, 33) (5, 13, 33) (4, 0, 46)->(5, 13, 33) 1 (5, 0, 33) (5, 0, 33) (4, 0, 46)->(5, 0, 33) 0 (6, 12, 18) (6, 12, 18) (5, 12, 33)->(6, 12, 18) 0 (0, 0, 1, 0, 0, 1) (0, 0, 1, 0, 0, 1) (6, 12, 18)->(0, 0, 1, 0, 0, 1) 1

(待选数下标, 已选数之和, 剩余数之和)

子集和数 定长元组

def subset_sum(i, s, left, x):
    # i s left x = 待选数下标 已选数之和 剩余数之和 当前元组
    # print(i, s, left)
    if i > n:
        return
    else:
        if s + w[i] == M:  # 若加上w[i]是一个解 输出 回溯
            x.append(1)
            print(x)
            x.pop()
            return
        if s + left >= M and s + w[i] + w[i + 1] <= M:  # 否则 若选择w[i]可满足限界条件 递归
            x.append(1)
            subset_sum(i + 1, s + w[i], left - w[i], x)
            x.pop()
        if s - w[i] + left >= M and s + w[i + 1] <= M:  # 否则 若不选择w[i]可满足限界条件 递归
            x.append(0)
            subset_sum(i + 1, s, left - w[i], x)
            x.pop()


w, M = [0, 5, 10, 12, 13, 15, 18], 30
n = len(w)
x = []
subset_sum(1, 0, sum(w), x)
# ---------------------------
# 1 0 73
# 2 5 68
# 3 15 58
# 4 15 46
# 5 15 33
# [1, 1, 0, 0, 1]
# 3 5 58
# 4 17 46
# [1, 0, 1, 1]
# 4 5 46
# 5 5 33
# 2 0 68
# 3 10 58
# 4 10 46
# 5 10 33
# 3 0 58
# 4 12 46
# 5 12 33
# 6 12 18
# [0, 0, 1, 0, 0, 1]
# 4 0 46
# 5 13 33
# 5 0 33
子集和数 变长元组

$n=6$$(w_1, w_2, w_3, w_4, w_5, w_6) = (5, 10, 12, 13, 15, 18)$$M = 30$

g (1, 0, 73) (1, 0, 73) (2, 5, 68) (2, 5, 68) (1, 0, 73)->(2, 5, 68) 1 (3, 10, 58) (3, 10, 58) (1, 0, 73)->(3, 10, 58) 2 (4, 12, 46) (4, 12, 46) (1, 0, 73)->(4, 12, 46) 3 (5, 13, 33) (5, 13, 33) (1, 0, 73)->(5, 13, 33) 4 n17 (1, 0, 73)->n17 5, 6 (3, 15, 58) (3, 15, 58) (2, 5, 68)->(3, 15, 58) 2 (4, 17, 46) (4, 17, 46) (2, 5, 68)->(4, 17, 46) 3 n14 (2, 5, 68)->n14 4, 5, 6 n1 (3, 15, 58)->n1 3, 4 (1, 2, 5) (1, 2, 5) (3, 15, 58)->(1, 2, 5) 5 n3 (3, 15, 58)->n3 6 (1, 3, 4) (1, 3, 4) (4, 17, 46)->(1, 3, 4) 4 n4 (4, 17, 46)->n4 5, 6 n6 (3, 10, 58)->n6 3, 4, 5, 6 n10 (4, 12, 46)->n10 4, 5 (3, 6) (3, 6) (4, 12, 46)->(3, 6) 6 n12 (5, 13, 33)->n12 5, 6

(待选数最小下标, 已选数之和, 剩余数之和)

子集和数 变长元组

def subset_sum(i, s, left, x):
    # i s left x = 待选数最小下标 已选数之和 剩余数之和 当前元组
    print(i, s, left)
    if i > n:
        return
    else:
        for j in range(i, n):  # 依次尝试选择 w_i, w_i+1, ..., w_n
            if s + w[j] == M:  # 若加上w[j]是一个解 输出
                x.append(j)
                print(x)
                x.pop()
            elif j < n-1:  # 否则 若选择w[j]可满足限界条件 递归
                if s + left >= M and s + w[j] + w[j + 1] <= M:
                    x.append(j)
                    left = 0
                    for k in range(j+1, n):
                        left += w[k]
                    subset_sum(j + 1, s + w[j], left, x)
                    x.pop()


w, M = [0, 5, 10, 12, 13, 15, 18], 30
n = len(w)
x = []
subset_sum(1, 0, sum(w), x)
# ---------------------------
# 1 0 73
# 2 5 68
# 3 15 58
# [1, 2, 5]
# 4 17 46
# [1, 3, 4]
# 3 10 58
# 4 12 46
# [3, 6]
# 5 13 33
小结

回溯法:带剪枝的搜索,最坏情况下时间复杂度和穷举法相同

但是对某些困难的组合难题,可以求解其较大实例

改进:

  • 利用问题的对称性:镜像对称、旋转对称
  • 重新排列:优先对取值少的分量先做选择,从而剪枝收益增大

g 1 2 1->2 3 1->3 4 2->4 5 2->5 6 2->6 7 3->7 8 3->8 9 3->9 10 4->10 11 4->11 12 4->12 13 4->13 14 5->14 15 5->15 16 5->16 17 5->17 18 6->18 19 6->19 20 6->20 21 6->21 22 7->22 23 7->23 24 7->24 25 7->25 26 8->26 27 8->27 28 8->28 29 8->29 30 9->30 31 9->31 32 9->32 33 9->33 100 101 34 35 34->35 36 34->36 37 34->37 38 35->38 39 35->39 40 35->40 41 35->41 42 36->42 43 36->43 44 36->44 45 36->45 46 37->46 47 37->47 48 37->48 49 37->49 50 38->50 51 38->51 52 39->52 53 39->53 54 40->54 55 40->55 56 41->56 57 41->57 58 42->58 59 42->59 60 43->60 61 43->61 62 44->62 63 44->63 64 45->64 65 45->65 66 46->66 67 46->67 68 47->68 69 47->69 70 48->70 71 48->71 72 49->72 73 49->73

作业

分派问题:给$n$个人分派$n$件工作,把工作$j$分派给第$i$个人的成本为$\text{cost}(i,j)$,设计一个回溯算法,在给每个人分派一件不同工作的情况下使得总成本最小

设集合$W=(5,7,10,12,15,18,20)$$M=35$,找出$W$中使得和数等于$M$的全部子集并画出所生成的部分状态空间树