多米诺性质
设 为问题的解, 而 和 分别是第 层和第 层的解, 若 , 即若 满足条件则 也满足, 那么称该问题满足多米诺性质
背包问题
设 以及
由于 且 , 因此
反例
由于 成立而 不一定成立, 所以不满足多米诺性质
取 , 则原式子改为 , 满足多米诺性质
算法效率
以四皇后为例
随机选择路径, 由于是四皇后, 因此假设 4 条路径节点数是一致的
装载问题
问题描述
个集装箱装到载重量为 和 的轮船上, 集装箱重 , 是否存在一个合理的装载方案将 个集装箱装到两艘轮船上?
解决思路
-
尽可能的将第一艘轮船装满
-
剩余的集装箱装到第二艘轮船
需要剪去如下分支
class Loading:
def __init__(self, w, c1, c2) -> None:
self.w = list(w) # 集装箱重量
self.n = len(w) # 集装箱数
self.c1 = c1 # 第一个轮船载重
self.c2 = c2 # 第二个轮船载重
self.cw = 0 # 当前载重量
self.best = 0 # 最优载重量
self.temp = [] # 记录当前方案
self.solution = [] # 装载方案
def maxLoading(self):
self.backtrack(0)
s1 = set(range(self.n))
s2 = set(self.solution)
solution2 = list(s1 - s2)
w = 0
for i in solution2:
w += self.w[i]
if w > self.c2:
return False # 无法装载
return self.solution, solution2
def backtrack(self, i):
# 搜索第 i 层节点
if i >= self.n:
# 到达叶节点
if self.cw > self.best:
self.best = self.cw
self.solution = list(self.temp)
return
# 装载的情况
if self.cw + self.w[i] <= self.c1:
self.cw += self.w[i]
self.temp.append(i)
self.backtrack(i + 1)
self.cw -= self.w[i]
self.temp.pop()
# 不装载的情况
self.backtrack(i + 1)
if __name__ == "__main__":
w = [90, 80, 40, 30, 20, 12, 10]
c1 = 152
c2 = 130
loading = Loading(w, c1, c2)
# ([0, 2, 5, 6], [1, 3, 4])
print(loading.maxLoading())
需要 的栈空间
由于搜索树有 个节点( 装或不装 ), 因此回溯的时间复杂度为
上界函数
维护一个剩余重量 , 若
则说明剩下的集装箱即使全部加起来都无法达到最优, 因此没必要考虑
class Loading:
def __init__(self, w, c1, c2) -> None:
# ...
# 剩余重量
self.r = sum(w)
def backtrack(self, i):
# ...
self.r -= w[i]
if self.cw + self.w[i] <= self.c1:
# ...
# 不装载 w[i] 的情况下, 要求加上剩余重量要大于最优装载
if self.cw + self.r > self.best:
self.backtrack(i + 1)
self.r += w[i]
批处理作业调度
问题描述
项作业, 需要在两台机子上依次完成, 所有作业在机器 上完成处理的时间和称为该作业调度的完成时间和, 记为
当作业调度方案为 1, 2, 3 时, 作业调度完成时间和为
解决思路
在递归前尝试交换两个作业的调度顺序, 回溯时换回来
class FlowShop:
def __init__(self, m):
self.n = len(m) # 作业数
self.f1 = 0 # 机器 1 完成处理时间
self.f2 = [0 for _ in range(self.n)] # 机器 2 完成处理时间
self.f = 0 # 完成时间和
self.best = float("inf") # 当前最优解
self.m = m # 各作业处理时间
self.x = list(range(self.n)) # 当前作业调度, 默认 1, 2, 3, ... 顺序
self.bestx = None # 当前最优作业调度
def schedule(self):
self.backtrack(0)
return [x + 1 for x in self.bestx]
def backtrack(self, i):
if i >= self.n:
self.best = self.f
self.bestx = list(self.x)
return
for j in range(i, self.n):
# 尝试先执行作业 i 及后面的调度(即作业 j)
self.f1 += self.m[self.x[j]][0]
if self.f2[i - 1] > self.f1:
# 机器 1 执行结束后机器 2 还在运行, 需要等待
self.f2[i] = self.f2[i - 1] + self.m[self.x[j]][1]
else:
self.f2[i] = self.f1 + self.m[self.x[j]][1]
self.f += self.f2[i]
if self.f < self.best:
# 尝试使用不同的调度方案
self.x[i], self.x[j] = self.x[j], self.x[i]
self.backtrack(i + 1)
self.x[i], self.x[j] = self.x[j], self.x[i]
self.f1 -= self.m[self.x[j]][0]
self.f -= self.f2[i]
if __name__ == "__main__":
m = [[2, 1], [3, 1], [2, 3]]
f = FlowShop(m)
# [1, 3, 2]
print(f.schedule())
时间复杂度为 , 可以理解为计算每一个调度组合, 如 时调度组合数为
符号三角形(不考)
问题描述
两个同号下面都是 + 号, 两个异号下面都是 - 号, 符号三角形第一行有 个字符, 要求对于给定的 , 计算有多少个不同的符号三角形, 使其所有的 + 和 - 个数相同
解决思路
第一行有 个, 则符号三角形由 个符号构成, 要求 + 号与 - 号个数相同, 则其个数均不超过
当 为奇数时, 不存在该符号三角形
只需要在 [1:i] 的符号三角形右边增加一边, 即可扩展成 [1:i+1]
尝试在第一行最右边加上 + 或 - 号, 则底下的所有符号都固定了, 尝试添加这些符号
class Triangles:
def __init__(self, n) -> None:
self.n = n # 第一行符号数
self.half = 0 # +/- 号限制
self.add = 0 # + 号个数
self.p = None # 符号三角形
self.sum = 0 # 符号三角形数
def compute(self):
self.half = self.n * (self.n + 1) // 2
if self.half % 2 == 1:
print("not triangle.")
return 0
self.half /= 2
self.p = [[0 for _ in range(self.n + 1)] for _ in range(self.n + 1)]
self.backtrack(1)
return self.sum
def backtrack(self, t):
if self.add > self.half or t * (t - 1) // 2 - self.add > self.half:
return
if t > self.n:
self.sum += 1
for r in self.p:
print(r)
print()
else:
for i in range(2):
self.p[1][t] = i # 0 代表 -, 1 代表 +
self.add += i
# 获取右边的符号
for j in range(2, t + 1):
p = int(not (self.p[j - 1][t - j + 1] ^ self.p[j - 1][t - j + 2]))
self.p[j][t - j + 1] = p
self.add += p
# 右边加一条边
self.backtrack(t + 1)
for j in range(2, t + 1):
self.add -= self.p[j][t - j + 1]
self.add -= i
if __name__ == "__main__":
t = Triangles(7)
print(t.compute()) # 12
计算可行性约束 , 最坏情况下需要计算 个节点的可行性约束, 因此时间复杂度为
n 后问题
问题描述
在 的方格中放置彼此不受攻击的 个皇后, 国际象棋中皇后可以攻击同一行、同一列、同一斜线上的棋子
解决思路
在某一行中尝试所有列, 若该列不存在冲突则尝试放置皇后, 若冲突则继续尝试下一列
class Queen:
def __init__(self, n) -> None:
self.n = n
self.x = [0 for _ in range(n)] # 当前方案(存放第 i 行皇后所在列数)
self.t = [] # 全部方案
def solution(self):
self.backtrack(0)
return self.t
def place(self, k):
for i in range(k):
# 斜率不一样且不在同一列, 由于 i < k, 因此肯定不在同一行
if abs(k - i) == abs(self.x[i] - self.x[k]) or self.x[i] == self.x[k]:
return False
return True
def backtrack(self, t):
if t >= self.n:
self.t.append(list(self.x))
return
for i in range(self.n):
self.x[t] = i # 在 i 位置放置皇后
if self.place(t): # 如果可以放置
self.backtrack(t + 1)
if __name__ == "__main__":
q = Queen(4)
# [[1, 3, 0, 2], [2, 0, 3, 1]]
print(len(q.solution())) # 2
非递归版
class Queen:
def __init__(self, n) -> None:
self.n = n
self.x = [0 for _ in range(n)] # 当前方案(存放第 i 行皇后所在列数)
self.t = [] # 全部方案
def solution(self):
k = 0
while k >= 0:
# 不断查找第 k 行可以放置的位置
while self.x[k] < self.n and (not self.place(k)):
self.x[k] += 1
if self.x[k] < self.n:
if k == self.n - 1: # 已经到最后一行了
self.t.append(list(self.x))
self.x[k] += 1
else:
k += 1 # 下一行
self.x[k] = 0
else:
# 没有合适的位置则回溯上一行
k -= 1
# 上一行皇后偏移一个位置继续
self.x[k] += 1
return self.t
def place(self, k):
for i in range(k):
if abs(k - i) == abs(self.x[i] - self.x[k]) or self.x[i] == self.x[k]:
return False
return True
if __name__ == "__main__":
q = Queen(4)
print(q.solution())
0-1 背包问题
判断加入货物后是否超过容量, 若没有超过, 则尝试加入
class Element:
def __init__(self, id, d):
self.id = id # 物品编号
self.d = d # 单位重量价值
class Knapsack:
def __init__(self, c, w, p):
self.c = c # 环岛公路
self.n = len(w) # 物品数量
self.cw = 0 # 当前重量
self.cp = 0 # 当前价值
self.best = 0 # 当前最优价值
elements = []
for i in range(self.n):
elements.append(Element(i, p[i] / w[i]))
# 根据单位重量价值排序
elements.sort(key=lambda x: x.d, reverse=True)
self.w = [w[x.id] for x in elements] # 物品重量
self.p = [p[x.id] for x in elements] # 物品价值
def solution(self):
self.backtrack(0)
return self.best
def backtrack(self, i):
if i >= self.n:
self.best = self.cp
return
if self.cw + self.w[i] <= self.c:
self.cw += self.w[i]
self.cp += self.p[i]
self.backtrack(i + 1)
self.cw -= self.w[i]
self.cp -= self.p[i]
# 即使不加入当前物品价值依旧能够超过最优
if self.bound(i + 1) > self.best:
self.backtrack(i + 1)
def bound(self, i):
r = self.c - self.cw # 剩余容量
b = self.cp # 价值量
# 剩余容量能够容纳 w[i], 按重量从小到大的装入物品
while i < self.n and self.w[i] <= r:
r -= self.w[i]
b += self.p[i]
i += 1
return b
if __name__ == "__main__":
p = [9, 10, 7, 4]
w = [3, 5, 2, 1]
c = 7
k = Knapsack(c, w, p)
print(k.solution()) # 20
计算上界 , 回溯 , 时间复杂度
最大团问题
问题描述
最大团
给定的无向图 , 若 且 有 , 则 是 的 完全子图
当且仅当 不包含在 更大的完全子图中, 则称 为 的 团
图中 两两邻接, 每条边都在 中, 因此是一个完全子图, 且不被更大的完全子图包含, 是 的一个团
可以看出, 中没有比 更大的完全子图了 ( 虽然还有 , 但它们一样大 ), 因此也称为 的 最大团
最大独立集
下图是 的补图
若 且 有 , 则称 是 的 空子图 , 空子图一定是从补图中找的
当且仅当 不被更大的独立集包含, 则称 是 的 独立集
图中 是一个独立集, 同时也是最大独立集, , 同理
解决思路
判断节点是否与当前团内所有点邻接, 若邻接则尝试加入
class MaxClique:
def __init__(self, g):
self.n = len(g) # 顶点数
self.x = [0 for _ in range(self.n)] # 当前解
self.cn = 0 # 当前顶点数
self.best = 0 # 最大顶点数
self.bestx = [] # 最优解
self.graph = g # 邻接矩阵
def solution(self):
self.backtrack(0)
res = []
for x in self.bestx:
res.append([i + 1 for i in range(len(x)) if x[i] == 1])
return self.best, res
def backtrack(self, i):
if i >= self.n:
self.bestx.append(list(self.x))
self.best = self.cn
return
ok = True
for j in range(i):
# 判断点 i 与当前团中所有点的连接情况
if self.x[j] == 1 and not self.graph[i][j]:
ok = False
break
# 点 i 与当前团全部点邻接
if ok:
self.x[i] = 1
self.cn += 1
self.backtrack(i + 1)
self.cn -= 1
# 不加点 i 且加上剩余点数还有可能比 best 更大(或相等)
if self.cn + self.n - i - 1 >= self.best:
self.x[i] = 0
self.backtrack(i + 1)
if __name__ == "__main__":
graph = [
[0, 1, 0, 1, 1],
[1, 0, 1, 0, 1],
[0, 1, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 0],
]
m = MaxClique(graph)
# (3, [[1, 2, 5], [1, 4, 5], [2, 3, 5]])
print(m.solution())
时间复杂度为
图着色
问题描述
无向连通图, 每条边的两个顶点有着不同的颜色, 至少需要 种颜色才能实现, 则称 为该图的 色数
解决思路
class Coloring:
def __init__(self, g, m) -> None:
self.n = len(g) # 顶点数
self.m = m # 可用颜色数
self.graph = g # 邻接矩阵
self.x = [0 for _ in range(self.n)] # 当前解
self.sum = 0 # 着色方案
def solution(self):
self.backtrack(0)
return self.sum
def backtrack(self, t):
if t >= self.n:
self.sum += 1
return
# 遍历所有颜色
for i in range(1, self.m + 1):
self.x[t] = i # 尝试颜色 i
if self.ok(t):
self.backtrack(t + 1)
# 恢复颜色
self.x[t] = 0
# 检测颜色可用性
def ok(self, k):
for j in range(self.n):
# k, j 邻接且颜色相等时不符合条件
if self.graph[k][j] and self.x[j] == self.x[k]:
return False
return True
if __name__ == "__main__":
graph = [
[0, 1, 1, 1, 0],
[1, 0, 1, 1, 1],
[1, 1, 0, 1, 0],
[1, 1, 1, 0, 1],
[0, 1, 0, 1, 0],
]
for i in range(1, 10):
b = Coloring(graph, i)
sum = b.solution()
if sum != 0:
print(f"m is {i}") # 4
break
时间复杂度为
货郎问题
问题描述
找到最优的回路, 使得售货员的行程最短
解决思路
class Bttsp:
def __init__(self, g):
self.n = len(g) # 顶点数
self.graph = g # 邻接矩阵
self.bestx = None # 当前最优解集
self.best = float("inf") # 当前最优值
self.x = [i for i in range(self.n)] # 当前解(默认路径 1 -> 2 -> ...)
self.cc = 0 # 当前路费
def solution(self):
self.backtrack(1)
return self.best, [i + 1 for i in self.bestx]
def backtrack(self, i):
if i == self.n - 1:
if (
# 要求 n-1 -> n 与 n -> 1 是连通的
self.graph[self.x[-2]][self.x[-1]] < float("inf")
and self.graph[self.x[-1]][0] < float("inf")
and self.best == float("inf")
# 当前路费 + n-1 -> n + n -> 1 的路费若小于最小路费
or self.cc
+ self.graph[self.x[-2]][self.x[-1]]
+ self.graph[self.x[-1]][0]
< self.best
):
self.bestx = list(self.x)
self.best = (
self.cc
+ self.graph[self.x[-2]][self.x[-1]]
+ self.graph[self.x[-1]][0]
)
return
for j in range(i, self.n):
if (
# i-1 -> j 连通
self.graph[self.x[i - 1]][self.x[j]] < float("inf")
and self.best == float("inf")
# 仅关注路费更便宜的路径
or self.cc + self.graph[self.x[i - 1]][self.x[j]] < self.best
):
# 那么让 i 点变成 j 点尝试获取路费(本来 i-1 下一个点是 i)
self.x[i], self.x[j] = self.x[j], self.x[i]
self.cc += self.graph[self.x[i - 1]][self.x[i]]
self.backtrack(i + 1)
self.cc -= self.graph[self.x[i - 1]][self.x[i]]
self.x[i], self.x[j] = self.x[j], self.x[i]
if __name__ == "__main__":
graph = [
[0, 5, 9, 4],
[5, 0, 15, 2],
[9, 15, 0, 7],
[4, 2, 7, 0],
]
b = Bttsp(graph)
# (23, [1, 2, 4, 3])
print(b.solution())
时间复杂度为
圆排列
问题描述
要求各圆与矩形底边相切, 找出最小长度的圆排列
由于规定 , 因此矩形长
解决思路
import math
class Circle:
def __init__(self, id, r) -> None:
self.id = id # 编号
self.r = r # 半径
self.x = 0.0 # 圆心坐标
class Circles:
def __init__(self, r):
self.n = len(r)
self.circles = [Circle(i, r[i]) for i in range(self.n)]
self.min = float("inf") # 当前最优值
self.res = None # 排序结果
def solution(self):
self.backtrack(0)
return self.min, [x.id + 1 for x in self.res]
def backtrack(self, t):
if t >= self.n:
self.compute()
return
# t 之前是已经排好的, t 及之后是等待排序的
for j in range(t, self.n):
# 尝试将圆 j 放到 t 位置处
self.circles[t], self.circles[j] = self.circles[j], self.circles[t]
c = self.center(t)
# 当前排列
if c + self.circles[t].r + self.circles[0].r < self.min:
self.circles[t].x = c
self.backtrack(t + 1)
self.circles[t], self.circles[j] = self.circles[j], self.circles[t]
# 计算当前圆中心横坐标
def center(self, k):
if k == 0:
return 0
d = 2 * math.sqrt(self.circles[k].r * self.circles[k - 1].r)
return self.circles[k - 1].x + d
# 计算当前圆排列长度
def compute(self):
low = self.circles[0].x - self.circles[0].r
high = self.circles[-1].x + self.circles[-1].r
if high - low < self.min:
self.min = high - low
self.res = list(self.circles)
if __name__ == "__main__":
r = [1, 1, 2, 2, 3, 5]
c = Circles(r)
(l, s) = c.solution()
# 24.14 [3, 2, 6, 1, 5, 4]
print(f"{l:.2f}", s)
时间复杂度
电路板排列(不考)
问题描述
将 块电路板以最佳的排列方案插入带有 个插槽的机箱中, 使其具有 最小密度 , 排列密度指跨越相邻电路板插槽的最大连接数
上图为一种可能的电路板排列, 跨越插槽(电路板) 2 和 3, 4 和 5, 5 和 6 都是两条连接线, 而 6 和 7 之间没有连接线, 其他的均只有一条连接线, 因此密度为 2
解决思路
class Board:
def __init__(self, bb):
self.n = len(bb) # 电路板数量
self.m = len(bb[0]) # 连接块数量
self.x = [i for i in range(self.n)] # 当前解
self.bestx = None # 最优解
self.total = [0 for _ in range(self.m)] # 连接块的电路板数
self.now = [0 for _ in range(self.m)] # 当前解中连接块的电路板数
self.b = bb # 连接块数组
self.best = self.m + 1 # 最优密度
# 计算连接块的电路板数
for i in range(self.n):
for j in range(self.m):
self.total[j] += self.b[i][j]
def solution(self):
# 从 0 号电路板开始
self.backtrack(0, 0)
return self.best, [i + 1 for i in self.bestx]
def backtrack(self, i, dd):
if i == self.n - 1:
self.bestx = list(self.x)
self.best = dd
return
for j in range(i, self.n):
d = 0 # 第 j 个电路板密度
# 获取当前解中各连接块的电路板数
for k in range(self.m):
self.now[k] += self.b[self.x[j]][k]
if self.now[k] > 0 and self.total[k] != self.now[k]:
d += 1
if dd > d:
d = dd
if d < self.best:
self.x[i], self.x[j] = self.x[j], self.x[i]
self.backtrack(i + 1, d)
self.x[i], self.x[j] = self.x[j], self.x[i]
for k in range(self.m):
self.now[k] -= self.b[self.x[j]][k]
if __name__ == "__main__":
# bb[i][j] = 1 表示电路板 i 在连接块 Nj 中
bb = [
[0, 0, 1, 0, 0],
[0, 1, 0, 0, 0],
[0, 1, 1, 1, 0],
[1, 0, 0, 0, 0],
[1, 0, 0, 0, 0],
[1, 0, 0, 1, 0],
[0, 0, 0, 0, 1],
[0, 0, 0, 0, 1],
]
b = Board(bb)
# (2, [1, 2, 3, 4, 5, 6, 7, 8])
print(b.solution())
时间复杂度
连续邮资问题
问题描述
给定 种不同面值的邮票, 每个信封 至多 贴 张, 试给出邮票的最佳设计, 使得从 开始, 增量为 的连续邮资区间达到最大?
如 时, 面值 的最大连续邮资区间为
即 可以表示为 , 表示为 , 可以表示为 , 以此类推
而 的最大连续区间为
我们要找出一个面值组合, 使得这个区间最大
解决思路
-
由于邮资需要从 开始, 因此, 面值具体值中必然有
-
当面值为 时, 可形成的连续邮资区间为
-
第二个面值必然要在 中取( 不能超过 , 因为一张 面值就会直接导致 缺失 )
-
一般情况, 的最大邮资区间若为 , 则 的取值为
class Stamps:
def __init__(self, n, m):
self.n = n
self.m = m # 最多票数
self.best = 0 # 当前最优值
self.bestx = [0 for _ in range(self.n + 1)] # 当前最优解
self.maxl = 1500
self.y = [float("inf") for _ in range(self.maxl + 1)] # 资产所需最少的邮票数
self.y[0] = 0
self.x = [0 for _ in range(self.n + 1)]
self.x[1] = 1 # 唯一选择
def solution(self):
self.backtrack(2, 1)
return self.best, self.bestx[1:]
# i 种面值, 资产 r
def backtrack(self, i, r):
for j in range(self.x[i - 2] * (self.m - 1) + 1):
if self.y[j] < self.m:
for k in range(1, self.m - self.y[j] + 1):
if self.y[j] + k < self.y[j + self.x[i - 1] * k]:
self.y[j + self.x[i - 1] * k] = self.y[j] + k
while self.y[r] < float("inf"):
r += 1
if i > self.n:
# 能表示更大的资产时, 则更新
# 因为 i 已经超过 n 了, 所以 r 要减去 1 才能表示 n 面值最大资产
if r - 1 > self.best:
self.best = r - 1
for j in range(self.n + 1):
self.bestx[j] = self.x[j]
return
z = [0 for _ in range(self.maxl + 1)]
for k in range(1, self.maxl + 1):
z[k] = self.y[k]
# 尝试 x[i - 1] + 1 ~ r 之间的所有面值
for j in range(self.x[i - 1] + 1, r + 1):
if self.y[r - j] < self.m:
self.x[i] = j
self.backtrack(i + 1, r + 1)
for k in range(1, self.maxl + 1):
self.y[k] = z[k]
if __name__ == "__main__":
s = Stamps(5, 4)
# (70, [1, 3, 11, 15, 32])
print(s.solution())