扩展KMP的应用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_38786088/article/details/82080006

扩展KMP的应用

扩展kmp在字符串匹配专题的应用很广,但很多时候也还是能用kmp、马拉车等字符串匹配算法代替,这篇文章是笔者为18年湖南省赛准备的复习文,文章整理了笔者遇到过的扩展kmp在一些方面的应用

  • 题目中出现的字符串名称及其定义:
    • 循环同构串: abcd->bcda->cdab->dabc ,将首字符逐一左移至字符串尾部,得到
      的所有字符串。
    • 后缀子串:Si表示以第i个字符为首的S的后缀字符串。
    • 重复不重叠字符串:abcabc,abc重复了两次。书面表示为AAA (A表示字符串)
  • 应用分类:
    • 定义题:裸题(求T与S的所有后缀子串的最长前缀公共子串长度)
      FZU-1901
      ZOJ 3587
    • 回文前缀子串和回文后缀子串上的应用
      HDU 3613
    • 分治 + ex_kmp
      HUNNU OJ 11565 可惜标程错了

一句总结: 大部分应用都是将自己与反串匹配,就能处理题目要求的字符串性质

一、定义题

FZU-1901
题意:字符串S,问满足S[i] = S[i+P],(i ∈[0,Size -P-1]),升序顺序输出P的取值。


很裸的ex_kmp的定义题!判断Next[j] == Size - j +1 ,以S[j]开头后缀子串是S的一个前缀子串。

/*********************************
*** FZU 1901 ex_kmp 的 Next数组
*********************************/

#include <iostream>
#include <fstream>
#include <stdio.h>
#include <algorithm>
#include <math.h>
#include <map>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#define llt long long

using namespace std;

const int N = 1e6+7777;
int Next[N];
char s[N];
void get_Next(int n){
    Next[0] = n;//以s[0]开头后缀子串显然是本串
    int i=0;
    while(i+1<n&&s[i]==s[i+1])++i;//前面有i+1个数相同
    Next[1] = i;
    int po = 1;// 到达最远比较位置
    for(int i=2;i<n;++i){
        if(i+Next[i-po]<Next[po]+po)//不能等于
            Next[i] = Next[i-po];
        else{
            //if(i==9) cout<<i<<endl;
            int j = max(0,Next[po]+po-i);
            while(i+j<n&&s[j]==s[i+j])++j;
            Next[i] = j;
            po = i;
        }
    }
    Next[n] = 0;
}

int ans[N];
int main(){
    int T;
    scanf("%d",&T);
    int cas = 0;
    while(T--){
        scanf("%s",s);
        int n = strlen(s);
        get_Next(n);
        //cout<<Next[9]<<endl;
        int cnt = 0;
        for(int i=1;i<=n;++i)
            if(n-i==Next[i]) ans[++cnt] = i;
        printf("Case #%d: %d\n",++cas,cnt);
        for(int i=1;i<=cnt;++i)
            printf("%d%c",ans[i]," \n"[i==cnt]);

    }
    return 0;
}

zoj 3587
题意:求S的两个子串拼接成T的方案数。


ex_kmp 统计S的子串与T的前后缀长度为k的子串数。

/*************************************************
*** zoj 3587  拓展kmp的定义题:求出S的后缀子串i
                  与T的最长前缀k,实际上表示已si
                  开头的子串段长度<=k,均有一种方案!
              同理,找到S反串与T的反串的最长前缀!

*** 实际运用ex_kmp求出与T的前缀长度为k的子串的个数!
*************************************************/
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <math.h>
#define llt long long
#include <map>
#define fi first
#define se second
#define lson (rt<<1)
#define rson ((rt<<1)+1)
using namespace std;

const int N = 1e5+777;
char s[N],t[N];
char ss[N],tt[N];//反串
llt sum[N],a[N];//a 记录 S的子串是T的长度为k的后缀子串的个数
                // sum 记录 S的子串是T的长度不超过k的后缀子串的个数和
int Next[N],extend[N];
void getNext(char S[],int n){
    Next[0] = n;
    int i =0 ;
    while(i+1<n&&S[i]==S[i+1]) ++i;
    Next[1] = i;
    int po = 1;
    for(int i=2;i<n;++i){
        if(i+Next[i-po]<po+Next[po])
           Next[i] = Next[i-po];
        else {
            int j = max(0,po+Next[po]-i);
            while(i+j<n&&S[j]==S[i+j]) ++j;
            Next[i] = j;
            po = i;
        }
    }
}

void getExtend(char S[],char T[],int slen,int tlen){
    getNext(T,tlen);
    int i=0;
    int mixn = min(slen,tlen);
    while(i<mixn&&S[i]==T[i]) ++i;
    extend[0] = i;
    int po = 0;
    for(int i=1;i<slen;++i){
        if(i+Next[i-po]<po+extend[po])
            extend[i] = Next[i-po];
        else {
            int j = max(0,po+extend[po]-i);
            while(j<tlen&&i+j<slen&&T[j]==S[i+j]) ++j;
            extend[i] = j;
            po = i;
        }
    }
}
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        scanf("%s",s);
        scanf("%s",t);
        int slen = strlen(s);
        int tlen = strlen(t);
        for(int i=0;i<slen;++i) ss[slen-1-i] = s[i];
        for(int i=0;i<tlen;++i) tt[tlen-1-i] = t[i];
        /*统计S的子串为T的后缀子串的长度为k的个数*/
        memset(a,0,sizeof a);
        getExtend(ss,tt,slen,tlen);
        for(int i=0;i<slen;++i)
            ++a[extend[i]];
        for(int i=tlen;i>0;--i)
            a[i-1] += a[i];
        sum[0] = 0;
        for(int i=1;i<=tlen;++i)
            sum[i] = sum[i-1] + a[i];
        /*对于S的第i后缀子串与T的最长公共前缀长度为k,即1-k的前缀子串,
          我们统计tlen-k -- tlen-1的与T的后缀子串相同的个数*/
        llt ans = 0;
        getExtend(s,t,slen,tlen);
        for(int i=0;i<slen;++i){
            if(extend[i]==0) continue;
            if(extend[i]==tlen) extend[i] = tlen-1;
            //cout<<extend[i]<<endl;
            ans += sum[tlen-1] - sum[tlen-extend[i]-1];
        }
        printf("%lld\n",ans);
    }
}

hdu 4763
题意:问子串S = A**A**A,问A最大长度。


涉及本串的后缀子串为前缀子串|T|,另外一个后缀子串与本串的最长公共子串的长度不小于|T|,显然是ex_kmp的定义题。
我们仅需要预处理:pre数组,pre[i] 维护的是后缀长度不大于i的子串中与本串的前缀交是本身的最大后缀子串的长度。 然后遍历中间的那个A,因为不能有交部分,遍历到i时,前面部分的最长为i,中间A的最长为Next[i] ,取mixn=min(i,Next[i]).后面的A长度不能超过(n-i)/2(AA),取mixn = min(mixn,(n-i)/2).前面的A长度均保证不低于mixn,pre[mixn]即为答案!

/*************************************************
*** hdu 4763 ex_kmp  A***A***A问A子串的最大长度
*** 后缀子串与本串的最长公共前缀子串长度问题。
*************************************************/

#include <iostream>
#include <fstream>
#include <stdio.h>
#include <algorithm>
#include <math.h>
#include <map>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#define llt long long

using namespace std;
const int N = 1e6+777;

char s[N];
int Next[N],pre[N];
void get_Next(int n){
    Next[0] = n;
    int i = 0;
    while(i+1<n&&s[i]==s[i+1]) ++i;
    Next[1] = i;
    int po = 1;
    for(int i=2;i<n;++i){
        if(i-po+Next[i-po]<Next[po])
            Next[i] = Next[i-po];
        else {
            int j = max(0,po+Next[po]-i);
            while(i+j<n&&s[j]==s[i+j])++j;
            Next[i] = j;
            po = i;
        }
    }
}
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        scanf("%s",s);
        int n = strlen(s);
        get_Next(n);
        /*提前统计后缀子串长度不超过i的子串中也是一个前缀子串的最大长度*/
        pre[0] = 0;
        for(int i=1;i<=n;++i)
            pre[i] = max(pre[i-1],Next[n-i]);
        int ans = 0;
        for(int i=1;i<n-1;++i){
            int mixn = min(i,Next[i]);
            mixn = min(mixn,(n-i)/2);
            ans = max(ans,pre[mixn]);
        }
        printf("%d\n",ans);
    }
    return 0;
}


二、回文前缀子串和回文后缀子串方面应用

hdu 3613
题意: 每个字母都有权值,将字符串拆成前后两个字符串,如果拆分后的字符串是回文串,统计其字母权值和,如果不是回文串,和为0,问拆分两个字符串的权值最大和。


ex_kmp的一个应用:判断前缀、后缀字符串是否为回文串。
回文串正读和反读必须相同,则制造反串S1,求出原串的所有后缀子串S[i……n-1]与S1的最长公共前缀长度,如果长度等于后缀子串的长度,那么证明该后缀子串正反读相同,即此后缀子串为一个后缀回文子串。
同理,求出反串的所有后缀子串S1[i,n-1](即为前缀反读)与原串S的最长公共前缀长度,如果长度等于反串的后缀子串的长度,那么证明该前缀为前缀回文子串。

/*************************************************
*** hdu 3613 ex_kmp 求前缀子串回文 后缀子串回文
*************************************************/

#include <iostream>
#include <fstream>
#include <stdio.h>
#include <algorithm>
#include <math.h>
#include <map>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#define llt long long

using namespace std;

const int N = 5e5+7777;
char s[2][N];
int Next[N],extend[2][N];
// 一维为0即表示本串为模式串,为1即表示反串为模式串
int w[30];//记录字母的权值
int sum[N];//前i项权值和
void get_Next(char S[],int n){
    Next[0] = n;
    int i = 0;
    while(i+1<n&&S[i]==S[i+1]) ++i;
    Next[1] = i;
    int po = 1;
    for(int i=2;i<n;++i){
        if(i+Next[i-po]<po+Next[po])
            Next[i] = Next[po];
        else{
            int j =max(0,po+Next[po]-i);
            while(i+j<n&&S[j]==S[i+j]) ++j;
            Next[i] = j;
            po = i;
        }
    }
}
// 模式串为S 匹配串为T
// 匹配T的所有后缀子串与S的最长公共前缀长度
void extend_KMP(char S[],char T[],int n,int ex[]){
    get_Next(S,n);
    int i = 0;
    while(i<n&&S[i]==T[i]) ++i;
    ex[0] = i;
    int po = 0;//当前匹配的最远位置的点
    for(int i=1;i<n;++i){
        if(i+Next[i-po]<po+ex[po])
            ex[i] = Next[i-po];
        else{
            int j = max(0,po+ex[po]-i);
            while(i+j<n&&S[j]==T[i+j]) ++j;
            ex[i] = j;
            po = i;
        }
    }
}
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        for(int i=1;i<=26;++i)
            scanf("%d",&w[i]);

        scanf("%s",s[0]);
        int n = strlen(s[0]);
        /*构造反串*/
        for(int i=0;i<n;++i)
            s[1][n-1-i] = s[0][i];
        /*正串为模式串,反串为匹配串,得到前缀回文串*/
        extend_KMP(s[0],s[1],n,extend[0]);
        /*反串为模式串,正串为匹配串,得到后缀回文串*/
        extend_KMP(s[1],s[0],n,extend[1]);
        sum[0] = 0;
        for(int i=1;i<=n;++i)
            sum[i] = sum[i-1] + w[s[0][i-1]-'a'+1];
        int ans = -1e9;
        for(int i=1;i<n;++i){
            int tmp = 0;
            /*前缀[0,i-1]为回文串*/
            if(extend[0][n-i]==i) tmp += sum[i];
            /*后缀[i,n-1]为回文串*/
            if(extend[1][i]==n-i) tmp += sum[n] - sum[i];
           // cout<<extend[0][n-i]<<" "<<extend[1][i]<<endl;
            ans = max(ans,tmp);
        }
        printf("%d\n",ans);
    }
    return 0;
}
/*****************
5555
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
abaa
*/

SPOJ - VPALIN
题意:求n个字符串,两两前后拼接成回文串的对数。


01字典树 + ex_kmp 处理 字符串的前缀、后缀回文串

扫描二维码关注公众号,回复: 4166854 查看本文章
/**************************************
*** hdu 3294 Manacher算法
**************************************/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <math.h>
#include <map>
#include <cstdlib>
#define llt long long
#define lson (rt<<1)
#define rson ((rt<<1)+1)
using namespace std;

const int N = 2e6+777;
char s[N],t[N];
int nxt[N],ext[2][N];
struct node{
    int id;
    int len;
    bool operator <(const node &a)const{
        return len<a.len;
    }
}p[N];

struct tried{
    int val;//记录以此节点为末尾的单词个数
    int son[26];
}q[N*2];

int toUsed = 2;
void Insert(char A[],int loc,int len){// loc 指1 表示正序字典树 loc指2 表示反序字典树
    for(int i=0;i<len;++i){
        int sn = A[i]-'a';
        if(!q[loc].son[sn]) q[loc].son[sn] = ++toUsed;
        loc = q[loc].son[sn];
    }
    ++q[loc].val;
}

llt query(char A[],int loc,int id,int len){// id为1 表示正串跑反序字典树
    llt ans =0;
    for(int i=0;i<len;++i){
        int sn = A[i]-'a';
        if(!q[loc].son[sn]) break;
        loc = q[loc].son[sn];
        if(q[loc].val!=0&&ext[id][i+1]+i+1==len) ans += q[loc].val;
    }
    return ans;
}

void get_nxt(char *S,int n){
    nxt[0] = n;
    int i = 0;
    while(i+1<n&&S[i]==S[i+1])++i;
    nxt[1] = i;
    int po = 1;
    for(int i=2;i<n;++i){
        if(i+nxt[i-po]<po+nxt[po])
            nxt[i] = nxt[i-po];
        else {
            int j = max(0,po+nxt[po]-i);
            while(i+j<n&&S[j]==S[i+j])++j;
            nxt[i] = j;
            po = i;
        }
    }
}

void get_ext(char *S,char *T,int n,int id){
//    for(int i=0;i<n;++i)
//        cout<<S[i];
    //cout<<endl;
//    for(int i=0;i<n;++i)
//        cout<<T[i];
//    cout<<endl;
    get_nxt(T,n);
    int i = 0;
    while(i<n&&S[i]==T[i])++i;
    ext[id][0] = i;
    int po = 0;

    for(int i=1;i<n;++i){
        if(i+nxt[i-po]<po+ext[id][po])
            ext[id][i] = nxt[i-po];
        else{
            int j = max(0,po+ext[id][po]-i);
            while(i+j<n&&T[j]==S[i+j]) ++j;
            ext[id][i] = j;
            po = i;
        }
    }
}
int main(){
    int n;
    while(~scanf("%d",&n)){
        int tot = 0;
        for(int i=0;i<n;++i){
            p[i].id = tot;
            scanf("%d%s",&p[i].len,s+tot);
            tot += p[i].len;
        }
        for(int i=0;i<tot;++i)
            t[tot-1-i] = s[i];
        sort(p,p+n);
        llt ans = 0;
        for(int i=0;i<n;++i){
            int tmp = tot - (p[i].len+p[i].id);
            //cout<<tmp<<endl;
            get_ext(s+p[i].id,t+tmp,p[i].len,1);
            get_ext(t+tmp,s+p[i].id,p[i].len,0);
            if(ext[1][0]==p[i].len) ++ans;
            //cout<<ans<<" ";
            ans += query(s+p[i].id,2,1,p[i].len);
            //cout<<ans<<" ";
            ans += query(t+tmp,1,0,p[i].len);
            //cout<<ans<<endl;
            Insert(s+p[i].id,1,p[i].len);
            Insert(t+tmp,2,p[i].len);
        }
        printf("%lld\n",ans);
    }
    return 0;
}
/****************
3
2 aa
2 aa
2 aa
**********/

hdu 2609
题意:给n个01等长字符串,所有本串循环同构串,等价为相同字符串,问n个字符串中不同的字符串个数。


hash 做法: 在线对每个字符串枚举其所有循环同构串,在map中查找是否之前出现过,如果其中有一个出现过,这个字符串与之前相同!如果未出现过,插入本串即可!

/*************************************************
*** hdu 2609 hash做法
*************************************************/
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <math.h>
#define llt long long
#include <map>
#define fi first
#define se second
#define lson (rt<<1)
#define rson ((rt<<1)+1)
using namespace std;

const int N = 1e6+777;
char s[N*2];
struct strCmp{
    bool operator ()(char const *a,char const *b)const{
        return strcmp(a,b)<0;
    }
};
map<char*,int,strCmp> Hash;
int main(){
    int n;
    while(~scanf("%d",&n)){
        Hash.clear();
        int ans = 0;
        int t = 0;
        for(int i=0;i<n;++i){
            scanf("%s",s+t);
            int len = strlen(s+t);
            int flag = 1;
            for(int i=0;i<len&&flag;++i){
                s[i+len+t] = s[i+t];
                //s[i+len+1+t]='\0';
                map<char*,int,strCmp>::iterator it = Hash.find(s+i+1+t);
                if(it!=Hash.end()){flag=0;break;}
            }
            //cout<<endl;
            s[len+t]='\0';
           // cout<<s<<endl;
            if(flag) {++ans;Hash[s+t] = 1;}
            t += 2*len;
        }
        memset(s,0,sizeof s);
        printf("%d\n",ans);
    }
}

三、分治 + 拓展KMP

1、最长回文子串的长度

POJ 3974

/**************************************
*** poj 3974 ex_kmp 处理 最长回文子串
    时间复杂度 O(nlog n) 还是 O(n^2)
**************************************/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <math.h>
#include <map>
#include <cstdlib>
#define llt long long
#define lson (rt<<1)
#define rson ((rt<<1)+1)
using namespace std;
typedef long long ll;

const int N = 1e6+777;

char s[N],ss[N];

int nxt[N],ext1[N],ext2[N];
void get_next(char *S,int n){
    nxt[0] = n;
    int i = 0;
    while(i+1<n&&S[i]==S[i+1]) ++i;
    nxt[1] = i;
    int po = 1;
    for(int i=2;i<n;++i){
        if(i+nxt[i-po]<po+nxt[po])
            nxt[i] = nxt[i-po];
        else{
            int j = max(0,po+nxt[po]-i);
            while(i+j<n&&S[j]==S[i+j])++j;
            nxt[i] = j;
            po = i;
        }
    }
}

void get_ext(char *S,char *T,int *ext,int n,int m){
    get_next(T,m);
    int i = 0;
    while(i<n&&i<m&&S[i]==T[i])++i;
    ext[0] = i;
    int po = 0;
    for(int i=1;i<n;++i){
        if(i+nxt[i-po]<po+ext[po])
            ext[i] = nxt[i-po];
        else {
            int j = max(0,po+ext[po]-i);
            while(j<m&&i+j<n&&T[j]==S[i+j]) ++j;
            ext[i] = j;
            po = i;
        }
    }
}

int ans = 0;

/*求出S的回文后缀子串 */
/*左部反向的后缀子串与右部的最长前缀长度*/
void solve(char *S,char *SS,char *T,int n,int m){
    get_ext(S,SS,ext1,n,n);
    get_ext(SS,T,ext2,n,m);

    ans = max(ans,2*ext2[0]);
    for(int i=1;i<n;++i){
        int len = n-i;
        if(ext1[i]!=len||ext2[len]==0) continue;
        ans = max(ans,ext1[i]+2*ext2[len]);
    }
}
void Find(int l,int r,int n){
    if(l==r) return ;


    int mid = (l+r)>>1;
    solve(s+l,ss+n+1-mid,s+mid+1,mid-l+1,r-mid);
    solve(ss+n+1-r,s+mid+1,ss+n+1-mid,r-mid,mid-l+1);
    //cout<<l<<" "<<r<<" "<<ans<<endl;

    if(ans<mid-l+1)Find(l,mid,n);
    if(ans<r-mid)  Find(mid+1,r,n);
}
int main(){
    int cas = 0;
    while(~scanf("%s",s+1)&&s[1]!='E'){
        int n = strlen(s+1);
        for(int i=1;i<=n;++i) ss[n+1-i] = s[i];

        ans = 1;
        Find(1,n,n);

        printf("Case %d: %d\n",++cas,ans);
    }


    return 0;
}

2、统计不重叠重复次数为2的子串长度为k的个数

hunnu oj 11565
题意:q次询问,每次问长度为k的不重叠重复次数为2的连续序列的个数。


这题有点可惜,是道非常好的题!但标程错了!
标程将所有整数强制转换为char,如果为负数,都变成了’?’,太可悲了!
我wa了两天!


/***************************************************************
*** hunnu oj11565 分治+ex_kmp统计不同长度的不重叠重复次数子串的个数
****************************************************************/
#include <iostream>
#include <fstream>
#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <math.h>
#define llt long long
#define uint unsigned int
#include <map>
#define fi first
#define se second
#define lson (rt<<1)
#define rson ((rt<<1)+1)
using namespace std;

const int N = 5e5+777;
int n,q;
int a[N],aa[N];
int sum[N]={0};
int nxt[N];
int extend[N];

void getNext(int *S,int n){
    // fill(nxt,nxt+n,0);
    nxt[0] = n;
    int i = 0;
    while(i+1<n&&S[i]==S[i+1])++i;
    nxt[1] = i;
    int po = 1;
    for(int i=2;i<n;++i){
        if(i+nxt[i-po]<po+nxt[po])
            nxt[i] = nxt[i-po];
        else {
            int j = max(0,po+nxt[po]-i);
            while(i+j<n&&S[j]==S[i+j]) ++j;
            nxt[i] = j;
            po = i;
        }
    }
}

void getExtend(int *S,int *T,int n,int m){
    getNext(T,m);
    int mixn = min(n,m);
    int i = 0;
    while(i<mixn&&S[i]==T[i])++i;
    extend[0] = i;
    int po = 0;
    for(int i=1;i<n;++i){
        if(i+nxt[i-po]<po+extend[po])
            extend[i] = nxt[i-po];
        else {
            int j = max(0,po+extend[po]-i);
            while(j<m&&i+j<n&&T[j]==S[i+j])++j;//不能小于mixn
            extend[i] = j;
            po = i;
        }
    }
}

void solve(int *S,int *T,int l,int mid,int r,int sgn){
    /*统计跨过mid的nice序列*/
    getExtend(S+l,S+mid+1,mid-l+1,r-mid);
    getNext(T+n+1-mid,mid-l+1);
    /*以a-mid 、 a-(mid+1)为对称的个数*/
    if(sgn==1)
        for(int i=l;i<=mid;++i)
            if(extend[i-l]==mid+1-i) ++sum[2*(mid-i+1)];
    /*左部居多*/
    for(int i=l+1;i<=mid;++i){
        int len = mid+1-i;

        int s1 = min(extend[i-l],len-1);
        int s2 = min(nxt[len],len-1);
        if(s1+s2<len||s1==0||s2==0) continue;
        sum[2*len] += 1+s1+s2-len;
    }
}
void find(int l,int r){//统计区间[l,r]内nice序列的个数
    if(l==r) return;

    int mid = (l+r)>>1;
    solve(a,aa,l,mid,r,1);
    solve(aa,a,n+1-r,n-mid,n+1-l,0);
    find(l,mid);
    find(mid+1,r);
}
int main(){
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);


    while(~scanf("%d%d",&n,&q)){
        memset(sum,0,sizeof sum);
        for(int i=1;i<=n;++i){
             scanf("%d",&a[i]);
             aa[n-i+1]=a[i];
        }

        find(1,n);
        while(q--){
            int x;
            scanf("%d",&x);
            printf("%d\n",sum[x]);
        }
    }

    return 0;
}
/*
9 4
1 1 -1 1 1 -1 1 1 -1
2 4 6 8
*/

猜你喜欢

转载自blog.csdn.net/qq_38786088/article/details/82080006