查找与一组比特串最不相似的比特串

10

我有一组二进制字符串:{'0011', '1100', '1110'}(集合内的所有二进制字符串长度相同)。

我想要快速找到与该集合具有最小最大相似度的二进制字符串。 最大相似度可以按如下方式计算:

def max_similarity(bitstring, set):
    max = 0
    for item in set:
        temp = 0
        for i in range(len(bitstring)):
            if bitstring[i] == item[i]:
                temp += 1
        if temp > max:
            max = temp
    return max

我知道可以遍历该长度的所有可能位串,计算每个位串的最大相似度,最终保留这些迭代中最小的一个。但是,这种方法的时间复杂度为O(2^n)。我想知道是否有更快的替代方案。

我一直在尝试使用Python的异或操作:

def int2bin(integer, digits):
    if integer >= 0:
        return bin(integer)[2:].zfill(digits)
    else:
        return bin(2**digits + integer)[2:]


def XOR(bitset):  
    intset = [int('{}'.format(bitstring), 2) for bitstring in bitset]

    digits = len(bitset.pop())

    if len(intset) == 1:
        return int2bin(~intset.pop(), digits)        
    else:
        curr = 0    
        while intset:
            curr = curr ^ intset.pop()

        return int2bin(curr, digits)

if __name__ == '__main__':
    bitset1 = {'0011', '1100', '1110'}
    bitset2 = {'01001', '11100', '10111'}
    print(XOR(bitset1))
    print(XOR(bitset2))

>>> python test.py
0001
00010

(函数int2bin从这里抄袭而来)

但我发现它适用于一些输入,但不适用于其他输入。 在上面的测试中,它为bitset2找到了正确的解决方案,但没有为bitset1找到。 有任何O(2^n)以下的解决方案可用吗?


4
暴力算法的时间复杂度是O(2^n),而不是O(n^2)! - Davis Herring
我投票关闭这个问题,因为它是关于抽象算法而不是编程。你可以尝试在计算机科学 StackExchange 上提问。 - Davis Herring
1
谢谢您的纠正。似乎我的问题在任何地方都是“离题”的。然而,它是一个具体的问题,而不是抽象的。 - RoyM
1
当然,确切的重复可以被丢弃,但如果两个字符串除了一位之外完全相同,应该对所有位进行异或操作而忽略它们吗? - Davis Herring
1
嗯,太好了!(它没有向我建议算法。)如果这个问题没有被关闭,你应该回答它。 - Davis Herring
显示剩余10条评论
3个回答

11

此问题在一定程度上涉及算法(获得解决方案的最佳算法是什么)和Python问题(使用Python的哪些部分来有效地实现最佳算法)。

就算法而言:您可以定义位字符串到一组(相同大小的)位字符串的最大距离为目标位字符串与集合中任何一个字符串不同的位数。该算法的目标是查找具有与集合中字符串相同长度的新位字符串,其具有最低的最大距离。

假设所有起始位字符串都不同(因为它被定义为集合,而不是列表)。要计算的距离称为汉明距离,因此您正在寻找具有最小汉明距离的新位字符串以与一组起始字符串匹配。

生成正确长度的所有可能位字符串并计算到每个起始字符串的最大距离会暴力解决问题,可以使用回溯(一旦候选位字符串的最低当前最大值超过,则立即放弃结果)进行优化(*)。

(*: 对于想要纠正我的拼写的人,请考虑我使用的是英式英语,而不是美式英语-请随意提出改进意见)

但是,问题也可以被视为以下形式。

对于长度为1的位字符串,整个字符串空间只有两种选择,{'0','1'}。这可以可视化为'0''1'位于长度为1的线段的两端,彼此距离为1。

对于长度为2的位字符串,整个字符串空间有4种选择,即0到3的位表示形式{'00','01','10','11'}。 0距离1和2都是1,而1和2都距离3为1。当可视化时,它们形成正方形的四个角落,其中任何一个都不会比其他任何一个更远超过2步。

对于长度为3的位字符串,整个空间有8个选项,即0到7的位表示形式。可视化时,它们形成立方体的8个角,其中任何一个都不会比其他任何一个更远超过3步。

该模式继续(进入4D超立方体,5D等),找到答案的有效方法转化为:给定这些图表中的一组角落,请找到最低最大距离与任何一个角落。

给定这样的图表并查找这样一个点的算法将是:

  1. 首先将集合中的元素单独列出来; 如果只有一个元素,那就是微不足道的答案。
  2. 将当前距离设置为1。
  3. 针对所有集合,将仅与已在集合中的点相隔一步的任何点添加到其中。
  4. 交集所有产生的集合; 如果交集不为空,则这些点都是从起始点集开始的当前距离(或更少)的所有点; 否则,将当前距离增加1并转到步骤3。

对于长位字符串,通过跟踪添加到集合中的点来避免重复添加相同的点,可以进一步优化代码效率。即,不是将{'001'}变成{'001'、'101'、'011'、'000'},而是从[{'001'}]变为[{'001'},{'101'、'011'、'000'}]——集合的并集仍然可以得到您可以到达的所有在1个或更少步内的点,但系列中的下一步将更容易计算,因为可以找到所有向外步进一步的点,但排除前面方向的点。

实际上,查找一步点非常简单,如果将字符串转换为它们表示的数字,并将这些数字与具有相同位字符串长度的所有单个“1”位数字计算排他或运算,即要找到所有与'001'相隔一步的点,则可以将1和4、2和1异或,得到{5,3,0},匹配正确的点。

将所有内容组合在一起,形成Python代码(未优化长字符串):

def closest(strings):
    if len(strings) == 1:
        return next(iter(strings))

    size = len(next(iter(strings)))
    points = [{int(s, 2)} for s in strings]
    powers = {1 << n for n in range(size)}

    d = 0
    while True:
        d += 1
        points = [{n ^ p for p in powers for n in nums} | nums for nums in points]
        intersection = set.intersection(*points)
        if len(intersection) > 0:
            return d, {f"{n:b}".zfill(size) for n in intersection}


print(closest({'1000', '0001', '0011'}))

注意,closest返回实际距离和所有最优答案,而不仅仅是一个。输出:

(2, {'0000', '0010', '1001', '0001', '1011'})

将所讨论的优化添加到closest函数中:

def closest_optimised(strings):
    if len(strings) == 1:
        return next(iter(strings))

    size = len(next(iter(strings)))
    points = [({int(s, 2)}, {int(s, 2)}) for s in strings]
    powers = {1 << n for n in range(size)}

    d = 0
    while True:
        d += 1
        new_points = [{n ^ p for p in powers for n in rp} - ap for ap, rp in points]
        points = [(ap | np, np) for (ap, _), np in zip(points, new_points)]
        intersection = set.intersection(*[ap for ap, _ in points])
        if len(intersection) > 0:
            return d, {f"{n:b}".zfill(size) for n in intersection}
请注意,通过性能分析器运行此代码后,优化后的代码在这些设置下平均运行时间减少了约一半。
from random import randint

s = 10
x = 500
numbers = [randint(0, 2**s-1) for _ in range(x)]
number_strings = {f"{n:b}".zfill(s) for n in numbers}
print(number_strings)
print(closest_optimised(number_strings))
print(closest(number_strings))

编辑:出于好奇,我将我的示例与问题中提供的原始示例进行了比较,发现它经常返回远非最优结果。我没有尝试找出原因,但我觉得有必要提一下。

有人指出,OP实际上可能希望找到与所有提供的位串具有最大汉明距离的点。采用类似的方法:

def farthest(strings):
    s = next(iter(strings))
    size = len(s)
    if len(strings) == 1:
        return ''.join(['0' if c == '1' else '1' for c in s])

    all_visited = {int(s, 2) for s in strings}
    visited = [set(), all_visited]
    powers = {1 << n for n in range(size)}

    d = 0
    while True:
        d += 1
        visited.append({n ^ p for p in powers for n in visited[-1]} - all_visited)
        all_visited = all_visited | visited[-1]
        if len(all_visited) == 2**size:
            return d, {f"{n:b}".zfill(size) for n in visited[-1]}

请注意,我假设 Python 的 set.intersection() 实现是最优的,即当前几个集合的交集已经为空时,它不会继续尝试求交集 - 否则,这是另一个可以通过自己编写 set.intersection() 进行优化的地方。 - Grismar
我认为OP想要找到最大的汉明距离,因为那是最不相似的。而不是最小的汉明距离。 - stacksonstacks
相应地更新,具有最近和最远的汉明距离。 - Grismar
有一些小问题,比如它不能将一组仅由零解释为仅由1。但这很容易修复。 - RoyM
1
修复的问题是在return ''.join(['0' if c == '1' else '0' for c in s])中有一个打字错误,应该是return ''.join(['0' if c == '1' else '1' for c in s])。如果你想要更改它,我不能对少于6个字符进行编辑,显然。 - RoyM
显示剩余3条评论

0
这是一个算法,其成本为O(n * b),其中n是集合的大小,b是固定的位长度。
该算法的直觉是检查每个位索引(0或1)的大多数位位置,并相应地进行评分。
更高的得分意味着给定的位串在大多数情况下都与大多数位相反。尽管如此,我还没有处理并列的情况。
import operator

def max_hamming(bitstrings):
    n_bits = len(bitstrings[0])
    # Track bit set/unset for each bit position
    scores = {
        n: {'0': [], '1': []} for n in range(n_bits)
    }
    # Increment on each bit position if not with the majority
    total = {b: 0 for b in bitstrings}

    # O(b * n)
    for n in range(n_bits):
        n_set = 0
        for b in bitstrings:
            is_set = b[n]
            scores[n][is_set].append(b)
            if is_set:
                n_set += 1

        # If majority have this bit set, give a point to those with unset or vice versa
        outliers = scores[n]['0'] if n_set > len(bitstrings) else scores[n]['1']
        for s in outliers:
            total[s] += 1

    return max(total.items(), key=operator.itemgetter(1))[0]

另外一点需要注意的是,我传递的是一个列表而不是一个集合,因为Python集合在排序方面是不确定的。

用法:

bitset1 = [
    '0011',
    '1100',
    '1110'
]
bitset2 = [
    '01001',
    '11100',
    '10111'
]
print(max_hamming(bitset1))
print(max_hamming(bitset2))

我可能在原帖中表达目标不够清晰,对此我很抱歉。但基本上我正在寻找与给定的 all_possible_bitstrings_of_len_n (APBN) 相同的输出,for bitstring in APBN: current = max(similarity(bitstring, bstring) for bstring in bitstrings),其中 similarity 如 OP 中所述,然后将 current 最小化。这将始终产生所需的结果。但是速度非常慢。 - RoyM

0

我能使用numpy吗,还是它应该是一个算法?让我们假设一切都像您拥有的那样是bitstring

import numpy as np

def bitstring2np(bitstring):
    """
    Convert a bitstring to np.array
    i.e. '0011' to np.array([0, 0, 1, 1])
    """
    return np.array([int(bit) for bit in bitstring], dtype=int)

def unlike(bitset):
    """
    Gets the most 'unlike' string between a bitset.
    Accomplishes this by creating a 2D array from the bitsets,
    figuring out the number of 1s in a column, and if that
    number of 1s is >=50%, then gives it a 0 in that place, otherwise
    gives it a 1.
    """
    bset = list(bitset)
    # Create an empty 2D array to store the bitsets into
    arr = np.empty((len(bset), len(bset[0])), dtype=int)
    for idx in range(len(bset)):
        # Store that bitset into the row of our array
        arr[idx,:] = bitstring2np(bset[idx])

    # Count the number of 1's in each column
    nonzero = np.count_nonzero(arr, axis=0)
    total = len(bset) # how many to compare against
    # Since you want the most unlike and since we are counting
    # number of 1s in a column, if the rate is >=.5 give it a 0, otherwise 
    # 1
    most_unlike = ''.join('0' if count/total >=.5 else '1' for count in nonzero)

    return most_unlike


>>> print(unlike(bitset1))
0001
>>> print(unlike(bitset2))  
00010

现在我知道你说0001不是bitset的正确解决方案,但我很确定除非我没有正确理解问题。

1
和 @stacksonstacks 一样,你正在计算具有最大平均汉明距离的字符串,而不是最小距离。 - Davis Herring
@DavisHerring 那两个提供的位集合示例的答案应该是什么? - Chrispresso
对于0011/1100/1110,答案可以是0000、0101或1001中的任意一个。对于01001/11100/10111,最佳答案之一是00010;我尚未检查它是否存在并列。 - Davis Herring
这实际上是第二种情况的最佳独特答案。 - Davis Herring
哦,我现在明白问题了。我创建的是一个与整个位集最不相似的位串,而不是与单个位串最不相似的。那我得再考虑一下…… - Chrispresso

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接