文章目录

  • 字符串匹配
    • 一、基本概念
      • 字符串匹配问题
      • 符号和术语
      • 后缀重叠引理
    • 二、朴素字符串匹配算法
    • 三、Rabin-Karp算法(字符串Hash算法)
      • 进制Hash
      • 质数Hash
    • 四、利用有限自动机进行字符串匹配
      • 有限自动机
      • 字符串匹配自动机
      • 计算状态转移函数
      • 代码
    • 五、Knuth-Morris-Pratt算法
      • 模式的前缀函数
      • 代码
    • 六、Z-函数(扩展KMP)

字符串匹配

一、基本概念

字符串匹配问题

假设文本是一个长度为nnn的数组T[1…n]T[1 \ldots n]T[1…n],而模式是一个长度为mmm的数组P[1…m]P[1 \ldots m]P[1…m],其中m≤nm \leq nm≤n,进一步假设PPP和TTT的元素都是来自一个有限的字母集∑\sum∑的字符。数组TTT和PPP通常被称为字符串

如果0≤s≤n−m0 \leq s \leq n-m0≤s≤n−m,并且T[s+1…s+m]=P[1…m]T[s+1 \ldots s+m]=P[1 \ldots m]T[s+1…s+m]=P[1…m],那么称模式PPP在文本TTT中出现过,且偏移为sss。如果PPP在TTT中以偏移sss出现,那么称sss为有效偏移,否则为无效偏移

字符串匹配问题就是在TTT中找到PPP的所有有效偏移sss。

符号和术语

  1. 字符串集合:我们用∑∗\sum^{*}∑∗来表示包含所有有限长度的字符串的集合,该字符串是由字母表∑\sum∑中的字符串组成。在本文中,我们只考虑有限长度的字符串。长度为000的字符串用ϵ\epsilonϵ表示,也属于∑∗\sum^{*}∑∗。
  2. 字符串的长度:一个字符串TTT的长度用∣T∣|T|∣T∣表示。特别的,∣ϵ∣=0|\epsilon| = 0∣ϵ∣=0。
  3. 连结:两个字符串xxx和yyy的连结表示为xyxyxy,意义为xxx后接yyy,长度为∣x∣+∣y∣|x| + |y|∣x∣+∣y∣。
  4. 前缀:如果对于三个个字符串x=wyx=wyx=wy,那么称字符串www是xxx的前缀,记作w⊏xw \sqsubset xw⊏x,且∣w∣≤∣x∣|w| \leq |x|∣w∣≤∣x∣。
  5. 后缀:如果对于三个个字符串x=ywx=ywx=yw,那么称字符串www是xxx的后缀,记作w⊐xw \sqsupset xw⊐x,且∣w∣≤∣x∣|w| \leq |x|∣w∣≤∣x∣。特别的,空字符串ϵ\epsilonϵ是任意一个字符串的前缀和后缀。

后缀重叠引理

假设xxx,yyy和zzz满足x⊐zx \sqsupset zx⊐z和y⊐zy \sqsupset zy⊐z。如果∣x∣≤∣y∣|x| \leq |y|∣x∣≤∣y∣,那么x⊐yx \sqsupset yx⊐y,反过来如果∣y∣≤∣x∣|y| \leq |x|∣y∣≤∣x∣,那么y⊐xy \sqsupset xy⊐x。如果∣x∣=∣y∣|x| = |y|∣x∣=∣y∣,那么x=yx = yx=y。

为了符号简洁,我们把模式P[1…m]P[1 \ldots m]P[1…m]的由kkk个字符串组成的的前缀P[1…k]P[1 \ldots k]P[1…k],记作PkP_{k}Pk​。因此P0=ϵ,Pm=P=P[1…m]P_{0} = \epsilon, P_{m}=P=P[1 \ldots m]P0​=ϵ,Pm​=P=P[1…m]。与此类似,我们把文本TTT中由kkk个字符组成的前缀记作TkT_{k}Tk​。

字符串匹配问题转化为:找到所有的偏移s(0≤s≤n−m)s(0 \leq s \leq n-m)s(0≤s≤n−m),使得P⊐Ts+mP \sqsupset T_{s+m}P⊐Ts+m​。


二、朴素字符串匹配算法

朴素字符串算法是通过一个循环找到所有的有效偏移,该循环对n−m+1n-m+1n−m+1个可能的sss值进行检测,看是否满足条件P[1…m]=T[s+1…s+m]P[1 \ldots m]=T[s+1 \ldots s+m]P[1…m]=T[s+1…s+m]。

NAIVE-STRING-MATCHER(T,P):n = T.lengthm = P.lengthfor s = 0 to n-m:if P[1...m] = T[s+1 ... s+m]:print "Pattern occurs whit shift",s

伪代码中的循环枚举sss的位置,for中的判断隐含了一个循环,这个循环检测偏移sss是否为有效偏移。

在最坏情况下,朴素算法的运行时间为O((n−m+1)m)O((n-m+1)m)O((n−m+1)m)。如果m=n/2m = n/2m=n/2,则运行时间为Θ(n2)\Theta(n^{2})Θ(n2)。

我们看到,朴素字符串匹配算法不是解决该问题的最佳过程。是因为当其他无效的sss值存在时,他只关心一个有效的sss的值,而完全忽略了检测无效的sss值的时候所带来的有用信息。例如对于P=aaabP=aaabP=aaab并且我们发现s=0s=0s=0是一个有效的偏移值,由于T[4]=bT[4] = bT[4]=b,所以s=1,2,3s=1,2,3s=1,2,3都不是有效的。

三、Rabin-Karp算法(字符串Hash算法)

进制Hash

Rabin-Karp算法运用了初等数论的知识,其实就是对字符串进行HashHashHash运算,比较每一位上的HashHashHash值是否相等即可。

为了便于说明,我们假设∑={0,1,2,3,4,5,6,7,8,9}\sum = \{0,1,2,3,4,5,6,7,8,9\}∑={0,1,2,3,4,5,6,7,8,9},这样每个字符都是十进制数字。(在通常情况下,可以假定每个字符都是以ddd为基数表示的数字,其中∣∑∣=d|\sum| = d∣∑∣=d),每一个字符串就是相当与一个十进制数,其长度为kkk。

给定一个模式P[1…m]P[1 \ldots m]P[1…m],令ppp为表示的十进制数。类似的,有字符串T[1…n]T[1 \ldots n]T[1…n],假设tst_{s}ts​表示子串T[s+1…s+m]T[s+1 \ldots s+m]T[s+1…s+m]对应的十进制数。当然,只有当sss是一个有效偏移的时候,p=tsp = t_{s}p=ts​,如果能在时间Θ(m)\Theta(m)Θ(m)内计算出ppp的值,并且在总时间Θ(n−m+1)\Theta(n-m+1)Θ(n−m+1)内计算出所有的tst_{s}ts​值,那么通过比较tst_{s}ts​和ppp的值就可以在时间Θ(m)+Θ(n−m+1)=Θ(n)\Theta(m) + \Theta(n-m+1) = \Theta(n)Θ(m)+Θ(n−m+1)=Θ(n)时间内找到所有的有效偏移。

其实这是一种HashHashHash运算,根据霍纳法则,我们可以在规定的时间内计算出ppp:

p=P[m]+10(P[m−1]+10(P[m−2]+…+10(P[2]+10P[1])…))p = P[m] + 10(P[m-1] + 10(P[m-2] + \ldots + 10(P[2] + 10P[1]) \ldots )) p=P[m]+10(P[m−1]+10(P[m−2]+…+10(P[2]+10P[1])…))

类似的,我们可以通过规定的时间计算出t0t_{0}t0​的值,并且能在O(1)O(1)O(1)的时间内利用t0t_{0}t0​的值计算出t1t_{1}t1​的值,依次类推,总结出递推公式:

ts+1=10(ts−10m−1T[s+1])+T[s+m+1]t_{s+1} = 10(t_{s}-10^{m-1}T[s+1])+T[s+m+1] ts+1​=10(ts​−10m−1T[s+1])+T[s+m+1]

减去10m−1T[s+1]10^{m-1}T[s+1]10m−1T[s+1],表示减去最高位的数字,乘以十表示将这个数向左移动一位,加上T[s+m+1]T[s+m+1]T[s+m+1],表示加上最低位。

预先计算出常数10m−110^{m-1}10m−1,可以使用快速幂计算。

到目前为止,我们就始终在回避一个问题,如果mmm的值过大,则tit_{i}ti​的值就可能很大,以至于超过了运算的范围,因此我们就需要运用数论的知识,将其进行取模运算。

选定一个素数qqq,使得dqdqdq在一个机器字长内,我们就可以进行取余运算:

ts+1=(d(ts−hT[s+1])+T[s+m+1])modqt_{s+1} = (d(t_{s}-hT[s+1])+T[s+m+1]) \mod q ts+1​=(d(ts​−hT[s+1])+T[s+m+1])modq

其中ddd为ddd进制,hhh为h≡dm−1modqh \equiv d^{m-1} \mod qh≡dm−1modq。

但是这种结果并不是很完美,因为ts≡pmodqt_{s} \equiv p \mod qts​≡pmodq,并不能说明ts=pt_{s} = pts​=p,因此,我们还必须仿照朴素算法那样,验证一下这个答案。

如果qqq是一个素数,并且这个qqq足够大,这种伪命中点的概率发生的几率很小。

Rabin-Karp算法的期望时间复杂度为O(n)+O(m(v+n/q))O(n) + O(m(v+n/q))O(n)+O(m(v+n/q))。预计较好的情况(有效偏移比较少,素数大,mmm远远小于nnn)。这个算法的时间复杂度为O(n)O(n)O(n)。

还可以使用链接法或开放寻址法来进行无错Hash。

同时,我们可以定义h[0]=s[0]h[0] = s[0]h[0]=s[0],h[k]=(h[k−1]A+s[k])modBh[k] = (h[k-1]A + s[k]) \mod Bh[k]=(h[k−1]A+s[k])modB,位权p[0]=1p[0] = 1p[0]=1,p[k]=(p[k−1]A)modBp[k] = (p[k-1]A) \mod Bp[k]=(p[k−1]A)modB。

这样我们就能在O(1)O(1)O(1)的时间内计算出某个子串S[a…b]S[a \ldots b]S[a…b]的hash值(h[b]−h[a−1]p[b−a+1])modB(h[b]-h[a-1]p[b-a+1]) \mod B(h[b]−h[a−1]p[b−a+1])modB。

质数Hash

如果我们想判断两个单词是不是同体词(即两个字符串每个字母出现的次数都相同,例如eat和ate)。此时我们将每一个字母替换成一个唯一的质数,那么根据算数基本定理,我们就可以把一个字符串看成是一个由多个质数相乘得到的唯一的整数,那么如果两个单词是同体词,那么这个两个Hash值一定相等。

四、利用有限自动机进行字符串匹配

有限自动机

一个有限自动机MMM是一个五元组(Q,q0,A,∑,δ)(Q,q_{0},A,\sum,\delta)(Q,q0​,A,∑,δ),其中:

  • QQQ是状态的有限集合
  • q0∈Qq_{0} \in Qq0​∈Q是初始状态
  • A⊆QA \subseteq QA⊆Q是一个特殊接受状态的集合
  • ∑\sum∑是有限输入字母表
  • δ\deltaδ是一个从Q×∑Q \times \sumQ×∑到QQQ的函数,称为MMM的转移函数

有限自动机状态开始于q0q_{0}q0​,读入一个字符aaa,则他从状态qqq变成了状态δ(q,a)\delta(q,a)δ(q,a)。当自动机的状态属于QQQ的时候,我们说自动机接受了迄今为止所有的字符。

有限自动机MMM引入一个终态函数ϕ(w)\phi(w)ϕ(w),他是一个从∑∗\sum^{*}∑∗到QQQ的一个函数,函数的值为自动机MMM处理完字符串www之后所处的最终状态。因此当且仅当ϕ(w)∈A\phi(w) \in Aϕ(w)∈A的时候,MMM接受字符串www。存在递归定义:

ϕ(ϵ)=q0ϕ(wa)=δ(ϕ(w),a)\phi(\epsilon)=q_{0} \\ \phi(wa) = \delta(\phi(w),a) ϕ(ϵ)=q0​ϕ(wa)=δ(ϕ(w),a)

字符串匹配自动机

对于给定一个模式PPP,我们可以进行预处理构造一个有限状态自动机,之后,对于字符串TTT,就可以在O(n)O(n)O(n)的时间内求出所有的有效偏移。

为了构造自动机,我们要先定义后缀函数σ(x)\sigma(x)σ(x),函数是一个从∑∗\sum^{*}∑∗到{0,1,2,3,…,m}的映射函数\{0,1,2,3,\ldots,m\}的映射函数{0,1,2,3,…,m}的映射函数:

给定模式PPP,模式PPP的后缀函数为

σ(x)=max⁡(k:Pk⊐x)\sigma(x)=\max(k:P_{k} \sqsupset x) σ(x)=max(k:Pk​⊐x)

也就是说存在一个最大长度的子串,既是PPP的前缀又是xxx的后缀,子串的长度就是后缀函数的值。

给定模式P[1…m]P[1 \ldots m]P[1…m]对应的字符串匹配自动机为:

  • 状态集合QQQ为{0,1,2,3,…,m}\{0,1,2,3,\ldots,m\}{0,1,2,3,…,m}。开始状态q0q_{0}q0​为000,并且只有状态mmm是唯一的接受状态。
  • 对于任意的状态qqq和字符aaa,转移状态函数δ(q,a)\delta(q,a)δ(q,a)的定义为δ(q,a)=σ(Pqa)\delta(q,a)=\sigma(P_{q}a)δ(q,a)=σ(Pq​a)

也就是说,存在有效偏移sss,那么PPP一定是Ts+mT_{s+m}Ts+m​的后缀,状态机的状态就是后缀函数的值。并且存在有效偏移sss,当且仅当状态q=mq=mq=m。

状态机维护一个不变式:

ϕ(Ti)=σ(Ti)\phi(T_{i}) = \sigma(T_{i}) ϕ(Ti​)=σ(Ti​)

TiT_{i}Ti​表示T[1…i]T[1 \ldots i]T[1…i]。


图片中描述了模式P=ababacaP=ababacaP=ababaca的自动机的DAGDAGDAG模型,除了这个模型,还有二维表模型。

考虑一下两种情况:

  • a=P[q+1]a=P[q+1]a=P[q+1],此时我们可以放心的把状态转移到σ(Pqa)=q+1\sigma(P_{q}a)=q+1σ(Pq​a)=q+1,这个状态沿着主轴的方向进行。
  • a≠P[q+1]a \neq P[q+1]a​=P[q+1],这时我们必须找到一个更小的子串,确保这个子串满足σ(Pqa)\sigma(P_{q}a)σ(Pq​a)

此时,构造完自动机,我们就可以根据自动机处理文本TTT,找到状态q=mq=mq=m的时候,偏移i−mi-mi−m就是有效偏移。

为了证明有限自动机的正确性,我们首先要说明两个引理。

后缀不等式:对于任意字符串xxx和字符aaa,σ(xa)≤σ(x)+1\sigma(xa) \leq \sigma(x) + 1σ(xa)≤σ(x)+1。

也就是说如果字符aaa正好是下一个字符,等号成立,如果不是,那么后缀不可能再长了。

后缀函数递归定理:对任意字符串xxx和字符aaa,若q=σ(x)q=\sigma(x)q=σ(x),则σ(xa)=σ(Pqa)\sigma(xa)=\sigma(P_{q}a)σ(xa)=σ(Pq​a)。

也就是说,PqP_{q}Pq​这个后缀已经到了左边界,即从左边界开始求即可。

定理:如果ϕ\phiϕ是有限自动机的终态函数,那么ϕ(Ti)=σ(Ti)\phi(T_{i}) = \sigma(T_{i})ϕ(Ti​)=σ(Ti​)。

证明对iii进行归纳即可。

计算状态转移函数

先确定一个kkk的上界,然后在缩小范围即可,这种方式效率很低,接下来的KMPKMPKMP算法可以优化这个思路。

代码

int sp[100][10];int eval(const string & p)
{for(int q = 0; q<=p.size(); q++){for(char a = '0'; a<='9'; a++){int k = q + 1;string x = p.substr(0,q) + a;while(1){bool ok = true;for(int i = x.size() - 1,j=k-1; j>=0; i--,j--){if(p[j] != x[i]){ok = false;break;}}if(ok) break;else k--;}sp[q][a] = k;}}
}void match(const string & t,int end)
{int q = 0;for(int i = 0; i<t.size(); i++){q = sp[q][t[i]];if(q == end) cout << i - end + 1 << endl;}
}int main()
{string p = "01011";string t = "0101111010111010101011111";int end = p.size();eval(p);match(t,end);return 0;
}

五、Knuth-Morris-Pratt算法

现在来介绍一种线性的字符串匹配算法KMPKMPKMP算法。和自动机算法相比,他只需要用到一个辅助函数π\piπ,而且这个函数和字符集无关,不需要遍历字符集。并且在O(m)O(m)O(m)的时间计算出所有的函数值以留备用,在O(n)O(n)O(n)的时间内完成匹配。

模式的前缀函数

面对一个偏移sss,匹配长度为qqq,字符q+1q+1q+1匹配失败,按照朴素算法,我们应该测试下一个偏移s+1s+1s+1,但是我们知道,偏移s+1s+1s+1必然失败,因为P[1]P[1]P[1]必然不可能匹配T[s+2]T[s+2]T[s+2],可以匹配的最短偏移为s+2s+2s+2。这个222我们可以通过计算前缀函数计算出来。

也就是说,假设模式字符串P[1…q]P[1 \ldots q]P[1…q]与文本字符串T[s+1…s+q]T[s+1 \ldots s+q]T[s+1…s+q]匹配,s′s's′是最小的偏移量,s′>ss' \gt ss′>s,那么对某些k<qk \lt qk<q满足:

P[1…k]=T[s′+1…s′+k]P[1 \ldots k] = T[s'+1 \ldots s'+k] P[1…k]=T[s′+1…s′+k]

的最小偏移s′s's′是多少,其中s′+k=s+qs' + k=s+qs′+k=s+q。

这时,我们就需要借助前缀函数π(q)\pi(q)π(q),其值为存在最长的子串,既是PPP的前缀,又是PqP_{q}Pq​的真后缀

已知一个模式P[1…m]P[1 \ldots m]P[1…m],模式PPP的前缀函数是函数π:{1,2,3,4,…,m}→{0,1,…,m−1}\pi: \{1,2,3,4,\ldots,m\} \to \{0,1,\ldots,m-1\}π:{1,2,3,4,…,m}→{0,1,…,m−1}的一个函数,满足:

π[q]=max(k:k<q∣Pk⊐Pq)\pi[q]=max(k: k < q | P_{k} \sqsupset P_{q}) π[q]=max(k:k<q∣Pk​⊐Pq​)

之后我们就可以利用辅助函数匹配文本。

代码

#include <bits/stdc++.h>using namespace std;#define FR freopen("in.txt", "r", stdin)typedef long long ll;int nxt[1000005];void buildNext(string &p)
{nxt[0] = 0;for (int i = 1; i < p.size(); i++){nxt[i] = nxt[i - 1];while (p[nxt[i]] != p[i] && nxt[i] > 0){nxt[i] = nxt[nxt[i] - 1];}nxt[i] = p[nxt[i]] == p[i] ? nxt[i] + 1 : 0;}
}void kmp(string &t, string &p)
{int ti = 0;int pi = 0;while (ti < t.size()){if (t[ti] == p[pi]){if (pi == p.size() - 1){cout << ti + 2 - p.size() << endl;pi = nxt[pi];}else{pi++;}ti++;}else if (pi == 0){ti++;}else{pi = nxt[pi - 1];}}
}int main()
{string s1, s2;cin >> s1 >> s2;buildNext(s2);kmp(s1, s2);for (int i = 0; i < s2.size(); i++){cout << nxt[i] << " ";}return 0;
}

六、Z-函数(扩展KMP)

定义ZZZ数组Z[i]Z[i]Z[i]为,字符串s[0…i−1]s[0 \ldots i - 1]s[0…i−1]和字符串s[i…n−1]s[i \ldots n-1]s[i…n−1]的最长公共前缀长度。特别的Z[0]=0Z[0] = 0Z[0]=0。

我们有线性时间O(n)O(n)O(n)的算法来计算Z-函数,叫做Z-算法。

字符串——字符串匹配相关推荐

  1. LeetCode刷题笔记 字符串 字符串匹配

    28 实现 strStr() 判断一个字符串是不是另一个字符串的子字符串,并返回其位置. 输入一个母字符串和一个子字符串,输出一个整数,表示子字符串在母字符串的位置,若不存在则返回-1. 输入:hay ...

  2. c int转字符串_【C++实现python字符串函数库】字符串匹配函数startswith与endswith

    [C++实现python字符串函数库]字符串匹配函数startswith与endswith 这两个函数用于匹配字符串的开头或末尾,判断是否包含另一个字符串,它们返回bool值.startswith() ...

  3. 【字符串系列】字符串匹配中的位并行算法

    [字符串系列]字符串匹配中的位并行算法 最近一段时间看了一点"柔性字符串匹配", 发现位并行算法在字符串匹配这个领域还是很有用的, 下面抒发一下鄙见. 首先, 字符串位并行算法在a ...

  4. java中字符串的精确匹配_Java最佳实践–字符串性能和精确字符串匹配

    java中字符串的精确匹配 在使用Java编程语言时,我们将继续讨论与建议的实践有关的系列文章,我们将讨论String性能调优. 我们将专注于如何有效地处理字符串创建, 字符串更改和字符串匹配操作. ...

  5. Java最佳实践–字符串性能和精确字符串匹配

    在使用Java编程语言时,我们将继续讨论与建议的实践有关的系列文章,我们将讨论String性能调优. 我们将专注于如何有效地处理字符串创建, 字符串更改和字符串匹配操作. 此外,我们将提供我们自己的用 ...

  6. # 字符串从右往左查找_字符串匹配(搜索,查找)算法

    (一)前言 所谓的字符串匹配就是在一个长字符串(可称文本T)中找一个短字符串(可称模式P),看长字符串中是否存在短字符串,若存在则返回出现的第一个位置,若不存在则返回一个标记.字符串搜索算法有很多,比 ...

  7. 对比两个字符串相等_字符串匹配问题

    0.题目 在一个主串S={a, b, c, c, b, c, a, b, d}, 模式串T={a, b, d};请找出模式串在主串中第一次出现的位置 提示: 不需要考虑字符串大小写问题,字符均为小写字 ...

  8. java 字符串匹配_多模字符串匹配算法原理及Java实现代码

    多模字符串匹配算法在这里指的是在一个字符串中寻找多个模式字符字串的问题.一般来说,给出一个长字符串和很多短模式字符串,如何最快最省的求出哪些模式字符串出现在长字符串中是我们所要思考的.该算法广泛应用于 ...

  9. C语言(CED)多组字符串匹配,输出所有重复出现的字符串,多次重复出现的只输出一次就好。

    最近遇到一道题目:多组字符串匹配,输出所有重复出现的字符串,多次重复出现的只输出一次就好. 举个例子: input:qwer qwer sdfgh output:qwer 题意大致就是这样,下面给出我 ...

最新文章

  1. SDUT-2144 图结构练习——最小生成树
  2. qt 编译mysql wince_Qt4.8.6开发WinCE 5.0环境搭建
  3. 初试WebStorage之localstorage
  4. LeetCode 97: 交错字符串
  5. nginx子目录301重定向到其他域名URL
  6. e会学安庆师范大学c语言程序设计答案,宜春学院期末考试试卷C语言程序设计(4份,有答案)...
  7. Docker安装Nacos教程
  8. SHELL字符串使用总结
  9. 查询软件和硬件列表清单[将文章里代码另存为 list.vbs,双击运行就会出现一个html页面]...
  10. 金融评分卡项目—4.GBDT模型在流失预警模型中的应用
  11. IDC机房建设的关键技术有哪些?
  12. spring-boot设置静态文件路径
  13. 服务器虚拟化2种架构,服务器虚拟化常用架构详解
  14. 产业分析:2021抖音数据报告
  15. wav转mp3,wav转换成mp3
  16. xp计算机共享能否指定用户,XP怎么设置局域网共享?
  17. PC-DARTS 网络结构搜索程序分析
  18. 状态码 https dns解析过程
  19. SD卡与TF卡基础知识
  20. 微信wifi路由器怎么连接服务器,介绍下微信WIFI路由器怎么设置的方法

热门文章

  1. 太肝了 5年来最火183个Java面试问题含答案(
  2. 跟我一起读《Hadoop权威指南》 第四篇 -- 关于Yarn
  3. eclipse设置经典黑色主题样式
  4. 腾讯防刷负责人:基于用户画像大数据的电商防刷架构
  5. 上海宝付提醒网友警惕这些打着招聘旗号搞事情的人
  6. 洛谷P1164小A点菜
  7. js的可枚举属性与不可枚举属性
  8. iPhone4怎样鉴别翻新机
  9. rdkit、mordred分子描述计算molecular descriptors、Lipinski
  10. CodeForces 1732B Ugu 题解