原始问题陈述是这样的:
给定一个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
的那些数字与具有0
的数字分开进行异或。如果对于这两个分区,结果值不为零,我们知道我们已将非重复数字分成两组,每组至少有一个成员但有一个特殊情况需要考虑。如果在对一个组进行分区后,其中一个组的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 << n
和m << n
),这也需要很少的额外空间(例如,不会接受对输入进行排序的方法,因为我们至少需要O(n)
个额外的空间,因为我们无法修改输入!)。
编辑:既然上面的算法被证明是不正确的,那么看看如何使它变得正确当然很好,可能是因为它的效率要低一些。空间复杂度应该在o(n*m)
中(即输入位总数中的次线性)。如果这使得任务更容易,可以将k
作为附加输入。
答案 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 word
和width spec == 32
,那么
word == bits spec
这是算法的关键思想:我们可以扩展一个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
以下是至关重要的声明:如果spec
与word
匹配,如果匹配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
功能
函数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 唯一值分成两组的可能性。例如,以下数字
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>
solve(ary, h + 1...
)。相反,你应该从头开始重新搜索。可以在第31位拆分该组,并且对于位0上的一个结果子集具有唯一的拆分可能性。y = compute_xors(ary, m, bits)
)。您已经拥有整个集合的XOR和分割位非零的子集的XOR。这意味着您可以立即计算y
:y = x ^ old_xor
。这不是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
方法之外)。
这里我们假设应用了原始算法的所有建议改进。
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 的最坏情况复杂性。
这可以被视为以下算法的证明草图:
最坏情况复杂度为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。
算法如下:
<= k
。 (在这种情况下,误报是看起来不像的独特元素。)k
个解决方案。这使用2m
位空格(独立于n
)。时间复杂度更复杂,但是知道在步骤2中找不到任何给定唯一元素的概率大约是(1 - e^(-kn/m))^k
,我们将很快解决一个解,但不幸的是我们在{{1 }}
我理解这不能满足你的约束,因为它在时间上是超线性的,并且是概率性的,但考虑到原始条件可能不满足这个 方法可能值得考虑。
答案 3 :(得分:1)
对于k = 3的情况,这是一个适当的解决方案,只需要很小的空间,空间要求是O(1)。
让'transform'成为一个函数,它将m位无符号整数x和索引i作为参数。 i介于0 ... m - 1之间,变换将整数x变为
在下面的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'的实际值不会影响奇偶校验错误。
因此,算法是
这显然是因为重复元素从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作为实现语言。
对于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实现
提交了代码(麻省理工学院许可证)答案 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 ))。
预期时间复杂度介于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]的情况数。 当然,这将花费比指定时间更长的时间。
我会非常惊讶地看到满足原始约束的解决方案。