AC自动机:初步理解可以理解为就是KMP加上trie     AC自动机讲解超详细 - Hastieyua - 博客园 (cnblogs.com)

这个是模板:187 AC自动机 - 董晓 - 博客园 (cnblogs.com)  求是否出现过,只要匹配到就清0,不管重复的

https://www.bilibili.com/video/BV1tF41157Dy/?vd_source=23dc8e19d485a6ac47f03f6520fb15c2     董老师讲解的视频

重点就是:

ne[v]存的是节点v的回跳边的终点(四边形),ch[u][i]存的是节点u的树边或者转移边(三角形)

回跳边所指的是当前节点父节点回跳边指向节点的儿子

转移边所指的是当前节点的回跳边所指结点的儿子

 和KMP的对比:

 

P3796 【模板】AC 自动机(加强版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

有 N个由小写字母组成的模式串以及一个文本串 T。每个模式串可能会在文本串中出现多次。你需要找出哪些模式串在文本串 T 中出现的次数最多。保证没有两个模式串重复

这里就需要处理重复出现次数

//AC自动机加强版
#include<bits/stdc++.h>
#define maxn 1000001
using namespace std;
char s[151][maxn],T[maxn];
int n,cnt,vis[maxn],ans;
struct kkk{
	int son[26],fail,flag;
	void clear(){memset(son,0,sizeof(son));fail=flag=0;}
}trie[maxn];
queue<int>q;
void insert(char* s,int num){
	int u=1,len=strlen(s);
	for(int i=0;i<len;i++){
		int v=s[i]-'a';
		if(!trie[u].son[v])trie[u].son[v]=++cnt;
		u=trie[u].son[v];
	}
	trie[u].flag=num;			//变化1:标记为第num个出现的字符串
}
void getFail(){
	for(int i=0;i<26;i++)trie[0].son[i]=1;
	q.push(1);trie[1].fail=0;
	while(!q.empty()){
		int u=q.front();q.pop();
		int Fail=trie[u].fail;
		for(int i=0;i<26;i++){
			int v=trie[u].son[i];
			if(!v){trie[u].son[i]=trie[Fail].son[i];continue;}
			trie[v].fail=trie[Fail].son[i];
			q.push(v);
		}
	}
}
void query(char* s){
	int u=1,len=strlen(s);
	for(int i=0;i<len;i++){
		int v=s[i]-'a';
		int k=trie[u].son[v];
		while(k>1){
			if(trie[k].flag)vis[trie[k].flag]++;	//如果有模式串标记,更新出现次数
			k=trie[k].fail;
		}
		u=trie[u].son[v];
	}
}
void clear(){
	for(int i=0;i<=cnt;i++)trie[i].clear();
	for(int i=1;i<=n;i++)vis[i]=0;
	cnt=1;ans=0;
}
int main(){
	while(1){
		scanf("%d",&n);if(!n)break;
		clear();
		for(int i=1;i<=n;i++){
			scanf("%s",s[i]);
			insert(s[i],i);
		}
		scanf("%s",T);
		getFail();
		query(T);
		for(int i=1;i<=n;i++)ans=max(vis[i],ans);	//最后统计答案
		printf("%d\n",ans);
		for(int i=1;i<=n;i++)
		if(vis[i]==ans)
		printf("%s\n",s[i]);
	}
}

P5357 【模板】AC 自动机(二次加强版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

求出每个模式串在串中的出现次数,注意这里两个模式串可能是重复的

可以用两种方法做,一种是从底向上,用拓扑排序,第二种是之间建fail树,然后求子树和,这两种方法都可以,重点是理解fail边变为树,以及怎样通过这种方法降低复杂度,避免重复跳fail边,导致超时

1、拓扑排序

#include <queue>
#include <cstdlib>
#include <cmath>
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn =  2e6+100;
int n,cnt,mp[maxn],in[maxn],ans,vis[maxn];
char s[maxn],t[maxn];
struct node{
	int son[26],flag,ans,fail;
	void clea(){
		memset(son,0,sizeof(son));fail=flag=ans=0;
	}
}trie[maxn];
queue<int> q;
void inser(char *s,int num){
	int u=1,len=strlen(s);
	for(int i=0;i<len;i++){
		int v=s[i]-'a';
		if(!trie[u].son[v]) trie[u].son[v]=++cnt;
		u=trie[u].son[v];
	}
	if(!trie[u].flag) trie[u].flag=num;
	mp[num]=trie[u].flag; //注意这里是在处理重复子串 
	//这道题有相同字符串要统计,设当前字符串是第i个,我们用一个Map[i]数组(不是STL那个)存((当前字符串在Trie中的那个位置)的flag),
	//最后把vis[Map[i]]输出就OK了。另外flag只在第一次赋值时变化,其他都不变。 
}
void get_fa(){
	for(int i=0;i<26;i++) trie[0].son[i]=1;
	q.push(1);
	while(!q.empty()){
		int u=q.front();
		q.pop();
		int Fail=trie[u].fail;
		for(int i=0;i<26;i++){
			int v=trie[u].son[i];
			if(!v){
				trie[u].son[i]=trie[Fail].son[i];continue;
			}
			trie[v].fail=trie[Fail].son[i];
			in[trie[v].fail]++;  //从底到根算:拓扑排序 
			q.push(v);   
		}
	}
}
void tuop(){
	for(int i=1;i<=cnt;i++) if(in[i]==0) q.push(i);
	while(!q.empty()){
		int u=q.front();
		q.pop();
		vis[trie[u].flag]=trie[u].ans;
		int v=trie[u].fail;
		in[v]--;
		trie[v].ans+=trie[u].ans; //向上传递值
		//这里是拓扑排序了,如果入度为0了,也就是说他的儿子都算完了,就可以入队了
		if(in[v]==0) q.push(v); 
	}
}
void query(char *s){
	int u=1,len=strlen(s);
	for(int i=0;i<len;i++){
		u=trie[u].son[s[i]-'a'];
		trie[u].ans++;
	}
}
int main() {
	scanf("%d",&n);
	cnt=1;
	for(int i=1;i<=n;i++){
		scanf("%s",s);inser(s,i);
	}
	get_fa();
	scanf("%s",t);
	query(t);
	tuop();
	for(int i=1;i<=n;i++) printf("%d\n",vis[mp[i]]);
    return 0;
}

2、建fail树统计子树和

#include <cstdio>
#include <iostream>
#include <algorithm>
#include<queue>
#define base 139
const int maxn=2e5+10;
using namespace std;
const int maxm=2e6+10;
typedef unsigned long long ll;


char s[maxm]; 
queue<int> q;
int head[maxn],nex[maxn],to[maxn],cnt;
int n,tr[maxn][26],fail[maxn],match[maxn],size[maxn];
int tot=1;
void dfs(int u){
	for(int i=head[u];i;i=nex[i]){
		int v=to[i];
		dfs(v);
		size[u]+=size[v];
	}
}
void add(int u,int v){//建边 
	nex[++cnt]=head[u];
	to[cnt]=v;
	head[u]=cnt;
}
int main(){
   scanf("%d",&n);
   for(int i=1;i<=n;i++){
   	scanf("%s",s);
   	int u=1;
   	for(int j=0;s[j];j++){
   		int c=s[j]-'a';
   		if(!tr[u][c]) tr[u][c]=++tot;
   		u=tr[u][c];
	   }
	   match[i]=u; //记录每个模式串在 Trie 树上的终止节点
   }
   for(int i=0;i<26;i++) tr[0][i]=1;
   q.push(1);
   while(!q.empty()){
   		int u=q.front();q.pop();
   		for(int i=0;i<26;i++){
   			if(tr[u][i]){
   				fail[tr[u][i]]=tr[fail[u]][i];
   				q.push(tr[u][i]);
			   }
			else tr[u][i]=tr[fail[u]][i];
		   }
   }
   scanf("%s",s);
   for(int u=1,i=0;s[i];i++){
   	u=tr[u][s[i]-'a'];
   	++size[u]; //记录匹配次数 
   }
   for(int i=2;i<=tot;i++) add(fail[i],i); //建fail树
   dfs(1);
   for(int i=1;i<=n;i++) printf("%d\n",size[match[i]]); 
  return 0;
}

  

1479:【例题1】Keywords Search

这个也是模板题

注意开的数据范围,重点是看getfail函数和que函数的写法(记住呀)

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1e4+10;
const int M=1e6+10;
const int INF=0x3fffffff;
typedef long long LL;
typedef unsigned long long ull;
//AC自动机模板题
char s[M];
int ch[maxn*32][26];
int fail[maxn*32],flag[maxn*32];
int t,n,tot=0;
void inti(){
	memset(ch,0,sizeof(ch));
	memset(flag,0,sizeof(flag));
	memset(fail,0,sizeof(fail));
	tot=0;
}
void inse(string s){
	int now=0;
	for(int i=0;i<s.length();i++){
		int x=s[i]-'a';
		if(!ch[now][x]){
			ch[now][x]=++tot;
		}
		now=ch[now][x];
	}
	flag[now]++; //这里单词数+1 
}
void getfail(){
	queue<int> q;
	for(int i=0;i<26;i++){
		if(ch[0][i]) {
			fail[ch[0][i]]=0;
			q.push(ch[0][i]);
		}
	} 
	while(!q.empty()){
		int op=q.front();
		q.pop();
		for(int i=0;i<26;i++){
			if(ch[op][i]){
				fail[ch[op][i]]=ch[fail[op]][i];
				q.push(ch[op][i]);
			}
			else ch[op][i]=ch[fail[op]][i];
		}
	}
}
int que(string s){
	int now=0,ans=0;
	for(int i=0;i<s.size();i++){
		now=ch[now][s[i]-'a'];
		for(int j=now;j&&flag[j]!=-1;j=fail[j]){
			ans+=flag[j];
			flag[j]=-1;
		}
	}
	return ans;
}
int main(){
	scanf("%d",&t);
	while(t--){
		inti();
		scanf("%d",&n);
		string tmp;
		for(int i=0;i<n;i++){
			cin>>tmp;
			inse(tmp);
		}
		fail[0]=0;
		getfail();
		scanf("%s",s);
		printf("%d\n",que(s));
			
	}
return 0;
}

  

1480:玄武密码

重点还是理解数据结构啊。。。

我们只需要先建立所有密码的trie树
再以母串为主串跑一个AC自动机
不过其中还是有一些需要改动的地方
原本字典树中用来记录某个节点是不是字符串结尾的数组不需要,直接删去
我们需要另一个数组来标记哪些点被匹配
跑完ac自动机后从trie树上找最后一个匹配的点即可
优化:由于nxt数组是递归到0的所以只要有一个点被标记过,那么这个点到0的所有点都已经被遍历过直接退出即可
//为什么一个点超时啊。。。。

有的说是SAM好做一些,还不会先留着

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1e7+10;
const int INF=0x3fffffff;
typedef long long LL;
typedef unsigned long long ull;
/*
我们只需要先建立所有密码的trie树
再以母串为主串跑一个AC自动机
不过其中还是有一些需要改动的地方
原本字典树中用来记录某个节点是不是字符串结尾的数组不需要,直接删去
我们需要另一个数组来标记哪些点被匹配
跑完ac自动机后从trie树上找最后一个匹配的点即可
优化:由于nxt数组是递归到0的所以只要有一个点被标记过,那么这个点到0的所有点都已经被遍历过直接退出即可
*/
//为什么一个点超时啊。。。。 
int ch[maxn][4];
int fail[maxn];
int flag[maxn];
int n,m,tot=0;
char ss[maxn];
char sa[100050][150];
int jud(char x){
	if(x=='E') return 0;
	else if(x=='S') return 1;
	else if(x=='W') return 2;
	else if(x=='N') return 3;
}
void inse(string s){
	int now=0;
	for(int i=0;i<s.length();i++){
		int x=jud(s[i]);
		if(!ch[now][x]){
			ch[now][x]=++tot;
		}
		now=ch[now][x];
	}  //flag不用加了,因为是用来判断有没有访问过的 
}
void getfail(){
	queue<int> q;
	for(int i=0;i<4;i++) {
		if(ch[0][i]) q.push(ch[0][i]);
	}
	while(!q.empty()){
		int u=q.front();q.pop();
		for(int i=0;i<4;i++){
			if(ch[u][i]) fail[ch[u][i]]=ch[fail[u]][i],q.push(ch[u][i]);
			else ch[u][i]=ch[fail[u]][i];
		}
	}
}
void biaoji(string s){
	int u=0;
	for(int i=0;i<s.length();i++){
		int c=jud(s[i]);
		u=ch[u][c];
		for(int j=u;j;j=fail[j]){
			if(flag[j]) break;
			flag[j]=1;
		}
	}
}
int getan(string s){
	int u=0,ans=0;
	for(int i=0;i<s.length();i++){
		int c=jud(s[i]);
		u=ch[u][c];
		if(flag[u]) ans=i+1; //这里仔细看,求的是最长的匹配值,如果不匹配了就不会改变这个ans了,所以ans里面放的是最大的
	}
	return ans;
}
int main(){
	scanf("%d %d",&n,&m);
	scanf("%s",ss);
	for(int i=0;i<m;i++){
		scanf("%s",sa[i]);
		inse(sa[i]);
	}
	getfail();
	biaoji(ss);
	for(int i=0;i<m;i++){
		printf("%d\n",getan(sa[i]));
	}
return 0;
}

 

1481:Censoring

 就是给一个原串,给出n个模式串,匹配后删掉,注意事项是删掉一个后可能又能够匹配另一个串了

和KMP里面的一道题一样,这里能够通的过了...

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
char s[100001],ori[100001];
int n,tot,w,top;
int trie[100001][26],fail[100001],heap[100001],sign[100001];
int isend[100001];
void insert(char *s){
    int now=0,len=strlen(s);
    for(int i=0;i<len;i++){
        int x=s[i]-'a';
        if(!trie[now][x])trie[now][x]=++tot;
        now=trie[now][x];
    }
    isend[now]=len;
}
void makefail(){
    queue<int> q;
    for(int i=0;i<26;i++)
    if(trie[0][i])q.push(trie[0][i]);
    while(!q.empty()){
        int now=q.front();q.pop();
        for(int i=0;i<26;i++){
            if(!trie[now][i]){
                trie[now][i]=trie[fail[now]][i];
                continue;
            }       
            fail[trie[now][i]]=trie[fail[now]][i];
            q.push(trie[now][i]);
        }
    }
}
void solve(char *s){
    int now=0,len=strlen(s),i=0;
    w=0;
    while(i<len){
        int x=s[i]-'a';
        now=trie[now][x];
        sign[++top]=now;
        heap[top]=i;
        if(isend[now]){
            top-=isend[now];
            if(!top) now=0;
            else now=sign[top];
        }
        i++;
    }
}
int main()
{
    scanf("%s",s);
    scanf("%d",&n);
    int len=strlen(s);
    for(int i=1;i<=n;i++){
        scanf("%s",ori);
        insert(ori);
    }
    makefail();
    solve(s);
    for(int i=1;i<=top;i++)
    printf("%c",s[heap[i]]);
    return 0;
}

  

1482:单词

 

由模式串组成的原串

 

把所有字符串放在Trie里,并记cnt[i]为Trie的节点i为多少个字符串的前缀。
一个字符串是另一个字符串的子串,那么它也是该字符串某个前缀s[0,m]的后缀。
那么一个想法就出来了:
求出fail数组,然后以fail指针为边建出fail树,那么一个字符串的出现个数为:设它的结尾是节点x,那么fail树上以x为根的子树的cnt值的总和即为答案。
原文链接:https://blog.csdn.net/worldwide_d/article/details/51819862
//https://blog.csdn.net/worldwide_d/article/details/51819862

这道题也可以直接用AC自动机做,就是自己构建原串,在模式串中间加一个字符例如%

/*
把所有字符串放在Trie里,并记cnt[i]为Trie的节点i为多少个字符串的前缀。 
一个字符串是另一个字符串的子串,那么它也是该字符串某个前缀s[0,m]的后缀。 
那么一个想法就出来了: 
求出fail数组,然后以fail指针为边建出fail树,那么一个字符串的出现个数为:设它的结尾是节点x,那么fail树上以x为根的子树的cnt值的总和即为答案。
原文链接:https://blog.csdn.net/worldwide_d/article/details/51819862
//https://blog.csdn.net/worldwide_d/article/details/51819862
*/
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<queue>
const int maxn=1e6+10;
using namespace std;
int n,cnt,ch[maxn][26],size[maxn],fail[maxn];
int mp[maxn],h[maxn];
char s[maxn];
struct node{
	void ins(int x){
		scanf("%s",s+1);
		int now=0,len=strlen(s+1);
		for(int i=1;i<=len;i++){
			int u=s[i]-'a';
			if(!ch[now][u]) ch[now][u]=++cnt;
			now=ch[now][u];
			size[now]++; //
		}
		mp[x]=now; //标记这个模式串在树上的位置 
	}
	void build(){
//		queue<int> q;
//		for(int i=0;i<26;i++) if(tr[0][i]) q.push(tr[0][i]);
//		while(!q.empty()){
//			int u=q.front();q.pop();
//			for(int i=0;i<26;i++){
//				if(tr[u][i]) {
//					fail[tr[u][i]]=tr[fail[u]][i];
//				}
//				else ch[u][i]=tr[fail[u]][i];
//			}
//		}
	//这里用手写队列 
	int head=0,tail=0;
	for(int i=0;i<26;i++) if(tr[0][i]) h[++tail]=ch[0][i];
	while(head<tail){
		int x=h[++head];
		for(int i=0;i<26;i++){
			int y=tr[x][i];
			if(y) {
				fail[y]=tr[fail[x]][i];
				h[++tail]=y;
			}
			else ch[x][i]=tr[fail[x]][i];
		}
	}
	}
	void solve(){
		for(int i=cnt;i>=0;i--) size[fail[f[i]]]+=size[h[i]];
		for(int i=1;i<=n;i++) printf("%d\n",size[a[i]]);
	}
}ac;




int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) ac.ins(i);
	ac.build();
	ac.solve();
	return 0;
}

 

1483:最短母串

 

 

 

【题解】

类似于搜索+二进制记录状态的题目

搜索时利用BFS来跑,每一个结点的位置都可以用状态数组存起来,

判断是否为 (1<<n)- 1 即可。

在输出答案时需要递归实现,所以要一个辅助数组fa来记录上一个结点的位置。

洛谷 P2322 最短母串问题 状压+AC自动机_二货RK的博客-CSDN博客

#include<queue>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream> 
using namespace std; 
const int maxn=6600;
const int maxm=2e6+200;
int tr[maxn][26],fail[maxn],end[maxn]; //这里END就是标记节点属于哪一条字符串的
int vis[maxn][605]; //这里是节点--状态
int fa[maxm],q[maxm],st[maxm];
//fa是为了标记上一个节点属于哪一个 
//这个例子是可以模拟了,勉勉强强理解了

//?????编译错误 
char str[maxm];
char mm[660];
int n,cnt;
void prin(int x){
	if(x==1) return;
	prin(fa[x]);
	printf("%c",str[x]+'A');
} 
void inse(char s[],int id){
	int p=0;
	for(int i=0;s[i];i++){
		int x=s[i]-'A';
		if(!tr[p][x]){
			tr[p][x]=++cnt;
		}
		p=tr[p][x];
	}
	end[p]|=(1<<id);  //结尾打上标记  使用的数位思想 
}
void build(){
	int head=1,tail=0;
	for(int i=0;i<26;i++){
		if(tr[0][i]){
			q[++tail]=tr[0][i];
			fail[tr[0][i]]=0;
		}
	}
	while(head<=tail){
		int u=q[head++];
		for(int i=0;i<26;i++){
			if(tr[u][i]){
				fail[tr[u][i]]=tr[fail[u]][i];
				q[++tail]=tr[u][i];
				end[tr[u][i]]|=end[fail[tr[u][i]]]; //这里也打上标记 
			}
			else tr[u][i]=tr[fail[u]][i];
		}
	}
}
void solve(){
	memset(q,0,sizeof(q));
	int head=0,tail=1;
	q[1]=st[1]=0; //节点队列和状态队列
	vis[0][0]=1; //预处理
	while(head<tail){
		int u=q[++head];
		int s=st[head];
		for(int i=0;i<26;i++){
			int to=tr[u][i];
			int ts=s|end[to];
			if(vis[ts][to]) continue; //避免重复计算
			fa[++tail]=head;
			q[tail]=to;
			str[tail]=i; //记录一下字符串 
			vis[ts][to]=1;
			st[tail]=ts;
			if(ts==(1<<n)-1) {
				prin(tail);
				printf("\n");return;
			} 
		}
	} 
}
int main(){
	scanf("%d",&n);
	for(int i=0;i<n;i++){
		scanf("%s",mm);
		
		inse(mm,i);
	}
	build();
	solve();
	return 0;
}



  #include<queue>
   #include<cstdio>
   #include<cstring>
   #include<algorithm>
   #include<iostream> 
   using namespace std;
 const int N = 6e3+5;
   const int M = 2e6+50;
   const int Str_N = 605;
   int Trie[N][26],fail[N],End[N];
  int vis[N][Str_N];
  int Q[M],St[M];
 int Fa[M];
  char str[M];
  int Head,Tail;
  int n,idx;
  char Str[Str_N];
  void print(int x){
      if(x==1) return ;
      print(Fa[x]);
     putchar(str[x]+'A');
 }
  void Insert( char s[] , int Id ){
      int p = 0 ;
      for(int i=0;s[i];i++){
          int t = s[i]-'A';
          if( !Trie[p][t] )
              Trie[p][t] = ++idx;
          p = Trie[p][t];
      }
      End[p] |= (1<<Id);
      //cout<<p<<"  "<<End[p]<<endl;
  }
  void Build(){
      Head = 1 , Tail = 0;
  
      for(int i=0;i<26;i++){
          if( Trie[0][i] ){
              Q[++Tail] = Trie[0][i];
              fail[Trie[0][i]] = 0;
          }
      }
  //cout<<"now"<<endl;
      while( Head <= Tail ){
      	
      
          int u = Q[Head];
  
          for(int i=0;i<26;i++){
              int To = Trie[u][i];
             if( To ){
                 fail[To] = Trie[fail[u]][i];
                  Q[++Tail] = To ;
                  End[To] |= End[fail[To]];
              }else{
                  Trie[u][i] = Trie[fail[u]][i];
              }
          }
          	//cout<<Q[Head]<<"  "<<fail[Q[Head]]<<"   "<<End[Q[Head]]<<endl;
          Head++; 
      }
  }
  void Solve(){
      memset(Q,0,sizeof Q );
  
  
      Head = 0 , Tail = 1;
      Q[1] = St[1] = 0 ;
      vis[0][0] = 1 ;
  //cout<<"new"<<endl;
      while( Head < Tail ){
          int u = Q[++Head],S = St[Head];
          for(int i=0;i<26;i++){
              int To = Trie[u][i];
              int Ts = S | End[To] ;
            
              if( vis[Ts][To] ) continue;
 
              Fa[++Tail] = Head ; Q[Tail] = To ;str[Tail] = i;
            vis[Ts][To] = 1 ;St[Tail] = Ts ;
   /// cout<<To<<"   "<<Ts<<"  "<<Tail<<"  "<<Fa[Tail]<<endl;
              if( Ts == (1<<n)-1 ){
                  print(Tail);
                  putchar('\n');
                  return ;
              }
          }
      }
  }
  int main()
  {
      scanf("%d",&n);
      for(int i=0;i<n;i++){
          scanf("%s",Str);
          Insert(Str,i);
      }
      Build();
      //cout<<endl<<endl;
      //for(int i=0;i<26;i++) cout<<Trie[4][i]<<"   ";
     // cout<<endl<<endl;
      Solve();
      return 0;
  }
  
  /*
 96  *
 97 4
 98 HNOI
 99 NOIP
100 NOI
101 IOI
102 
103 HNOIPIOI
104  */

  

 
 posted on 2020-04-19 18:09  shirlybabyyy  阅读(348)  评论(0编辑  收藏  举报