如何加快代码解决位删除难题

时间:2013-10-11 09:14:12

标签: python performance algorithm math

[这与Minimum set cover]

有关

我想通过计算机解决以下小巧的n难题。考虑长度为n的所有2 ^ n个二进制向量。对于每一个,您只删除n / 3个位,留下二进制向量长度2n / 3(假设n是3的整数倍)。目标是选择您删除的位,以便最小化保留在最后的长度为2n / 3的不同二进制向量的数量。

例如,对于n = 3,最佳答案是2个不同的向量11和00.对于n = 6,它是4,对于n = 9,它是6,对于n = 12,它是10。

我以前尝试将此问题解决为以下类型的最小集合覆盖问题。所有列表仅包含1和0。

如果您可以通过准确插入A符号从B B A,我就会说列表x包含列表n

考虑所有2 ^ n个长度为x = n/3的1和0的列表,并设置2n/3。我想计算一组长度为from collections import defaultdict from itertools import product, combinations def all_fill(source, num): output_len = (len(source) + num) for where in combinations(range(output_len), len(source)): poss = ([[0, 1]] * output_len) for (w, s) in zip(where, source): poss[w] = [s] for tup in product(*poss): (yield tup) def variable_name(seq): return ('x' + ''.join((str(s) for s in seq))) n = 12 shortn = ((2 * n) // 3) x = (n // 3) all_seqs = list(product([0, 1], repeat=shortn)) hit_sets = defaultdict(set) for seq in all_seqs: for fill in all_fill(seq, x): hit_sets[fill].add(seq) print('Minimize') print(' + '.join((variable_name(seq) for seq in all_seqs))) print('Subject To') for (fill, seqs) in hit_sets.items(): print(' + '.join((variable_name(seq) for seq in seqs)), '>=', 1) print('Binary') for seq in all_seqs: print(variable_name(seq)) print('End') 的最小列表,涵盖所有这些列表。 David Eisenstat提供了将这个最小集合覆盖问题转换为混合整数编程问题的代码,该问题可以输入到CPLEX(或http://scip.zib.de/这是开源的)。

{{1}}

问题是,如果你设置n = 15,那么它输出的实例对于我能找到的任何解算器来说都太大了。有没有更有效的方法来解决这个问题所以我可以解决n = 15甚至n = 18?

2 个答案:

答案 0 :(得分:4)

这并不能解决你的问题(好吧,不够快),但是你没有得到很多想法,而其他人可能会在这里找到一些有用的东西。

这是一个简短的纯Python 3程序,使用回溯搜索和一些贪婪的订购启发式方法。它可以非常快速地解决N = 3,6和9个实例。它也很快找到N = 12的大小为10的封面,但显然需要花费更长的时间来耗尽搜索空间(我已经没时间了,而且它还在运行)。对于N = 15,初始化时间已经很慢。

Bitstrings在这里由纯N位整数表示,因此消耗很少的存储空间。这样可以用更快的语言轻松重新编码。它确实大量使用整数集,但没有其他“高级”数据结构。

希望这有助于某人!但很明显,随着N的增加,可能性的组合爆炸确保了在没有深入研究问题数学的情况下,没有什么能够“足够快”。

def dump(cover):
    for s in sorted(cover):
        print("    {:0{width}b}".format(s, width=I))

def new_best(cover):
    global best_cover, best_size
    assert len(cover) < best_size
    best_size = len(cover)
    best_cover = cover.copy()
    print("N =", N, "new best cover, size", best_size)
    dump(best_cover)

def initialize(N, X, I):
    from itertools import combinations
    # Map a "wide" (length N) bitstring to the set of all
    # "narrow" (length I) bitstrings that generate it.
    w2n = [set() for _ in range(2**N)]
    # Map a narrow bitstring to all the wide bitstrings
    # it generates.
    n2w = [set() for _ in range(2**I)]
    for wide, wset in enumerate(w2n):
        for t in combinations(range(N), X):
            narrow = wide
            for i in reversed(t):  # largest i to smallest
                hi, lo = divmod(narrow, 1 << i)
                narrow = ((hi >> 1) << i) | lo
            wset.add(narrow)
            n2w[narrow].add(wide)
    return w2n, n2w

def solve(needed, cover):
    if len(cover) >= best_size:
        return
    if not needed:
        new_best(cover)
        return
    # Find something needed with minimal generating set.
    _, winner = min((len(w2n[g]), g) for g in needed)
    # And order its generators by how much reduction they make
    # to `needed`.
    for g in sorted(w2n[winner],
                    key=lambda g: len(needed & n2w[g]),
                    reverse=True):
        cover.add(g)
        solve(needed - n2w[g], cover)
        cover.remove(g)

N = 9  # CHANGE THIS TO WHAT YOU WANT

assert N % 3 == 0
X = N // 3 # number of bits to exclude
I = N - X  # number of bits to include

print("initializing")
w2n, n2w = initialize(N, X, I)
best_cover = None
best_size = 2**I + 1  # "infinity"
print("solving")
solve(set(range(2**N)), set())

N = 9的示例输出:

initializing
solving
N = 9 new best cover, size 6
    000000
    000111
    001100
    110011
    111000
    111111

跟进

对于N = 12,这最终完成,确认最小覆盖集包含10个元素(它在开始时很快发现)。我没有时间,但至少花了5个小时。

为什么?因为它接近脑死亡;-) 完全天真的搜索将尝试256个8位短字符串的所有子集。有2 ** 256个这样的子集,大约1.2e77 - 它不会在宇宙的预期寿命中完成; - )

这里的排序噱头首先检测到“全0”和“全1”短字符串必须在任何覆盖集中,所以选择它们。这让我们只看了254个剩余的短字符串。然后,贪婪的“选择一个覆盖最多的元素”策略很快找到 a 覆盖集合,总共有11个元素,此后不久将覆盖10个元素。这恰好是最佳的,但耗费所有其他可能性需要很长时间。

此时,任何覆盖集合达到10个元素的尝试都将被中止(它不可能小于而不是10个元素!)。如果完全天真地完成,则需要尝试添加(对于“全0”和“全1”字符串)剩余254的所有8个元素子集,并且254-选择-8约为3.8e14。远小于1.2e77 - 但仍然太大而不实用。这是一个有趣的练习,可以理解代码如何设法做得更好。提示:它与此问题中的数据有很大关系。

工业强度求解器无比复杂和复杂。我对这个简单的小程序在较小的问题实例上的表现感到惊喜!幸运的是。

但是对于N = 15,这种简单的方法是没有希望的。它很快就找到了一个包含18个元素的封面,但至少在几个小时内没有更明显的进展。在内部,它仍然使用包含数百(甚至数千)个元素的needed个集合,这使solve()的正文非常昂贵。它仍然需要考虑2 ** 10 - 2 = 1022个短字符串,1022-choose-16约为6e34。即使这个代码加速了一百万,我也不指望它会有明显的帮助。

尝试这很有趣: - )

和一个小的重写

这个版本在完整的N = 12运行中运行速度至少快6倍,只需在一级之前切断无效搜索。还可以加速初始化,并通过将2 ** N w2n个集合更改为列表来减少内存使用(对这些集合不使用任何设置操作)。然而,对于N = 15,它仍然没有希望: - (

def dump(cover):
    for s in sorted(cover):
        print("    {:0{width}b}".format(s, width=I))

def new_best(cover):
    global best_cover, best_size
    assert len(cover) < best_size
    best_size = len(cover)
    best_cover = cover.copy()
    print("N =", N, "new best cover, size", best_size)
    dump(best_cover)

def initialize(N, X, I):
    from itertools import combinations
    # Map a "wide" (length N) bitstring to the set of all
    # "narrow" (length I) bitstrings that generate it.
    w2n = [set() for _ in range(2**N)]
    # Map a narrow bitstring to all the wide bitstrings
    # it generates.
    n2w = [set() for _ in range(2**I)]
    # mask[i] is a string of i 1-bits
    mask = [2**i - 1 for i in range(N)]
    for t in combinations(range(N), X):
        t = t[::-1]  # largest i to smallest
        for wide, wset in enumerate(w2n):
            narrow = wide
            for i in t:  # delete bit 2**i
                narrow = ((narrow >> (i+1)) << i) | (narrow & mask[i])
            wset.add(narrow)
            n2w[narrow].add(wide)
    # release some space
    for i, s in enumerate(w2n):
        w2n[i] = list(s)
    return w2n, n2w

def solve(needed, cover):
    if not needed:
        if len(cover) < best_size:
            new_best(cover)
        return
    if len(cover) >= best_size - 1:
        # can't possibly be extended to a cover < best_size
        return
    # Find something needed with minimal generating set.
    _, winner = min((len(w2n[g]), g) for g in needed)
    # And order its generators by how much reduction they make
    # to `needed`.
    for g in sorted(w2n[winner],
                    key=lambda g: len(needed & n2w[g]),
                    reverse=True):
        cover.add(g)
        solve(needed - n2w[g], cover)
        cover.remove(g)

N = 9  # CHANGE THIS TO WHAT YOU WANT

assert N % 3 == 0
X = N // 3 # number of bits to exclude
I = N - X  # number of bits to include

print("initializing")
w2n, n2w = initialize(N, X, I)

best_cover = None
best_size = 2**I + 1  # "infinity"
print("solving")
solve(set(range(2**N)), set())

print("best for N =", N, "has size", best_size)
dump(best_cover)

答案 1 :(得分:-1)

首先考虑一下你是否有6位。你可以扔掉2位。因此,任何模式平衡6-0,5-1或4-2都可以转换为0000或1111.在3-3零一平衡的情况下,任何模式都可以转换为四种情况之一:1000,0001 ,因此,6位的一个可能的最小值是:

0000
0001
0111
1110
1000
1111

现在考虑9位丢弃3。您有以下14种主模式:

000000
100000
000001
010000
000010
110000
000011
001111
111100
101111
111101
011111
111110
111111

换句话说,每个模式集在中心都有1/0,每端都有n / 3-1位的每个排列。例如,如果你有24位,那么你将在中心有17位,在末端有7位。由于2 ^ 7 = 128,您将有4 x 128 - 2 = 510种可能的模式。

要找到正确的删除,有各种算法。一种方法是找到当前位集和每个主模式之间的编辑距离。具有最小编辑距离的模式是要转换的模式。此方法使用动态编程。另一种方法是使用一组规则对模式进行树搜索以找到匹配模式。