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

оригинальная постановка проблемы такова:

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

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

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

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

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

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

существует особый случай, который следует рассмотреть. Если после разбиения группы значения 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) дополнительное пространство для этого, как мы не можем изменить вход!).

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

10 ответов


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

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

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

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

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


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

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

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

проблема: нам дана последовательность n ненулевые 32-битные слова в который каждый элемент либо синглтон или doubleton:

  • если слово появляется только один раз, это синглтон.

  • если слово появляется ровно дважды, это doubleton.

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

проблема в том, чтобы найти синглтоны. Если их три синглеты, мы должны использовать линейное время и постоянное пространство. Больше вообще, если есть 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 вводим 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 }

вот некоторые утверждения о Specs:

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

  • если matches spec word и width spec == 32, тогда word == bits spec

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

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

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. Это утверждение настолько важно, что я назовем это Леммой один вот тест:--59-->

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

мы собираемся определить функцию, которая предоставлена 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'

спасибо поток fusion (см. 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. В этом случае list понимание даст список слов, которые точно равно bits spec. Функция hasSingleton вернутся True если и только если этот список содержит ровно один элемент, который будет верно точно когда bits spec синглтон в words.

  • теперь давайте докажем, что если singletonsMatching правильно с m+1, это также правильно для width m, где *m

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

    вызов extend spec на spec шириной m возвращает две характеристики которые имеют ширину $m+1$. По гипотезе,singletonsMatching is правильно по этим спецификациям. Чтобы доказать: что результат содержит точно эти синглеты, которые соответствуют spec. По Лемме один, любое слово, которое спички 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
        ]

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

вот случайный генератор для спецификаций:

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

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

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

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

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

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

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

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

01000
00001
10001

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

01000

и

00001
10001

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

если это правильно реализовано, это работает для k 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 может использоваться точно так же as x использовался в исходном алгоритме-для разбиения множеств и XORing значений.

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

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

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

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

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


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

  • при вычислении XOR всех (еще не разделенных) значений разумно проверить уникальное нулевое значение в массиве. Если есть один, просто decrement k.
  • на каждом шаге рекурсии мы не всегда можем знать точный размер каждого раздела. Но мы знайте, если это нечетно или даже: каждое разделение на ненулевой бит дает нечетное подмножество, четность другого подмножества "переключается" четность исходного подмножества.
  • на последних шагах рекурсии, когда единственное неразделенное подмножество имеет размер 1, мы можем пропустить поиск бита разделения и немедленно сообщить о результате (это оптимизация для очень малого k).
  • если мы получим нечетное подмножество после некоторого разделения (и если мы не знаем наверняка, что его размер равен 1), сканируйте массив и попытайтесь найти уникальное значение, равное XOR этого подмножества.
  • нет необходимости перебирать каждый кусочек разделить еще набора. Просто используйте любой ненулевой бит его XORed значений. XORing одного из результирующих подмножеств может привести к нулю, но это разделение по-прежнему верно, потому что у нас есть odd количество "единиц" для этого бит расщепления, но даже размер комплекта. Это также означает, что любое разделение, которое производит подмножество четного размера, которое не равно нулю когда XORed, является допустимым разделением, даже если оставшееся подмножество XORs равно нулю.
  • вы не должны продолжать разбиение битового поиска на каждую рекурсию (например,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-чисел имеет равные биты в этой битовой позиции, но числа все равно должны быть разными, поэтому должна быть еще одна битовая позиция разделения (очевидно, справа от первой). Второй шаг рекурсии легко разбивает это 2-числовое подмножество на два одиночных числа. Трюк с i * 3 + 1 здесь избыточно: это только удваивает сложность алгоритма.

вот иллюстрация для первого разделения в наборе из 3 цифры:

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

у нас есть цикл, который повторяет каждую битовую позицию и вычисляет XOR всех слов, но отдельно, одно значение XOR (A) для истинных битов в данной позиции, другое значение XOR (B) для ложного бита. Если число A имеет нулевой бит в этой позиции, A содержит XOR некоторого четного подмножества значений,если ненулевое нечетное подмножество. То же самое верно и для B. нас интересует только подмножество четного размера. Он может содержать 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). Так != 0 и соответствует подмножеству четного размера. Это разделение справедливо, потому что мы можем разделить это четное подмножество далее либо очень простым алгоритмом, либо следующей рекурсией этого алгоритма.

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

сложность O (m2 * 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 всех битов в подмножестве в и позиции. Это предлагает нам использовать дополнительную сплит-позицию: перебирать битовые позиции с двумя отдельными указателями, группировать все числа в массиве в два подмножества, где одно подмножество имеет оба ненулевых бита в этих позициях, а другое-все остальные числа. Это увеличивает наихудшую сложность my m, но позволяет больше ценности для k. Как только больше нет возможности получить подмножество размером менее 5, добавьте третий "указатель разбиения" и так далее. Каждый раз k удваивается, нам может понадобиться дополнительный "указатель расщепления", который увеличивает сложность наихудшего случая my m еще раз.

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

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

в худшем случае сложность O (k * m2 * n * mmax (0, floor (log (floor (k/4))))), который может быть аппроксимирован O (k * n * mlog (k)) = O (k * n * klog (m)).

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


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

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

  • сам x, Если I-й бит x не установлен
  • в x ^ (x

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

Я теперь утверждать, что если А, B, C являются три М-разрядных целых чисел без знака и A', B', С' и другие три М-разрядных целых чисел без знака таким образом, что б исключающее ИЛИ исключающее или С == а' исключающее или в операции XOR с', но наборы {а, б} и {А', B', С'} - это два разных множеств, то существует индекс I, такой, что Т(А, я) Исключающее ИЛИ т(б, я) исключающее или Т(с, я) отличается от Т(А, я) т исключающее или(б, я) исключающее или Т(с-я).

чтобы увидеть это, пусть a '= = a XOR a", b '= = b XOR b "и c' == c XOR c", то есть пусть a "обозначает XOR a и A' и т. д. Поскольку XOR b XOR c равен a 'XOR b' XOR c 'в каждом бите, из этого следует, что a" XOR b" XOR c == 0. Это означает, что в каждой битовой позиции либо a', 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 'имеет бит-флип по сравнению с a, b' имеет бит-флип по сравнению с b, А c' имеет то же битовое значение, что и c в этой позиции P.

в дополнение к битовой позиции P должна быть еще одна битовая позиция Q, где A' и b ' отличаются (в противном случае множества не состоят из трех различных целых чисел, или листать бит в позиции P не создает новый набор целых чисел, случай, который не нужно рассматривать). XOR of the бочкообразный вариант положения бита 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 и i между 0 .. м - 1
  • поиск в постоянном пространстве трех 32-разрядных целых чисел a, b, c таких, что A XOR b XOR c и T(a, i) XOR b(a, i) XOR c(a, i) для всех допустимых значений i соответствуют вычисленным из массива

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

Я РЕАЛИЗОВАНО ЭТО и это работает. Вот исходный код моей тестовой программы, которая используются 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();
}

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

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

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

таким образом, мы определяем метод xorSum в коллекции классов (таким образом, 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 bitXOr: s2), xor двух синглетов

разделить на наименьший бит суммы, и 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, на самом деле мы используем тройное разделение, маска может иметь value1 или 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.

но это не дает никакой информации о битах для разделения... Поэтому мы пробуем каждый бит i=0..М-1.

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

если вы получить (sum1,sum2) == (0,СГМ), то вы unlickily получил 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 отличаются от 0, случай решен.
(0,4) легко распознать, оба размера четные, и по крайней мере одна сумма равна нулю, поэтому повторите поиск с увеличенным битовым рисунком на сумке с суммой != 0
(1,3) сложнее, потому что оба размера нечетны, и мы возвращаемся к случаю неизвестного числа синглетов... Хотя, мы можем легко узнать одиночный синглтон, если элемент мешка равен сумме xor, что невозможно при 3 разных числах...

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

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

EDIT: I commited код (лицензия MIT) наhttp://ss3.gemstone.com/ss/SONiklasBContest.html


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

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

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

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

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

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

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

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


ваш алгоритм не O( n), потому что нет никакой гарантии разделить числа на две группы одинакового размера на каждом шаге, а также потому, что нет никаких ограничений в ваших размерах чисел (они не связаны с n), нет ограничений для ваших возможных шагов, если у вас нет ограничений на размеры входных номеров (если они не зависят от n), время выполнения вашего алгоритма может быть ω (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

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

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


это просто интуиция, но я думаю, что решение состоит в том, чтобы увеличить количество разделов, которые вы оцениваете, пока не найдете тот, где его сумма 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";
    }
}

решение первой проблемы (поиск уникальных чисел 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*2M) и использование памяти по-прежнему O(1).

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

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);
    }
  }
}

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


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

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

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

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