26

最初的问题陈述是这样的:

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

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

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 值都为零,我们不知道得到的子组之一是否为空。在这种情况下,我的算法只是忽略了这一位并继续下一个,这是不正确的,例如它对 input 失败[0,1,2,3,4,5,6]

现在我的想法是不仅要计算元素的 XOR,还要计算应用某个函数后的值的 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输入元素的数量(异或是O(m),最多k分区操作可以成功)和空间复杂度O(m²)(因为m最大递归深度和临时数字可以是长度m)。

问题当然是是否存在具有良好渐近运行时的正确k << n、有效的方法(为了完整起见,我们假设m << n这里),它也需要很少的额外空间(例如,对输入进行排序的方法将不被接受,因为我们至少需要O(n)额外的空间,因为我们不能修改输入!)。

编辑:既然上面的算法被证明是不正确的,当然很高兴看到它是如何正确的,可能会降低它的效率。空间复杂度应该是 in o(n*m)(即,在输入比特的总数中是次线性的)。如果这样可以使任务更容易,则可以将其k作为附加输入。

4

10 回答 10

10

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

http://pastebin.com/BHCKGVaV上的可编译代码。

美丽的理论被丑陋的事实扼杀

问题:给定一个由n 个非零 32 位字组成的序列,其中每个元素要么是单例,要么是双

  • 如果一个词只出现一次,那就是单例

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

  • 没有单词出现三次或更多次。

问题是找到单身人士。如果有三个单例,我们应该使用线性时间和常数空间。更一般地,如果有k个单例,我们应该使用O(k*n)时间和O(k)空间。该算法基于一个未经证实的关于异或的猜想。

我们从这些基础开始:

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

关键抽象:词的部分说明

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

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

Spec如果最低有效w位等于 ,则A匹配一个单词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

关键思想:“扩展”部分规范

这是算法的关键思想:我们可以通过在规范中添加另一个位来扩展a Spec。扩展 aSpec 会产生一个包含两个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 specmatch的两个规范之一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

我们将定义一个函数,它给定一个Spec和一个 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。(“如果”方向是微不足道的。“仅当”方向是 Evgeny Kluev 已反驳的猜想;反例请参见testb下面的数组。我可以通过添加第三个函数使 Evgeny 的示例工作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

高效搜索单身人士

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

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才会返回,这将在中为单例时为真。Truebits specwords

  • 现在让我们证明如果singletonsMatching对于m+1是正确的,那么对于宽度m也是正确的,其中 *m < 32$。(这与感应的方向相反,但没关系。)

    这是被破坏的部分:对于更窄的宽度,即使给定一个单例数组hasSingleton也可能返回。False这是悲剧。

    调用宽度为mextend spec的 a返回两个宽度为 $m+1$ 的规范。根据假设,在这些规格上是正确的。证明:结果完全包含匹配的那些单例。根据引理一,任何匹配的单词都与扩展规范之一完全匹配。根据假设,递归调用完全返回匹配扩展规范的单例。当我们将这些调用的结果与 结合起来时,我们会得到完全匹配的单例,没有重复也没有遗漏。specsingletonsMatchingspecspecconcat

实际上解决问题是虎头蛇尾的:单例是与空规范匹配的所有单例:

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

这是规格的随机生成器:

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

这是另一个属性,它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
于 2012-08-13T23:59:21.023 回答
8

k >= 7的 OP 中算法的反证

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

01000
00001
10001

可以分为

01000

00001
10001

使用最低有效位的值。

如果实施得当,这适用于k <= 6。但这种方法对于k = 8 和k = 7 无效。假设m = 4 并使用 0 到 14 之间的 8 个偶数:

0000
0010
0100
0110
1000
1010
1100
1110

每个位,除了最低有效位,正好有 4 个非零值。如果我们尝试分割这个集合,由于这种对称性,我们总是会得到一个包含 2 个或 4 个或 0 个非零值的子集。这些子集的 XOR 始终为 0。这不允许算法进行任何拆分,因此elsepart 仅打印所有这些唯一值的 XOR(单个零)。

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

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

由于任何一组唯一值都可能被分成一组 7 或 8 个值和其他一些组,因此该算法在k > 8 时也会失败。


概率算法

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

每次算法访问输入数组的一个元素时,它都应该对这个元素应用一些转换函数y=transform(x):这个转换后的值y可以完全按照x原始算法中使用的方式使用——用于对集合进行分区和对值进行异或。

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

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

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

要为算法的每一步获得不同的哈希函数,可以使用Universal hashing。散列函数的一个必要属性是可逆性——原始值应该(理论上)可以从散列值重构。这需要避免将几个“唯一”值散列到相同的散列值。由于使用任何可逆的m位散列函数都没有多少机会解决“反例”问题,因此散列值应该比m位长。这种散列函数的一个简单示例是原始值和该值的一些单向散列函数的串联。

如果k不是很大,我们不太可能得到与该反例相似的一组数据。(我没有证据表明没有其他结构不同的“坏”数据模式,但我们希望它们也不太可能)。在这种情况下,平均时间复杂度不会比 O( k * m 2 * n ) 大多少。


原始算法的其他改进

  • 在计算所有(尚未分区的)值的 XOR 时,检查数组中唯一的零值是合理的。如果有,只需递减k
  • 在每个递归步骤中,我们并不总是知道每个分区的确切大小。但是我们知道它是奇数还是偶数:非零位上的每个拆分都会给出奇数大小的子集,另一个子集的奇偶校验是原始子集的“切换”奇偶校验。
  • 在最近的递归步骤中,当唯一的非拆分子集大小为 1 时,我们可以跳过对拆分位的搜索并立即报告结果(这是对非常小的k的优化)。
  • 如果我们在一些拆分后得到一个奇数大小的子集(如果我们不确定它的大小是 1),扫描数组并尝试找到一个唯一值,等于这个子集的 XOR。
  • 无需遍历每一位来拆分偶数大小的集合。只需使用其 XORed 值的任何非零位。对结果子集之一进行异或可能会产生零,但这种拆分仍然有效,因为对于这个拆分位,我们有奇数个“1”,但设置的大小是偶数。这也意味着,任何在异或时产生非零大小的子集的分割都是有效的分割,即使剩余的子集异或为零也是如此。
  • 您不应该在每个递归(如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 数子集在该位位置具有相等的位,但数字仍然应该不同,因此应该还有一个拆分位位置(显然,在第一个位的右侧)。第二个递归步骤很容易将这个 2 数字子集拆分为两个单个数字。Trick withi * 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) - 在一个数字中。让我们看看这个偶数子集中的值的异或。从 A 和 B 选择值 (C),在位置 1 处包含位 0。但 C 只是两个不相等值的 XOR。它们在位位置 1 处相等,因此它们必须在至少一个位位置(位置 2,位 a 和 b)上有所不同。所以 C != 0 并且它对应于偶数大小的子集。这种拆分是有效的,因为我们可以通过非常简单的算法或通过该算法的下一次递归进一步拆分这个偶数大小的子集。

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

复杂度为 O( m 2 * n )。如果你应用我之前建议的改进,这个算法扫描数组的预期次数是m / 3 + 2。因为第一个分割位位置预期是m / 3,所以需要单次扫描来处理 2-元素子集,每个 1 元素子集不需要任何数组扫描,并且最初需要再扫描一次(solve方法之外)。


k = 4 .. 6 的 OP 算法证明

在这里,我们假设应用了对原始算法的所有建议改进。

k=4 和 k=5:由于至少有一个位置具有不同的位,因此可以将这组数字拆分为其中一个子集的大小为 1 或 2。如果子集的大小为 1,则它是非-zero(我们没有零唯一值)。如果子集的大小为 2,我们有两个不同数字的异或,这是非零的。因此,在这两种情况下,拆分都是有效的。

k=6:如果整个集合的异或非零,我们可以通过这个异或具有非零位的任何位置来分割这个集合。否则,我们在每个位置都有偶数个非零位。由于至少有一个位置具有不同的位,因此该位置将集合拆分为大小为 2 和 4 的子集。大小为 2 的子集始终具有非零 XOR,因为它包含 2 个不同的数字。同样,在这两种情况下,我们都有有效的拆分。


确定性算法

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

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

  1. 使用原始(改进的)算法来查找零个或多个唯一值以及零个或多个不可拆分子集。当没有更多不可分割的子集时停止。
  2. 对于任何这些不可拆分的子集,尝试拆分它同时增加“拆分指针”的数量。找到拆分后,继续执行步骤 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 )。

于 2012-08-13T21:36:00.913 回答
6

一种概率方法是使用计数过滤器

算法如下:

  1. 线性扫描数组并“更新”计数过滤器。
  2. 线性扫描数组并创建所有元素的集合,这些元素在过滤器中肯定不是计数为 2,这将是<= k真正的解决方案。(在这种情况下,误报是看起来不是的独特元素)。
  3. 选择一个新的哈希函数基础并重复,直到我们得到所有k解决方案。

这使用2m了一些空间(独立于n)。时间复杂度更多,但是知道在第 2 步中找不到任何给定唯一元素的概率是近似(1 - e^(-kn/m))^k的,我们将很快解决一个解决方案,但不幸的是,我们不是很线性n

我很欣赏这不能满足您的约束,因为它在时间上是超线性的,并且是概率性的,但是鉴于原始条件可能无法满足,这种方法可能值得考虑。

于 2012-08-14T00:19:05.807 回答
1

我假设你事先知道 k
我选择 Squeak Smalltalk 作为实现语言。

  • inject:into: 是 reduce,在空间上是 O(1),在时间上是 O(N)
  • select: 是过滤器,(我们不使用它,因为 O(1) 空间要求)
  • collect: 是地图,(我们不使用它,因为需要 O(1) 空间)
  • do:是forall,在空间上是O(1),在时间上是O(N)
  • 方括号中的块是一个闭包,或者如果它不关闭任何变量并且不使用返回,则为纯 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),两个单例的异或

在总和的最低设置位处拆分,并像您建议的那样对两个序列进行异或,您将得到 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,

我们定义了一个特定的类来实现异或拆分的细微变化,实际上我们使用三元拆分,掩码可以有 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) 很容易识别,两个大小都是偶数,并且两个异或和都不为0,案例已解决。
(0,4) 很容易识别,两个大小都是偶数,并且至少有一个和为零,因此在总和 != 0
(1,3) 的包上使用递增的位模式重复搜索更难,因为两个大小是奇数,我们回退到未知数量的单身人士的情况......虽然,我们可以很容易地识别出单身人士,如果袋子的一个元素等于异或和,这对于 3 个不同的数字是不可能的......

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

如果你有反例,直接提交,我会检查上面的 Smalltalk 实现

编辑:我在http://ss3.gemstone.com/ss/SONiklasBContest.html提交了代码(许可证 MIT)

于 2012-08-17T15:45:57.483 回答
1

对于 k = 3 的情况,这是一个仅占用最少空间的适当解决方案,并且空间需求为 O(1)。

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

  • x 本身,如果 x 的第 i 位未设置
  • 到 x ^ (x <<< 1) 其中 <<< 表示桶形移位(旋转)

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

我现在声称如果 a、b、c 是三个不同的 m 位无符号整数和 a'、b'、c' 和其他三个不同的 m 位无符号整数,使得 a 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' == a XOR a'', b' == b XOR b'' 和 c' == c XOR c'',即让 a'' 表示 a 和 a' 的 XOR 等。因为 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'

除了位位置 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) T(x, i) 对 0 .. m - 1 之间的所有元素 x 和 i 的 XOR
  • 在常量空间中搜索三个 32 位整数 a、b、c,使得 i 的所有有效值的 a 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();
}
于 2012-08-15T09:12:12.797 回答
1

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

但这是一种概率算法,对空间的要求更轻。

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

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

位集应该足够稀疏,以尽可能少地给出误报,以减少不需要的重复值的数量,从而降低空间复杂度。位集高度稀疏的另一个好处是减少了假阴性的数量,从而稍微提高了运行时间。

要确定位集的最佳大小,请在位集和包含唯一值和误报的临时数组之间均匀分配可用空间(假设k << n):s = n * m * k / s,这给出s = sqrt( n **)。并且预期的空间要求是 O(sqrt( n * m * k ))。

  1. 扫描输入数组并切换位集中的位。
  2. 扫描输入数组并过滤bitset中具有相应非零位的元素,将它们写入临时数组。
  3. 使用任何简单的方法(分布排序或散列)从临时数组中排除重复项。
  4. 如果临时数组的大小加上目前已知的唯一元素的数量小于k,则更改哈希函数,清除对应于已知唯一值的位集和切换位,继续执行步骤 1。

预期的时间复杂度介于 O( n * m ) 和 O( n * m * log( n * m * k ) / log( n * m / k )) 之间。

于 2012-08-17T17:32:58.527 回答
0

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

例如,对于 [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";
    }
}
于 2012-08-17T12:54:31.873 回答
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)并且不难证明这一点。

于 2012-08-11T18:40:52.057 回答
-1

前一个问题的解决方案(在 O(N) 中使用 O(1) 内存使用找到唯一的 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空间划分为频带。然后,您将不得不遍历所有波段,并在每个波段内使用位向量技术找到唯一值。

于 2012-08-17T08:55:42.290 回答
-4

有两种方法可行。

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

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

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

于 2012-08-11T18:15:19.323 回答