在列表中找到具有“少量”额外空间的k个非重复元素

时间:2012-08-11 15:50:39

标签: algorithm time-complexity xor space-complexity

原始问题陈述是这样的:

  

给定一个32位无符号整数数组,其中每个数字正好显示两次,除了其中三个(只出现一次),使用O(1)额外空格在O(n)时间内找到这三个数字。输入数组是只读的。如果有k个例外而不是3个怎么办?

如果由于输入限制而接受非常高的常数因子,则在Ο(1)时间和Ο(1)空间中很容易解决此问题(数组最多可以有2个 33 条目):

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

因此,为了这个问题,让我们放弃比特长度的限制,专注于数字最多可能有m位的更普遍的问题。

Generalizing an algorithm for k = 2,我想到的是以下内容:

  1. 将具有最低有效位1的那些数字与具有0的数字分开进行异或。如果对于这两个分区,结果值不为零,我们知道我们已将非重复数字分成两组,每组至少有一个成员
  2. 对于每个组,尝试通过检查第二个最低有效位来进一步对其进行分区,依此类推
  3. 但有一个特殊情况需要考虑。如果在对一个组进行分区后,其中一个组的XOR值都为零,我们不知道其中一个结果子组是否为空。在这种情况下,我的算法只留下这一位并继续下一个,这是不正确的,例如输入[0,1,2,3,4,5,6]失败。

    现在我的想法是不仅要计算元素的XOR,还要计算应用某个函数后的值的异或(我在这里选择了f(x) = 3x + 1)。有关此附加检查的反例,请参阅下面的Evgeny的答案。

    现在虽然以下算法对于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 << nm << n),这也需要很少的额外空间(例如,不会接受对输入进行排序的方法,因为我们至少需要O(n)个额外的空间,因为我们无法修改输入!)。

    编辑:既然上面的算法被证明是不正确的,那么看看如何使它变得正确当然很好,可能是因为它的效率要低一些。空间复杂度应该在o(n*m)中(即输入位总数中的次线性)。如果这使得任务更容易,可以将k作为附加输入。

10 个答案:

答案 0 :(得分:10)

我离线了,证明原始算法受到XOR技巧工作的猜想的影响。碰巧,XOR技巧不起作用,但下面的论点可能仍然引起一些人的兴趣。 (我在Haskell中重新做了,因为当我有递归函数而不是循环时我发现证明更容易,我可以使用数据结构。但对于观众中的Pythonistas,我试图尽可能使用列表推导。)

http://pastebin.com/BHCKGVaV处的可编辑代码。

美丽的理论被一个丑陋的事实杀死

问题:我们给出了一系列 n 非零32位字 其中每个元素都是 singleton doubleton

  • 如果单词出现一次,则为 singleton

  • 如果一个单词出现两次,则为 doubleton

  • 没有任何字出现过三次或更多次。

问题是找到单身人士。如果有三个 单身人士,我们应该使用线性时间和恒定空间。更多 通常,如果有 k 单例,我们应该使用 O(k * n)时间 和 O(k)空间。该算法依赖于未经证实的猜想 关于独家或。

我们从这些基础知识开始:

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

密钥抽象:单词的部分规范

要解决这个问题,我将引入一个抽象:to 描述一个32位字的最低有效$ w $位,I 介绍一个Spec

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

如果最低有效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 wordwidth spec == 32,那么 word == bits spec

关键理念:&#34;延伸&#34;部分规范

这是算法的关键思想:我们可以扩展一个Spec 在规范中添加另一位。扩展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

以下是至关重要的声明:如果specword匹配,如果匹配width spec extend spec小于32,恰好是两个规格中的一个 来自word匹配word。证据是通过案例分析 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 的相关位。这个说法非常重要,我还是 打电话给Lemma One Here这是一个测试:

Spec

我们要定义一个赋予xorWith f ws和a的函数 32位字序列,返回单例字的列表 符合规范。该功能需要时间成比例 输入的长度乘以答案的大小乘以32,和 额外的空间与答案时间的大小成正比32.之前 我们解决了主要功能,我们定义了一些恒定空间XOR 功能

破坏XOR的想法

函数f将函数ws应用于xorWith :: (Word32 -> Word32) -> [Word32] -> Word32 xorWith f ws = reduce xor 0 [f w | w <- ws] where reduce = foldl' 中的每个单词 并返回独占或结果。

xorWith

感谢 stream fusion (参见ICFP 2007),函数3 * w + 1需要 恒定空间。

非零单词列表有一个单例,当且仅当有 排他性或非零,或testb的排他性或非g 非零。 (&#34;如果&#34;方向是微不足道的。&#34;只有&#34;方向是 一个猜想,Evgeny Kluev已经证实了这一点;作为反例, 请参阅下面的数组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 。我可以通过添加使Evgeny的示例工作 第三个函数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

有效搜索单身人士

我们的main函数返回一个匹配a的所有单例的列表 规格。

spec

我们通过感应宽度来证明其正确性 bits spec

  • 基本情况是hasSingleton的宽度为32.在这种情况下, list comprehension将给出完全正确的单词列表 等于True。如果,函数bits spec将返回words 并且只有当这个列表只有一个元素时才会成立 恰好singletonsMatching中的hasSingleton是单身人士。

  • 现在让我们来证明False是否正确 对于 m + 1 ,对于宽度 m 也是正确的,其中* m&lt; 32 $。 (这与感应通常相反,但它 没关系。)

    以下是被破坏的部分:对于较窄的宽度,即使给定单个数组,extend spec也可能返回spec。这很悲惨。

    singletonsMatching宽度 m 上调用spec会返回两个规格 宽度为$ m + 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 ] 调用,我们可以得到完全匹配的单例 没有重复,没有遗漏。

实际上解决问题是虎头蛇尾:单身人士 所有符合空规的单身人士:

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

除此之外,如果你想关注正在发生的事情,你需要 要知道QuickCheck

这是规格的随机生成器:

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

这是另一个针对a测试快速{{1}}的属性 使用排序的较慢算法。

{{1}}

答案 1 :(得分:8)

对于 k &gt; = 7

的OP中的算法的反对

当这些组中的至​​少一个被异或为非零值时,该算法使用以一个比特的值递归地将一组 k 唯一值分成两组的可能性。例如,以下数字

01000
00001
10001

可能会分成

01000

00001
10001

使用最低有效位的值。

如果实施得当,这适用于 k &lt; = 6.但是这种方法对 k = 8且 k = 7失败。我们假设 m = 4并使用0到14之间的8个偶数:

0000
0010
0100
0110
1000
1010
1100
1110

除了最不重要的位之外,每个位都有4个非零值。如果我们尝试对此集进行分区,由于这种对称性,我们总是会得到一个具有2或4或0非零值的子集。这些子集的XOR始终为0.这不允许算法进行任何拆分,因此else部分只打印所有这些唯一值的XOR(单个零)。

3x + 1技巧无济于事:它只会将这8个值混洗并切换最低有效位。

如果我们从上面的子集中删除第一个(全零)值,则完全相同的参数适用于 k = 7。

由于任何一组唯一值可能被分为7或8个值组以及其他一些组,因此该算法也不能用于 k &gt; 8。


概率算法

有可能不发明一种全新的算法,而是修改OP中的算法,使其适用于任何输入值。

每次算法访问输入数组的元素时,它都应该对该元素应用一些转换函数:y=transform(x)。此变换值y可以与原始算法中使用的x完全相同 - 用于对集合进行分区和对值进行异或。

最初transform(x)=x(未经修改的原始算法)。如果在此步骤之后我们得到的结果少于 k (某些结果是几个唯一值XORed),我们将transform更改为某个哈希函数并重复计算。这应该重复(每次使用不同的散列函数),直到我们得到 k 值。

如果在算法的第一步获得这些 k 值(没有散列),则这些值是我们的结果。否则,我们应该再次扫描数组,计算每个值的哈希并报告那些与 k 哈希值匹配的值。

具有不同散列函数的每个后续计算步骤可以在 k 值的原始集合上执行,或者(在上一步骤中找到的每个子集上单独执行(更好)。

要为算法的每个步骤获取不同的哈希函数,可以使用Universal hashing。散列函数的一个必要属性是可逆性 - 原始值应该(理论上)可以从散列值重建。这是为了避免散列几个&#34; unique&#34;值为相同的哈希值。由于使用任何可逆的 m 位哈希函数没有太多机会解决&#34;反例&#34;的问题,哈希值应该长于 m 位。这种散列函数的一个简单示例是原始值的连接和该值的一些单向散列函数。

如果 k 不是很大,我们就不太可能得到一组与该反例相似的数据。 (我没有证据证明没有其他&#34;糟糕的数据模式,具有不同的结构,但我们希望它们也不太可能)。在这种情况下,平均时间复杂度不会比O大很多( k * m 2 * n )。< / p>


原算法的其他改进

  • 在计算所有(尚未分区的)值的XOR时,检查数组中的唯一零值是合理的。如果有,只需减少 k
  • 在每个递归步骤中,我们无法始终知道每个分区的确切大小。但我们知道它是奇数还是偶数:非零位上的每个分裂给出奇数大小的子集,另一个子集的奇偶校验是&#34;切换&#34;原始子集的奇偶校验。
  • 在最新的递归步骤中,当唯一的非拆分子集大小为1时,我们可以跳过搜索拆分位并立即报告结果(这是非常小的 k 的优化)。
  • 如果我们在一些拆分后得到一个奇数大小的子集(如果我们不确定其大小是1),扫描数组并尝试找到一个唯一值,等于该子集的XOR。
  • 没有必要迭代每一位来拆分偶数大小的集合。只需使用其XORed值的任何非零位。对其中一个结果子集进行异或可能会产生零,但是这种分割仍然有效,因为我们有奇数数量的&#34; 1&#34;对于此拆分位,但甚至设置大小。这也意味着,任何产生偶数大小的子集的分裂,在异或时都是非零的,即使剩余的子集XOR为零,也是有效的分裂。
  • 您不应该在每次递归时继续拆分位搜索(例如solve(ary, h + 1...)。相反,你应该从头开始重新搜索。可以在第31位拆分该组,并且对于位0上的一个结果子集具有唯一的拆分可能性。
  • 您不应该扫描整个数组两次(因此不需要第二个y = compute_xors(ary, m, bits))。您已经拥有整个集合的XOR和分割位非零的子集的XOR。这意味着您可以立即计算yy = x ^ old_xor

OP中针对k = 3

的算法证明

这不是OP中实际程序的证明,而是它的想法。当其中一个结果子集为零时,实际程序当前拒绝任何拆分。当我们接受某些此类拆分时,请参阅建议的改进案例。因此,只有在if x is None or y is None更改为考虑子集大小的奇偶校验的某个条件之后或者在添加预处理步骤以从数组中排除唯一的零元素之后,才可以将以下证据应用于该程序。

我们有3个不同的数字。它们在至少2位位置应该是不同的(如果它们仅在一位中不同,则第三位必须等于其他位中的一位)。 solve函数中的循环查找这些位位置中最左边的位置,并将这3个数字分成两个子集(单个数字和2个不同数字)。 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),包含位置1的位0.但C只是两个不相等值的XOR。 它们在位位置1处相等,因此它们必须在至少一个位位置(位置2,位a和b)上不同。所以C!= 0并且它对应于偶数大小的子集。 这种拆分是有效的,因为我们可以通过非常简单的算法或通过该算法的下一次递归来进一步拆分这个偶数大小的子集。

如果阵列中没有唯一的零元素,则可以简化此证明。我们总是将唯一数字分成2个子集 - 一个具有2个元素(并且由于元素不同,它不能异或为零),另外一个元素(根据定义非零)。因此,几乎没有预处理的原始程序应该可以正常工作。

复杂度为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个不同的数字。同样,在这两种情况下,我们都有有效的分割。


确定性算法

k &gt; = 7的反色显示原始算法不起作用的模式:我们有一个大于2的子集,在每个位位置我们有偶数个非零位。但是我们总能找到一对非零位在单个数字中重叠的位置。换句话说,始终可以在大小为3或4的子集中找到一对位置,其中两个位置中的子集中的所有位的非零XOR。这建议我们使用额外的分割位置:使用两个单独的指针迭代位位置,将数组中的所有数字分组为两个子集,其中一个子集在这些位置具有非零位,以及其他 - 所有剩余数字。这会增加我的 m 的最坏情况复杂度,但允许 k 的更多值。一旦没有更多可能获得大小小于5的子集,添加第三个&#34;拆分指针&#34;,依此类推。每当 k 加倍时,我们可能需要额外的&#34;分割指针&#34;,这会再次增加我 m 的最坏情况复杂性。

这可以被视为以下算法的证明草图:

  1. 使用原始(改进)算法查找零个或多个唯一值以及零个或多个不可拆分子集。当没有更多不可拆分的子集时停止。
  2. 对于任何这些不可拆分的子集,尝试将其拆分,同时增加&#34;拆分指针&#34;的数量。找到拆分后,继续执行步骤1.
  3. 最坏情况复杂度为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 )。

答案 2 :(得分:6)

采用一种概率方法是使用counting filter

算法如下:

  1. 线性扫描数组并“更新”计数过滤器。
  2. 线性扫描数组并创建所有元素的集合,这些元素在过滤器中肯定不是第2项,这将是真实解决方案的<= k。 (在这种情况下,误报是看起来不像的独特元素。)
  3. 选择哈希函数的新基础并重复,直到我们拥有所有k个解决方案。
  4. 这使用2m位空格(独立于n)。时间复杂度更复杂,但是知道在步骤2中找不到任何给定唯一元素的概率大约是(1 - e^(-kn/m))^k,我们将很快解决一个解,但不幸的是我们在{{1 }}

    我理解这不能满足你的约束,因为它在时间上是超线性的,并且是概率性的,但考虑到原始条件可能不满足这个 方法可能值得考虑。

答案 3 :(得分:1)

对于k = 3的情况,这是一个适当的解决方案,只需要很小的空间,空间要求是O(1)。

让'transform'成为一个函数,它将m位无符号整数x和索引i作为参数。 i介于0 ... m - 1之间,变换将整数x变为

  • x本身,如果未设置x的第i位
  • 至x ^(x <&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;表示桶移(旋转)

在下面的T(x,i)中用作transform(x,i)的简写。

我现在声称,如果a,b,c是三个不同的m位无符号整数和一个',b',c'和其他三个不同的m位无符号整数,那么XOR b XOR c == a' XOR b'XOR c',但是集合{a,b,c}和{a',b',c'}是两个不同的集合,那么有一个索引i使得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和a的异或'等等因为XOR b XOR c在每一位等于'XOR b'XOR c',所以它跟随''XOR b''XOR c''== 0.这意味着在每个位位置,要么是',b',c'与a,b,c相同,或恰好其中两个使所选位置的位翻转(0-> 1或1-> 0)。因为',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) 。假设没有失去一般性,'与b相比,'具有位翻转',b'与b相比具有位翻转,并且c'在该位置P具有与c相同的位值。

除了位位置P之外,还必须有另一个位位置Q,其中a和b'不同(否则这些位不包含三个不同的整数,或者在位置P处翻转位不会创建新的位整数,一个不需要考虑的案例)。桶位置Q的桶旋转版本的XOR在位位置(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)所有元素x和i的T(x,i)的XOR在0 .. m - 1
  • 之间
  • 在恒定空间中搜索三个32位整数a,b,c,使得对于所有有效值,XOR b XOR c和T(a,i)XOR b(a,i)XOR c(a,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();
}

答案 4 :(得分:1)

我认为你提前知道了 我选择Squeak Smalltalk作为实现语言。

  • 注入:进入:减少并且空间为O(1),时间为O(N)
  • select:is filter,(我们不使用它,因为O(1)空间要求)
  • collect:是map,(我们不使用它,因为O(1)空间要求)
  • do:是forall,并且在空间中是O(1),在时间上是O(N)
  • 方括号中的一个块是一个闭包,如果它没有关闭任何变量并且不使用return,则为纯lambda,前缀为冒号的符号是参数。
  • ^表示返回

对于k = 1,通过用位xor

减少序列来获得单例

因此我们在类Collection中定义了一个方法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.

sum与0不同,等于(s1 bitXOr:s2),两个单身的xor

在sum的最低设置位分割,并且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的轻微变化,实际上我们使用三元分割,mask可以有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..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和都不同于0,解决了案例 (0,4)很容易识别,两种尺寸都是均匀的,并且至少有一个总和为零,所以在包装上用增量位模式重复搜索!= 0
(1,3)更难,因为两个大小都是奇数,我们回到单身数量不明的情况......但是,如果包的元素等于xor总和,我们可以很容易地识别单个单身,3个不同的数字是不可能的......

我们可以推广k = 5 ......但是上面会很难,因为我们必须找到一个针对案例的技巧(4,2)和(1,5),记住我们的假设,我们必须提前知道k ......我们必须做假设并在之后验证它们......

如果你有一个反例,只需提交它,我将检查上面的Smalltalk实现

编辑:我在http://ss3.gemstone.com/ss/SONiklasBContest.html

提交了代码(麻省理工学院许可证)

答案 5 :(得分:1)

根据空间复杂度要求,放松到O( m * n ),此任务可以在O( n )时间内轻松解决。只需使用哈希表计算每个元素的实例数,然后筛选计数器等于1的条目。或者使用任何分配排序算法。

但这是一种概率算法,具有更小的空间要求。

此算法使用大小 s 的其他位集。对于输入数组中的每个值,计算散列函数。此哈希函数确定位集中的索引。我们的想法是扫描输入数组,切换每个数组条目的位集中的相应位。重复的条目将相同的位切换两次。由唯一条目(几乎所有条目)切换的位保留在bitset中。这几乎与计算布隆过滤器相同,其中每个计数器中唯一使用的位是最低有效位。

再次扫描数组,我们可能会提取唯一值(不包括一些误报)以及一些重复值(误报)。

bitset应该足够稀疏,以尽可能少地提供误报,以减少不需要的重复值的数量,从而降低空间复杂度。 bitset的高度稀疏性的另一个好处是减少了漏报的数量,从而略微改善了运行时间。

要确定bitset的最佳大小,请在包含唯一值和误报的bitset和临时数组之间均匀分配可用空间(假设 k &lt;&lt; n ): s = n * m * k / s , strong> s = sqrt( n * m * k )。预期空间要求为O(sqrt( n * m * k ))。

  1. 扫描输入数组并在位集中切换位。
  2. 扫描位组中具有相应非零位的输入数组和滤波器元素,将它们写入临时数组。
  3. 使用任何简单的方法(分发排序或散列)来排除临时数组中的重复项。
  4. 如果临时数组的大小加上到目前为止已知的唯一元素数小于 k ,请更改散列函数,清除与已知唯一值对应的位集和切换位,继续执行步骤1
  5. 预期时间复杂度介于O( n * m )和O( n * m *之间记录( n * m * k )/ log( n * m /的ķ))。

答案 6 :(得分:0)

您的算法不是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

你的算法将运行第一个m-n位,并且每一步都会O(n),直到现在你到达的O((mn)* n)大于O(n ^ 2) )。

PS:如果你总是有32位数字,你的算法是O(n)并且不难证明这一点。

答案 7 :(得分:0)

这只是一种直觉,但我认为解决方案是增加你评估的分区数量,直到找到一个xor总和不为零的分区。

例如,对于[0,m]范围内的每两位(x,y),请考虑由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";
    }
}

答案 8 :(得分:-1)

前一个问题的解决方案(在O(1)内存使用情况下在O(N)中查找唯一的uint32数字)非常简单,但不是特别快:

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)。

更新:使用位图的补充解决方案导致复杂度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 空间。然后,您将不得不迭代所有波段,并在每个波段内使用位向量技术找到唯一值。

答案 9 :(得分:-4)

两种方法都可行。

(1)创建一个临时哈希表,其中键是整数,值是数字 重复。当然,这会占用比指定的空间更多的空间。

(2)对数组(或副本)进行排序,然后计算数组[n + 2] == array [n]的情况数。 当然,这将花费比指定时间更长的时间。

我会非常惊讶地看到满足原始约束的解决方案。