Подтвердить что ты не робот

Найдите k неповторяющихся элементов в списке с "маленьким" дополнительным пространством

Исходная постановка задачи - это:

Учитывая массив из 32-битных целых без знака, в которых каждое число отображается ровно дважды, за исключением трех из них (которые появляются ровно один раз), найдите эти три числа в O (n), используя O (1) дополнительное пространство. Входной массив доступен только для чтения. Что делать, если есть k исключений вместо 3?

Легко решить это в Ο(1) времени и Ο(1) пространстве, если вы принимаете очень высокий постоянный коэффициент из-за ограничения ввода (массив может иметь не более 2 33 записей):

for i in lst:
    if sum(1 for j in lst if i == j) == 1:
        print i

Итак, для этого вопроса давайте отбросить ограничение по длине бит и сконцентрироваться на более общей проблеме, где числа могут иметь до m бит.

Обобщая алгоритм для k = 2, я имел в виду следующее:

  • XOR эти числа с наименьшим значащим разрядом 1 и те, у которых 0 отдельно. Если для обоих разделов результирующее значение не равно нулю, мы знаем, что мы разделили неповторяющиеся числа на две группы, каждая из которых имеет хотя бы один член
  • Для каждой из этих групп попробуйте разбить его дальше, исследуя второй-младший бит и т.д.

Однако есть особый случай, который следует учитывать. Если после разбиения группы значения XOR одной из групп равны нулю, мы не знаем, является ли одна из результирующих подгрупп пустой или нет. В этом случае мой алгоритм просто покидает этот бит и продолжает следующий, что неверно, например, он терпит неудачу для ввода [0,1,2,3,4,5,6].

Теперь идея состояла в том, чтобы вычислить не только XOR элемента, но и XOR значений после применения некоторой функции (здесь я выбрал f(x) = 3x + 1). См. Нижеприведенный ответ Евгения для встречного примера для этой дополнительной проверки.

Теперь, хотя приведенный ниже алгоритм не подходит для k >= 7, я по-прежнему включаю реализацию здесь, чтобы дать вам представление:

def xor(seq):
  return reduce(lambda x, y: x ^ y, seq, 0)

def compute_xors(ary, mask, bits):
  a = xor(i for i in ary if i & mask == bits)
  b = xor(i * 3 + 1 for i in ary if i & mask == bits)
  return a if max(a, b) > 0 else None

def solve(ary, high = 0, mask = 0, bits = 0, old_xor = 0):
  for h in xrange(high, 32):
    hibit = 1 << h
    m = mask | hibit
    # partition the array into two groups
    x = compute_xors(ary, m, bits | hibit)
    y = compute_xors(ary, m, bits)
    if x is None or y is None:
      # at this point, we can't be sure if both groups are non-empty,
      # so we check the next bit
      continue
    mask |= hibit
    # we recurse if we are absolutely sure that we can find at least one
    # new value in both branches. This means that the number of recursions
    # is linear in k, rather then exponential.
    solve(ary, h + 1, mask, bits | hibit, x)
    solve(ary, h + 1, mask, bits, y)
    break
  else:
    # we couldn't find a partitioning bit, so we output (but 
    # this might be incorrect, see above!)
    print old_xor

# expects input of the form "10 1 1 2 3 4 2 5 6 7 10"
ary = map(int, raw_input().split())
solve(ary, old_xor=xor(ary))

Из моего анализа этот код имеет худшую временную сложность O(k * m² * n), где n - количество входных элементов (XORing - O(m) и не более k операции секционирования могут быть успешными) и пространства сложность O(m²) (поскольку m - максимальная глубина рекурсии, а временные числа могут быть длиной m).

Конечно, вопрос заключается в правильном и эффективном подходе с хорошей асимптотической средой выполнения (предположим, что k << n и m << n здесь для полноты), что также требует небольшого дополнительного пространства (например, подходы что сортировка ввода не будет принята, потому что для этого нам понадобится как минимум O(n) дополнительное пространство, так как мы не можем изменить вход!).

РЕДАКТИРОВАТЬ: Теперь, когда доказанный алгоритм окажется неправильным, было бы, конечно, приятно видеть, как это можно сделать правильным, возможно, сделав его немного менее эффективным. Космическая сложность должна быть в o(n*m) (т.е. Сублинейно в общем числе входных битов). Было бы хорошо принять k в качестве дополнительного ввода, если это облегчит задачу.

4b9b3361

Ответ 1

Одним вероятностным подходом к использованию будет использовать счетный фильтр.

Алгоритм выглядит следующим образом:

  • Линейно сканировать массив и "обновлять" счетный фильтр.
  • Линейно сканируйте массив и создайте коллекцию всех элементов, которые не обязательно имеют счетчик 2 в фильтре, это будет <= k реальных решений. (В этом случае ложные срабатывания являются уникальными элементами, которые выглядят так, как будто они не являются).
  • Выбираем новый базис хэш-функций и повторяем до тех пор, пока не получим все решения k.

Использует 2m бит пространства (независимо от n). Сложность времени больше связана, но, зная, что вероятность того, что какой-либо данный уникальный элемент не будет найден на шаге 2, будет приблизительно (1 - e^(-kn/m))^k, мы будем решать решение очень быстро, но, к сожалению, мы не вполне линейны в n.

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

Ответ 2

Я вышел в автономный режим и доказал исходный алгоритм, согласно гипотезе, что трюки XOR сработали. Как это бывает, трюки XOR не работают, но следующий аргумент может по-прежнему интересовать некоторых людей. (Я повторно сделал это в Haskell, потому что я нахожу доказательства намного проще, когда у меня есть рекурсивные функции вместо циклов, и я могу использовать структуры данных. Но для Pythonistas в аудитории я старался использовать списки, когда это возможно.)

Компилируемый код http://pastebin.com/BHCKGVaV.

Красивая теория, убитая уродливым фактом

Проблема: нам предоставляется последовательность из n отличных от нуля 32-битных слов в каждый элемент которого является либо одноточечным, либо двойным:

  • Если слово появляется ровно один раз, оно равно одному.

  • Если слово отображается ровно дважды, оно является двойным.

  • Ни слова не появляются три или более раз.

Задача состоит в том, чтобы найти одиночные числа. Если есть три синглетонов, мы должны использовать линейное время и постоянное пространство. Больше в общем случае, если существует k однотонных чисел, мы должны использовать время O (k * n) и O (k). Алгоритм опирается на недоказанную гипотезу об эксклюзиве или.

Начнем с этих основ:

module Singleton where
import Data.Bits
import Data.List
import Data.Word
import Test.QuickCheck hiding ((.&.))

Абстракция ключа: частичная спецификация слова

Чтобы решить эту проблему, я собираюсь ввести абстракцию: описать наименее значимые $w $биты 32-битного слова, I введем a Spec:

data Spec = Spec { w :: Int, bits :: Word32 }
   deriving Show
width = w -- width of a Spec

A Spec соответствует слову, если младшие значащие бит w равны до bits. Если w равно нулю, по определению все слова соответствуют:

matches :: Spec -> Word32 -> Bool
matches spec word = width spec == 0 ||
                    ((word `shiftL` n) `shiftR` n) == bits spec
  where n = 32 - width spec

universalSpec = Spec { w = 0, bits = 0 }

Вот некоторые утверждения о Spec s:

  • Все слова соответствуют universalSpec, которые имеют ширину 0

  • Если matches spec word и width spec == 32, то word == bits spec

Основная идея: "расширить" частичную спецификацию

Вот ключевая идея алгоритма: мы можем расширить a Spec на добавив еще один бит в спецификацию. Расширение a Spec выдает список из двух Spec s

extend :: Spec -> [Spec]
extend spec = [ Spec { w = w', bits = bits spec .|. (bit `shiftL` width spec) }
              | bit <- [0, 1] ]
  where w' = width spec + 1

И здесь ключевое утверждение: если Spec соответствует word, и если width spec меньше 32, то точно один из двух спецификаций от extend spec соответствует word. Доказательство в каждом случае соответствующий бит word. Это требование настолько важно, что я назовем его Лемма 1 Здесь тест:

lemmaOne :: Spec -> Word32 -> Property
lemmaOne spec word =
  width spec < 32 && (spec `matches` word) ==> 
      isSingletonList [s | s <- extend spec, s `matches` word]

isSingletonList :: [a] -> Bool
isSingletonList [a] = True
isSingletonList _   = False

Мы будем определять функцию, которая задает a Spec и a последовательность 32-битных слов, возвращает список одноточечных слов которые соответствуют спецификации. Функция будет занимать время пропорционально длина ввода умножает время ответа 32 и дополнительное пространство, пропорциональное размеру времени ответа 32. До мы решаем основные функции, мы определяем XOR функции.

Идеи XOR, которые сломаны

Функция xorWith f ws применяет функцию f к каждому слову в ws и возвращает исключение или результат.

xorWith :: (Word32 -> Word32) -> [Word32] -> Word32
xorWith f ws = reduce xor 0 [f w | w <- ws]
  where reduce = foldl'

Благодаря потоку слияния (см. ICFP 2007) функция xorWith принимает постоянное пространство.

Список ненулевых слов имеет одноэлементность тогда и только тогда, когда исключительным или отличным от нуля, или если эксклюзивный или 3 * w + 1 отлична от нуля. ( "Если" направление тривиально. "Только если" направление гипотеза, которую Евгений Клюев опроверг; для контрпримера, см. массив testb ниже. Я могу сделать пример Евгения, добавив третья функция g, но, очевидно, эта ситуация требует доказательство, и у меня его нет.)

hasSingleton :: [Word32] -> Bool
hasSingleton ws = xorWith id ws /= 0 || xorWith f ws /= 0 || xorWith g ws /= 0
  where f w = 3 * w + 1
        g w = 31 * w + 17

Эффективный поиск синглтонов

Наша основная функция возвращает список всех синглтонов, соответствующих спецификации.

singletonsMatching :: Spec -> [Word32] -> [Word32]
singletonsMatching spec words =
 if hasSingleton [w | w <- words, spec `matches` w] then
   if width spec == 32 then
     [bits spec]       
   else
     concat [singletonsMatching spec' words | spec' <- extend spec]
 else
   []

Мы докажем его правильность индукцией по ширине Spec.

  • Основной случай заключается в том, что Spec имеет ширину 32. В этом случае список будет давать список слов, которые точно равной bits spec. Функция hasSingleton вернет True, если и только если этот список имеет ровно один элемент, который будет истинным точно, когда bits spec является одноточечным в words.

  • Теперь докажем, что если singletonsMatching верно для с m + 1, это также верно для ширины m, где * m < 32 $. (Это направление, как обычно, для индукции, но оно не имеет значения.)

    Вот фрагмент, который сломан: для более узкой ширины hasSingleton может возвращать False даже при заданном массиве одиночных чисел. Это трагично.

    Вызов extend spec на Spec ширины m возвращает две спецификации которые имеют ширину $m + 1 $. По условию singletonsMatching исправьте эти спецификации. Чтобы доказать: что результат точно те синглтоны, которые соответствуют Spec. По лемме 1 любое слово, которое совпадения Spec соответствуют точно одному из расширенных спецификаций. От гипотеза, рекурсивные вызовы возвращают именно одноточие соответствующие спецификации расширения. Когда мы объединяем результаты этих звонки с помощью concat, мы получаем точно совпадающие синглтоны с нет дубликатов и без упущений.

Фактически решение проблемы является неустойчивым: синглтоны все синглтоны, которые соответствуют пустой спецификации:

singletons :: [Word32] -> [Word32]
singletons words = singletonsMatching universalSpec words

Код тестирования

testa, testb :: [Word32]
testa = [10, 1, 1, 2, 3, 4, 2, 5, 6, 7, 10]
testb = [ 0x0000
        , 0x0010
        , 0x0100
        , 0x0110
        , 0x1000
        , 0x1010
        , 0x1100
        , 0x1110
        ]

Помимо этого, если вы хотите следить за тем, что происходит, вам нужно знать QuickCheck.

Здесь случайный генератор для specs:

instance Arbitrary Spec where
  arbitrary = do width <- choose (0, 32)
                 b <- arbitrary
                 return (randomSpec width b)
  shrink spec = [randomSpec w' (bits spec) | w' <- shrink (width spec)] ++
                [randomSpec (width spec) b | b  <- shrink (bits spec)]
randomSpec width bits = Spec { w = width, bits = mask bits }     
  where mask b = if width == 32 then b
                 else (b `shiftL` n) `shiftR` n
        n = 32 - width

Используя этот генератор, мы можем проверить лемму 1, используя quickCheck lemmaOne.

Мы можем проверить, что любое слово, заявленное как синглтон, находится в факт singleton:

singletonsAreSingleton nzwords = 
  not (hasTriple words) ==> all (`isSingleton` words) (singletons words)
  where isSingleton w words = isSingletonList [w' | w' <- words, w' == w]
        words = [w | NonZero w <- nzwords]

hasTriple :: [Word32] -> Bool
hasTriple words = hasTrip (sort words)
hasTrip (w1:w2:w3:ws) = (w1 == w2 && w2 == w3) || hasTrip (w2:w3:ws)
hasTrip _ = False

Здесь другое свойство, которое проверяет быстрый singletons на медленный алгоритм, который использует сортировку.

singletonsOK :: [NonZero Word32] -> Property
singletonsOK nzwords = not (hasTriple words) ==>
  sort (singletons words) == sort (slowSingletons words)
 where words = [w | NonZero w <- nzwords ]
       slowSingletons words = stripDoubletons (sort words)
       stripDoubletons (w1:w2:ws) | w1 == w2 = stripDoubletons ws
                                  | otherwise = w1 : stripDoubletons (w2:ws)
       stripDoubletons as = as

Ответ 3

Устранение алгоритма в OP для k >= 7

Этот алгоритм использует возможность рекурсивного разделения набора k уникальных значений на две группы с использованием значения одного бита, когда хотя бы одна из этих групп XORed имеет ненулевое значение. Например, следующие числа

01000
00001
10001

можно разделить на

01000

и

00001
10001

используя значение младшего значащего бита.

Если это правильно реализовано, это работает для k <= 6. Но этот подход не подходит для k= 8 и k= 7. Пусть предположим m= 4 и используйте 8 четных чисел от 0 до 14:

0000
0010
0100
0110
1000
1010
1100
1110

Каждый бит, кроме наименее значимого, имеет ровно 4 ненулевых значения. Если мы попытаемся разбить это множество, из-за этой симметрии мы всегда получим подмножество с 2 или 4 или 0 ненулевыми значениями. XOR этих подмножеств всегда 0. Это не позволяет алгоритму делать какие-либо расщепления, поэтому часть else просто печатает XOR всех этих уникальных значений (один ноль).

3x + 1 трюк не помогает: он только перемещает эти 8 значений и переключает младший значащий бит.

Точно такие же аргументы применимы для k= 7, если мы удалим первое (все-нулевое) значение из вышеуказанного подмножества.

Так как любая группа уникальных значений может быть разбита на группу из 7 или 8 значений и некоторую другую группу, этот алгоритм также терпит неудачу для k > 8.


Вероятностный алгоритм

Можно не изобретать совершенно новый алгоритм, но вместо этого изменить алгоритм в OP, чтобы он работал для любых входных значений.

Каждый раз, когда алгоритм обращается к элементу входного массива, он должен применить к этому элементу некоторую функцию преобразования: y=transform(x). Это преобразованное значение y может использоваться точно так же, как x было использовано в исходном алгоритме - для разбиения наборов и XORing значений.

Изначально transform(x)=x (немодифицированный исходный алгоритм). Если после этого шага мы получаем меньше k результатов (некоторые из результатов представляют собой несколько уникальных значений XORed), мы меняем transform на некоторую хеш-функцию и повторяем вычисления. Это должно повторяться (каждый раз с другой функцией хэша), пока мы не получим ровно k.

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

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

Чтобы получить различную хеш-функцию для каждого шага алгоритма, вы можете использовать универсальное хэширование. Одним из необходимых свойств хэш-функции является обратимость - исходное значение должно (теоретически) восстанавливаться из хэш-значения. Это необходимо, чтобы избежать хэширования нескольких "уникальных" значений с тем же значением хэш-функции. Поскольку использование любой обратимой хешированной функции m не имеет большого шанса решить проблему "контрпример", значения хэша должны быть длиннее бит m. Одним простым примером такой хеш-функции является конкатенация исходного значения и некоторая односторонняя хеш-функция этого значения.

Если k не очень большой, вряд ли мы получим набор данных, аналогичный этому встречному примеру. (У меня нет доказательств того, что нет других "плохих" шаблонов данных с разной структурой, но пусть надеется, что они также не очень вероятны). В этом случае средняя временная сложность не намного больше O (k * m 2 * n).


Другие улучшения исходного алгоритма

  • При вычислении XOR всех (еще нераспределенных) значений разумно проверять уникальное нулевое значение в массиве. Если он есть, просто уменьшите k.
  • На каждом этапе рекурсии мы не всегда можем знать точный размер каждого раздела. Но мы знаем, что это нечетно или четно: каждый разброс на ненулевом бите дает подмножество нечетного размера, другая четность подмножества - это "перевернутая" четность исходного подмножества.
  • На последних этапах рекурсии, когда единственное нерасщепимое подмножество имеет размер 1, мы можем пропустить поиск бита разделения и немедленно сообщить результат (это оптимизация для очень маленького k).
  • Если мы получим подмножество нечетного размера после некоторого split (и если мы точно не знаем, что его размер равен 1), сканируйте массив и попытайтесь найти уникальное значение, равное XOR этого подмножества.
  • Нет необходимости перебирать каждый бит, чтобы разделить набор с четным размером. Просто используйте любой ненулевой бит его значений XORed. XORing одного из полученных подмножеств может привести к нулю, но этот раскол по-прежнему действителен, потому что у нас есть нечетное число "единиц" для этого бита разделения, но даже заданный размер. Это также означает, что любой split, который производит подмножество четного размера, отличное от нуля при XORed, является допустимым расщеплением, даже если остальные подмножества XOR равны нулю.
  • Вы не должны продолжать разбивать бит-поиск на каждую рекурсию (например, solve(ary, h + 1...). Вместо этого вы должны перезапустить поиск с самого начала. Можно разбить набор на бит 31 и иметь единственную возможность расщепления для одного из полученных подмножеств на бит 0.
  • Вам не следует дважды сканировать весь массив (так что второй y = compute_xors(ary, m, bits) не нужен). У вас уже есть XOR всего набора и XOR подмножества, где бит разделения отличен от нуля. Это означает, что вы можете сразу вычислить y: y = x ^ old_xor.

Доказательство алгоритма в OP при k = 3

Это доказательство не для реальной программы в OP, а для ее идеи. Фактическая программа в настоящее время отклоняет любой раскол, когда одно из полученных подмножеств равно нулю. См. Предлагаемые улучшения для случаев, когда мы можем принять некоторые из таких расколов. Таким образом, следующее доказательство может быть применено к этой программе только после того, как if x is None or y is None будет изменено на какое-либо условие, которое учитывает четность размеров подмножества или после добавления шага предварительной обработки для исключения уникального нулевого элемента из массива.

У нас есть 3 разных числа. Они должны отличаться по крайней мере на 2 битовых позициях (если они отличаются только одним битом, третье число должно быть равно одному из других). Петля в функции solve находит самую левую из этих позиций битов и разделяет эти 3 числа на два подмножества (одного числа и из двух разных чисел). Подмножество с двумя номерами имеет равные биты в этой битовой позиции, но числа все равно должны быть разными, поэтому должно быть еще одно положение битов бит (очевидно, справа от первого). Второй шаг рекурсии легко разбивает это подмножество на 2 числа на два отдельных числа. Трюк с i * 3 + 1 здесь избыточен: он только удваивает сложность алгоритма.

Вот иллюстрация для первого раскола в наборе из трех чисел:

 2  1
*b**yzvw
*b**xzvw
*a**xzvw

У нас есть цикл, который выполняет итерацию через каждую позицию бита и вычисляет XOR всех слов, но отдельно, одно значение XOR (A) для истинных битов в заданной позиции, другое значение XOR (B) для ложного бита. Если число A имеет нулевой бит в этой позиции, A содержит XOR некоторого подмножества значений определенного размера, если не нуль - нечетное подмножество. То же самое верно для Б. Мы заинтересованы только в подмножестве четного размера. Он может содержать 0 или 2 значения.

Пока нет разницы в битовых значениях (биты z, v, w), мы имеем A = B = 0, что означает, что мы не можем разбить наши числа на эти биты. Но у нас есть 3 не равных числа, что означает, что в некоторой позиции (1) мы должны иметь разные биты (x и y). Один из них (x) можно найти в двух наших числах (подмножество четного размера!), Другое (y) - в одном числе. Посмотрите на XOR значений в этом подмножестве четного размера. Из A и B выберите значение (C), содержащее бит 0 в позиции 1. Но C является всего лишь XOR двух неравных значений. Они равны в битовой позиции 1, поэтому они должны отличаться, по меньшей мере, еще одной битовой позицией (позиция 2, бит a и b). Итак, C!= 0, и это соответствует подмножеству четного размера. Этот раскол действителен, потому что мы можем разделить это подмножество четного размера дальше либо очень простым алгоритмом, либо следующей рекурсией этого алгоритма.

Если в массиве нет уникальных нулевых элементов, это доказательство может быть упрощено. Мы всегда разбиваем уникальные числа на 2 подмножества - один с 2 элементами (и он не может XOR равняться нулю, потому что элементы разные), другие - с одним элементом (отличным от нуля по определению). Поэтому исходная программа с небольшой предварительной обработкой должна работать должным образом.

Сложность - это O (m 2 * n). Если вы примените улучшения, которые я предложил ранее, ожидаемое количество раз, когда этот алгоритм сканирует массив, m/3 + 2. Поскольку ожидается, что первая позиция бит расщепления будет m/3, требуется одно сканирование для работы с 2-элементным подмножеством, каждому подмножеству из 1 элемента не требуется никаких сканирований массивов, и сначала требуется другое сканирование (вне метода solve).


Доказательство алгоритма в OP при k = 4.. 6

Здесь мы предполагаем, что все предлагаемые улучшения исходного алгоритма применяются.

k = 4 и k = 5: поскольку существует хотя бы одна позиция с разными битами, этот набор чисел можно разбить таким образом, что одно из подмножеств имеет размер 1 или 2 Если размер подмножества равен 1, он отличен от нуля (у нас нет нулевых уникальных значений). Если размер подмножества равен 2, мы имеем XOR двух разных чисел, что отличает ненулевое значение. Таким образом, в обоих случаях раскол действителен.

k = 6. Если XOR всего множества отличное от нуля, мы можем разбить это множество на любую позицию, где этот XOR имеет ненулевой бит. В противном случае у нас есть четное число ненулевого бита в каждой позиции. Поскольку существует по крайней мере одно положение с разными битами, это положение разбивает набор на подмножества размеров 2 и 4. Подмножество размера 2 всегда отличает ненулевое значение XOR, поскольку оно содержит 2 разных числа. Опять же, в обоих случаях мы имеем действительный раскол.


Детерминированный алгоритм

Disproof для k >= 7 показывает шаблон, в котором исходный алгоритм не работает: у нас есть подмножество размером больше 2, и в каждой битовой позиции у нас есть четное число ненулевых битов. Но мы всегда можем найти пару позиций, где ненулевые биты перекрываются в одном числе. Другими словами, всегда можно найти пару позиций в подмножестве размером 3 или 4 с ненулевым XOR всех бит в подмножестве в позициях. Это позволяет нам использовать дополнительную разделенную позицию: итерацию по битовым позициям с двумя отдельными указателями, группирование всех чисел в массиве на два подмножества, где одно подмножество имеет как ненулевые биты в этих позициях, так и другие - все остальные числа. Это увеличивает худшую сложность моего m, но позволяет больше значений для k. Как только невозможно получить подмножество размером менее 5, добавьте третий "указатель расщепления" и т.д. Каждый раз, когда k удваивается, нам может понадобиться дополнительный "указатель расщепления", что еще раз увеличивает сложность худшего случая my m.

Это можно рассматривать как эскиз доказательства для следующего алгоритма:

  • Используйте оригинальный (улучшенный) алгоритм, чтобы найти нулевые или более уникальные значения и ноль или более нерасщепляемых подмножеств. Остановитесь, когда нет больше нерасщепляемых подмножеств.
  • Для любого из этих нерасщепляемых подмножеств попробуйте разбить его, одновременно увеличивая количество "указателей разбиения". Когда раскол найден, перейдите к шагу 1.

Худшая сложность случая - это O (k * m 2 * n * m max (0, floor (log (floor ( k/4))))), который может быть аппроксимирован O ( k * n * m log (k)) = O ( k * n * k log (m)).

Ожидаемое время выполнения этого алгоритма для малого k немного хуже, чем для вероятностного алгоритма, но все же не намного больше O (k * m 2 * n).

Ответ 4

Вот правильное решение для случая k = 3, которое занимает только минимальное пространство, а требование пространства - O (1).

Пусть 'transform' - это функция, которая принимает m-разрядное беззнаковое целое число x и индекс я в качестве аргументов. я находится между 0.. m - 1, а преобразование принимает целое число x в

  • x, если i-й бит x не установлен
  • до x ^ (x < 1), где < обозначает сдвиг ствола (вращение)

Используйте в следующем T (x, i) как сокращение для преобразования (x, i).

Теперь я утверждаю, что если a, b, c - три различных m-разрядных целых без знака и a ', b', c 'и другие три различных m-разрядных целых без знака, такие как XOR b XOR c == a' XOR b 'XOR c', но множества {a, b, c} и {a ', b', c '} - это два разных множества, то существует индекс я такой, что T (a, i) XOR T ( b, i) XOR T (c, i) отличается от T (a ', i) XOR T (b', i) XOR T (c ', i).

Чтобы увидеть это, дайте '== a XOR a' ', b' == b XOR b '' и c '== c XOR c' ', т.е. пусть a' 'обозначает XOR a и a 'и т.д. Поскольку XOR b XOR c равно' XOR b 'XOR c' на каждом бите, следует, что '' XOR b '' XOR c '' == 0. Это означает, что в каждой битовой позиции либо ', b', c 'идентичны a, b, c, или ровно два из них имеют бит в выбранной позиции, перевернутый (0- > 1 или 1- > 0). Поскольку a, b ', c' отличаются от a, b, c, пусть P является любой битовой позицией, где были два бита. Покажем, что T (a ', P) XOR T (b', P) XOR T (c ', P) отличается от T (a, P) XOR T (b, P) XOR T (c, P), Предположим без потери общности, что a имеет бит flip по сравнению с a, b 'имеет бит flip по сравнению с b, а c' имеет такое же значение бит, как c в этой позиции P.

В дополнение к битовой позиции P должно быть другое битовое положение Q, в котором a 'и b' различаются (в противном случае наборы не состоят из трех различных целых чисел, или переворачивание бит в позиции P не создает новый набор целых чисел, случай, который не нужно рассматривать). XOR цилиндра с поворотным вариантом битовой позиции Q создает ошибку четности в позиции бита (Q + 1) mod m, что приводит к утверждению, что T (a ', P) XOR T (b', P) XOR T (c ', P) отличается от T (a, P) XOR T (b, P) XOR T (c, P). Фактическое значение c ', очевидно, не влияет на ошибку четности.

Следовательно, алгоритм равен

  • выполняется через входной массив и вычисляет (1) XOR всех элементов и (2) XOR для T (x, i) для всех элементов x и я между 0.. m - 1
  • поиск в постоянном пространстве для трех 32-битовых целых чисел a, b, c таких, что XOR b XOR c и T (a, i) XOR b (a, i) XOR c (a, i) для всех допустимых значений of я соответствуют тем, которые были вычислены из массива

Это работает, очевидно, потому, что дублирующие элементы исключаются из операций XOR, а для остальных трех элементов имеет место рассуждение выше.

I ВЫПОЛНИТ ЭТО, и он работает. Вот исходный код моей тестовой программы, которая использует 16-разрядные целые числа для скорости.

#include <iostream>
#include <stdlib.h>
using namespace std;

/* CONSTANTS */
#define BITS  16
#define MASK ((1L<<(BITS)) - 1)
#define N   MASK
#define D   500
#define K      3
#define ARRAY_SIZE (D*2+K)

/* INPUT ARRAY */
unsigned int A[ARRAY_SIZE];

/* 'transform' function */
unsigned int bmap(unsigned int x, int idx) {
    if (idx == 0) return x;
    if ((x & ((1L << (idx - 1)))) != 0)
        x ^= (x << (BITS - 1) | (x >> 1));
    return (x & MASK);
}

/* Number of valid index values to 'transform'. Note that here
   index 0 is used to get plain XOR. */
#define NOPS 17

/* Fill in the array --- for testing. */
void fill() {
    int used[N], i, j;
    unsigned int r;
    for (i = 0; i < N; i++) used[i] = 0;
    for (i = 0; i < D * 2; i += 2)
    {
        do { r = random() & MASK; } while (used[r]);
        A[i] = A[i + 1] = r;
        used[r] = 1;
    }
    for (j = 0; j < K; j++)
    {
        do { r = random() & MASK; } while (used[r]);
        A[i++] = r;
        used[r] = 1;
    }
}

/* ACTUAL PROCEDURE */
void solve() {
    int i, j;
    unsigned int acc[NOPS];
    for (j = 0; j < NOPS; j++) { acc[j] = 0; }
    for (i = 0; i < ARRAY_SIZE; i++)
    {
        for (j = 0; j < NOPS; j++)
            acc[j] ^= bmap(A[i], j);
    }
    /* Search for the three unique integers */
    unsigned int e1, e2, e3;
    for (e1 = 0; e1 < N; e1++)
    {
        for (e2 = e1 + 1; e2 < N; e2++)
        {
            e3 = acc[0] ^ e1 ^ e2; // acc[0] is the xor of the 3 elements
            /* Enforce increasing order for speed */
            if (e3 <= e2 || e3 <= e1) continue;
            for (j = 0; j < NOPS; j++)
            {
                if (acc[j] != (bmap(e1, j) ^ bmap(e2, j) ^ bmap(e3, j)))
                    goto reject;
            }
            cout << "Solved elements: " << e1
                 << ", " << e2 << ", " << e3 << endl;
            exit(0);
          reject:
            continue;
        }
    }
}

int main()
{
    srandom(time(NULL));
    fill();
    solve();
}

Ответ 5

Я предполагаю, что вы знаете k заранее Я выбираю Squeak Smalltalk как язык реализации.

  • inject: in: уменьшается и является O (1) в пространстве, O (N) во времени
  • select: is filter, (мы не используем его, потому что требуется O (1))
  • collect: is map, (мы не используем его, потому что требуется O (1))
  • do: is forall и является O (1) в пространстве, O (N) во времени
  • блок в квадратных скобках является закрытием или чистой лямбдой, если он не закрывает какую-либо переменную и не использует return, символ, предваряемый двоеточиями, является параметрами.
  • ^ означает возврат

При k = 1 синглтон получается путем уменьшения последовательности с битом xor

Итак, мы определяем метод xorSum в классе Collection (таким образом, self является последовательностью)

Collection>>xorSum
    ^self inject: 0 into: [:sum :element | sum bitXor:element]

и второй метод

Collection>>find1Singleton
    ^{self xorSum}

Мы тестируем его с помощью

 self assert: {0. 3. 5. 2. 5. 4. 3. 0. 2.} find1Singleton = {4}

Стоимость O (N), пространство O (1)

При k = 2 мы ищем два синглтона, (s1, s2)

Collection>>find2Singleton
    | sum lowestBit s1 s2 |
    sum := self xorSum.

сумма отличается от 0 и равна (s1 битXOr: s2), xor двух синглетонов

Разделите бит с наименьшим набором бит, и как обе эти последовательности, как вы предложили, вы получите 2 синглтона

    lowestBit := sum bitAnd: sum negated.
    s1 := s2 := 0.
    self do: [:element |
        (element bitAnd: lowestBit) = 0
            ifTrue: [s1 := s1 bitXor: element]
            ifFalse: [s2 := s2 bitXor: element]].
    ^{s1. s2}

и

 self assert: {0. 1. 1. 3. 5. 6. 2. 6. 4. 3. 0. 2.} find2Singleton sorted = {4. 5}

Стоимость 2 * O (N), пробел O (1)

При k = 3,

Мы определяем конкретный класс, реализующий небольшое изменение xor split, на самом деле мы используем тройное разделение, маска может иметь значение1 или value2, любое другое значение игнорируется.

Object
    subclass: #BinarySplit
    instanceVariableNames: 'sum1 sum2 size1 size2'
    classVariableNames: '' poolDictionaries: '' category: 'SO'.

с помощью этих методов экземпляра:

sum1
    ^sum1

sum2
    ^sum2

size1
    ^size1

size2
    ^size2

split: aSequence withMask: aMask value1: value1 value2: value2
    sum1 := sum2 := size1 := size2 := 0.
    aSequence do: [:element |
    (element bitAnd: aMask) = value1
            ifTrue:
                [sum1 := sum1 bitXor: element.
                size1 := size1 + 1].
    (element bitAnd: aMask) = value2
            ifTrue:
                [sum2 := sum2 bitXor: element.
                size2 := size2 + 1]].

doesSplitInto: s1 and: s2
    ^(sum1 = s1 and: [sum2 = s2])
        or: [sum1 = s2 and: [sum2 = s1]]

И этот метод стороны класса, своего рода конструктор для создания экземпляра

split: aSequence withMask: aMask value1: value1 value2: value2
    ^self new split: aSequence withMask: aMask value1: value1 value2: value2

Затем мы вычисляем:

Collection>>find3SingletonUpToBit: m
    | sum split split2 mask value1 value2 |
    sum := self xorSum.

Но это не дает никакой информации о бит для разделения... Итак, мы пробуем каждый бит я = 0..m-1.

    0 to: m-1 do: [:i |
        split := BinarySplit split: self withMask: 1 << i value1: 1<<i value2: 0.

Если вы получаете (sum1, sum2) == (0, sum), то вы нечетко получили 3 синглтона в одной сумке...
Повторяйте, пока не получите что-то другое. В противном случае вы получите сумку с s1 (с нечетным размером), а другая с s2, s3 (ровный размер), поэтому просто примените алгоритм для k = 1 (s1 = sum1) и k = 2 с помощью модифицированный битовый рисунок

        (split doesSplitInto: 0 and: sum)
            ifFalse:
                [split size1 odd
                    ifTrue:
                        [mask := (split sum2 bitAnd: split sum2 negated) + (1 << i).
                        value1 := (split sum2 bitAnd: split sum2 negated).
                        value2 := 0.
                        split2 := BinarySplit split: self withMask: mask value1: value1 value2: value2.
                        ^{ split sum1. split2 sum1. split2 sum2}]
                    ifFalse:
                        [mask := (split sum1 bitAnd: split sum1 negated) + (1 << i).
                        value1 := (split sum1 bitAnd: split sum1 negated) + (1 << i).
                        value2 := (1 << i).
                        split2 := BinarySplit split: self withMask: mask value1: value1 value2: value2.
                        ^{ split sum2. split2 sum1. split2 sum2}]].

И мы тестируем его с помощью

self assert: ({0. 1. 3. 5. 6. 2. 6. 4. 3. 0. 2.} find3SingletonUpToBit: 32) sorted = {1. 4. 5}

Хуже стоит (M + 1) * O (N)

При k = 4,

Когда мы расщепляемся, мы можем иметь (0,4) или (1,3) или (2,2) одиночные числа.
(2,2) легко распознать, оба размера четные, и обе xor sum отличаются от 0, если они решены.
(0,4) легко распознать, оба размера четные, и по крайней мере одна сумма равна нулю, поэтому повторите поиск с добавочным битовым рисунком на сумке с суммой!= 0
(1,3) сложнее, так как оба размера нечетны, и мы возвращаемся к случаю с неизвестным числом синглтонов... Хотя мы можем легко распознать единственный синглтон, если элемент мешка равен сумме xor, что невозможно с 3 разными номерами...

Мы можем обобщить для k = 5... но выше будет сложно, потому что мы должны найти трюк для случая (4,2) и (1,5), помните нашу гипотезу, мы должны знать k заранее... Мы должны будем сделать гипотезу и проверить их потом...

Если у вас есть встречный пример, просто отправьте его, я проверю с помощью реализации Smalltalk

EDIT: Я совершил код (лицензия MIT) на http://ss3.gemstone.com/ss/SONiklasBContest.html

Ответ 6

С требованиями к сложности пространства ослабьте до O (m * n), эта задача может быть легко решена в O ( n) времени, Просто подсчитайте количество экземпляров для каждого элемента, используя хеш-таблицу, затем отфильтруйте записи с помощью счетчика, равного единице. Или используйте любой алгоритм сортировки сортировки.

Но вот вероятностный алгоритм, имеющий более легкие требования к пространству.

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

Сканирование массива еще раз, мы можем извлечь уникальные значения (исключая некоторые ложные отрицания), а также некоторые повторяющиеся значения (ложные срабатывания).

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

Чтобы определить оптимальный размер для битового набора, распределите доступное пространство равномерно между битетом и временным массивом, содержащим как уникальные значения, так и ложные срабатывания (при условии, что k < n): s= n * m * k/ s, что дает s= sqrt (n * m * k). И ожидаемое требование к пространству - O (sqrt ( n * m * k)).

  • Сканировать входной массив и переключать бит в битете.
  • Сканировать входной массив и элементы фильтра с соответствующим ненулевым битом в битете, записать их во временный массив.
  • Используйте любой простой подход (сортировка или хеш распределения), чтобы исключить дубликаты из временного массива.
  • Если размер временного массива плюс количество уникальных элементов, известных до сих пор, меньше k, измените хеш-функцию, очистите биты и биты переключения, соответствующие известным уникальным значениям, продолжайте с шага 1.

Ожидаемая временная сложность находится где-то между O (n * m) и O ( n * m * log ( n * m * k)/log ( n * m/ к)).

Ответ 7

Ваш алгоритм не O (n), потому что нет никакой гарантии разделить числа на две одинаковые группы размеров на каждом шаге, также потому, что в ваших размерах нет ограничений (они не связаны с n), нет ограничений для ваших возможных шагов, если у вас нет ограничений на размеры ваших входных номеров (если они не зависят от n), время выполнения вашего алгоритма может быть & omega; (n), принять ниже числа размера бит m и только их первые бит n могут быть разными: (предположим m > 2n)

---- n bits --- ---- m-n bits --
111111....11111 00000....00000
111111....11111 00000....00000
111111....11110 00000....00000
111111....11110 00000....00000
....
100000....00000 00000....00000

Ваш алгоритм будет работать для первых бит m-n, и он будет O(n) на каждом шаге, до сих пор вы пришли O ((mn) * n), который больше, чем O (n ^ 2).

PS: если у вас всегда есть 32-битные числа, ваш алгоритм O(n) и не сложно это доказать.

Ответ 8

Это просто интуиция, но я думаю, что решение состоит в том, чтобы увеличить количество разделов, которые вы оцениваете, до тех пор, пока не найдете тот, где его xor-сумма не равна нулю.

Например, для каждых двух битов (x, y) в диапазоне [0, m) рассмотрим разделы, определяемые значением a & ((1<<x) || (1 << y)). В 32-битном случае это приводит к 32 * 32 * 4 = 4096 разделам и позволяет правильно решить случай, когда k = 4.

Интереснее было бы найти связь между k и количеством разделов, необходимых для решения проблемы, что также позволило бы вычислить сложность алгоритма. Другой открытый вопрос - если есть лучшие схемы разбиения.

Некоторый код Perl для иллюстрации идеи:

my $m = 10;
my @a = (0, 2, 4, 6, 8, 10, 12, 14, 15, 15, 7, 7, 5, 5);

my %xor;
my %part;
for my $a (@a) {
    for my $i (0..$m-1) {
        my $shift_i = 1 << $i;
        my $bit_i = ($a & $shift_i ? 1 : 0);
        for my $j (0..$m-1) {
            my $shift_j = 1 << $j;
            my $bit_j = ($a & $shift_j ? 1 : 0);
            my $k = "$i:$bit_i,$j:$bit_j";
            $xor{$k} ^= $a;
            push @{$part{$k} //= []}, $a;
        }
    }
}

print "list: @a\n";
for my $k (sort keys %xor) {
    if ($xor{$k}) {
        print "partition with unique elements $k: @{$part{$k}}\n";
    }
    else {
        # print "partition without unique elements detected $k: @{$part{$k}}\n";
    }
}

Ответ 9

Решение первой проблемы (поиск уникальных чисел uint32 в O (N) с использованием памяти O (1)) довольно просто, хотя и не особенно быстро:

void unique(int n, uint32 *a) {
  uint32 i = 0;
  do {
    int j, count;
    for (count = j = 0; j < n; j++) {
      if (a[j] == i) count++;
    }
    if (count == 1) printf("%u appears only once\n", (unsigned int)i);
  } while (++i);
}

В случае, когда количество бит M не ограничено, сложность становится O (N * M * 2 M), а использование памяти по-прежнему равно O (1).

update: дополнительное решение с использованием растрового изображения приводит к сложности O (N * M) и использованию памяти O (2 M):

void unique(int n, uint32 *a) {
  unsigned char seen[1<<(32 - 8)];
  unsigned char dup[1<<(32 - 8)];
  int i;
  memset(seen, sizeof(seen), 0);
  memset(dup,  sizeof(dup),  0);
  for (i = 0; i < n; i++) {
    if (bitmap_get(seen, a[i])) {
      bitmap_set(dup, a[i], 1);
    }
    else {
      bitmap_set(seen, a[i], 1);
    }
  }
  for (i = 0; i < n; i++) {
    if (bitmap_get(seen, a[i]) && !bitmap_get(dup, a[i])) {
      printf("%u appears only once\n", (unsigned int)a[i]);
      bitmap_set(seen, a[i], 0);
    }
  }
}

Интересно, что оба подхода можно объединить, разделив пространство 2 M в полосах. Затем вам придется перебирать все полосы и внутри каждой группы находить уникальные значения, используя технику бит-вектора.

Ответ 10

Два подхода будут работать.

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

(2) сортировать массив (или копию), а затем подсчитывать количество случаев, когда array [n + 2] == array [n]. Конечно, это будет использовать больше времени, чем указано.

Я буду очень удивлен, увидев решение, которое удовлетворяет исходным ограничениям.