数据结构day6

回溯

   回溯法(back tracking)(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

1 基本思想

   从一条路往前走,能进则进,不能进则退回来,换一条路再试。

2 一般步骤

  • 定义一个解空间(子集树、排列树二选一)
  • 适于搜索的方法组织解空间。
  • 利用深度优先法搜索解空间。
  • 利用剪枝函数避免移动到不可能产生解的子空间。

3 子集树模板

   遍历子集树,时间复杂度 O(2^n)
   如果解的长度是不固定的,那么解和元素顺序无关,即可以从中选择0个或多个。例如:子集,迷宫,...
   如果解的长度是固定的,那么解和元素顺序有关,即每个元素有一个对应的状态。例如:子集,8皇后,...
   解空间的个数是指数级别的,为2^n,可以用子集树来表示所有的解,适用于:幂集、子集和、0-1背包、装载、8皇后、迷宫、...

# 子集树递归模板
n = 4
#a = ['a','b','c','d']
a = [1, 2, 3, 4]
x = []   # 一个解(n元0-1数组)
X = []   # 一组解

# 冲突检测:无
def conflict(k):
    global n, x, X, a
    
    return False # 无冲突
    
# 一个例子
# 冲突检测:奇偶性相同,且和小于8的子集
def conflict2(k):
    global n, x, X, a
    
    if k==0:
        return False
    
    # 根据部分解,构造部分集
    s = [y[0] for y in filter(lambda s:s[1]!=0, zip(a[:k+1],x[:k+1]))]
    if len(s)==0:
        return False
    if 0 < sum(map(lambda y:y%2, s)) < len(s) or sum(s) >= 8: # 只比较 x[k] 与 x[k-1] 奇偶是否相间
        return True
    
    return False # 无冲突

    
# 子集树递归模板
def subsets(k): # 到达第k个元素
    global n, x, X
    
    if k >= n:  # 超出最尾的元素
        #print(x)
        X.append(x[:]) # 保存(一个解)
    else:
        for i in [1, 0]: # 遍历元素 a[k] 的两种选择状态:1-选择,0-不选
            x.append(i)
            if not conflict2(k): # 剪枝
                subsets(k+1)
            x.pop()              # 回溯


# 根据一个解x,构造一个子集
def get_a_subset(x):
    global a
    
    return [y[0] for y in filter(lambda s:s[1]!=0, zip(a,x))]

# 根据一组解X, 构造一组子集
def get_all_subset(X):
    return [get_a_subset(x) for x in X]

# 测试
subsets(0)

# 查看第3个解,及对应的子集
#print(X[2])
#print(get_a_subset(X[2]))

print(get_all_subset(X))

[[1, 3], [1], [2, 4], [2], [3], [4], []]

4 实例

4.1 八皇后问题

我们可以先将八皇后简化为四皇后,解题思路如下:

  1. 尝试先放置第一枚皇后,被涂黑的地方是不能放皇后


    step1
  2. 第二行的皇后只能放在第三格或第四格,比方我们放第三格,则:


    step2
  3. 可以看到再难以放下第三个皇后,此时我们就要用到回溯算法了。我们把第二个皇后更改位置,此时我们能放下第三枚皇后了。


    step3
  4. 虽然是能放置第三个皇后,但是第四个皇后又无路可走了。返回上层调用(3号皇后),而3号也别无可去,继续回溯上层调用(2号),2号已然无路可去,继续回溯上层(1号),以此类推继续回溯。

   这样就能在一步一步的测试中把八皇后的问题解决出来了

import random


# 随机模块
#state  保存的是已经确定皇后的列的坐标 [0,2]
#pos  表示新增加的皇后列的坐标 2

def conflict(state, pos):
    nextY = len(state)
    if pos in state: return True
    '''判断斜线'''
    for i in range(nextY):
        if nextY-pos == i-state[i]: return True
        if nextY+pos == i+state[i]: return True
         
    return False


def queens(num=8, state=()):
    # 生成器函数
    for pos in range(num):
        #print(state, pos)
        if not conflict(state, pos):
            #print(state,pos)
            if len(state) == num - 1:
                yield (pos,)  #生成器  range(10)
            else:
                for result in queens(num, state + (pos,)):
                    yield (pos,) + result


def queenprint(solution):
    # 打印函数
    def line(pos, length=len(solution)):
        #print('pos:',pos)
        return '. ' * (pos) + 'X ' + '. ' * (length - pos - 1)

    for pos in solution:
        print(line(pos))


# for solution in list(queens(8)):
#     print("aaaaa")
#     print(solution)
    

# print('  total number is ' + str(len(list(queens()))))
# print('  one of the range is:\n')
queenprint(random.choice(list(queens())))

X . . . . . . .
. . . . . . X .
. . . . X . . .
. . . . . . . X
. X . . . . . .
. . . X . . . .
. . . . . X . .
. . X . . . . .

   这里随机给出了把皇后的一种答案

4.2 背包问题

4.2.1 背包问题的解题思路

   有N件物品和一个容量为V的背包。第i件物品的体积是w[i],价值是c[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
   这是最基础的背包问题,总的来说就是:选还是不选,这是个问题
   相当于用s[i]表示放进背包的前i个物体的总体积,v[i]表示放进背包的前i个物体的总价值,只有两种情况:

  • 第i件不放进去,这时背包内的物品体积为s[i-1],背包的价值为v[i-1]
  • 第i件放进去,这时背包内的物品体积为s[i],背包的价值为v[i]

4.2.2 问题的解空间

   用回溯法解问题时,应明确定义问题的解空间。问题的解空间至少包含问题的一个(最优)解。对于 n=3 时的 0/1 背包问题,可用一棵完全二叉树表示解空间,如图所示:


背包问题的解空间

1表示选取,0表示不选

4.2.3 代码实现

#bestV 最优的价值
bestV=0
#当前重量
curW=0
#当前价值
curV=0
#当价值最优的时候的,背包的物品列表
bestx=None
def backtrack(i):
    global bestV,curW,curV,x,bestx
    if i>=n:
        if bestV

15
[True, True, False, False, True]

总结

问题:
八皇后中的queens()函数中没有用return,而使用的是yield,不太清楚这个函数是什么意思?
答:yield和return的关系和区别,带yield的函数是一个生成器,而不是一个函数了,这个生成器有一个函数就是next函数,next就相当于“下一步”生成哪个数,这一次的next开始的地方是接着上一次的next停止的地方执行的,所以调用next的时候,生成器并不会从foo函数的开始执行,只是接着上一步停止的地方开始,然后遇到yield后,return出要生成的数,此步就结束。
   需要详细了解yield()函数的可以访问以下链接:
python中yield的用法详解——最简单,最清晰的解释

你可能感兴趣的:(数据结构day6)