我离线并证明了原算法受到异或技巧的猜想。事实上,异或技巧不起作用,但以下论点仍可能对某些人有兴趣。(我重新使用Haskell进行了计算,因为当我使用递归函数而不是循环和数据结构时,我发现证明要容易得多。但是对于观众中的Pythonistas,我尽可能使用列表理解。)
可编译的代码位于http://pastebin.com/BHCKGVaV。
美丽的理论被丑陋的事实击败
问题:我们在一个非零32位单词序列中给定每个元素都是singleton或doubleton:
问题是查找singleton。如果有三个singleton,我们应该使用线性时间和常数空间。更一般地,如果有k个singleton,我们应该使用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
如果最低有效位为bits
,则Spec
与单词匹配。 如果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
的一些声明:
关键思想:“扩展”部分规格
这是该算法的关键思想:我们可以通过增加另一个位来扩展 Spec
。扩展Spec
会产生两个Spec
列表。
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
相关位的情况分析。这个声明非常重要,我将称其为引理一。以下是一个测试:
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
函数应用于 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
。我可以通过添加第三个函数g
使Evgeny的示例效果正常,但显然这种情况需要证明,而我没有证明。)
hasSingleton :: [Word32] -> Bool
hasSingleton ws = xorWith id ws /= 0 || xorWith f ws /= 0 || xorWith g ws /= 0
where f w = 3 * w + 1
g w = 31 * w + 17
高效搜索单例
我们的主要函数返回一个匹配特定条件的所有单例对象列表。
singletonsMatching :: Spec -> [Word32] -> [Word32]
singletonsMatching spec words =
if hasSingleton [w | w <- words, spec `matches` w] then
if width spec == 32 then
[bits spec]
else
concat [singletonsMatching spec' words | spec' <- extend spec]
else
[]
我们将通过对
spec
的宽度进行归纳来证明其正确性。
基本情况是spec
的宽度为32。在这种情况下,列表推导会返回与bits spec
完全相等的单词列表。函数hasSingleton
仅当此列表恰好包含一个元素时才返回True
,并且当且仅当bits spec
在words
中是单一的时才为真。
现在让我们证明如果singletonsMatching
对于m+1是正确的,则对于宽度为m也是正确的,其中*m < 32$。(这与归纳的通常方向相反,但无关紧要。)
这里出现了问题:对于较窄的宽度,即使给出单独的数组,hasSingleton
可能仍然返回False
。这很悲惨。
对宽度为m
的spec
调用extend spec
会返回两个宽度为$m+1$的spec。根据假设,singletonsMatching
在这些specs上是正确的。需要证明的是:结果仅包含与spec
匹配的那些单例。根据引理一,与spec
匹配的任何单词都恰好与扩展的specs中的一个匹配。根据假设,递归调用返回完全匹配扩展specs的单例。当我们将这些调用的结果与concat
组合时,我们得到完全匹配的单例,没有重复项和省略项。
实际解决问题有点无聊:所有与空spec匹配的单例都是单例:
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
以下是另一个属性,用于测试使用排序算法的较慢算法与快速的单例
算法。
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
O(1)
,但我认为因为2^32 >= N,所以声称你的解决方案是O(N^2)
是合理的。就像在这个领域中,O(2**32N)
>=O(N^2)
[稍微滥用一下O符号]。 - cmh