• 二叉树的知识点、最小生成树


    首先是最最熟悉的算法笔记上面的知识点,PAT上面有些题还蛮基础。

    下面的知识点

    • 二叉树存储方式与操作
    • 二叉树的遍历:先序、中序、后序(这三个差不多)、层序。树的遍历
    • 二叉查找树BST
    • 平衡二叉树AVL,构建方法,左旋与右旋
      1 #include<iostream>
      2 #include<cstring>
      3 #include<cmath>
      4 #include<algorithm>
      5 #include<stack>
      6 #include<cstdio>
      7 #include<queue>
      8 #include<map>
      9 #include<vector>
     10 #include<set>
     11 using namespace std;
     12 const int maxn=1010;
     13 const int INF=0x3fffffff;
     14 //树复习
     15 //存储
     16 struct node{
     17     int data;
     18     node *lchild,*rchild;
     19 }; 
     20 //新建节点
     21 node *create(int data){
     22     node* temp=new node;
     23     temp->data=data;
     24     temp->lchild=temp->rchild=NULL;
     25     return temp;
     26 } 
     27 //修改
     28 //插入
     29 void insert(node* &root,int data){  //注意要有引用 
     30     if(root==NULL){
     31         create(data);
     32         return;
     33     }//查找失败地位置:插入位置
     34     if(性质) insert(root->lchild,data);
     35     else insert(root->rchild,data); 
     36 } 
     37 //完全二叉树地另外一种存储方式:数组,顺序即层序遍历
     38 //二叉树地遍历:先序、中序、后序,都是以DFS,写法一样
     39 //层序遍历:bfs
     40 void levelorder(node* root){
     41     queue<*node> q; //如果需要修改树地内容的话,就需要设置为*node类型的queue,这样才可以修改 
     42     q.push(root);
     43     while(!q.empty){
     44         node* top=q.front();
     45         q.pop();
     46         cout<<top->data<<" ";
     47         if(top->lchild!=NULL) q.push(top->lchild);
     48         if(top->rchild!=NULL) q.push(top->rchild); 
     49 }
     50 }
     51 //根据先序遍历序列、中序遍历重建二叉树
     52 node* cre(int prel,int prer,int inl,int inr){
     53     if(prel>prer) return NULL;
     54     node* root=new node;//记住这些,才是关键
     55     root->data=pre[prel]; //这个是应该存在地位置
     56     int k;
     57     for(k=inl,k<inr;k++) if(in[k]==pre[prel]) break;
     58     int num=k-inl;//不用再加减1
     59     root->lchild=cre(prel+1,prel+num,inl,k-1);
     60     root->rchild=cre(prel+num+1,prer,k+1,inr);
     61     return root; 
     62 }
     63 //根据后序序列和中序序列重建二叉树
     64 node* cre2(int postl,int postr,int inl,int inr){
     65     if(postl>postr) return NULL;
     66     node* root=new node;
     67     root->data=post[postr]; //应该存在的位置
     68     int k;
     69     for(k=inl;k<inr;k++) if(post[postr]==in[k]) break;
     70     int num=k-inl;
     71     root->lchild=cre2(postl,postl+num-1,inl,k-1); //注意是num-1 
     72     root->rchild=cre2(postl+num,postr-1,k+1,inr);
     73     return root;  
     74 } 
     75 //一般的树(节点不定为2)的结构
     76 //涉及数据域: 
     77 struct node2{
     78     int data;
     79     vector<int> child;
     80 };
     81 //不涉及数据域 
     82 vector<int> adj[maxn]; //图的邻接表在树中的运用 
     83 //从DFS、BFS的角度来看树的遍历 
     84 //对树的先根遍历:DFS  
     85 //对树的层序遍历:BFS 
     86 //剪枝的概念:从树的角度来看
     87 //题:1053
     88 
     89 //二叉查找树(BST)
     90 //查找:是由选择的选择左或者右子树的
     91 void find(node* root,int da){
     92     if(root==NULL){
     93         cout<<"failed"<<endl;
     94         return;//查找失败 
     95         //如果是插入,则再查找失败的地方插入,如果是不存在才插入的话,在查找到数据相同的地方的时候就应该return(已经存在) 
     96     }
     97     if(root->data==da) cout<<root->data;
     98     else if(root->data<da) find(root->lchild,da); //往左子树找
     99     else find(root->rchild,da); 
    100 } 
    101 //注意:如果二叉树的插入顺序不同的话,建出来的树也是不同的
    102 //BST的删除 
    103 //前驱:左子树中最右边的那个节点,后继:右子树最左边的那个节点
    104 //找前驱
    105 node* find1(node* root){
    106     while(root->rchild!=NULL) root=root->rchild;
    107     return root;  //调用的时候:find1(root->lchild) 
    108 } 
    109 node* fin2(node* root){
    110     while(root->lchild=NULL) root=root->lchild;
    111     return root;  //调用的时候:fin(root->rchild) 
    112 }
    113 //删除
    114 //考虑三种情况:(1)root为空,说明不存在,直接返回
    115 //(2)如果root的值等于给定的值,1)root无左右子树,说明是叶子节点,直接删除
    116                                 //2)root有左子树,找前驱pre删除
    117                                 //3)root无左子树,有右子树,找后继next
    118 //(3)如果root的值大于给定的值,往左子树找
    119 //(4)如果root的值小于给定的值,往右子树找 
    120 void deletenode(node* root,int x){
    121     if(root==NULL) return;
    122     if(root->data==x){
    123         if(root->lchild==NULL&&root->rchild==NULL) root=NULL; //!!!直接设置为NULL,父节点就引用不到了
    124         else if(root->lchild!=NULL){
    125             node* pre=find1(root->lchild); //往左子树找前驱
    126             root->data=pre->data;
    127             deletenode(root->lchild,pre->data); //需要往左子树递归删除 
    128         } 
    129         else if(root->rchild!=NULL){
    130             node* next=fin2(root->rchild);
    131             root->data=next->data;
    132             deletenode(root->rchild,next->data); //往右子树递归删除 
    133         }
    134     }
    135     else if(root->data<x){
    136         deletenode(root->rchild,x);
    137     }
    138     else deletenode(root->lchild,x);
    139 } 
    140 //题目:1043
    141 
    142 //AVL树,二叉平衡树,这个的构建方式最容易考,一定要记住*
    143 //需要记住的point一共有:结构中需要有Height,函数有:node* newnode(int v), int getheight(node* root)
    144 //int getbalance(node* root)  ,  void updataheight(node* root)
    145 //查找操作:简单
    146 //插入操作:  左旋、右旋函数
    147 // 判断:root的balacnce等于2,root->lchild的balance等于1 直接右旋   LL型 
    148                             // root->lchild的balance等于-1,先左旋(root->lchild),再右旋root   LR型 
    149     //   root的balance等于-2,root->rchild的balance等于-1,直接左旋  RR型 
    150                            //root->rchild的balance等于1 ,先右旋(root->rchild),再左旋root   RL型 
    151 struct node2{
    152     int data,height;
    153     node2 *lchild,*rchild;
    154 }; 
    155 node2* newnode2(int x){
    156     node2* temp=new node;
    157     temp->data=x;
    158     temp->height=1;
    159     temp->lchild=temp->rchild=NULL;
    160     return temp;
    161 }
    162 int getheight(node2* root){
    163     if(root==NULL) return 0;
    164     else return root->height;
    165 }
    166 //getheight函数需要最先写,因为后面两个都要用 
    167 int getbalance(node2* root){
    168     return getheight(root->lchild)-getheight(root->rchild);
    169 }
    170 void updataheight(node2 *root){
    171     //更新高度操作
    172     root->height=max(getheight(root->lchild),getheight(root->rchild))+1; //更新 
    173 }
    174 void l(node2* &root){  //左旋函数 (需要修改的都需要引用!!!!我又忘了)
    175     node2* temp=root->rchild;
    176     root->rchild=temp->lchild;
    177     temp->lchild=root;
    178     updataheight(root);   //要先更新 
    179     updataheight(temp);
    180     root=temp;           //再改变 
    181 }
    182 void r(node2* root){  //右旋函数 
    183     node2* temp=root->lchild;
    184     root->lchild=temp->rchild;
    185     temp->rchild=root;
    186     updataheight(temp);
    187     updataheight(root);
    188     root=temp;
    189 }
    190 //先解释以下:由于需要考虑平衡,所以在插入结点的时候,需要从下往上判断节点是否失衡,所以在每次插入后需要更新当前子树的height 
    191 void insert2(node2* temp,int x){
    192     //到达空节点:创建:
    193     if(root==NULL){
    194         root=newnode2(x);
    195         return;
    196     } 
    197     if(temp->data>x){//往左子树差 
    198     insert(temp->lchild,x);
    199     updataheight(temp);  //在这里是更新root!!!!!!!!注意位置是在插入后立即更新
    200     if(getbalance(temp)==2){
    201         if(getbalance(temp->lchild)==1) r(temp);   //LL型 右旋 
    202         else if(getbalance(temp->lchild==-1){   //LR型  先左旋再右旋 
    203             l(temp->lchild);
    204             r(temp);
    205         }
    206     }
    207     } 
    208     else {
    209         insert(temp->rchild,x); //往右子树插入
    210         updataheight(temp);
    211          if(getbalance(temp)==-2){
    212          if(getbalance(temp->rchild)==-1) l(temp); //RR型,左旋 
    213          else if(getbalance(temp->rchild)==1) {  //RL型,先右旋再左旋 
    214             r(temp->rchild);
    215             l(temp);
    216         }
    217     } 
    218     } 
    219 } 
    220 int main(){
    221     
    222     
    223 }

    一些例题

    1053 Path of Equal Weight

    int wei[101];
    int n,m,summ;
    //不要慌,首先建立数据结构
    struct node{
    	int data;
    	vector<int> child; //因为不是二叉树,可能有许多孩子节点 
    }no[101]; 
    int path[101]; //记录路径的数组 
    bool cmp(int a,int b){
    	return no[a].data>no[b].data; //非递增顺序:从大往小排序 
    }
    void find(int nowsum,int now,int nownodes){
    			//所需的三个参数:当前的和,当前访问的节点下表,当前访问的节点个数
    		if(nowsum>summ) return; 
    	if(nowsum==summ){
    		//第一种:如果不是叶子节点就返回
    		if(no[now].child.size()!=0) return ;    //适当的剪枝 
    		//第二种:是叶子节点就输出 
    		for(int i=0;i<nownodes;i++){
    			if(i!=nownodes-1) cout<<path[i]<<" ";
    			else cout<<path[i]<<endl;
    		} 
    		return;
    	}
    	else{
    		for(int i=0;i<no[now].child.size();i++){
    			int ch=no[now].child[i];
    			path[nownodes]=no[ch].data;//加入路径 
    			find(nowsum+no[ch].data,ch,nownodes+1);
    			 
    		}
    	}
    }
    
    
    int main(){
    	cin>>n>>m>>summ;
    	for(int i=0;i<n;i++){
    		cin>>no[i].data;
    	}
    	int id1,k,id2;
    	for(int i=0;i<m;i++){
    		cin>>id1>>k;
    		for(int j=0;j<k;j++) {
    			cin>>id2;
    			no[id1].child.push_back(id2);//在孩子节点中进行push_back; 
    		}
    		//然后就对孩子节点进行排序
    		sort(no[id1].child.begin(),no[id1].child.end(),cmp);
    	}
    	path[0]=no[0].data;//路径第一个节点 
    	find(no[0].data,0,1); 
    return 0;
    }
    

      1043 Is It a Binary Search Tree  镜像树的概念,反转一下左右子树OK

    #include<iostream>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    #include<stack>
    #include<cstdio>
    #include<queue>
    #include<map>
    #include<vector>
    using namespace std;
    int n;
    vector<int> ori,pre,mirpre,pos,mirpos;
    struct node{
    	int data;
    	node* lchild;
    	node* rchild;
    };
    //先依据原有的序列建树
    //然后进行先序遍历和镜像树先序遍历,进行比较
    //首先要进行后序遍历和镜像树的后序遍历,如果匹配成功的话,就只直接输出
    void insert(node* &root,int data){
    	//插入树
    	if(root==NULL){
    		root=new node;
    		root->data=data;
    		root->lchild=NULL;
    		root->rchild=NULL;
    		return;
    	} 
    	if(data<root->data) insert(root->lchild,data);
    	else insert(root->rchild,data);
    } 
    //原树先序遍历
    void preor(node* root,vector<int> &v){
    	if(root==NULL) return;
    	v.push_back(root->data);
    	preor(root->lchild,v);
    	preor(root->rchild,v);
    } 
    //镜像术先序遍历
    void mirpreor(node *root,vector<int> &v){
    	if(root==NULL) return;
    	v.push_back(root->data);
    	mirpreor(root->rchild,v);
    	mirpreor(root->lchild,v);
    } 
    //原树后序遍历
    void postor(node *root,vector<int> &v){
    	if(root==NULL) return;
    	postor(root->lchild,v);
    	postor(root->rchild,v);
    	v.push_back(root->data);
    } 
    //镜像术后序遍历 
    void mirpostor(node *root,vector<int> &v){
    	if(root==NULL) return;
    	mirpostor(root->rchild,v);
    	mirpostor(root->lchild,v);
    	v.push_back(root->data);
    } 
    
    int main(){
    	cin>>n;
    	int o;
    	node *root=NULL;
    
    	for(int i=0;i<n;i++){
    		cin>>o;
    		ori.push_back(o);
    		insert(root,o);
    	}
    	//输入原始的
    	preor(root,pre);
    	mirpreor(root,mirpre);
    	postor(root,pos);
    	mirpostor(root,mirpos);
    	if(ori==pre){
    		cout<<"YES"<<endl;
    		for(int i=0;i<pos.size();i++){
    			if(i!=pos.size()-1) cout<<pos[i]<<" ";
    			else cout<<pos[i]<<endl;
    		}
    	}
    	else if(ori==mirpre){
    		cout<<"YES"<<endl;
    		for(int i=0;i<mirpos.size();i++){
    			if(i!=mirpos.size()-1) cout<<mirpos[i]<<" ";
    			else cout<<mirpos[i]<<endl;
    		}
    	}else{
    		cout<<"NO"<<endl;
    	} 
    	
    	
    return 0;
    }
    
    • 哈夫曼树---哈夫曼编码

    经典的合并果子问题,WPL:all(叶子节点的权值乘以其路径长度的结果称为这个叶子节点的带权路径长度),带权路径长度最小的树叫做哈夫曼树,哈夫曼树可以不唯一,但WPL一定是唯一的。(用了堆和并查集的思想,合并--权值最小)

    哈夫曼树不存在度为1的节点

    权值越高的点越接近根节点(编码长度最短)

    一般不用真的去构建一棵哈夫曼树,只需要知道WPL的话,那么就可以用优先队列实现了,注意是小顶堆

    //哈夫曼树---不用构建 
    priority_queue<int,vector<int>,greater<int>> q;
    void haf(){
    	int n,x,y,ans=0;
    	cin>>n;
    	for(int i=0;i<n;i++){
    		cin>>x;
    		q.push(x);
    	}
    	while(q.size()>1){
    		x=q.top();q.pop();
    		y=q.top();q.pop();
    		q.push(x+y);
    		ans+=x+y;
    	}
    	cout<<ans<<endl;
    }
    

    哈夫曼编码:前缀编码:任何一个字符的编码都不是另一个字符编码的前缀,满足这种编码方式的就叫做前缀编码

    how:把字符出现的次数作为叶子节点的权值,构建哈夫曼树,构建后的WPL就是字符编码成01串后长度。

    哈夫曼编码是能使给定字符串编码成01串后长度最短的前缀编码,向左为0,向右为1(ps.哈夫曼编码是针对确定的字符串来讲的)

    下面是代码(...)

    #include<iostream>
    #include<cstring>
    #include<cmath>
    #include<algorithm>
    #include<stack>
    #include<cstdio>
    #include<queue>
    #include<map>
    #include<vector>
    using namespace std;
    //构建哈夫曼树,并写出哈夫曼编码
    //我太难了。。。
    class Node{
    	public: //这个别忘了 
    	char c;//字符
    	int frequency;//权重:该字符出现的次数或者频率
    	Node *lchild;
    	Node *rchild;
    	Node(char _c,int f,Node *l = NULL,Node *r= NULL)
    	:c(_c),frequency(f),lchild(l),rchild(r) {} //构造函数 
    	bool operator <(const Node &node) const{
    	return frequency>node.frequency; //重载<运算法,使在加入优先队列的时候决定好顺序 
    	}
    };
    //首先建立优先队列 
    void initNode(priority_queue<Node> &q,int Nodenum){
    	char c;
    	int frequency;
    	for(int i=0;i<Nodenum;i++){
    		cin>>c>>frequency;
    		Node node(c,frequency); //左右子树默认为NULL 
    		q.push(node);
    	}
    }
    //输入完了可以展示一下?
    void showNode(priority_queue<Node> q){
    	while(!q.empty()){
    		Node now=q.top();
    		q.pop();
    		cout<<now.c<<" "<<now.frequency<<endl;
    	}
    } 
    //构造哈夫曼树
    void huffmanTree(priority_queue<Node> &q){
    	while(q.size()!=1){
    		Node *left=new Node(q.top());q.pop(); //分别取两个最小的构造树,然后加入
    		Node *right=new Node(q.top());q.pop();
    		Node node('R',left->frequency+right->frequency,left,right);
    		q.push(node); 
    	}
    } 
    //打印哈夫曼编码
    void huffmanCode(Node *root,string &prefix,map<char,string> &result){
    	string m_prefix=prefix; //用到了回溯(因为左右子树)
    	if(root->lchild==NULL) return;
    	prefix+="0"; //左子树为0
    	//如果是叶子节点则输出,否则递归打印
    	if(root->lchild->lchild==NULL) result[root->lchild->c]=prefix;
    	else huffmanCode(root->lchild,prefix,result);
    	//还原原来的路径:回溯,开始处理右子树
    	prefix=m_prefix;
    	prefix+="1"; //右子树为1
    	if(root->rchild->rchild) result[root->rchild->c]=prefix;
    	else huffmanCode(root->rchild,prefix,result); 
    	 
    } 
    void testResult(map<char,string> result){
    	//迭代输出
    	//用迭代器
    	map<char,string>::const_iterator it=result.begin();
    	while(it!=result.end()){
    		cout<<it->first<<" "<<it->second<<endl;
    		it++;
    	} 
    }
    int main(){
    	priority_queue<Node> q;
    	int nodenum;
    	cin>>nodenum;
    	//初始化建堆啦!
    	initNode(q,nodenum);	
    	//建树了
    	huffmanTree(q);
    	//打印哈夫曼编码了 
    	Node root=q.top();
    	string prefix="";
    	map<char,string> result;
    	huffmanCode(&root,prefix,result);
    	testResult(result);
    	
    	
    	
    	
    return 0;
    }
    • 最小生成树(其实这个应该放在图里面)

    最小生成树:在一个给定的无向图中求一棵树,使得这棵树包含所有的顶点,而且整棵树的边权和最小

    性质:1、边数=顶点数-1       

         2、最小生成树可以不唯一,但是边权之和一定是唯一的

         3、根节点可以为任意一个节点,通常会给定

    两种算法:prim和kruskal算法,都使用了贪心的思想,但是思路不一样

    一、prim算法

    prim算法: dijkstra算法类似(dis[i]的含义不同,dijkstra中是起点,prim中是已经访问过的所有点)  稠密图时使用

    对图G(V,E)设置集合S,存放已经被访问过的点,每次从集合V-S中选择与集合S最短距离的一个点u,访问并加入S,之后,令u为中介点,优化所有能从u到达的顶点v与集合S的最短距离,执行n次后,集合S会包含所有的点。复杂度为O(V^2),适合顶点数少、边数多的情况。

    struct node{
    	int v,dis;
    	node(int _v,int _dis) : v(_v),dis(_dis){}
    };
    vector<node> adj[maxn];
    int dis[maxn],vis[maxn];
    int n,m,st,ed;
    int ans=0;
    int prim(int st){
    	//为什么返回的是int呢?因为当前的边等于点数-1的话就算连接好了,如果计算到最后边数!=点数-1,就不能连接为树,即不连通
    	//如果联通且计算完成,当然返回总权值啊1!!! 
    
    	//int now_edge=0;
      //写错了!!!now_edge是在kruskal算法里面的 
    	
    	//树的总权值,以及当前所有的连接好了的边
    
    	 for(int i=0;i<n;i++){
    	 	int u=-1,mini=INF; //与dijkstra是不是超级像!!!!!
    		 for(int j=0;j<n;j++){
    		 	if(mini>dis[j]&&vis[j]==0) {
    		 		mini=dis[j];
    				 u=j;
    			 }
    		 } 
    		 if(u==-1) return;
    		 vis[u]=1;
    		 ans+=dis[u]; //!!!!!啊啊啊记住这个 
    		 for(int j=0;j<adj[u].size();j++){
    		 	int id=adj[u][j].v;
    		 	int diss=adj[u][j].dis;
    		 	if(!vis[id]&&dis[id]>diss){
    		 		dis[id]=diss;//!!!! 
    			 }
    		 }
    	 }
    	 
    }
    

      二、kruskal算法。

    边贪心的思想。并查集思想,每次都找到最小的边,判断这两个边连接的点是不是在同一个集合中,如果不是就连接起来   稀疏图时使用 

    注意这个数据结构的写法

    step 1.对所有的边按照边权排序

    step 2.按照边权从小到大测试所有的边,如果当前边连接的点是不是在同一个集合中,就连接起来

    step 3.当边数=顶点数-1或者是测试完所有的边后边数<顶点数-1,就可以返回ans或者-1了

    //kruskal
    struct edge{
    	int from,to;
    	int dis;
    }E[maxn]; 
    
    int fa[maxn];
    int findfather(int x){
    	if(x!=fa[x]) return findfather(fa[x]);
    	return fa[x];
    }
    bool cmp(edge a,edge b){
    	return a.dis<b.dis;
    }
    void kruskal(int n,int m){
    	//n是顶点数,m是边数
    	for(int i=0;i<n;i++) fa[i]=i;  //先弄这个
    	fill(dis,dis+maxn,INF);
    	memset(vis,0,sizeof(vis));
    	dis[0]=0;
    	int ans=0,numedge=0; //这里才有!!总的权值和现在有了的边
    	 sort(E,E+m,cmp); //对边进行排序
    	 for(int i=0;i<m;i++){
    	 	int fa1=findfather(E[i].from);
    	 	int fa2=findfather(E[i].to);
    	 	if(fa1!=fa2){
    	 		fa[fa2]=fa1;
    	 		numedge++;
    	 		ans+=E[i].dis;
    	 		if(numedge==n-1) break; //!!!!!!!如果边数已经够了的话就可以break了 
    		 }
    	 }
    	 if(numedge!=n-1) {
    	 	cout<<"error no liantong"<<endl;
    	 	return;
    	 } 
    	  else{
    	  	cout<<ans<<endl;
    	  	return;
    	  }
    }
    
    
    for(int i=0;i<m;i++){
    		cin>>E[i].from>>E[i].to>>E[i].dis;
    	}
    	kruskal(n,m);
    

    给定一棵树的前序遍历序列和后序遍历序列,求有多少种可能的二叉树,可以取余
    就是求有多少种可能的中序序列
    总是考虑前序序列的第一个元素x,如果在后序序列是最后一个,那么可以去掉,如果为空,则over,不然余下的前序序列的第一个数y是x的左/右子树的根,
    在后序序列中找到y的位置,如果是最后一个,那么x只有一个子树,但是左/右子树都有可能,所以乘以2
    继续递归计算
    否则可以确定y是x的左子树,同时也知道了左右子树具体所在的区间,于是分成两部分继续递归计算,然后将答案相乘就可以了

    int a[maxn],b[maxn]; //前、后 
    int posa[maxn],posb[maxn];  //所在的位置
    int solve(int al,int ar,int bl,int br){
    	if(al==ar) return 1; //只有一个字符 
    	int y=a[al+1];
    	if(posb[y]==br-1) return (long long)(solve(al+1,ar,bl,bl-1)*2%MOD); //无法确定是左子树还是右子树
    	else return (long long)(solve(al+1,al-posb[y]-bl+1,bl,posb[y])*solve(al+posb[y]-bl+2,ar,posb[y]+1,br-1)%MOD); //不然就是左右子树相乘 
    } 
    for(int i=1;i<=n;i++) posa[a[i]]=i; 
    

      

  • 相关阅读:
    Flask 开启跨域
    pandas to dict
    mongodb 聚合查询
    flask 获取请求参数
    CSS dppx详解
    用CSS做出漂亮的字体动画
    VMware虚拟主机安装完成后连接不上网络
    wokerman中自定义协议的使用和测试
    使用workerman写一个小的聊天室
    telnet不能使用怎么办?
  • 原文地址:https://www.cnblogs.com/shirlybaby/p/12273898.html
Copyright © 2020-2023  润新知