码迷,mamicode.com
首页 > 其他好文 > 详细

字符串的一些基础操作

时间:2019-10-19 20:33:53      阅读:88      评论:0      收藏:0      [点我收藏+]

标签:top   fine   有关   ons   typename   bre   merge   在线   集中   

填不完的坑,有一点是一点。

会在这里记录一些字符串相关的题目以及口胡的题解和漏洞百出的代码。权且放着罢。

同时还会大量抄袭WC课件,代码可能不太友好

T1

给一个长为 \(n\leq 10^5\) 的字符串,询问其每个前缀的每个子串的fail树的每个节点的深度和,根的深度为-1并且不计入答案。fail树,就是KMPfail指针构成的树。

考虑先进行一次差分,这时候的答案变成求每个前缀的每个后缀的fail树的节点深度和。

引理1:每个串的fail树的节点深度和,为这个串的每个前缀在原串中出现次数的和(还要减掉 \(n\) )。结论十分显然。

应用引理1,就变成了求每个前缀中相同子串对(出现位置相同的不算)个数。

把以上的东西再差分,就变成了:求原串每个前缀的每个后缀在这个前缀中出现次数的和,原位置不算。

这个东西看起来清真不少

然后是考虑怎么用反前缀树求这个东西。就是求每个前缀出现的次数和。显然,就是每次插入一个字符后,沿parent树向上跳,然后把答案加上每个节点的 \(right\) 集合大小乘上Max(s)-Max(p)。这个东西可以在整个串的前缀树上用线段树合并维护一些首项为0的等差数列就行了,然后大力打标记。

/* programed by white-55kai */
#if 0
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<map>
#include<set>
#include<queue>
#include<vector>
#include<utility>
#include<stdlib.h>
#include<time.h>
#else
#include<bits/stdc++.h>
#endif

#if 0
#include<cmath>
#endif

#define REP(i,l,r) for (reg int i=(l);i<=(r);++i)
#define RREP(i,r,l) for (int i=(r);i>=(l);--i)
#define rep(i,l,r) for (int i=(l);i<(r);++i)
#define rrep(i,r,l) for (int i=(r);i>(l);--i)
#define foredge(i,u) for (int i=la[u];i;i=ne[i])
#define mem(a) memset(a,0,sizeof(a))
#define memid(a) memset(a,63,sizeof(a))
#define memax(a) memset(a,127,sizeof(a))
#define dbg(x) cout<<#x<<" = "<<x<<endl
#define reg register
#define tpn typename
#define fr(a) freopen(a,"r",stdin)
#define fw(a) freopen(a,"w",stdout)
using namespace std;

typedef long long ll;
typedef unsigned long long ull;
typedef double db;
typedef long double ldb;

template <tpn A> inline A Max(const A &x,const A &y){
    return x>y?x:y;
}
template <tpn A> inline A Min(const A &x,const A &y){
    return x<y?x:y;
}
template <tpn A> inline void Swap(A &x,A &y){
    x^=y,y^=x,x^=y;
}
template <tpn A> inline A Abs(const A &x){
    return x>0?x:-x;
}
#if 1
template <tpn A> inline void read(A &x){
    char c;
    A neg=1;
    do{
        c=getchar();
    }while ((c<'0'||c>'9')&&c!='-');
    if (c=='-') neg=-1,c=getchar();
    x=0;
    do{
        x=x*10+c-48;
        c=getchar();
    }while (c>='0'&&c<='9');
    x*=neg;
}
template <tpn A,tpn B> inline void read(A &a,B &b){
read(a),read(b);}
template <tpn A,tpn B,tpn C> inline void read(A &a,B &b,C &c){
read(a),read(b),read(c);}
template <tpn A,tpn B,tpn C,tpn D> inline void read(A &a,B &b,C &c,D &d){
read(a),read(b),read(c),read(d);}
template <tpn A> inline void put(const A &tmp){
    A x=tmp;
    if (x==0){
        putchar('0');
        return;
    }
    if (x<0) putchar('-'),x=-x;
    char buf[30];
    int buf_size=0;
    while (x){
        buf[++buf_size]=x%10+48;
        x/=10;
    }
    RREP(i,buf_size,1) putchar(buf[i]);
}
#else
namespace fastIO {
    #define buf_size 100000
    #define LL long long
    bool error;
    inline char gc() {
        static char buf[buf_size + 1], *l = buf, *r = buf;
        if (l == r) {
            l = buf;
            r = buf + fread(buf, 1, buf_size, stdin);
            if (l == r) {
                error = 1;
                return -1;
            }
        }
        return *l ++;
    }
    inline bool blank(char ch) {
        return ch == '\n' || ch == '\t' || ch == ' ' || ch == '\r' || error;
    }
    inline bool read_int(int &x) {
        char ch;
        int f = 1;
        while (blank(ch = gc()));
        if (error) return false;
        x = 0;
        if (ch == '-') f = -1, ch = gc();
        while (1) {
            x = (x << 1) + (x << 3) + ch - '0';
            if (!isdigit(ch = gc())) break;
        }
        x *= f;
        return true;
    }
    inline bool read_LL(LL &x) {
        char ch;
        LL f = 1;
        while (blank(ch = gc()));
        if (error) return false;
        x = 0;
        if (ch == '-') f = -1, ch = gc();
        while (1) {
            x = (x << 1) + (x << 3) + ch - '0';
            if (!isdigit(ch = gc())) break;
        }
        x *= f;
        return true;
    }
    inline bool read_char(char &x) {
        char ch;
        while (blank(ch = gc()));
        if (error) return false;
        x = ch;
        return true;
    }
    inline void put_int(int x) {
        if (!x) {
            putchar('0');
            return;
        }
        if (x < 0) {
            x = -x;
            putchar('-');
        }
        static int out[13];
        int len = 0;
        while (x) {
            out[++ len] = x % 10;
            x /= 10;
        }
        while (len) putchar(out[len --] + '0');
    }
    inline void put_LL(LL x) {
        if (!x) {
            putchar('0');
            return;
        }
        if (x < 0) {
            x = -x;
            putchar('-');
        }
        static LL out[20];
        int len = 0;
        while (x) {
            out[++ len] = x % 10;
            x /= 10;
        }
        while (len) putchar(out[len --] + '0');
    }
    #undef buf_size
    #undef LL
}
using namespace fastIO;
#endif
inline int mul_mod(int a,int b,int mo){
    int ret;
    __asm__ __volatile__ ("\tmull %%ebx\n\tdivl %%ecx\n":"=d"(ret):"a"(a),"b"(b),"c"(mo));
    return ret;
}
const int N = 100005, p = 1000000007;
char s[N];
int n;
namespace segment {
    struct node {
        int l, r, lson, rson;
        int a, d, siz;
    };
    node tree[N * 18];
    int cnt;
    int build(int l, int r, int x) {
        int now = ++cnt;
        tree[now].l = l, tree[now].r = r, tree[now].siz = 1;
        if (l == r) return now;
        int mid = l + r >> 1;
        if (x <= mid) tree[now].lson = build(l, mid, x);
        else tree[now].rson = build(mid + 1, r, x);
        return now;
    }
    inline void pushdown(int k) {
        static int ls, rs;
        ls = tree[k].lson, rs = tree[k].rson;
        (tree[ls].a += tree[k].a) %= p;
        (tree[ls].d += tree[k].d) %= p;
        tree[rs].a += (tree[k].a + (ll)tree[ls].siz * tree[k].d % p) % p;
        tree[rs].d += tree[k].d;
        tree[rs].a %= p, tree[rs].d %= p;
        tree[k].a = tree[k].d = 0;
    }
    int merge(int le, int ri) {
        if (!le) return ri;
        if (!ri) return le;
        if (tree[le].a || tree[le].d) pushdown(le);
        if (tree[ri].a || tree[ri].d) pushdown(ri);
        tree[le].lson = merge(tree[le].lson, tree[ri].lson);
        tree[le].rson = merge(tree[le].rson, tree[ri].rson);
        tree[le].siz += tree[ri].siz;
        return le;
    }
    void add(int rt, int val) {
        tree[rt].d += val;
        tree[rt].d %= p;
    }
    void query(int rt, int *a) {
        if (tree[rt].l == tree[rt].r) {
            *(a + tree[rt].l) = tree[rt].a;
            return;
        }
        if (tree[rt].a || tree[rt].d) pushdown(rt);
        query(tree[rt].lson, a);
        query(tree[rt].rson, a);
    }
}
int la[N << 1], en[N << 1], ne[N << 1], top;
inline void add(int x,int y){
    ne[++top] = la[x];
    en[top] = y;
    la[x] = top;
}
struct SAM {
    int len[N << 1], par[N << 1], ch[N << 1][26];
    int root[N << 1];
    int lp, p, np, q, nq, sz;
    SAM() { lp = sz = 1; }
    inline void insert(int x) {
        len[np = ++sz] = len[p = lp] + 1;
        par[lp = np] = 1;
        for (; p; p = par[p]) {
            if (!ch[p][x]) ch[p][x] = np;
            else {
                q = ch[p][x];
                if (len[q] == len[p] + 1) par[np] = q;
                else {
                    nq = ++sz;
                    len[nq] = len[p] + 1;
                    par[nq] = par[q];
                    par[q] = par[np] = nq;
                    memcpy(ch[nq], ch[q], sizeof(ch[q]));
                    for (; p && ch[p][x] == q; p = par[p]) ch[p][x] = nq;
                }
                return;
            }
        }
    }
    inline void build() {
        p = 1;
        REP(i, 1, n) {
            p = ch[p][s[i] - 'a'];
            root[p] = segment::build(1, n, i);
        }
        REP(i, 2, sz) add(par[i], i);
    }
    void dfs(int u) {
        foredge(i, u) {
            dfs(en[i]);
            root[u] = segment::merge(root[u], root[en[i]]);
        }
        if (u == 1) return;
        segment::add(root[u], len[u] - len[par[u]]);
    }
    inline void getans() { dfs(1); }
};
SAM van;
int a[N];
int main(){
    read(n);
    scanf("%s", s + 1);
    REP(i, 1, n) van.insert(s[i] - 'a');
    van.build();
    van.getans();
    segment::query(van.root[1], a);
    REP(i, 1, n) a[i] = (a[i] + a[i - 1]) % p;
    REP(i, 1, n) {
        a[i] = (a[i] + a[i - 1]) % p;
        printf("%d\n", a[i]);
    }
    return 0;
}

T2

原题:CF700E猫咪(多倍经验)

给出字符串 \(T\) ,长度不超过 \(2\ast 10^5\) ,问 \(k\) 最大是多少,使得存在 \(S_1,\ldots,S_k\) ,满足这些串都是 \(T\) 的子串并且 \(\forall i\in [2,n], S_i\)\(S_{i-1}\) 中出现至少两次。

首先,发现把条件变成\(\forall i\in [2,n], S_i\)\(S_{i-1}\)border对答案没有影响。

然后考虑如何在SAM上求这一坨东西。假设 \(T\) 对应的反前缀树不压缩(反前缀Trie),我们令f[x]代表第一个串是从x这个节点沿反前缀树走到根产生的这个串的情况下最大的 \(k\)

首先如果 \(y\) 能转移到 \(x\) ,那么有(以下用 \(r(x)\) 表示这个节点right集合中某一个位置) \(\exists r(y)\in [r(x)-len(x)+len(y),r(x)-1]\) (由于没有压缩,因此这里的len对于每一个节点只有一个),就是说y代表的字符串是x代表的字符串中某一不是后缀的子串。显然,满足这个条件的节点是从x到根的某个深度以上的所有节点组成的一条链。

然后又非常显然f[x]是随深度递增的。因此,最优的转移一定是在最深的可转移的节点上。显然每个节点应该尝试从与其父亲的f相同的最浅的节点转移,若这个点上不能转移,那么f[x]=f[fa[x]],否则f[x]=f[pos[fa[x]]]+1。其中pos[x]表示与xf值相同的最浅的节点编号。

现在考虑正常的反前缀树。每个点对应了 \([min(x),max(x)]\) 的一段区间。有结论是,如果x能从yfa[y]之间的某一个串转移,那么也就是说x能从min(y)代表的区间转移,也就是说任意x代表的最长子串出现的地方,都会出现至少两个min(y),又因为 \([min(y),max(y)]\) 属于同一个节点,因此所有min(y)出现的地方都能延长为max(y),因此max(y)只能也在x代表的最长子串中出现两次。因此只要从反前缀树上的节点上转移就好了。

/* programed by white-55kai */
#if 0
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<map>
#include<set>
#include<queue>
#include<vector>
#include<utility>
#include<stdlib.h>
#include<time.h>
#else
#include<bits/stdc++.h>
#endif

#if 0
#include<cmath>
#endif

#define REP(i,l,r) for (reg int i=(l);i<=(r);++i)
#define RREP(i,r,l) for (int i=(r);i>=(l);--i)
#define rep(i,l,r) for (int i=(l);i<(r);++i)
#define rrep(i,r,l) for (int i=(r);i>(l);--i)
#define foredge(i,u) for (int i=la[u];i;i=ne[i])
#define mem(a) memset(a,0,sizeof(a))
#define memid(a) memset(a,63,sizeof(a))
#define memax(a) memset(a,127,sizeof(a))
#define dbg(x) cout<<#x<<" = "<<x<<endl
#define reg register
#define tpn typename
#define fr(a) freopen(a,"r",stdin)
#define fw(a) freopen(a,"w",stdout)
using namespace std;

typedef long long ll;
typedef unsigned long long ull;
typedef double db;
typedef long double ldb;

template <tpn A> inline A Max(const A &x,const A &y){
    return x>y?x:y;
}
template <tpn A> inline A Min(const A &x,const A &y){
    return x<y?x:y;
}
template <tpn A> inline void Swap(A &x,A &y){
    x^=y,y^=x,x^=y;
}
template <tpn A> inline A Abs(const A &x){
    return x>0?x:-x;
}
#if 1
template <tpn A> inline void read(A &x){
    char c;
    A neg=1;
    do{
        c=getchar();
    }while ((c<'0'||c>'9')&&c!='-');
    if (c=='-') neg=-1,c=getchar();
    x=0;
    do{
        x=x*10+c-48;
        c=getchar();
    }while (c>='0'&&c<='9');
    x*=neg;
}
template <tpn A,tpn B> inline void read(A &a,B &b){
read(a),read(b);}
template <tpn A,tpn B,tpn C> inline void read(A &a,B &b,C &c){
read(a),read(b),read(c);}
template <tpn A,tpn B,tpn C,tpn D> inline void read(A &a,B &b,C &c,D &d){
read(a),read(b),read(c),read(d);}
template <tpn A> inline void put(const A &tmp){
    A x=tmp;
    if (x==0){
        putchar('0');
        return;
    }
    if (x<0) putchar('-'),x=-x;
    char buf[30];
    int buf_size=0;
    while (x){
        buf[++buf_size]=x%10+48;
        x/=10;
    }
    RREP(i,buf_size,1) putchar(buf[i]);
}
#else
namespace fastIO {
    #define buf_size 100000
    #define LL long long
    bool error;
    inline char gc() {
        static char buf[buf_size + 1], *l = buf, *r = buf;
        if (l == r) {
            l = buf;
            r = buf + fread(buf, 1, buf_size, stdin);
            if (l == r) {
                error = 1;
                return -1;
            }
        }
        return *l ++;
    }
    inline bool blank(char ch) {
        return ch == '\n' || ch == '\t' || ch == ' ' || ch == '\r' || error;
    }
    inline bool read_int(int &x) {
        char ch;
        int f = 1;
        while (blank(ch = gc()));
        if (error) return false;
        x = 0;
        if (ch == '-') f = -1, ch = gc();
        while (1) {
            x = (x << 1) + (x << 3) + ch - '0';
            if (!isdigit(ch = gc())) break;
        }
        x *= f;
        return true;
    }
    inline bool read_LL(LL &x) {
        char ch;
        LL f = 1;
        while (blank(ch = gc()));
        if (error) return false;
        x = 0;
        if (ch == '-') f = -1, ch = gc();
        while (1) {
            x = (x << 1) + (x << 3) + ch - '0';
            if (!isdigit(ch = gc())) break;
        }
        x *= f;
        return true;
    }
    inline bool read_char(char &x) {
        char ch;
        while (blank(ch = gc()));
        if (error) return false;
        x = ch;
        return true;
    }
    inline void put_int(int x) {
        if (!x) {
            putchar('0');
            return;
        }
        if (x < 0) {
            x = -x;
            putchar('-');
        }
        static int out[13];
        int len = 0;
        while (x) {
            out[++ len] = x % 10;
            x /= 10;
        }
        while (len) putchar(out[len --] + '0');
    }
    inline void put_LL(LL x) {
        if (!x) {
            putchar('0');
            return;
        }
        if (x < 0) {
            x = -x;
            putchar('-');
        }
        static LL out[20];
        int len = 0;
        while (x) {
            out[++ len] = x % 10;
            x /= 10;
        }
        while (len) putchar(out[len --] + '0');
    }
    #undef buf_size
    #undef LL
}
using namespace fastIO;
#endif
inline int mul_mod(int a,int b,int mo){
    int ret;
    __asm__ __volatile__ ("\tmull %%ebx\n\tdivl %%ecx\n":"=d"(ret):"a"(a),"b"(b),"c"(mo));
    return ret;
}
const int N = 200005;
int n;
char s[N];
namespace segmerge {
struct node {
    int l, r, lson, rson;
};
node tree[N * 80];
int cnt;
int newnode(int x, int l, int r) {
    ++cnt;
    tree[cnt].l = l, tree[cnt].r = r;
    if (l == r) return cnt;
    int mid = l + r >> 1, now = cnt;
    if (x <= mid) tree[now].lson = newnode(x, l, mid);
    else tree[now].rson = newnode(x, mid + 1, r);
    return now;
}
int merge(int lt, int rt) {
    if (!lt) return rt;
    if (!rt) return lt;
    int now = ++cnt;
    tree[now].l = tree[lt].l, tree[now].r = tree[lt].r;
    tree[now].lson = merge(tree[lt].lson, tree[rt].lson);
    tree[now].rson = merge(tree[lt].rson, tree[rt].rson);
    return now;
}
int query(int k, int l, int r) {
    if (!k) return 0;
    if (tree[k].l > r || tree[k].r < l) return 0;
    if (tree[k].l >= l && tree[k].r <= r) return 1;
    if (query(tree[k].lson, l, r)) return 1;
    else return query(tree[k].rson, l, r);
}
}
struct SAM {
    int len[N << 1], par[N << 1], ch[N << 1][26];
    int lp, p, np, q, nq, sz;
    SAM() { lp = sz = 1; }
    inline void insert(int x) {
        len[np = ++sz] = len[p = lp] + 1;
        par[lp = np] = 1;
        for (; p; p = par[p]) {
            if (!ch[p][x]) ch[p][x] = np;
            else {
                q = ch[p][x];
                if (len[q] == len[p] + 1) par[np] = q;
                else {
                    nq = ++sz;
                    len[nq] = len[p] + 1;
                    par[nq] = par[q];
                    par[q] = par[np] = nq;
                    memcpy(ch[nq], ch[q], sizeof(ch[q]));
                    for (; p && ch[p][x] == q; p = par[p]) ch[p][x] = nq;
                }
                return;
            }
        }
    }
    int root[N << 1], la[N << 1], ne[N << 1], en[N << 1], edgenum;
    int ans;
    int f[N << 1], pos[N << 1], r[N << 1];
    inline void add(int x, int y) {
        ne[++edgenum] = la[x];
        en[edgenum] = y;
        la[x] = edgenum;
    }
    void dfs(int u) {
        foredge(i, u) {
            dfs(en[i]);
            root[u] =segmerge::merge(root[u], root[en[i]]);
            r[u] = max(r[u], r[en[i]]);
        }
    }
    inline void build() {
        p = 1;
        REP(i, 1, n) {
            p = ch[p][s[i] - 'a'];
            r[p] = i;
            root[p] = segmerge::newnode(i, 1, n);
        }
        REP(i, 2, sz) add(par[i], i);
        dfs(1);
    }
    inline void get(int u) {
        if (u != 1) {
            if (par[u] == 1) f[pos[u] = u] = 1;
            else {
                int v = pos[par[u]];
                if (segmerge::query(root[v], r[u] - len[u] + len[v], r[u] - 1))
                    f[pos[u] = u] = f[v] + 1;
                else f[u] = f[par[u]], pos[u] = v;
            }
            ans = max(ans, f[u]);
        }
        foredge(i, u) get(en[i]);
    }
    inline void getans() { get(1); }
};
SAM van;
int main() {
    // read(n);
    scanf("%s", s + 1);
    n = strlen(s + 1);
    REP(i, 1, n) van.insert(s[i] - 'a');
    van.build();
    van.getans();
    cout << van.ans << endl;
    return 0;
}

好像border相关的问题有很多都是用(可持久化)线段树合并维护一下 \(right\) 集呢。

SAM+LCT

听说这些东西比较神奇。

考虑用LCT动态维护parent树上每一个节点 \(Right\) 集中最大的值,把这个值称为 \(val_u\) 。发现当新加入一个反前缀时,LCT会改变均摊 \(\log\) 个重儿子,也就是在后缀树上改变 \(\log\) 段的 \(val\) ,此时如果所求的信息与 \(val\) 有关,那么就可以维护了。

T3

两个 \(\log\) 预处理一个 \(\log\) 回答 \(S[l,r]\) 中本质不同的子串数量。

不太能线段树合并的样子呢。

考虑用上面那个trick,离线扫描线解决。方便起见,字符从后往前加,也就是建的是 \(S[l,n]\) 的后缀树,每次插入一个原串的后缀,然后用线段树维护当前点 \(i\) 为左端点、 \(x\in [i,n]\) 为右端点的答案。

考虑当前段 \(val_x\) ,其子串长度区间为 \([minlen,mxlen]\) 。显然,对于长度区间中某个长度 \(len\) ,贡献是在线段树上 \([i+len,val_x+len)\) 这一区间上加一。一段字串就是再线段树上加上一个由1组成的类似平行四边形的东西,XJB维护一下就好。

代码:题都没出,数据都没有写什么代码?!

T4

两个 \(\log\) 预处理一个 \(\log\) 回答 \(S[l,r]\) 的后缀树结点数。

(好♂啊,神仙题)

考虑仍然像上面一样用扫描线加线段树维护信息。

考虑先做两次差分,然后变成,求 \(\forall i,j\geq i\) ,在 \(ST[:j]\) 中插入 \(S[i,j]\) 的新增结点数之和。

考虑新增的节点,显然应该分为两种:新增的叶子 \(S[i:j]\) ,以及新增的nq节点。前者直接加 \(n-i+1\) 就行,关键是后者。

\(last_u,diff_u\) 分别表示根到 \(u\) 的子串这一次、上一次切换实儿子时对应的串的左端点。

考虑沿着LCT往上跳时,显然只有切换实儿子的点会对答案产生贡献。具体来说,是对右端点在 \([last_u+len_u,diff_u+len+_u)\) 的询问都加上1。然后还有细节是在原串的后缀自动机新建 \(nq\) 节点的时候,应当有last[nq]=last[q],diff[nq]=0,而不是继承q

到这里位置都是经典模型。但是考虑有一些不对,因为在 \(u\)\(left\) 集中,显然可能有一些在 \((last_u,diff_u)\) 之间的点,虽然这些串的下一个字符都是 \(S[last_u+len_u]\) ,但是对于以这些 \(left\) 集中的点作为询问右端点的询问,由于从根到 \(u\) 的串既是后缀节点,又是有两个孩子的节点,因此会多加一。

考虑如何处理以上问题。对于每个询问,只有它代表的子串的后缀才可能会造成重复。又因为如果长度为 \(d\) 的区间后缀能造成重复,那么长度为 \(c<d\) 的区间后缀显然也可以造成重复。一个区间后缀能造成重复,当且仅当在区间的后缀树上有两个及以上儿子。然后考虑一个区间后缀可以造成几次重复,答案显然是一次。于是可以二分+线段树合并(在另外的、相反的后缀自动机上)处理每一个询问,从而解决问题(虽然是两个 \(\log\) 的)。

那么如何优化到一个 \(\log\) 呢?咕咕咕。

拓展问题:问一个串的所有子串的后缀树节点数之和。

前面的部分不用修改。

考虑如何做后面的问题。(统计从 \(last\) 过来了多少个串,直接减掉就好了?一只 \(\log\)

字符串的一些基础操作

标签:top   fine   有关   ons   typename   bre   merge   在线   集中   

原文地址:https://www.cnblogs.com/pupuvovovovo/p/11704956.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!