26

元の問題ステートメントは次のとおりです。

32ビットの符号なし整数の配列で、3つ(1つだけ)を除いてすべての数値が正確に2回現れる場合、O(1)の余分なスペースを使用してO(n)時間でこれらの3つの数値を見つけます。入力配列は読み取り専用です。3ではなくkの例外がある場合はどうなりますか?

入力制限のために非常に高い定数係数を受け入れる場合、これをΟ(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. これらの数値の最下位ビット10個別の数値をXORします。両方のパーティションで、結果の値がゼロでない場合、繰り返されない数値を2つのグループに分割し、それぞれに少なくとも1つのメンバーが含まれていることがわかります。
  2. これらのグループごとに、2番目に最下位ビットなどを調べてさらに分割してみてください。

ただし、考慮すべき特別な場合があります。グループを分割した後、グループの1つのXOR値が両方ともゼロである場合、結果のサブグループの1つが空であるかどうかはわかりません。この場合、私のアルゴリズムはこのビットを省略して次のビットを続行しますが、これは正しくありません。たとえば、入力に対して失敗します[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入力要素の数(XORはO(m)、最大でk分割操作が成功する可能性があります)とスペースの複雑さO(m²)m最大の再帰深度であり、一時的な数は次のようになる可能性があるため)です。長さm)。

問題はもちろん、適切で効率的なアプローチがあり、漸近的なランタイムが良好であるかどうかです(完全を期すためにk << nm << nここでは、追加のスペースもほとんど必要ありません(たとえば、入力を並べ替えるアプローチは受け入れられません)。O(n)入力を変更できないため、少なくとも追加のスペースが必要になるためです!)。

編集:上記のアルゴリズムが正しくないことが証明されたので、もちろん、おそらく少し効率を下げることによって、どのように正しくすることができるかを確認するのは良いことです。スペースの複雑さはにある必要がありますo(n*m)(つまり、入力ビットの総数が劣線形です)。kタスクが簡単になる場合は、追加の入力として使用してもかまいません。

4

10 に答える 10

10

私はオフラインになり、XORトリックが機能したという推測に従って元のアルゴリズムを証明しました。たまたま、XORのトリックは機能しませんが、次の議論はまだ一部の人々に興味があるかもしれません。(ループの代わりに再帰関数があり、データ構造を使用できると、証明がはるかに簡単になるため、Haskellでやり直しました。しかし、聴衆のPythonistaには、可能な限りリスト内包表記を使用しようとしました。)

http://pastebin.com/BHCKGVaVでコンパイル可能なコード。

醜い事実によって殺された美しい理論

問題:すべての要素がシングルトンまたはダブルトンのいずれかであるn個の非ゼロ32ビットワードのシーケンスが与えられます:

  • 単語が1回だけ出現する場合、それはシングルトンです。

  • 単語が正確に2回出現する場合、それはダブルトンです。

  • 3回以上単語が表示されない。

問題はシングルトンを見つけることです。シングルトンが3つある場合は、線形時間と一定の空間を使用する必要があります。より一般的には、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 }

Specsに関するいくつかの主張は次のとおりです。

  • すべての単語はuniversalSpec、幅0のに一致します

  • matches spec wordとの場合width spec == 32word == bits spec

重要なアイデア:部分的な仕様を「拡張」する

アルゴリズムの重要なアイデアは次のとおりです。仕様に別のビットを追加することで、を拡張できます。Specaを拡張すると、2つの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未満の場合、一致からの2つの仕様のうちの1つextend specですword。証明は、の関連ビットのケース分析ですword。この主張は非常に重要なので、私はそれをLemmaOneと呼ぶことにします。これがテストです。

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関数を適用 し、結果の排他的論理和を返します。fws

xorWith :: (Word32 -> Word32) -> [Word32] -> Word32
xorWith f ws = reduce xor 0 [f w | w <- ws]
  where reduce = foldl'

ストリームフュージョン(ICFP 2007を参照)のおかげで、関数xorWithは一定のスペースを取ります。

非ゼロの単語のリストは、排他的論理和または非ゼロの場合、または排他的論理和が非ゼロの場合にのみシングルトンを持ちます3 * w + 1。(「if」方向は自明です。「onlyif」方向はEvgeny Kluevが反証した推測です。反例については、testb以下の配列を参照してください。3番目の関数を追加することで、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ます。このリストに要素が1つだけある場合にのみ、関数hasSingletonが返されます。これは、がシングルトンの場合に正確に当てはまります。Truebits specwords

  • singletonsMatchingここで、m + 1で正しい場合は、幅mでも正しいことを証明しましょう。ここで* m<32$です。(これは誘導の通常の反対方向ですが、問題ではありません。)

    壊れている部分は次のとおりです。幅が狭い場合は、シングルトンの配列が指定されている場合でhasSingletonも戻る可能性があります。Falseこれは悲劇的です。

    幅mのaを呼び出すextend specと、幅$ m +1$の2つの仕様が返されます。仮説によれば、これらの仕様は正しいです。証明するには:結果に、に一致するシングルトンが正確に含まれていることを確認します。Lemma Oneによると、一致する単語はすべて、拡張仕様の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

このジェネレーターを使用すると、を使用してLemmaOneをテストできます 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でのアルゴリズムの反証

このアルゴリズムは、これらのグループの少なくとも1つがゼロ以外の値にXORされている場合に、単一ビットの値を使用してk個の一意の値のセットを再帰的に2つのグループに分割する可能性を使用します。たとえば、次の番号

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ます。つまり、セットを分割し、値をXORします。

最初はtransform(x)=x(変更されていない元のアルゴリズム)。このステップの後、結果がk未満の場合(結果の一部はXORされたいくつかの一意の値です)、transformいくつかのハッシュ関数に変更して計算を繰り返します。正確にk個の値が得られるまで、これを(毎回異なるハッシュ関数で)繰り返す必要があります。

これらのk値がアルゴリズムの最初のステップで(ハッシュなしで)取得された場合、これらの値が結果になります。それ以外の場合は、配列をもう一度スキャンして、各値のハッシュを計算し、k個のハッシュの1つに一致する値を報告する必要があります。

異なるハッシュ関数を使用した計算の後続の各ステップは、k値の元のセットに対して実行することも、前のステップで見つかったサブセットのそれぞれに対して(より適切に)個別に実行することもできます。

アルゴリズムのステップごとに異なるハッシュ関数を取得するには、ユニバーサルハッシュを使用できます。ハッシュ関数に必要なプロパティの1つは可逆性です。元の値は、(理論的には)ハッシュ値から再構築可能である必要があります。これは、複数の「一意の」値が同じハッシュ値にハッシュされないようにするために必要です。リバーシブルのmビットハッシュ関数を使用しても「反例」の問題を解決する機会はあまりないため、ハッシュ値はmビットより長くする必要があります。このようなハッシュ関数の簡単な例の1つは、元の値とこの値の一方向ハッシュ関数の連結です。

kがそれほど大きくない場合、その反例のようなデータセットを取得する可能性は低くなります。(構造が異なる他の「悪い」データパターンがないという証拠はありませんが、それらもあまりありそうにないことを願っています)。この場合、平均時間計算量はO( k * m 2 * n )よりもそれほど大きくありません。


元のアルゴリズムのその他の改善

  • すべての(まだパーティション化されていない)値のXORを計算している間、配列内の一意のゼロ値をチェックすることは合理的です。存在する場合は、kをデクリメントします。
  • 各再帰ステップで、各パーティションの正確なサイズを常に把握できるとは限りません。しかし、それが奇数か偶数かはわかっています。ゼロ以外のビットで分割されるたびに奇数サイズのサブセットが生成され、他のサブセットのパリティは元のサブセットの「切り替えられた」パリティになります。
  • 最新の再帰ステップでは、分割されていないサブセットのみがサイズ1の場合、分割ビットの検索をスキップして、結果をすぐに報告できます(これは非常に小さいkの最適化です)。
  • 分割後に奇数サイズのサブセットを取得した場合(およびそのサイズが1であるかどうかわからない場合)、配列をスキャンして、このサブセットのXORに等しい一意の値を見つけようとします。
  • 偶数サイズのセットを分割するためにすべてのビットを繰り返す必要はありません。XORされた値のゼロ以外のビットを使用するだけです。結果のサブセットの1つをXORするとゼロが生成される場合がありますが、この分割ビットには奇数の「1」があり、サイズも設定されているため、この分割は引き続き有効です。これは、残りのサブセットのXORがゼロになった場合でも、XORされたときにゼロ以外の偶数サイズのサブセットを生成する分割が有効な分割であることも意味します。
  • (のように)再帰ごとにビット検索を分割し続けるべきではありませんsolve(ary, h + 1...。代わりに、最初から検索を再開する必要があります。ビット31でセットを分割することが可能であり、ビット0で結果のサブセットの1つに対して唯一の分割の可能性があります。
  • アレイ全体を2回スキャンしないでください(したがって、2番目y = compute_xors(ary, m, bits)は必要ありません)。セット全体のXORと、分割ビットがゼロ以外のサブセットのXORがすでにあります。つまり、yすぐに計算できますy = x ^ old_xor

k=3のOPでのアルゴリズムの証明

これは、OPの実際のプログラムではなく、そのアイデアの証拠です。結果のサブセットの1つがゼロの場合、実際のプログラムは現在、分割を拒否します。そのような分割のいくつかを受け入れる可能性がある場合の提案された改善を参照してください。したがって、次の証明はif x is None or y is None、サブセットサイズのパリティを考慮した条件に変更された後、または配列から一意のゼロ要素を除外するために前処理ステップが追加された後にのみ、そのプログラムに適用できます。

3つの異なる番号があります。それらは少なくとも2ビット位置で異なっている必要があります(1ビットだけが異なっている場合、3番目の数値は他の1つと等しくなければなりません)。関数のループは、solveこれらのビット位置の左端を検出し、これらの3つの数値を2つのサブセット(単一の数値と2つの異なる数値)に分割します。2つの数値のサブセットは、このビット位置に等しいビットを持っていますが、数値はまだ異なるはずなので、もう1つの分割ビット位置(明らかに、最初のビット位置の右側)が必要です。2番目の再帰ステップでは、この2つの数値のサブセットを2つの単一の数値に簡単に分割します。ここでのトリックi * 3 + 1は冗長です。アルゴリズムの複雑さが2倍になるだけです。

これは、3つの数字のセットの最初の分割の図です。

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

すべてのビット位置を反復処理し、ワード全体のXORを計算するループがありますが、個別に、特定の位置の真のビットに対して1つのXOR値(A)、偽のビットに対して他のXOR値(B)があります。数値Aのこの位置にゼロビットがある場合、ゼロ以外の場合、Aには偶数サイズの値のサブセットのXORが含まれます(奇数サイズのサブセット)。同じことがBにも当てはまります。私たちは偶数サイズのサブセットにのみ関心があります。0または2の値を含めることができます。

ビット値(ビットz、v、w)に違いはありませんが、A = B = 0であるため、これらのビットで数値を分割することはできません。しかし、3つの等しくない数があります。つまり、ある位置(1)には、異なるビット(xとy)が必要です。それらの1つ(x)は、2つの数値(偶数サイズのサブセット!)にあり、他の(y)-1つの数値にあります。この偶数サイズのサブセットの値のXORを見てみましょう。AとBから、位置1にビット0を含む値(C)を選択します。ただし、Cは2つの等しくない値のXORにすぎません。これらはビット位置1で等しいため、少なくとももう1つのビット位置(位置2、ビットaおよびb)が異なる必要があります。したがって、C!= 0であり、偶数サイズのサブセットに対応します。この分割は、非常に単純なアルゴリズムまたはこのアルゴリズムの次の再帰によって、この偶数サイズのサブセットをさらに分割できるため、有効です。

配列に一意のゼロ要素がない場合、この証明は単純化される可能性があります。一意の数値を常に2つのサブセットに分割します。1つは2つの要素(要素が異なるためゼロにXORすることはできません)、もう1つは1つの要素(定義上ゼロ以外)です。したがって、前処理がほとんどない元のプログラムは正しく機能するはずです。

複雑さはO(m 2 * n)です。前に提案した改善を適用すると、このアルゴリズムが配列をスキャンする予想回数はm / 3+2です。最初の分割ビット位置はm /3であると予想されるため、2-を処理するには1回のスキャンが必要です。要素サブセット、すべての1要素サブセットは配列スキャンを必要とせず、最初にもう1つのスキャンが必要です(solveメソッド外)。


k = 4..6のOPでのアルゴリズムの証明

ここでは、元のアルゴリズムに対して提案されたすべての改善が適用されていると想定しています。

k=4およびk=5:ビットが異なる位置が少なくとも1つあるため、この数値のセットは、サブセットの1つがサイズ1または2になるように分割できます。サブセットのサイズが1の場合、それは非-ゼロ(ゼロの一意の値はありません)。サブセットのサイズが2の場合、ゼロ以外の2つの異なる数値のXORがあります。したがって、どちらの場合も分割は有効です。

k = 6:セット全体のXORがゼロ以外の場合、このXORがゼロ以外のビットを持つ任意の位置でこのセットを分割できます。それ以外の場合は、各位置に偶数の非ゼロビットがあります。異なるビットを持つ位置が少なくとも1つあるため、この位置はセットをサイズ2と4のサブセットに分割します。サイズ2のサブセットには、2つの異なる数値が含まれているため、常にゼロ以外のXORがあります。繰り返しますが、どちらの場合も有効な分割があります。


決定論的アルゴリズム

k > = 7の反証は、元のアルゴリズムが機能しないパターンを示しています。サイズのサブセットが2より大きく、各ビット位置にゼロ以外のビットが偶数あります。ただし、ゼロ以外のビットが単一の数値でオーバーラップする位置のペアを常に見つけることができます。言い換えると、サイズ3または4のサブセットで、両方の位置のサブセット内のすべてのビットのXORがゼロ以外の位置のペアを見つけることが常に可能です。これは、追加の分割位置を使用することを示唆しています。2つの別々のポインターを使用してビット位置を反復処理し、配列内のすべての数値を2つのサブセットにグループ化します。一方のサブセットでは、これらの位置にゼロ以外のビットがあり、その他は残りのすべての数値です。これにより、最悪の場合の複雑さ増しますが、k。5未満のサイズのサブセットを取得できなくなったら、3番目の「分割ポインター」を追加します。kが2倍になるたびに、追加の「分割ポインター」が必要になる場合があります。これにより、最悪の場合の複雑さがもう一度mymに増加します。

これは、次のアルゴリズムの証明のスケッチと見なすことができます。

  1. 元の(改善された)アルゴリズムを使用して、0個以上の一意の値と0個以上の分割不可能なサブセットを見つけます。分割できないサブセットがなくなったら停止します。
  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つは、カウントフィルターを使用することです。

アルゴリズムは次のとおりです。

  1. アレイを線形スキャンし、カウントフィルターを「更新」します。
  2. 配列を線形にスキャンし、フィルターでカウント2ではないすべての要素のコレクションを作成します。これが<= k実際のソリューションになります。(この場合の誤検知は、そうではないように見える固有の要素です)。
  3. ハッシュ関数の新しい基礎を選択し、すべてのk解決策が得られるまで繰り返します。

これは2m少しのスペースを使用します(に依存しませnん)。時間計算量はより複雑ですが、ステップ2で特定の一意の要素が見つからない確率がおおよそであることを知っていると、(1 - e^(-kn/m))^k非常に迅速に解に解決できますが、残念ながら、では完全に線形ではありませんn

時間的に超線形であり、確率的であるため、これは制約を満たさないことを理解していますが、元の条件を考慮すると、このアプローチを検討する価値があるかもしれません。

于 2012-08-14T00:19:05.807 に答える
1

スペースの複雑さの要件により、O(m * n )に緩めると、このタスクはO( n)時間で簡単に解決できます。ハッシュテーブルを使用して各要素のインスタンス数を数え、カウンターが1のエントリをフィルタリングするだけです。または、分散ソートアルゴリズムを使用します。

しかし、ここに確率的アルゴリズムがあり、より軽いスペース要件があります。

このアルゴリズムは、サイズsの追加のビットセットを使用します。入力配列の値ごとに、ハッシュ関数が計算されます。このハッシュ関数は、ビットセットのインデックスを決定します。アイデアは、入力配列をスキャンし、各配列エントリのビットセット内の対応するビットを切り替えることです。重複するエントリは、同じビットを2回切り替えます。一意のエントリ(ほとんどすべて)によって切り替えられたビットは、ビットセットに残ります。これは、ブルームフィルターのカウントと実質的に同じであり、各カウンターで使用されるビットは最下位ビットのみです。

配列をもう一度スキャンすると、一意の値(一部の誤検知を除く)と重複する値(誤検知)が抽出される場合があります。

ビットセットは、不要な重複値の数を減らし、スペースの複雑さを減らすために、誤検知をできるだけ少なくするために十分にスパースである必要があります。ビットセットのスパース性が高いことの追加の利点は、フォールスネガティブの数を減らすことです。これにより、実行時間が少し改善されます。

ビットセットの最適なサイズを決定するには、ビットセットと、一意の値と誤検知の両方を含む一時配列の間で使用可能なスペースを均等に分散します(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 ))の間のどこかにあります。

于 2012-08-17T17:32:58.527 に答える
1

これは、最小限のスペースしか必要としないk = 3の場合の適切なソリューションであり、スペース要件はO(1)です。

'transform'を、mビットの符号なし整数xとインデックスiを引数として取る関数とします。iは0..m --1の間にあり、変換は整数xをに取ります

  • x自体、xのi番目のビットが設定されていない場合
  • to x ^(x <<< 1)ここで、<<<はバレルシフタ(回転)を示します

次のT(x、i)で、transform(x、i)の省略形として使用します。

ここで、a、b、cが3つの異なるmビット符号なし整数であり、a'、b'、c'および他の3つの異なるmビット符号なし整数である場合、XOR b XOR c == a'XOR b' XOR c'ですが、セット{a、b、c}と{a'、b'、c'}は2つの異なるセットであり、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を示します。 XOR bXORcはすべてのビットでa'XORb'XOR c'に等しいため、a'' XOR b'' XOR c'' == 0になります。これは、すべてのビット位置でa'、bのいずれかであることを意味します。 '、c'はa、b、cと同じであるか、またはそれらの2つだけで、選択した位置のビットが反転します(0->1または1->0)。a'、b'、c'はa、b、cとは異なるため、Pを2ビットフリップがあった任意のビット位置とします。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に加えて、a'とb'が異なる別のビット位置Qが必要です(そうでない場合、セットは3つの異なる整数で構成されないか、位置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)0 .. m-1の間のすべての要素xとiのT(x、i)のXORを計算します。
  • 一定の空間で3つの32ビット整数a、b、cを検索し、XOR b XOR cおよびT(a、i)XOR b(a、i)XOR c(a、i)のiのすべての有効な値が一致するようにします。配列から計算されたもの

これは、重複する要素がXOR演算からキャンセルされるため、明らかに機能します。残りの3つの要素については、上記の理由が当てはまります。

私はこれを実装しました、そしてそれは働きます。これが私のテストプログラムのソースコードで、速度に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


実装言語としてSqueakSmalltalkを選択しているので、事前にkを知っていると思います。

  • inject:into:は削減され、空間ではO(1)、時間ではO(N)です。
  • select:is filter、(O(1)スペース要件のため、使用しません)
  • 収集:マップです(O(1)のスペース要件があるため、使用しません)
  • do:はすべてであり、空間ではO(1)、時間ではO(N)です。
  • 角かっこで囲まれたブロックはクロージャ、または変数を閉じずにreturnを使用しない場合は純粋なラムダであり、コロンで始まる記号がパラメーターです。
  • ^はリターンを意味します

k = 1の場合、シングルトンはビットxorでシーケンスを縮小することによって取得されます。

したがって、クラスCollectionでメソッドxorSumを定義します(したがって、selfはシーケンスです)

Collection>>xorSum
    ^self inject: 0 into: [:sum :element | sum bitXor:element]

そして2番目の方法

Collection>>find1Singleton
    ^{self xorSum}

私たちはそれをテストします

 self assert: {0. 3. 5. 2. 5. 4. 3. 0. 2.} find1Singleton = {4}

コストはO(N)、スペースO(1)です。

k = 2の場合、2つのシングルトン(s1、s2)を検索します。

Collection>>find2Singleton
    | sum lowestBit s1 s2 |
    sum := self xorSum.

合計は0とは異なり、2つのシングルトンのxorである(s1 bitXOr:s2)に等しくなります。

合計の最低セットビットで分割し、提案したように両方のシーケンスを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分割のわずかなバリエーションを実装する特定のクラスを定義します。実際には、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つのシングルトンが含まれ
ます
。 (奇数サイズのもの)と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)は認識しやすく、両方のサイズが偶数であり、少なくとも1つの合計がゼロであるため、合計が!= 0
(1,3)のバッグでビットパターンをインクリメントして検索を繰り返すのは困難です。両方のサイズがは奇妙で、シングルトンの数が不明な場合にフォールバックします...ただし、バッグの要素がxorの合計に等しい場合、単一のシングルトンを簡単に認識できます。これは、3つの異なる数では不可能です...

k = 5で一般化できますが、(4,2)と(1,5)の場合のトリックを見つける必要があるため、上記は難しいでしょう。仮説を覚えておいてください。事前にkを知っている必要があります...仮説を立てて、後で検証する必要があります...

反例がある場合は、それを送信してください。上記のSmalltalk実装で確認します。

編集:私はhttp://ss3.gemstone.com/ss/SONiklasBContest.htmlでコード(ライセンスMIT)をコミットしました

于 2012-08-17T15:45:57.483 に答える
0

アルゴリズムはO(n)ではありません。これは、各ステップで数値を2つの同じサイズのグループに分割する保証がないためです。また、数値のサイズに制限がないため(に関連していません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(n ^ 2)よりも大きいO((mn)* n)に到達しました。

PS:常に32ビットの数値がある場合、アルゴリズムはO(n)これを証明するのが難しいことではありません。

于 2012-08-11T18:40:52.057 に答える
0

これは単なる直感ですが、解決策は、xorの合計がゼロでないパーティションが見つかるまで、評価するパーティションの数を増やすことだと思います。

たとえば、[0、m)の範囲内の2ビット(x、y)ごとに、の値で定義されたパーティションを検討しますa & ((1<<x) || (1 << y))。32ビットの場合、32 * 32 * 4 = 4096パーティションになり、。の場合を正しく解決できますk = 4

ここで興味深いのは、kと問題を解決するために必要なパーティションの数との関係を見つけることです。これにより、アルゴリズムの複雑さを計算することもできます。もう1つの未解決の質問は、より適切なパーティショニングスキーマがあるかどうかです。

アイデアを説明するためのいくつかの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 に答える
-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);
    }
  }
}

興味深いことに、両方のアプローチを組み合わせて、2Mのスペースをバンドに分割することができます。次に、すべてのバンドを反復処理する必要があり、すべてのバンド内でビットベクトル手法を使用して一意の値を見つけます。

于 2012-08-17T08:55:42.290 に答える
-4

2つのアプローチが機能します。

(1)キーが整数、値が繰り返し回数である一時ハッシュテーブルを作成します。もちろん、これは指定されたよりも多くのスペースを使用します。

(2)配列(またはコピー)をソートしてから、array [n + 2] ==array[n]の場合の数を数えます。もちろん、これは指定されたよりも多くの時間を使用します。

元の制約を満たすソリューションを見て、私は非常に驚きます。

于 2012-08-11T18:15:19.323 に答える