算法设计与分析笔记5———回溯法与分支限界法

目录

回溯法

一,算法的总体思想

二,算法步骤

三,一些例子

1,0-1背包问题

2,旅行售货员问题

3,批处理作业调度

4,m着色问题

分支限界法

一,算法的总体思想

二,算法步骤

三,一些例子

1,0-1背包问题

2,旅行售货员问题

3,最小重量机器设计问题

一点总结:


        回溯法与分支限界法都是枚举法的改进,且思路大体相同,放在同一个笔记中,二者因适用性高,也被称为通用解题法。

回溯法

一,算法的总体思想

        回溯法是暴力枚举法的一种改进方法,与枚举法一样,将问题的所有可能一一枚举,改进是指,枚举过程改为构建解空间树,采用深度优先搜索(DFS)的方式遍历解空间树,在遍历过程中通过适当的剪枝函数排除一部分不可能的解,从而达到提高算法效率的目的。

二,算法步骤

1,分析问题结构,是否进行数据预处理;

2,构建解空间树;

3,采用DFS方式遍历解空间树;

遍历解空间树时,通常需要新的数据结构进行回溯。

4,设置约束函数与限界函数,检测是否删去该子树;

约束函数用于过滤不符合要求的解,限界函数用于过滤非最优解。

5,回溯到根节点后,表示遍历完解空间树,输出最优解。

三,一些例子

1,0-1背包问题

若背包载重量为c,有n个物品,v[n]表示n个物品各自价值,w[n]表示n个物品各自重量。有二维数组m[i][j],i表示装入从第i个物品到第n个物品,j表示背包载重量,m[i][j]为第i个物品到第n个物品装入背包,当背包载重量为j时的价值最大值。

以 c = 30, w = [16, 15, 15], v = [45, 25, 25] 为例

分析问题结构:

得出的结果为第 i 个物品是否放入背包,因此放入的顺序对结果没有影响,不需要数据预处理;

构建解空间树:

根据第i个物品是否放入背包可知每个物品有放入背包(左子树x[i] = 1)和不放入背包(右子树x[i] = 0)两种情况,并以此构造解空间树。

约束函数与限界函数:

根据问题的特点,可设约束函数为当装入第i个物品后,若超出背包承重量,则将以该节点为根节点的子树剪去,而对于右子树而言,因为右子树表示不装入第i个物品,因此不需要进行判断;

可设限界函数为,当得到第一个用约束函数无法减去的最优解后,将书包的价值记为当前最优解,对于所有的右子树,计算其可能的最大值,意为如果不装入第i个物品(即进入右子树),剩余物品的总价值能否高于当前最优解。若剩余所有物品全装入背包,价值都不超过最优解,则以该节点为根节点的子树一定不包含比当前解更好的解,可以剪枝。

代码实现如下:

def bound(i, cv, n, v) :    # 限界函数
    for j in range(i, n) :
        cv += v[i]
    return cv > v_max

def backtrack(i, c, w, v, cw, cv, n, selected_items) :
    global v_max, ans
    if i == n :
        if v_max < cv :
            v_max = cv
            ans = [j for j in range(n) if selected_items[j]]
            return
    if cw + w[i] <= c :    # 约束条件
        selected_items[i] = True
        backtrack(i + 1, c, w, v, cw + w[i], cv + v[i], n, selected_items)
        selected_items[i] = False

    if bound(i, cv, n, v) :
        backtrack(i + 1, c, w, v, cw, cv, n, selected_items)


v_max = 0
ans = []
c = int(input())
w = list(map(int, input().split()))
v = list(map(int, input().split()))
selected_items = [False] * len(w)
backtrack(0, c, w, v, 0, 0, len(w), selected_items)

print(ans, v_max)

代码中,用了新的数据函数selected_items记录节点的遍历情况,实现回溯操作。

结果:

# 输入
30
16 15 15
45 25 25

# 输出
[1, 2] 50
2,旅行售货员问题

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

要求从城市 1 出发,最后回到城市 1,路程最短

分析问题结构:

需要将抽象为邻接矩阵c

构建解空间树:

与先前的子集树不同,本题的解空间树为排列数

本题没有限制路径长度,因此没有约束函数,可以设置限界函数为若当前路径已经超过当前最优路径长度,则剪枝。

代码实现如下:

import copy
def backtrack(count, cs, visited, ccity, n) :
    global s_min, c, ans, path
    if count == n :
        if s_min > cs + c[ccity][1] :
            s_min = cs + c[ccity][1]
            ans = copy.deepcopy(path)    # 采用深拷贝,记录当前结果
            ans.append(1)
        return
    for i in range(1, n + 1) :
        if not visited[i] and c[ccity][i] and cs + c[ccity][i] < s_min:    # 限界函数
            visited[i] = True
            path.append(i)
            backtrack(count + 1, cs + c[ccity][i], visited, i, n)
            path.pop()
            visited[i] = False


s_min = float('inf')
ans = []
path = [1]
n = int(input())
visited = [False] * (n + 1)
visited[1] = True
selected_items = [False] * (n + 1)
c = []
for i in range(n + 1) :
    line = list(map(int, input().split()))
    c.append(line)

backtrack(1, 0, visited, 1, n)
print(s_min, ans)

代码中同样使用了新的数据结构 visited 来记录DFS时访问过的城市,实现回溯

结果:

# 输入
4
0	1	2	3	4
1	-1	30	6	4
2	30	-1	5	10
3	6	5	-1	20
4	4	10	20	-1

# 输出
25 [1, 3, 2, 4, 1]
3,批处理作业调度

给定n个作业的集合{J1,J2,…,Jn}。每个作业必须先由机器1处理,然后由机器2处理。  

t[i][j] 表示作业 i 由机器 j 处理所用时间。

t = [

        [ 2, 1]

        [ 3, 1]

        [ 3, 2]

]

求完成作业最早时间,并给出作业顺序

本题不需要预处理

构建解空间树:

可见本题为排列树

与上一题相同,题目没有限制路径,因此没有约束函数,设限界函数为,若当前时间已经大于当前最优值,则剪枝。

代码如下:

import copy
def backtrack(n, count, a, b, i) :
    global mini_time, ans, path, t, selected_items, last_b
    if count == n :
        if a > b :
            current_time = a + t[i][1]
        else :
            current_time = b + t[i][1]
        if current_time < mini_time :
            mini_time = current_time
        ans = copy.deepcopy(path)
        return
    for j in range(n) :
        if not selected_items[j] :
            selected_items[j] = True
            path.append(j)
            if b > mini_time :    # 限界函数
                continue
            if a >= b :
                backtrack(n, count + 1, a + t[j][0], a + t[j][1], j)
            else :
                backtrack(n, count + 1, a + t[j][0], b + t[j][1], j)
            path.pop()
            selected_items[j] = False


mini_time = float('inf')
ans = []
path = []
n = 3
selected_items = [False] * (n + 1)
t = [
    [3, 1],
    [2, 1],
    [2, 3]
]
backtrack(n, 0, 0, 0, -1)
print(mini_time, ans)



# 输出为:
8 [2, 1, 0]

该题需要分析作业在机器 2 上时,前一个作业的情况:

第 i 个作业在机器 1 上执行完时,第 i - 1 个作业已经在机器 2 上完成,此时第 i 个作业完成时间为第 i - 1 个作业在机器 1 上的完成时间加第 i 个作业在机器 2 上需要的时间;

第 i 个作业在机器 1 上执行完时,第 i - 1 个作业还没有在机器 2 上完成,此时第 i 个作业完成时间为第 i - 1 个作业在机器 2 上完成的时间加第 i 个作业在机器 2 上需要的时间。

4,m着色问题

给定无向连通图G=(V,E)和m种不同的颜色。用这些颜色为图G的各顶点着色,每个顶点着一种颜色。是否有一种着色法使G中每条边的2个顶点着不同颜色。

问题分析:需要将无向连通图抽象为邻接矩阵 c

本题没有限界函数,约束函数为,若当前节点与其他节点相连,那么二者颜色不能相同

import copy
def judge_color(c, count, path) :
    for i in range(len(c[count])) :
        if  c[count][i] != -1 and path[count] == path[i] :
            return False
    return True

def backtrack(count, path, n, c) :
    global s, m
    if count == n :
        an = copy.deepcopy(path)
        ans.append(an)
        s += 1
    else :
        for color in range(m) :
            path[count] = color
            if judge_color(c, count, path) :
                backtrack(count + 1, path, n, c)
            path[count] = -1

n, m = map(int, input().split())
c = []
for i in range(n) :
    line = list(map(int, input().split()))
    c.append(line)
path = [-1] * n
s = 0
ans = []
backtrack(0, path, n, c)
print(s)
for line in ans :
    print(line)

代码中使用了judge_color函数判断,若邻接矩阵中,二者有通路,并且在染色过程中二者又为相同颜色,则返回False,否则返回True

# 输入 :
7 3
-1	1	-1	-1	-1	1	1
1	-1	1	-1	-1	-1	1
-1	1	-1	1	-1	-1	1
-1	-1	1	-1	1	1	-1
-1	-1	-1	1	-1	1	-1
1	-1	-1	1	1	-1	1
1	1	1	-1	-1	1	-1

# 输出 :
6
[0, 1, 0, 2, 0, 1, 2]
[0, 2, 0, 1, 0, 2, 1]
[1, 0, 1, 2, 1, 0, 2]
[1, 2, 1, 0, 1, 2, 0]
[2, 0, 2, 1, 2, 0, 1]
[2, 1, 2, 0, 2, 1, 0]

回溯法的主要步骤为构造解空间树,通过各种剪枝函数,在以DFS方式遍历解空间树时,过滤不符合要求的解与非最优解 

分支限界法

一,算法的总体思想

        与回溯法类似,是在构造解空间树的基础上搜索问题解的一种方法,而与回溯法不同的是,分支限界法采用广度优先(BFS)的方式遍历解空间树。如果说,回溯法求得满足约束条件的所有解,那分支限界法则是找到满足约束条件的一个解。

        在具体实现上,分支限界法采用BFS方式,逐层的检测当前求解节点的条件,并使用队列记录需要进一步检测的节点,直到队列为空。

        关于存储节点的队列,分支限界法有先进先出(FIFO)和优先队列两种

        先进先出的队列按照入队列的顺序,逐个检测节点;

        优先队列对每个节点按照需求记录其优先级,并优先检测优先级高的节点。

两种方法适应不同的题目,如若是旅行售货员问题,可使用优先队列,将当前路程短的节点赋予更高的优先级,以便于更快找到一个解,使限界函数best更早发挥作用;若是着色问题,则没有优先级之分,可使用先进先出的队列,以避免每个节点的优先级计算。

        在分支限界法中,需要创建类对象扩展子节点,而不再使用递归回溯的方法。

二,算法步骤

1,分析问题,数据预处理;

2,构造解空间树;

3,BFS方式遍历解空间树,符合条件子节点入队列,过程中用剪枝函数过滤不必要的节点;

4,找到解或队列为空,算法结束。

三,一些例子

回溯法与分支限界法是对解空间树的不同遍历方式,因此会采用部分相同例题,凸显差别

1,0-1背包问题

问题描述与上述一致

背包载重量c = 30,

各物体重量w = [16, 15, 15],

各物体价值v = [45, 25, 25]。

构造解空间树如下

代码:

from queue import PriorityQueue

class Node :    # 定义Node对象,元素有当前节点count,当前重量,当前价值,选择路径
    def __init__(self, count, current_weight, current_value, path):
        self.count = count
        self.current_weight = current_weight
        self.current_value = current_value
        self.path = path

    def __lt__(self, other):    # 重载< 运算符,用于优先队列排序,价值大的在前,若价值一样,则重量少的在前
        if self.current_value != other.current_value :
            return self.current_value > other.current_value
        elif self.current_weight != other.current_weight :
            return self.current_weight < other.current_weight

N_list = PriorityQueue()

def branch(c, w, n, v) :
    global ans, ans_list
    for i in range(2) :     # 对于初始节点,只有0, 1两种情况
        t = Node(1, i * w[0], i * v[0], [i])
        if t.current_weight <= c :    # 约束函数,不超重进入队列
            N_list.put(t)

    while not N_list.empty() :
        item = N_list.get()
        if item.count < n :    # BFS方式,遍历解空间树
            for i in range(2) :
                t = Node(item.count + 1, item.current_weight + i * w[item.count], item.current_value + i * v[item.count], item.path + [i])
                if t.current_weight <= c :    # 遍历时的约束函数
                    N_list.put(t)
        if item.count == n :
            if item.current_weight <= c and item.current_value > ans :
                ans = item.current_value
                ans_list = item.path

ans_list = []
ans = 0
c = int(input())
w = list(map(int, input().split()))
n = len(w)
v = list(map(int, input().split()))
branch(c, w, n, v)
print(ans)
print(ans_list)
# 输入 :
30
16 15 15
45 25 25

# 输出:
50
[0, 1, 1]
2,旅行售货员问题

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

邻接矩阵:

构建解空间树

对节点Node有如下属性:

当前路程s,走过的城市及顺序path,当前所在城市current,走过的城市数level

首先构造根节点,根节点的属性为s = 0;path = [0];current = 0, level = 1

扩展非根节点,注意当前城市与下一个城市是否有通路连接,s的值越小,在优先队列中的权重越高,取得当前s最小的节点为下一个准备扩展的节点。

使用visited记录当前节点已经访问过哪些城市

当level等于4,即已经走过四个城市后,到达叶子节点,首次到达叶子节点时更新当前最优值,限界函数开始发挥作用。节点中path记录了到达该节点所经过的城市顺序。此时要检查是否有从当前城市current回到初始城市0的通路,若没有,则不更新结果。

from queue import PriorityQueue

class Node :
    def __init__(self, s, path, current, level):
        self.s = s
        self.path = path
        self.current = current
        self.level = level

    def __lt__(self, other):
        if self.s != other.s :
            return self.s < other.s

q = PriorityQueue()

def branch(c, n, visited) :
    global ans_path, ans
    for i in range(1, n) :
        if c[0][i] != -1 :
            t = Node(c[0][i], [0] + [i], i, 1)
            q.put(t)
    while not q.empty() :
        item = q.get()
        if item.level == n - 1 :
            if c[item.current][0] != -1 :
                its = item.s + c[item.current][0]
                if ans > its :
                    ans = its
                    ans_path = item.path + [0]
        else :
            for _ in item.path :
                visited[_] = True
            for j in range(n) :
                if not visited[j] and c[item.current][j] != -1 :
                    t = Node(item.s + c[item.current][j], item.path + [j], j, item.level + 1)
                    if ans > t.s:
                        q.put(t)
            visited = [False] * n


ans_path = []
ans = float('inf')
n = int(input())
c = []
for i in range(n) :
    line = list(map(int, input().split()))
    c.append(line)
visited = [False] * n
branch(c, n, visited)
print(ans_path, ans)

结果为:

# 输入为:
4
-1	30	6	4
30	-1	5	10
6	5	-1	20
4	10	20	-1

# 输出为:
[0, 3, 1, 2, 0] 25
3,最小重量机器设计问题

最小重量机器设计问题:设某一机器由n个部件组成,每一种部件都可以从m个不同的供应商处购得。设 w[i][j] 是从供应商j处购得的部件 i 的重量, v[i][j]是相应的价格。试设计一个算法,给出总价格不超过 c 的最小重量机器设计。

构造解空间树

有n个物品,每个物品有m种选择,可知解空间树为完全m叉树

设置优先队列,实现对解空间树的BFS遍历即可

from queue import PriorityQueue
class Node :
    def __init__(self, c_w, c_v,selected_store, level):
        self.c_w = c_w
        self.c_v = c_v
        self.selected_store = selected_store
        self.level = level

    def __lt__(self, other):
        if self.c_w != other.c_w :
            return self.c_w < other.c_w

q = PriorityQueue()

def branch(n, m, c, w, v) :
    global ans_w, store
    for i in range(m) :
        t = Node(w[0][i], v[0][i],[i],  1)
        if t.c_v <= c :
            q.put(t)

    while not q.empty() :
        item = q.get()
        if item.level == n :
            if item.c_w < ans_w :
                ans_w = item.c_w
                store = item.selected_store
        else :
            for j in range(m) :
                t = Node(item.c_w + w[item.level][j], item.c_v + v[item.level][j], item.selected_store + [j], item.level + 1)
                if t.c_v <= c and t.c_w < ans_w : # 这里使用了限界函数和约束函数
                    q.put(t)



ans_w = float('inf')
store = []
n, m, c = map(int, input().split())
v = []
w = []
for i in range(n) :
    line = list(map(int, input().split()))
    w.append(line)
for i in range(n) :
    line = list(map(int, input().split()))
    v.append(line)

branch(n, m, c, w, v)
print(ans_w, store)

分支限界法的难点在于需要新的数据结构用于扩展下一层的节点

结果为:

# 输入为 :
3 2 60
12 15
23 24
78 70
8 6
13 9
14 25

# 输出为 :
105 [0, 0, 1]

一点总结:

        至此,算法设计与分析的笔记告一段落,本篇的回溯法与分支限界法是枚举法的该机算法,在构造解空间的过程中完成了枚举,通过剪枝函数提高算法效率,而贪心算法不要求得到全局最优解,因此思路简单且效率较高。

        前文的分治法与动态规划法则是从问题的角度出发,找到问题的最优子结构性质,当分解的子问题相互独立时,使用分治法,而有重叠子问题性质时使用动态规划。二者与枚举法的本质区别是出发点不同。

        枚举法是从解向量出发,枚举每一个 X[i] 的可能取值,进而判断当前解向量是否满足题目要求,如果满足,当前解向量是否为当前最优解,而分治法与动态规划算法则是从问题的角度出发,将问题缩小为规模更小的子问题,而子问题的最优解又含于原问题的最优解中。

        总之,算法笔记到此结束。

  • 50
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值