Generalizing GC-Content

A length k substring of a genetic string is commonly called a k-mer. A genetic string of length n can be seen as composed of n−k+1 overlapping k-mers. The k-mer composition of a genetic string encodes the number of times that each possible k-mer occurs in the string. See Figure 1.
k-Mer Composition - 图1
Figure 1. The 2-mer composition of TTGATTACCTTATTTGATCATTACACATTGTACGCTTGTGTCAAAATATCACATGTGCCT

The 1-mer composition is a generalization of the GC-content of a strand of DNA, and the 2-mer, 3-mer, and 4-mer compositions of a DNA string are also commonly known as its di-nucleotide, tri-nucleotide, and tetra-nucleotide compositions.

The biological significance of k-mer composition is manyfold. GC-content is helpful not only in helping to identify a piece of unknown DNA (see “Computing GC Content”), but also because a genomic region having high GC-content compared to the rest of the genome signals that it may belong to an exon. Analyzing k-mer composition is vital to fragment assembly as well.

In “Computing GC Content”, we also drew an analogy between analyzing the frequency of characters and identifying the underlying language. For larger values of k, the k-mer composition offers a more robust fingerprint of a string’s identity because it offers an analysis on the scale of substrings (i.e., words) instead of that of single symbols. As a basis of comparison, in language analysis, the k-mer composition of a text can be used not only to pin down the language, but also often the author.

Problem

For a fixed positive integer k, order all possible k-mers taken from an underlying alphabet lexicographically.
Then the k-mer composition of a string s can be represented by an array A for which A[m] denotes the number of times that the mth k-mer (with respect to the lexicographic order) appears in s.
Given: A DNA string s in FASTA format (having length at most 100 kbp).
Return: The 4-mer composition of s.

Sample Dataset

Rosalind_6431
CTTCGAAAGTTTGGGCCGAGTCTTACAGTCGGTCTTGAAGCAAAGTAACGAACTCCACGG
CCCTGACTACCGAACCAGTTGTGAGTACTCAACTGGGTGAGAGTGCAGTCCCTATTGAGT
TTCCGAGACTCACCGGGATTTTCGATCCAGCCTCAGTCCAGTCTTGTGGCCAACTCACCA
AATGACGTTGGAATATCCCTGTCTAGCTCACGCAGTACTTAGTAAGAGGTCGCTGCAGCG
GGGCAAGGAGATCGGAAAATGTGCTCTATATGCGACTAAAGCTCCTAACTTACACGTAGA
CTTGCCCGTGTTAAAAACTCGGCTCACATGCTGTCTGCGGCTGGCTGTATACAGTATCTA
CCTAATACCCTTCAGTTCGCCGCACAAAAGCTGGGAGTTACCGCGGAAATCACAG

Sample Output

4 1 4 3 0 1 1 5 1 3 1 2 2 1 2 0 1 1 3 1 2 1 3 1 1 1 1 2 2 5 1 3 0 2 2 1 1 1 1 3 1 0 0 1 5 5 1 5 0 2 0 2 1 2 1 1 1 2 0 1 0 0 1 1 3 2 1 0 3 2 3 0 0 2 0 8 0 0 1 0 2 1 3 0 0 0 1 4 3 2 1 1 3 1 2 1 3 1 2 1 2 1 1 1 2 3 2 1 1 0 1 1 3 2 1 2 6 2 1 1 1 2 3 3 3 2 3 0 3 2 1 1 0 0 1 4 3 0 1 5 0 2 0 1 2 1 3 0 1 2 2 1 1 0 3 0 0 4 5 0 3 0 2 1 1 3 0 3 2 2 1 1 0 2 1 0 2 2 1 2 0 2 2 5 2 2 1 1 2 1 2 2 2 2 1 1 3 4 0 2 1 1 0 1 2 2 1 1 1 5 2 0 3 2 1 1 2 2 3 0 3 0 1 3 1 2 3 0 2 1 2 2 1 2 3 0 1 2 3 1 1 3 1 0 1 1 3 0 2 1 2 2 0 2 1 1

Solution1:暴力算法

首先理解什么是 K-mer ,即长度为 k子串(注意是连续,不是子序列)。例如 CTTCGAAAGT4-mer 个数:

C T T C G A A A G T
C T T C
T T C G
T C G A
C G A A
G A A A
A A A G
A A G T

也即是说枚举 k-mer 起点 i ,对应终点 i+k-1 就行。

第二步:将所有的 k-mer 进行同类计数,然后按字典序升序排序返回结果。

  1. from typing import List
  2. import re
  3. class Solution:
  4. def generateKmer(self, bases: List[str], path: List[str], k: int) -> str:
  5. if len(path) == k:
  6. yield ''.join(path)
  7. return
  8. for base in bases:
  9. path.append(base)
  10. yield from self.generateKmer(bases, path, k)
  11. path.pop()
  12. def kmerLexicographically(self, DNA: str, k: int) -> List[int]:
  13. # 1. 寻找 DNA 所有 kmer 并计数
  14. from collections import Counter
  15. freq = Counter(DNA[i:i+k] for i in range(0, len(DNA) - k + 1))
  16. # 2. 按字典序生成 k-mer 所有组合
  17. return [freq[kmer] for kmer in self.generateKmer(['A', 'C', 'G', 'T'], [], k)]
  18. seqs = """
  19. >Rosalind_6431
  20. CTTCGAAAGTTTGGGCCGAGTCTTACAGTCGGTCTTGAAGCAAAGTAACGAACTCCACGG
  21. CCCTGACTACCGAACCAGTTGTGAGTACTCAACTGGGTGAGAGTGCAGTCCCTATTGAGT
  22. TTCCGAGACTCACCGGGATTTTCGATCCAGCCTCAGTCCAGTCTTGTGGCCAACTCACCA
  23. AATGACGTTGGAATATCCCTGTCTAGCTCACGCAGTACTTAGTAAGAGGTCGCTGCAGCG
  24. GGGCAAGGAGATCGGAAAATGTGCTCTATATGCGACTAAAGCTCCTAACTTACACGTAGA
  25. CTTGCCCGTGTTAAAAACTCGGCTCACATGCTGTCTGCGGCTGGCTGTATACAGTATCTA
  26. CCTAATACCCTTCAGTTCGCCGCACAAAAGCTGGGAGTTACCGCGGAAATCACAG
  27. """
  28. DNA = list(seq.replace('\n', '') for seq in re.split(r'>.*', seqs) if seq.replace('\n', ''))[0] # 第一个是 `\n`
  29. k = 4
  30. for idx in Solution().kmerLexicographically(DNA, k):
  31. print(idx, end=' ')

复杂度分析

  • 时间复杂度:,其中枚举所有 k-mer 可能组合为 k-Mer Composition - 图2;枚举 DNA 所有长度为 k 的子串然后哈希化为 k-Mer Composition - 图3
  • 空间复杂度k-Mer Composition - 图4,其中枚举所有 k-mer 可能组合为 k-Mer Composition - 图5;枚举 DNA 所有长度为 k 的子串为为 k-Mer Composition - 图6,哈希计数器为 k-Mer Composition - 图7

    Solution2: 二进制迭代优化

    一、k-mer 枚举二进制优化:

  1. 四进制 :由于输入序列只有四个字符,我们可以使用 两个比特位 来表示四个字符,由于涉及到字典序因此以 00 表示 A ;以 01 表示 C ;以 10 表示 G ;以 11 表示 T
  2. 枚举长度为 k 的所有字典序组合,只需要保证每层子树是按字典序枚举即可。 用二进制状态表示如下序列。

Kmer二进制字典序状态.drawio :::tips Q:为什么用二进制表示长度为 k 的子串?
A:本质上就是手动实现哈希函数,因为 python 中以 str 为字典键,它会先执行 hash(str) 获得一个哈希值。 k=4 的所有组合可能值为 0-25500000000 -> 11111111 ) :::

二、DNA 枚举 k-mer:滚动哈希优化

长度为 k = 4 如何快速计算其哈希值呢?如果暴力的方式就是每次计算,这样的时间复杂度为 k-Mer Composition - 图8

C T T C G A A A G T
01 11 11 01
11 11 01 10
11 01 10 00
01 10 00 00
10 00 00 00
00 00 00 10
00 00 10 11

注意上面相邻 k-mer 之间的关系,两相邻 k-mer 之间重叠部分有 k-1 ,因此我们可以想办法减少这部分的运算。

  1. 获取序列的哈希值,如上图中:
    1. CTTC 的哈希值为 k-Mer Composition - 图9
    2. TTCG 的哈希值为 k-Mer Composition - 图10
  2. 滚动哈希优化:假设 CTTC 的哈希值 值已知,如何快速计算其相邻序列 TTCG 的哈希值 k-Mer Composition - 图11 呢?
    1. CTTC 的后 3 位哈希值和为 k-Mer Composition - 图12
    2. TTCG 的前 3 位哈希值和为 k-Mer Composition - 图13
    3. 我们知道其是重叠片段,且易知 TTCG 的前 3 位哈希值和就是CTTC 的后 3 位哈希值和再乘上 4 。即有 k-Mer Composition - 图14

考虑由序列 CTTC 转变为 TTCG 后,我们需要对 k-Mer Composition - 图15 做什么转变才能得到 k-Mer Composition - 图16 呢?

  1. CTTC 要删除最左边C 字符哈希值,才能获得后 3 位哈希值和为 k-Mer Composition - 图17
  2. TTCG 不仅要使用CTTC 的后 3 位哈希值和再乘上 4 ,还要要增加最右侧G 字符哈希值,即 k-Mer Composition - 图18

综上整理得到滚动哈希一般表达式:。其中 表示权值, 中 表示进制, 表示,整个表达式表示位权

  1. from typing import List
  2. import re
  3. class Solution:
  4. def kmerLexicographically(self, DNA: str, k: int) -> List[int]:
  5. base = 4 # 4 进制
  6. # 1. k-mer 所有组合,以二进制表示
  7. cnt = [0] * pow(base, k) # 共有 4* k 个状态
  8. # 2. 使用滚动哈希寻找 DNA 所有 kmer 并计数
  9. bases = {'A': 0b00, 'C': 0b01, 'G': 0b10, 'T': 0b11}
  10. # (1) 初始化计算长度为 k 的子串哈希和
  11. h1 = 0
  12. for i in range(k):
  13. h1 = h1 * base + bases[DNA[i]]
  14. cnt[h1] += 1 # 别忘第一个
  15. # (2) 滚动哈希
  16. highPow = pow(base, k-1)
  17. for j in range(k, len(DNA)): # 枚举终点
  18. h1 = (h1 - bases[DNA[j-k]] * highPow) * base + bases[DNA[j]]
  19. cnt[h1] += 1
  20. return cnt
  21. seqs = """
  22. >Rosalind_6431
  23. CTTCGAAAGTTTGGGCCGAGTCTTACAGTCGGTCTTGAAGCAAAGTAACGAACTCCACGG
  24. CCCTGACTACCGAACCAGTTGTGAGTACTCAACTGGGTGAGAGTGCAGTCCCTATTGAGT
  25. TTCCGAGACTCACCGGGATTTTCGATCCAGCCTCAGTCCAGTCTTGTGGCCAACTCACCA
  26. AATGACGTTGGAATATCCCTGTCTAGCTCACGCAGTACTTAGTAAGAGGTCGCTGCAGCG
  27. GGGCAAGGAGATCGGAAAATGTGCTCTATATGCGACTAAAGCTCCTAACTTACACGTAGA
  28. CTTGCCCGTGTTAAAAACTCGGCTCACATGCTGTCTGCGGCTGGCTGTATACAGTATCTA
  29. CCTAATACCCTTCAGTTCGCCGCACAAAAGCTGGGAGTTACCGCGGAAATCACAG
  30. """
  31. DNA = list(seq.replace('\n', '') for seq in re.split(r'>.*', seqs) if seq.replace('\n', ''))[0] # 第一个是 `\n`
  32. k = 4
  33. ret = Solution().kmerLexicographically(DNA, k)
  34. for idx in Solution().kmerLexicographically(DNA, k):
  35. print(idx, end=' ')

复杂度分析

  • 时间复杂度:,其中枚举所有 k-mer 可能组合为 ;枚举所有长度为 k 的子串耗费 k-Mer Composition - 图19;一次长度为 k 的子串哈希化为 ,其后使用滚动哈希策略可以在 时间内计算处新的哈希值。
  • 空间复杂度k-Mer Composition - 图20,其中枚举所有 k-mer 可能组合为 k-Mer Composition - 图21;枚举 DNA 所有长度为 k 的子串哈希值为 k-Mer Composition - 图22