8

私は自分の仕事でいくつかのグループ化の問題に取り組んでいました。かなりの数の質問があります、ご容赦ください。とても面白いと思います。ここの誰かが組み合わせ論にも興味があるなら、私を助けてください。

さて、たくさんのキャラクターがいます。ここで私はエイズを取りました。

  1. 要素をグループ化する方法は何ですか?4文字のエイズがあるとしましょう。有効なグループ(順序を保持)は次のようになります。

    エイズaidsa id s ai ds
    ai dsaids エイズ エイズ





すべてのグループをどのように列挙しますか?n文字の組み合わせはいくつありますか?

2。特殊なケース

  • Aisdとaisdが2つのグループであるように、ケースが違いを生んだ場合はどうなりますか?

  • すべてのケースを列挙するのにどのくらい時間がかかりますか?4文字と5文字の大文字小文字を見つけることの時間差はどのくらいですか?

  • 「空白」をキャラクターにすると。すべての列挙の後、あなたは何文字を書きましたか?

  • 距離の観点から単語から別の単語への変換を定義する場合。文字「i」を1ステップ移動する必要があるため、「aids」と「aids」の距離は1であると言います。任意の単語の両側にn距離の単語を見つけることができますか?

編集:
「エイズ」は一言です。
「aids」「aids」は、元の単語「aids」から1つの距離にある2つの単語です。
「aids」は、元の単語「aids」から2距離離れた単語です。
「エイズ」とは、単語から3距離離れた単語のことです。

この問題は金鉱のようです。

ボーナス:2つの単語間の最小距離はどれくらいですか。「エイズ」のように、「エイズ」から1つの距離であり、2つの距離でもあります。列挙内の他の単語に最短距離で到達できる「中間」単語はありますか?単語から別の単語へのパスはいくつありますか?

4

5 に答える 5

18

組み合わせの数を計算するのは簡単です。基本的に、2つの文字の間に文字があります。「イプシロン」(空)またはスペースの場合があります。したがって、n文字の場合、そのような区切り文字はn-1個あります。各文字は2つの値しか持てないため、n-1桁の2進数と同じです。したがって、2のn-1の組み合わせの累乗を持つことができます。

aids = 4 characters (n = 4)
n - 1 = 3
2 ** (n-1) = 8 combinations

さて、特別な場合のために。各文字が小文字または大文字のいずれかである場合、それは文字のnのバリエーションの2の累乗です(文字である限り)。ここで、上記の各組み合わせには、大文字と小文字の区別に基づいて2 nのバリエーションがあるため、最終結果は(2(n-1))*(2 ** n)になり、これは2 **(2 * n -1)に等しくなります。

数式から簡単に理解できるように、追加の文字ごとに、それらを列挙するのにかかる時間は2倍または4倍になります(大文字化の内容によって異なります)。

文字の総数は少し難しいですが、各「スペース」が半分の時間「イプシロン」であることに注意してください。したがって、(2 **(n-1))* n(文字)+((2 **(n-1))*(n-1))/ 2(スペース)があります。例では:

(2 ** 3) * 4 = 32 characters
(2 ** 3) * 3 / 2 = 12 spaces
Total = 44 characters

最後に、距離の問題はレーベンシュタイン距離に関連しています。Burkhard-Kellerツリーを使用することを考えましたが、問題がかなり単純であるため、これはまったく必要ないことがわかりました。

まず、ある文字列を別の文字列と等しくするために必要な挿入/削除/変更の最小数は、レーベンシュタイン距離と呼ばれます。これは問題に直接当てはまります。必要に応じて、スペースを追加したり、スペースを削除したり、小文字/大文字を変更したりします。通常、これは動的計画法で最もよく解決されます。動的計画法は、一般に、問題の小さな部分の解決策に関するデータを保持し、他の部分/大きな部分の計算に再利用されると考えることができます。

しかし、私たちの問題の特定の制限を考えると、スペース/イプシロンを表す「2進数」を比較することができます。

関数f(word)が、その単語のスペースを表す2進数を返すとします。たとえば、「ai ds」の場合は「010」、「aids」の場合は「111」を返します。各組み合わせ間の変更の数は、f(010 xor 111 = 101)の結果をXORし、1に等しいビット数をカウントすることによって与えられます。そのためにScalaでいくつかの関数を記述しましょう。

def spacesToBinary(s: String) = {
  (s zip s.drop(1)) // transform "ai ds" into ((a,i), (i, ), ( ,d), (d, s))
  .foldLeft(0) { (binary, pair) => pair match {
      case (' ', _) => binary            // A space is always followed by a letter,
                                         // so we ignore it
      case (_, ' ') => (binary << 1) | 1 // If a letter is followed by a space, bit = 1
      case _ => binary << 1              // If not, bit = 0
  }}
}

def numberOfOnes(d: Int) = {
  var bits = 0
  var n = d
  while(n != 0) {
    if ((n & 1) == 1)
      bits += 1
    n >>= 1
  }
  bits
} 

def spaceDistance(s1: String, s2: String) = 
  numberOfOnes(spacesToBinary(s1) ^ spacesToBinary(s2))

スペースを破棄すると、小文字と大文字の比較は非常に簡単になります。

def caseDistance(s1: String, s2: String) = {
  val ns1 = s1 filter (_ != ' ')
  val ns2 = s2 filter (_ != ' ')
  (ns1 zip ns2).foldLeft(0){(acc, pair) => acc + (if (pair._1 == pair._2) 0 else 1)}
}

したがって、レーベンシュタイン距離は次のようになります。

def levenshtein(s1: String, s2: String) = spaceDistance(s1, s2) + caseDistance(s1, s2)

レーベンシュタイン距離については、次の特性もわかっています。d(x、y)は、xとyの間のレーベンシュタイン距離です。次に、私たちは知っています:

d(x, y) = 0 if, and only if, x = y
d(x, y) = d(y, x)
d(x, y) + d(y, z) >= d(x, z)

私が三角形の不等式として知っている最後の基準。簡単に言えば、xからzへのパスは、少なくともxからyへのパスにyからzへのパスを加えたものと同じくらい小さいです(頂点x、y、zを持つ三角形を考えてください)。

では、ボーナスの質問について考えてみましょう。

2つの単語の間にいくつのパスがありますか?レーベンシュタイン距離がnの場合、これは「n」個の一意の変更、a1、a2、...、anがあることを意味します。これらの変更の異なる順序ごとに、パスがあります。「n」要素の順列の数は、「n」(またはn!)の階乗です。

def factorial(n: Int): Int = n match {
  case 0 => 1
  case _ => n * factorial(n-1)
}

2! = 2
3! = 6
4! = 24
5! = 120
etc

「中央」の組み合わせはありますか?実は違う。戻って、組み合わせをスペース/スペースなしと小文字/大文字を表す2進数のペアと考えると、ビットを単純に反転して、選択したものまでの距離が可能な限り最大。

または、言い換えると、n文字のすべての組み合わせに対して、対応する組み合わせは1つだけであるため、2つの組み合わせ間のレーベンシュタイン距離は2 * n -1であり、任意の2つの組み合わせ間の可能な最大距離です。

sまでの(最小)距離がnであるすべての組み合わせを計算するのを忘れたようです。さて、各組み合わせは2つの2進数として表すことができ、各文字のスペースと大文字を表します。最初の文字の長さはn-1桁、2番目の文字の長さはn桁です。

また、これらの「数字」のいずれかを単純に反転して、違いを得ることができることもわかっています。したがって、2 * n-1桁の長さの1つの大きな2進数を取得し、そのすべての桁を列挙すると、それから最小距離dでの組み合わせは、サイズのグループ内の2*n-1桁の組み合わせによって与えられます。繰り返しのない「d」。N = 2 * n-1の場合、そのような組み合わせの数はN!/((Nd)!* d!)です。

たとえば、距離2と「エイズ」の場合、n = 4、d = 2、N = 2 * 4-1 = 7であり、組み合わせの数は7!/(5!* 2!)= 7 * 6 / 2=21。

このように組み合わせを構成できます。

def computeCombinations(n: Int, d: Int): List[List[Int]] = {
  def gen(d: Int, l: List[Int]): List[List[Int]] = d match {
    case 0 => List(List())
    case _ => for(el <- l;
                  sl <- gen(d-1, l.dropWhile(_ != el).tail))
              yield el :: sl
  }

  gen(d, (0 until n).toList)
}

これにより、変更する文字/スペースのリストのリストが返されます。切り替えたいビット数によって、どの文字またはスペースを変更する必要があるかを示します。簡単にするために、大文字の2進数とスペース/スペースなしの2進数が1つの2進数に連結されていると仮定します。

次に、この情報からバリエーションを生成する方法を見つける必要があります。スペースなしの単語を受け取ると仮定すると、大文字/小文字は単純です。

def toggleCharCase(c: Char) = (c ^ ('A' ^ 'a')).toChar
def toggleCases(s: String, bits: List[Int]) = (
  s.zipWithIndex
  map (t => if (Set(bits: _*) contains t._2) (toggleCharCase(t._1), t._2) else t)
  map (_._1)
)

スペースの切り替えはより困難です。上で定義したspacesToBinaryを使用し、それを設定されているビット番号のリストに変換し、要求されたビットを切り替えて返します。その後、別の関数を使用して、適切な場所にスペースを挿入します。

def toggleSpaces(s: String, bits: List[Int]): List[Int] = {
  val before = spacesToBinary(s)
  val beforeBits = Set() ++ 
                   (for(i <- 0 to 30; // Assuming 32 bits, 1 for sign
                        if (scala.Math.pow(2, i).toInt & before) != 0)
                    yield i)
  val afterBits = (beforeBits union Set(bits: _*)) diff 
                  (beforeBits intersect Set(bits : _*))
  afterBits.toList
}

def addSpaces(s: String, bits: List[Int]): String = (
  s.filter(_ != ' ').zipWithIndex 
  map (t => t._1.toString + (if (bits contains t._2) " " else ""))
  mkString
)

次に、スペースの差を計算し、スペースを削除し、ケースを切り替えてから、スペースを追加し直す必要があります。どれどれ:

def changeCombination(s: String, n: Int, bits: List[Int]) = {
  // Split the binary number with all changes into case changes and space changes
  val spaceBits = bits filter (_ >= n) map (_ - n)
  val caseBits = bits filter (_ < n)

  // Compute the set of spaces after changing them
  val newSpaces = toggleSpaces(s, spaceBits)

  // Remove spaces and toggle case
  val noSpaces = s filter (_ != ' ')
  val caseChanged = toggleCases(noSpaces, caseBits).mkString

  // Now add the spaces as computed before
  val spacesAdded = addSpaces(caseChanged, newSpaces).mkString
  spacesAdded
}

最後に、すべての組み合わせを計算し、それぞれに対して変更された文字列を生成します。

def makeCombinations(s: String, d: Int) = {
  val n = (s filter (_ != ' ')).length
  for(combination <- computeCombinations(n*2-1, d))
  yield changeCombination(s, n, combination)
}

このコードはすべてScala2.8でテストされています(私が作成した名前の変更を除く)。実行の結果は次のとおりです。

scala> makeCombinations("ai ds", 2) foreach println
AI ds
Ai Ds
Ai dS
A i ds
Aids
Ai d s
aI Ds
aI dS
a I ds
aIds
aI d s
ai DS
a i Ds
aiDs
ai D s
a i dS
aidS
ai d S
a ids
a i d s
aid s
于 2009-07-14T18:53:57.857 に答える
2

他の回答がすでに述べたように、ポイント1には2 ^(n-1)の可能性があります。いくつかの特殊なケース(ポイント2)について:

  • Aisdとaisdが2つのグループであるように、ケースが違いを生んだ場合はどうなりますか?

その場合、2 ^ nの異なるケースの組み合わせがあったので、2 ^ n * 2 ^(n-1)= 2 ^(2 * n-1)の可能性がありました。

  • 「空白」をキャラクターにすると。すべての列挙の後、あなたは何文字を書きましたか?

それはもっと興味深い質問です。スペースを配置しない可能性が1つ、スペースを1つ配置する可能性が3つ、スペースを2つ配置する可能性が3つ、スペースを3つ配置する可能性が1つあります。正しく思い出せば、これは二項分布であり、数値を計算する式があります。これには、パスカルの三角形を使用することもできます。

   1
  1 1
 1 2 1
1 3 3 1 <- your case (4th row)
  ...

これらの数値を取得したら、次のように合計文字数を計算します。

1*4 + 3*5 + 3*6 + 1*7 = 44 
于 2009-07-14T19:06:23.213 に答える
1

http://www-cs-faculty.stanford.edu/~knuth/fasc3b.ps.gz(postscriptを表示できない場合は、Ghostscript / Ghostviewをダウンロード)でパーティションについて詳しく説明しています。

長さnのシーケンスの場合、2 ^(n-1)個のパーティションがあります。連続するアイテムの各ペアの間に少し考えてみてください。ビットが設定されている場合、それらは(リストされているようにスペースで)区切られます。「aids」(長さ4)には2^3の可能なパーティションがあります。

他の質問への回答:

列挙する時間:O(n * 2 ^ n)-出力の長さは一定です。入力する長さとともに項目数が増えるだけでなく、各項目の文字数も増えます。

書き込まれた文字数:改行を数えないようにします(数えた場合は、さらに2 ^(n-1)文字を追加します)。次に、n * 2 ^(n-1)個の非スペース文字に加えて、すべての一意のn-1桁のビット文字列の1の数があります。k桁のビット文字列を書き出すと、k * 2 ^ kビットになり、その半分は1になります。したがって、文字の総数は[n +(n-1)/ 2] * 2 ^(n-1)になります。 )、改行はカウントしません。「エイズ」の8つのバリエーションのリストには、32個の非スペース文字と、それぞれ4 * 2 ^ 3と(3/2)* 2^3の12個のスペースがあります。

距離の編集:変換とそのコストについてより正確にする必要があります。「単語」とは、単一のパーティション(8つの例の行の1つ)を意味すると思います。編集が単一のスペースの削除または追加である場合、n-1桁のビット文字列のハミング距離について話していることになります。

于 2009-07-14T18:56:32.580 に答える
1

距離k以下の範囲内の各単語にアクセスするための単純なアルゴリズム:ハッシュテーブルを使用して各ビット文字列に1回だけアクセスします(または2 ^(n-1)ビットの配列ですが、大きすぎる可能性があります)。隣接する単一編集の違いの(ハミング距離を想定:1 ..(n-1)からのiの場合、ソースビット文字列とのXOR 2 ^ i、i番目のビットを切り替えます)。

これを深さkまで実行すると(深さは再帰とともに渡されます)、距離k内のすべての編集にアクセスしたことになります。もちろん、正確に深さkのものだけが必要な場合は、幅優先探索を使用することをお勧めします。各ネイバーをすぐに訪問するのではなく、訪問するキューに入れておきます。特定の世代(j)のアイテムのキューにアクセスしている間(すべて同じ最適な編集距離を持つ)、次の世代(j + 1)の別のキューに将来のアイテムをキューに入れます。このようにして、可能な限り少ない数の編集を使用して最初に各文字列にアクセスします(幅優先=各遷移のコストが同じ場合に最初に最適)。

幅優先探索を実行したくない場合は、いつでもk以下の単語のセットとk-1以下の単語のセットを計算し、差をとることができます(2つの別々のテーブルを使用します)。これは事実上「反復深化深さ探索」です。

構造化されていない単語のセット(一般的な辞書)を検討している場合を除いて、BKツリーはここでは適切ではありません。すでに1つの単語のパーティションの構造を正確に知っています。

于 2009-07-14T19:23:36.410 に答える
0

カウントの議論は正しい。

分枝限定法を使用して、このような問題をプログラムする一般的な方法があります。これが例です。

基本的に、文字列をスキャンするループを作成するのではなく、再帰関数を作成し、その引数の1つとしてコストを追跡します。次に、各ステップで、1)追加コスト0で文字列をステップダウンし、2)文字列に小さな変更を加え、コストに増分を追加してから、3)2を繰り返します。検討したいさまざまな種類の変更。

次に、全体的なコスト予算を立て、コストが予算を超えるような支店をとることを拒否します。

最後に、外側のループとして、予算0ですべてを1回実行します。それでも一致が生成されない場合は、1つ以上の一致が得られるまで、1のコストで再度実行します。

于 2009-07-14T19:35:59.020 に答える