回溯法&分支限界

312 阅读6分钟

多米诺性质

X=<x1,x2,,xn>X=<x_1,x_2,\cdots, x_n> 为问题的解, 而 Xi=<x1,x2,,xi>X_i=<x_1,x_2,\cdots,x_i>Xi+1=<x1,x2,,xi+1>X_{i+1}=<x_1, x_2,\cdots,x_{i+1}> 分别是第 ii 层和第 i+1i+1 层的解, 若 P(Xi+1)P(Xi)P(X_{i+1})\rightarrow P(X_i) , 即若 Xi+1X_{i+1} 满足条件则 XiX_i 也满足, 那么称该问题满足多米诺性质

背包问题

Xi=k=1i=wkxkX_i=\sum_{k=1}^i=w_kx_k 以及 Xi+1=k=1i+1=wkxkX_{i+1}=\sum_{k=1}^{i+1}=w_kx_k

由于 Xi+1CX_{i+1}\le Cwi+1xi+10w_{i+1}x_{i+1}\ge 0 , 因此 XiCX_i\le C

反例

5x1+4x2x310,1xi35x_1+4x_2-x_3\le 10, 1\le x_i \le 3

由于 5x1+4x2x3105x_1+4x_2-x_3\le 10 成立而 5x1+4x2105x_1+4x_2\le 10 不一定成立, 所以不满足多米诺性质

x3=3x3x_3'=3-x_3 , 则原式子改为 5x1+4x2+x3135x_1+4x_2+x_3'\le 13 , 满足多米诺性质

算法效率

以四皇后为例

image.png

随机选择路径, 由于是四皇后, 因此假设 4 条路径节点数是一致的

image.png

image.png

装载问题

问题描述

nn 个集装箱装到载重量为 c1c_1c2c_2 的轮船上, 集装箱重 wiw_i , 是否存在一个合理的装载方案将 nn 个集装箱装到两艘轮船上?

解决思路

  • 尽可能的将第一艘轮船装满

  • 剩余的集装箱装到第二艘轮船

需要剪去如下分支

cw=i=1nwixi>c1cw=\sum_{i=1}^nw_ix_i\gt c_1
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())

需要 O(n)O(n) 的栈空间

由于搜索树有 2n2^n 个节点( 装或不装 ), 因此回溯的时间复杂度为 O(2n)O(2^n)

上界函数

维护一个剩余重量 rr , 若

cw+rbestcw + r \le best

则说明剩下的集装箱即使全部加起来都无法达到最优, 因此没必要考虑

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]

批处理作业调度

问题描述

nn 项作业, 需要在两台机子上依次完成, 所有作业在机器 22 上完成处理的时间和称为该作业调度的完成时间和, 记为

f=i=1nF2if=\sum_{i=1}^nF_{2i}

image.png

当作业调度方案为 1, 2, 3 时, 作业调度完成时间和为

f=3+6+10=19f=3 + 6 + 10 = 19

image.png

解决思路

在递归前尝试交换两个作业的调度顺序, 回溯时换回来

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())

时间复杂度为 O(n!)O(n!) , 可以理解为计算每一个调度组合, 如 n=3n=3 时调度组合数为 A33=n!A_3^3=n!

符号三角形(不考)

问题描述

两个同号下面都是 + 号, 两个异号下面都是 - 号, 符号三角形第一行有 nn 个字符, 要求对于给定的 nn , 计算有多少个不同的符号三角形, 使其所有的 + 和 - 个数相同

image.png

解决思路

第一行有 ii 个, 则符号三角形由 i×(i+1)2\frac{i×(i+1)}{2} 个符号构成, 要求 + 号与 - 号个数相同, 则其个数均不超过 n×(n+1)4\frac{n×(n+1)}{4}

n×(n+1)2\frac{n×(n+1)}{2} 为奇数时, 不存在该符号三角形

只需要在 [1:i] 的符号三角形右边增加一边, 即可扩展成 [1:i+1]

image.png

尝试在第一行最右边加上 + 或 - 号, 则底下的所有符号都固定了, 尝试添加这些符号

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

计算可行性约束 O(n)O(n) , 最坏情况下需要计算 O(2n)O(2^n) 个节点的可行性约束, 因此时间复杂度为 O(n2n)O(n2^n)

n 后问题

问题描述

n×nn×n 的方格中放置彼此不受攻击的 nn 个皇后, 国际象棋中皇后可以攻击同一行、同一列、同一斜线上的棋子

解决思路

在某一行中尝试所有列, 若该列不存在冲突则尝试放置皇后, 若冲突则继续尝试下一列

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

计算上界 O(n)O(n) , 回溯 O(2n)O(2^n) , 时间复杂度 O(n2n)O(n2^n)

最大团问题

问题描述

最大团

给定的无向图 G=(V,E)G=(V,E) , 若 UVU\subseteq Vu,vU\forall u, v\in U(u,v)E(u, v)\in E , 则 UUGG完全子图

当且仅当 UU 不包含在 GG 更大的完全子图中, 则称 UUGG

image.png

图中 {1,2,5}\{1,2,5\} 两两邻接, 每条边都在 GG 中, 因此是一个完全子图, 且不被更大的完全子图包含, 是 GG 的一个团

可以看出, GG 中没有比 {1,2,5}\{1,2,5\} 更大的完全子图了 ( 虽然还有 {1,4,5}\{1,4,5\} , 但它们一样大 ), 因此也称为 GG最大团

最大独立集

下图是 GG 的补图

image.png

UVU\subseteq Vu,vU\forall u,v \in U(u,v)E(u,v)\notin E , 则称 UUGG空子图 , 空子图一定是从补图中找的

当且仅当 UU 不被更大的独立集包含, 则称 UUGG独立集

图中 {2,4}\{2,4\} 是一个独立集, 同时也是最大独立集, {4,3}\{4,3\} , {1,3}\{1,3\} 同理

解决思路

判断节点是否与当前团内所有点邻接, 若邻接则尝试加入

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())

时间复杂度为 O(n2n)O(n2^n)

图着色

问题描述

无向连通图, 每条边的两个顶点有着不同的颜色, 至少需要 mm 种颜色才能实现, 则称 mm 为该图的 色数

image.png

解决思路

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

时间复杂度为 O(nmn)O(nm^n)

货郎问题

问题描述

找到最优的回路, 使得售货员的行程最短

image.png

解决思路

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())

时间复杂度为 O(n!)O(n!)

圆排列

问题描述

要求各圆与矩形底边相切, 找出最小长度的圆排列

image.png

dk=(rk1+rk)2(rk1rk)2=2rk1rkd_k = \sqrt{(r_{k-1}+r_k)^2-(r_{k-1}-r_k)^2}=2\sqrt{r_{k-1}r_k}
xk=xk1+dkx_k = x_{k-1}+d_k

由于规定 x0=0x_0 = 0 , 因此矩形长

lk=xk+rk+r1l_k = x_k + r_k + r_1

image.png

解决思路

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)

时间复杂度 O((n+1)!)O((n+1)!)

电路板排列(不考)

问题描述

nn 块电路板以最佳的排列方案插入带有 nn 个插槽的机箱中, 使其具有 最小密度 , 排列密度指跨越相邻电路板插槽的最大连接数

image.png

上图为一种可能的电路板排列, 跨越插槽(电路板) 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())

时间复杂度 O(mn!)O(mn!)

连续邮资问题

问题描述

给定 nn 种不同面值的邮票, 每个信封 至多mm 张, 试给出邮票的最佳设计, 使得从 11 开始, 增量为 11 的连续邮资区间达到最大?

n=5n=5 时, 面值 {1,3,11,15,32}\{1,3,11,15,32\} 的最大连续邮资区间为 1701 \thicksim 70

7070 可以表示为 32+32+3+332+32+3+3 , 6969 表示为 32+15+11+1132+15+11+11 , 6868 可以表示为 32+32+3+132+32+3+1 , 以此类推

{1,6,10,20,30}\{1,6,10,20,30\} 的最大连续区间为 141\thicksim 4

我们要找出一个面值组合, 使得这个区间最大

解决思路

  • 由于邮资需要从 11 开始, 因此, 面值具体值中必然有 11

  • 当面值为 11 时, 可形成的连续邮资区间为 1m1\thicksim m

  • 第二个面值必然要在 2m+12\thicksim m+1 中取( 不能超过 m+2m+2 , 因为一张 m+2m+2 面值就会直接导致 m+1m+1 缺失 )

  • 一般情况, x[1:i1]x[1:i-1] 的最大邮资区间若为 [1:r][1:r] , 则 x[i]x[i] 的取值为 [x[i1]+1:r+1][x[i-1]+1:r+1]

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())