Поиск цепочки битов наиболее отличается от набора цепочек битов - PullRequest
10 голосов
/ 07 апреля 2019

У меня есть набор цепочек битов: {'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).Я хотел бы знать, видит ли кто-нибудь более быстрые альтернативы.

Я играл с Pythons XOR:

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) доступны?

Ответы [ 3 ]

11 голосов
/ 09 апреля 2019

Этот вопрос частично алгоритмический (какой алгоритм лучше всего подходит для решения) и частично вопрос Python (какие части Python использовать для эффективной реализации этого лучшего алгоритма).

По алгоритму: вы определяете максимальное расстояние для битовой строки до набора (одинакового размера) битовых строк как наибольшее число битов, на которое целевая битовая строка отличается от любой из строк в наборе. Цель алгоритма - найти новую битовую строку той же длины, что и строки в наборе, который имеет наименьшее максимальное расстояние.

Предполагается, что все начальные битовые строки различны (так как они определены как набор, а не список). Расстояние, которое вы вычисляете, называется расстоянием Хэмминга, поэтому вы ищете новую битовую строку с минимальным расстоянием Хемминга до начального набора строк.

Генерация всех возможных битовых строк правильной длины и вычисление максимального расстояния до каждой стартовой строки - грубая проблема, которая может быть оптимизирована (*) с помощью обратного отслеживания (отбрасывание результата, как только самый низкий максимальный ток будет превышен для битовая строка-кандидат).

(*: для людей, которые хотят исправить мое правописание, учтите, что я использую британский английский, а не американский английский - не стесняйтесь предлагать улучшения с учетом этого)

Однако проблему также можно рассматривать следующим образом.

Для битовых строк длиной 1 все пространство строк имеет только две опции, {'0', '1'}. Это можно представить как '0' и '1', сидящие на обоих концах отрезка линии длиной 1, оба на расстоянии 1 друг от друга.

Для битовых строк длиной 2 все пространство строк имеет 4 варианта, а именно битовые представления от 0 до 3 {'00', '01', '10', '11'} 0 - это расстояние 1 от 1 и 2, оба из которых находятся на расстоянии 1 от 3. Когда они визуализируются, они образуют четыре угла квадрата, ни один из которых не находится на расстоянии более двух шагов от любого другого.

Для битовых строк длиной 3 у всего пространства есть 8 опций, а именно битовые представления от 0 до 7. При визуализации образуют 8 углов куба, ни один из которых не превышает 3 шага от любого другого.

Этот паттерн продолжается (в 4-мерном гиперкубе, 5-мерном и т. Д.), И нахождение ответа на задачу эффективно приводит к следующему: при заданном наборе углов на одном из этих графиков найдите точку с наименьшим максимальным расстоянием до любого из им.

Алгоритм для нахождения такой точки, учитывая график, подобный этому:

  1. Начните со списка точек в наборе самих по себе; если есть только один, это тривиальный ответ.
  2. Установить текущее расстояние равным 1.
  3. Для всех наборов добавьте в него любую точку, находящуюся всего в одном шаге от точек, уже находящихся в наборе.
  4. Пересечь все получившиеся множества; если пересечение не пустое, это все точки, которые находятся на текущем (или меньшем) расстоянии от начального набора точек; в противном случае увеличьте текущее расстояние на 1 и перейдите к шагу 3.

Вероятно, это можно было бы оптимизировать и дальше, отслеживая посещенные точки по мере их добавления к наборам (для длинных битовых строк), чтобы избежать добавления одинаковых точек снова и снова, быстро замедляя данный алгоритм. То есть вместо того, чтобы превратить {'001'} в {'001', '101', '011', '000'}, вы можете перейти от [{'001'}] к [{'001'}, {'101', '011', '000'}] - объединение наборов по-прежнему дает вам все точки, достижимые за 1 или менее шагов, но следующий шаг в серии будет проще вычислить, найдя все точки, которые находятся на шаг дальше, но исключая точки в предыдущем направлении.

Нахождение точек на один шаг на самом деле довольно просто, если вы превращаете строки в числа, которые представляете, и вычисляете эксклюзивные побитовые числа или числа со всеми одиночными 1-битными числами с одинаковой битовой строкой. длина, т. е. чтобы найти все точки на расстоянии одного шага от '001', вы можете xor 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))

Редактировать: Из любопытства я проверил свой пример на оригинале, приведенном в вопросе, и обнаружил, что он часто возвращает далеко не оптимальный результат.Я не пытался выяснить, почему, но я решил, что стоит упомянуть.

Кто-то указал, что ОП может на самом деле хотеть точку с максимальным расстоянием Хэмминга до всех предоставленных битовых строк.С похожим подходом:

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]}
0 голосов
/ 10 апреля 2019

Могу ли я использовать 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, но я уверен, что это так, если я не правильно понял вопрос.

0 голосов
/ 09 апреля 2019

Вот алгоритм со стоимостью 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]

Также отметим, что я передаю этот список вместо набора, потому что наборы питонов недетерминированы с их порядком.

Использование:

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