【データ構造とアルゴリズム】反復DNA配列のアルゴリズム解

1.対象要件

  • すべてのDNAは、「A」、「C」、「G」、「T」と省略された一連のヌクレオチドで構成されています(例:「ACGAATTCCG」)。DNAを研究する場合、DNAの反復配列を特定することが非常に役立つ場合があります。
  • すべてのターゲットサブストリングを検索する関数を記述します。ターゲットサブストリングの長さは10で、DNAストリングに複数回表示されます。
  • 例1:
	输入:s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT"
	输出:["AAAAACCCCC","CCCCCAAAAA"]
  • 例2:
	输入:s = "AAAAAAAAAAAAA"
	输出:["AAAAAAAAAA"]
  • 促す:
    • 0 <= s.length <= 105
    • s [i]は「A」、「C」、「G」、または「T」です。

2.トピック分析

  • この問題の微分問題は、任意の長さLに対して同じ問題を解くことです。ここでは、問題を単純化するためにL = 10を使用します。
  • 3つの異なる方法について説明します。これらはすべてスライディングウィンドウとハッシュセットに基づいており、重要なのはウィンドウスライスの実装方法です。
  • 線形時間O(L)でウィンドウスライスを取得するのは簡単で不器用です。
  • 一般的に、これはO((N-L)L)の時間消費と巨大なスペースOsamuにつながります。一定時間のウィンドウスライスO(1)は優れた方法であり、実装に応じて2つの方法に分けることができます。
    • ラビン-カープアルゴリズム=回転ハッシュアルゴリズムを使用して、一定のウィンドウスライスを実現します。
    • ビット操作=マスクを使用して一定のウィンドウスライスを実装します。
  • 後者の2つの方法は、非常に長いシーケンスであっても、O(N-L)の時間計算量と、適度なスペース消費を伴います。

ここに画像の説明を挿入

アイデアとアルゴリズムの例を解く

①線形時間ウィンドウスライス+ HashSet
  • 長さLのスライディングウィンドウを長さNのストリングに沿って移動します。
  • スライディングウィンドウのシーケンスがハッシュセットに表示されているかどうかを確認します。
    • そうである場合、重複するシーケンスが検出され、出力が更新されます。
    • それ以外の場合は、表示されているHashSetにシーケンスを追加します。

ここに画像の説明を挿入

  • Javaの例:
class Solution {
    
    
  public List<String> findRepeatedDnaSequences(String s) {
    
    
    int L = 10, n = s.length();
    HashSet<String> seen = new HashSet(), output = new HashSet();

    // iterate over all sequences of length L
    for (int start = 0; start < n - L + 1; ++start) {
    
    
      String tmp = s.substring(start, start + L);
      if (seen.contains(tmp)) output.add(tmp);
      seen.add(tmp);
    }
    return new ArrayList<String>(output);
  }
}
  • 複雑さの分析
    • 時間計算量:O((N-L)L)。実行されたループには、長さLのN-L + 1部分文字列があり、
      O((N-L)L)時間計算量につながります。
    • スペースの複雑さ:O((N-L)L)はHashSetを格納するために使用されます。L= 10であるため、時間の複雑さはO(N)です。
②ラビン-カープ文字:回転ハッシュを使用して、一定時間のウィンドウスライスを実現します
  • ラビン-カープアルゴリズムはマルチパターン検索に使用され、2つ以上のタンパク質の類似性を見つけるために重複検出やバイオインフォマティクスでよく使用されます。
  • アイデアは、文字列をスライスし、スライディングウィンドウでシーケンスのハッシュ値を計算することです。どちらも一定時間で実行されます。
  • 例としてAAAAACCCCCAAAAACCCCCCAAAAAGGGTTTを使用してみましょう。まず、次のように文字列を整数配列に変換します。'A
    '-> 0、' C '-> 1、' G '-> 2、' T '-> 3;
  • 次に:
    AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT->
    00000111110000011111100000222333
  • 最初のシーケンスのハッシュ値を計算します:0000011111。基数が4の記数法では、シーケンスは数値と見なされ、次のようにハッシュされます。ここで、c 0 ... 4 = 0およびc5 ... 9 = 1は0000011111を表します。
    ここに画像の説明を挿入
  • 次に、スライスAAAAACCCCC-> AAAACCCCCAについて考えます。整数配列で> 0000111110もしハッシュ再計算し、先頭の0を削除し、末尾の0を追加する場合- 0000011111を表す:
    H 1 =(時間0 ×4 - C 0 4 L)+ CのL + 1。
  • ウィンドウスライスとハッシュ計算が一定時間で完了することがわかります。
  • アルゴリズムは次のとおりです。
    • シーケンスの初期位置からシーケンスをトラバースします:1からN-1。
      • start == 0の場合、最初のシーケンスs [0:L]のハッシュ値を計算します。
      • それ以外の場合、回転ハッシュは前のハッシュ値から計算されます。
      • ハッシュ値がハッシュセットに含まれている場合、重複するシーケンスが検出され、出力が更新されます。
      • それ以外の場合は、ハッシュ値をハッシュセットに追加するに追加します。
    • 出力リストを返します。
  • Javaの例は次のとおりです。
class Solution {
    
    
  public List<String> findRepeatedDnaSequences(String s) {
    
    
    int L = 10, n = s.length();
    if (n <= L) return new ArrayList();

    // rolling hash parameters: base a
    int a = 4, aL = (int)Math.pow(a, L);

    // convert string to array of integers
    Map<Character, Integer> toInt = new
            HashMap() {
    
    {
    
    put('A', 0); put('C', 1); put('G', 2); put('T', 3); }};
    int[] nums = new int[n];
    for(int i = 0; i < n; ++i) nums[i] = toInt.get(s.charAt(i));

    int h = 0;
    Set<Integer> seen = new HashSet();
    Set<String> output = new HashSet();
    // iterate over all sequences of length L
    for (int start = 0; start < n - L + 1; ++start) {
    
    
      // compute hash of the current sequence in O(1) time
      if (start != 0)
        h = h * a - nums[start - 1] * aL + nums[start + L - 1];
      // compute hash of the first sequence in O(L) time
      else
        for(int i = 0; i < L; ++i) h = h * a + nums[i];
      // update output and hashset of seen sequences
      if (seen.contains(h)) output.add(s.substring(start, start + L));
      seen.add(h);
    }
    return new ArrayList<String>(output);
  }
}
  • 複雑さの分析
    • 時間計算量:O(N-L)。
    • スペースの複雑さ:O(N-L)は、ハッシュセットを格納するために使用されます。L= 10であるため、最終はO(N)です。
③ビット演算:マスクを使用して一定時間のウィンドウスライスを実現
  • アイデアは、文字列をスライスし、スライディングウィンドウでシーケンスのマスクを計算することです。どちらも一定の時間で実行されます。
  • Rabin-Karpと同様に、文字列は次のように2ビット整数配列に変換されます:
    A −> 0 = 00 2、C −> 1 = 01 2、G −> 2 = 10 2、T −> 3 = 11 2
  • 次に:GAAAAACCCCCAAAAACCCCCCAAAAAGGGTTT-> 200000111110000011111100000222333;
  • 最初のシーケンスのマスクを計算します:200000111。シーケンス内の各桁(0、1、2、または3)が2ビット以下で占めない:0 = 00 2、1 = 01 2、2 = 10 2、3 = 11 2
  • したがって、マスクはループで計算できます。
    • 左に移動して、最後の2ビットを解放します。bitmask<< = 2;
    • 現在の番号を2000001111の下2桁に格納します:bitmask | = nums [i]。

ここに画像の説明を挿入

  • 次に、スライスを検討します:GAAAAACCCCC-> AAAAACCCCC。整数配列で20000011111-> 0000011111を表し、先頭の2を削除し、最後の1を追加します。

ここに画像の説明を挿入

  • エンド1の追加は非常に簡単で、上記と同じアイデアです。
    • 左に移動して、最後の2ビットを解放します。bitmask<< = 2;
    • 最後の2桁に1を追加します。bitmask| = 1;
  • ここでの問題は、先頭の2を削除することです。つまり、問題は2Lビットと(2L + 1)ビットをゼロに設定することです。トリックを使用して、n番目のビットの値をリセットできます:ビットマスク&=〜(1 << n)。これは:
    • 1 << nは、n番目のビットを1に設定します。
    • 〜(1 << n)は、n番目のビットを0に設定し、すべての下位ビットを1に設定します。
    • ビットマスク&=〜(1 << n)は、ビットマスクのn番目のビットを0に設定します。
  • このトリックを使用する簡単な方法は、最初に2Lビットを設定し、次に(2L + 1)ビットを設定することです。ビットマスク&=〜(1 << 2 * L)&〜(1 <<(2 * L + 1 )。ビットマスク&=〜(3 << 2 * L)の場合は簡略化できます。
    • 3 =(11)2なので、2Lビットと(2L + 1)ビットを1に設定できます。
    • 〜(3 << 2 * L)は、2Lビットと(2L + 1)ビットを0に設定し、すべての下位ビットは1です。
    • = bitmask&=〜(3 << 2 * L)は、ビットマスクの2Lビットと(2L + 1)ビットを0に設定します。

ここに画像の説明を挿入

  • ウィンドウのスライスとマスキングが一定の時間で完了することがわかります。
  • アルゴリズム:
    • シーケンスの開始位置を1からN-Lまでトラバースします。
    • start == 0の場合、最初のシーケンスs [0:L]のマスクを計算します。
    • それ以外の場合は、前のマスクから現在のマスクを計算します。
    • マスクがハッシュセットにある場合、それは繰り返しシーケンスであることを意味し、出力が更新されます。
    • それ以外の場合は、マスクをハッシュセットに追加します。
    • 出力リストを返します。
  • Javaのサンプルアルゴリズム:
class Solution {
    
    
  public List<String> findRepeatedDnaSequences(String s) {
    
    
    int L = 10, n = s.length();
    if (n <= L) return new ArrayList();

    // rolling hash parameters: base a
    int a = 4, aL = (int)Math.pow(a, L);

    // convert string to array of integers
    Map<Character, Integer> toInt = new
            HashMap() {
    
    {
    
    put('A', 0); put('C', 1); put('G', 2); put('T', 3); }};
    int[] nums = new int[n];
    for(int i = 0; i < n; ++i) nums[i] = toInt.get(s.charAt(i));

    int bitmask = 0;
    Set<Integer> seen = new HashSet();
    Set<String> output = new HashSet();
    // iterate over all sequences of length L
    for (int start = 0; start < n - L + 1; ++start) {
    
    
      // compute bitmask of the current sequence in O(1) time
      if (start != 0) {
    
    
        // left shift to free the last 2 bit
        bitmask <<= 2;
        // add a new 2-bits number in the last two bits
        bitmask |= nums[start + L - 1];
        // unset first two bits: 2L-bit and (2L + 1)-bit
        bitmask &= ~(3 << 2 * L);
      }
      // compute hash of the first sequence in O(L) time
      else {
    
    
        for(int i = 0; i < L; ++i) {
    
    
          bitmask <<= 2;
          bitmask |= nums[i];
        }
      }
      // update output and hashset of seen sequences
      if (seen.contains(bitmask)) output.add(s.substring(start, start + L));
      seen.add(bitmask);
    }
    return new ArrayList<String>(output);
  }
}

おすすめ

転載: blog.csdn.net/Forever_wj/article/details/111772303