Study Notes - Figures

table of Contents

 

FIG implemented by adjacency matrix

FIG implemented using adjacency list

BFS

Depth-first search

Shortest Path

Dijkstra's algorithm (single-source shortest path)

Floyd algorithm (shortest path between any two points)

Minimum spanning tree

Prim calculation complexity 0 (n ^ 2)

Kruskal's algorithm time complexity O (eloge)


FIG implemented by adjacency matrix

FIG implemented adjacency matrix structure is defined as

typedef struct graph *Graph;    //Graph是一个类型,为graph型指针
struct graph
{
    WItem NoEdge;//无边标记
    int n;        //顶点数
    int e;        //边数
    WItem **a;    //邻接矩阵,a是一个二级指针
}AWDgraph;

Function GraphInit (n, noEdge) to create an isolated vertices of the n

Graph GraphInit(int n,WItem noEdge) //返回指向一个图的指针
{
    Graph G=malloc(sizeof*G);    //G是变量名,sizeof要加*
    G->n=n;
    G->e=0;
    G->a=Make2DArray(G->n+1,G->n+1,noEdge);    //创建一个权值全为noEdge的矩阵
    //不赋权图将上一句的NoEdge改为0
    return G;
}

Function GraphVerticles (G) and GraphEdges (G), respectively return the number of vertices and edges of a graph G

int GraphVerticles(graph G)
{
    return G->n;
}

int GraphEdges(graph G)
{
    return G->e;
}

Function GraphExits (i, j, G) is determined in the current edge graph G (i, j) the presence or absence

int GraphExits(int i,int j,graph G)
{
    if(i<1||j<1||i>G->n||j>G->n||G->a[i][j]==G->NoEdge) return 0;
    //输入不在合法范围内或该边值为NoEdge
    //不赋权图把if条件中的NoEdge改为0
    return 1;
}

Function GraphAdd (i, j, w, G) join edge in G (i, j)

void GraphAdd(int i,int j,WItem w,Graph G)
{
    if(i<1||j<1||i>G->n||j>G->n||i==j||G->a[i][j]!=G->NoEdge)
    //若输入不在合法范围或在对角线上或该边已经存在,则报错
    //不赋权图把if条件中的NoEdge改为0
        Error("Bad input");
    G->a[i][j]=w;
  //G->a[j][i]=w;    无向图加一句这个
  //不赋权图只需要将上述语句的w改成1
    G->e++;    //更新边数
}

Function GraphDelete (i, j, G) deletes the edge (i, j) in G

void GraphDelete(int i,int j,graph G)
{
    if(i<1||j<1||i>G->n||j>G->n||G->a[i][j]==G->NoEdge)
    //输入不在合法范围内或待删边本不存在
    //不赋权图把if条件中的NoEdge改为0
        Error("Bad input");
    G->a[i][j]=NoEdge;
  //G->a[j][i]=NoEdge;    无向图加一句这个
  //不赋权图只需要将上述语句的w改成0
    G->e--;    //更新边数
}

Function InDegree (i, G) returns the degree of vertices of graph G

int InDegree(int i,graph G)
{
    int sum=0;
    /*可单独作为求入度的方法*/
    for(int j=0;j<G->n;j++)
    {
        if(G->a[i][j]!=NoEdge)   //无向图改NoEdge为0
            sum++;
    }
    /*如果是无向图加上以下代码*/
    /*也可单独作为求出度的方法*/
    for(int j=0;j<G->n;j++)
    {
        if(G->a[j][i]!=NoEdge)    //无向图改NoEdge为0
            sum++;
    }
    /**/
    return sum;
}

FIG implemented using adjacency list

Node adjacency list structure definition

typedef struct lnode *glink;    //glink是一个类型,为lnode型指针
struct lnode
{
    int v;    //边的另一个顶点
    glink next;    //邻接表指针
  //若是赋权图,加上w属性
  //WItem w;
};

Create a new node adjacency list

glink NewLNode(int v,glink next)    //返回指向lnode的指针
{
    glink x=malloc(sizeof*x);    //x是变量名,sizeof取变量名长度要加*
    x->v=v;
    x->next=next;
  //若是赋权图
  //x->w=w;
    return x;
}

FIG implemented using the structure defined adjacent table

typedef struct graph *Graph;
struct graph
{
    int n;    //顶点数
    int e;    //边数
    glink *adj;    //glink型的一维数组,数组中的成员都是指针
}Lgraph;

Function GraphInit (n) to create a table represented by adjacency with n vertices FIG isolated

Graph GraphInit(int n)
{
    Graph G=malloc(sizeof*G)     //G是变量名,sizeof前加*
    G->n=n;
    G->e=0;
    G->adj=malloc(n+1)*sizeof(glink))    //adj是一个指针数组,成员都是glink指针,v此时是0
    for(int i=0;i<=n)
        G->adj[i]=0;
    return G;
}

Function GraphVerticles (G) and functions GraphEdge (G) return the number of vertices and edges with the wording of FIG adjacency matrix slightly.

Function GraphExist (i, j, G) determining whether there is currently an edge in the graph G (i, j)

int GraphExist(int i,int j,graph G)

{
    if(i<1||j<1||i>G->n||j>G->n) return 0;
    glink p=G->adj[i];
    while(p&&p->v!=j) p=p->next;    //不走到最后且不为目标,则继续向前
    if(p) return 1;
    else return 0;
}

Function GraphAdd (i, j, G) was added to achieve the figure an edge (i, j) is operated by first inserting a vertex j of the adjacent vertex i exemplar

void GraphAdd(int i,int j,Graph G)    //赋权图加上WItem w
{
    if(i<1||j<1||i>G->n||j>G->n||i==j||GraphExits(i,j,G))
        Error("Bad input");
    G->adj[i]=NewLNode(j,G->adj[i]);    //直接取代G->a[i]
  //无向图加上下一句
  //G->adj[j]=NewLNode(i,G->adj[j]);
  //赋权图加上w
  //G->adj[i]=NewLNode(j,w,G->adj[i]);
    G->e++;
}

Function GraphDelete (i, j, G) to delete the edge graph G (i, j)

void GraphDelete(int i,int j,Graph G)
{
    glink p,q;
    if(i<1||j<1||i>G->n||j>G->n||!GraphExists(i,j,G))
    {
        Error("Bad input");
    }
    p=G->adj[i];
    if(p->v==j)    //如果第一个节点就是待删除节点,直接令其成为邻接表数组中的成员
    {
        G->adj[i]=p->next;
        free(p);
    }
    else
    {
        while(p&&p->v!=j) p=p->next;    //若未走到最后且未找到目标则继续向前
        if(p)    //若找到目标
        {
            q=p->next;
            p->next=q->next;
            free(q);
        }
    }
  //有向图加上下面的代码
  /*
    p=G->adj[j];
    if(p->v==i)    //如果第一个节点就是待删除节点,直接令其成为邻接表数组中的成员
    {
        G->adj[j]=p->next;
        free(p);
    }
    else
    {
        while(p&&p->v!=i) p=p->next;    //若未走到最后且未找到目标则继续向前
        if(p)    //若找到目标
        {
            q=p->next;
            p->next=q->next;
            free(q);
        }
    }
    */

    G->e--;
}

Function OutDegree (i, G) calculated by the adjacency list of vertex i long, there is a return to the vertex of graph G

int OutDegree(int i,Graph G)
{
    glink p;
    int sum=0;
    if(i<1||i>G->n) Error("Bad input");
    p=G->adj[i];
    while(p)
    {
        sum++;
        p=p->next;
    }
    return sum;
}

Function InDegree (i, G) returns to have the degree of vertex i of the graph G

int InDegree(int i,Graph G)
{
    int sum=0;
    for(int j=0;j<G->n;j++)
    {
        if(GraphExists(i,j,G))
            sum++;
    }
    return sum;
}

BFS

//用邻接矩阵实现的无向图G中的广度优先搜索算法bfs描述
bfs(G,i)
{
    /*从顶点v开始,广度优先搜索图G的算法*/
    标记顶点i;
    用顶点i初始化顶点队列Q;
    while(!QueueEmpty(Q))
    {
        i=DeleteQueue(Q);
        设j是i的邻接顶点;
        while(j)
        {
            if(j未标记)
            {
                标记顶点j;
                EnterQueue(j,Q);
            }
            j=i的下一个邻接顶点
        }
    }
}

===================================================================
//具体实现时,用一个数组pre来记录搜索到的顶点的状态。初始时对所有顶点v有pre[v]=0。
//用一全局变量cnt记录算法对图中顶点的访问次序。算法结束后,数组pre[i]中的值是算法访问顶点i的序号。


void bfs(Graph G,int i)
{
    Queue Q=QueueInit();
    EnterQueue(i,Q);    //顶点先入队
    while(!QueueEmpty(Q))
    {
        if(pre[i=DeleteQueue(Q)]==0)    //未访问过
        {
            pre[i]=cnt++;    //访问该结点并标记
            for(int j=1;j<=G->n;j++)
            {
                if(G->adj[i][j]&&pre[j]==0)    //若边存在且未访问过,入队
                    EnterQueue(j,Q);
            }
        }
    }
}

//图中可能不止一个连通分量,遍历全图算法如下

void GraphSearch(Graph G)
{
    cnt=1;
    for(int i=1;i<=G->n;i++) pre[i]=0;
    for(int i=1;i<=G->n;i++)
    {
        if(pre[i]==0)bfs(G,i);    //对每个连通分量调用一次bfs
    }
}
//天勤上面用邻接表表示图的bfs写法
int visit[MAXSIZE]=0;    //访问状态数组
void bfs(Graph *G,int v)
{
    ArcNode *p;
    visit[v]=1;
    //Visit(v);
    Queue<int> q;
    q.push(v);
    while(!IsEmpty(q))
    {
        int t=q.top();
        q.pop();
        p=G->adj[t].firstarc;    //p指向当前结点第一条边
        while(p)
        {
            if(visit[p->adjvex]==0)
            {
                visit[p->adjvex]=1;
                //Visit(p->adjvex);
                q.push(p->adjvex);    //若当前邻接顶点未被访问过,则置为已访问,并压入队列
            }
            p=p->nextarc;    //否则,p指向当前结点的下一条邻边
        }
    }
}
void BFS(Graph *G)
{
    for(int i=0;i<G->n;i++)
        if(visit[i]==0)
            bfs(G,i);
}

 

Depth-first search

//用邻接矩阵实现的无向图G中的深度优先搜索算法dfs如下
void dfs(Graph G,int i)
{
    pre[i]=cnt++;    //初始化时所有结点pre[i]=0,当pre[i]不为0使表示该结点已访问过,pre[i]的值为算法访问结点i的序号
    for(int j=1;j<=G->n;j++)    //顺序遍历与结点i相邻的结点,
    {
        if(G->[i][j])    //若边<i,j>存在
        {
            if(pre[j]==0)dfs(G,j);    //若结点j还未被访问过,对j递归调用dfs,若访问过,j++探测下一个相邻结点
        }
    }
}

//用邻接表实现的有向图G中的深度优先搜索算法dfs如下
void dfs(Graph G,int i)
{
    glink p;
    pre[i]=cnt++;
    for(p=G->adj[i];p;p=p->next)
    {
        if(pre[i]==0)dfs(G,p->v);    //若p指向的结点已访问过,结束当前层次的递归,返回上一层递归,执行p=p->next探测下一个相邻结点
    }   
}

void GraphSearch(Graph G)
{
    cnt=1;
    for(int i=1;i<=G->n;i++) pre[i]=0;
    for(int i=1;i<=G->n;i++) if(pre[i]==0) dfs(G,i);
}
//天勤上面用邻接表表示图的dfs写法
int visit[MAXSIZE]=0;    //全局变量用来保存访问状态
int dfs(Graph *G,int v)
{
    ArcNode *p;
    visit[v]=1;    //置当前结点为已访问
    Visit(v);
    p=G->adj[v].firstarc;    //p指向顶点v的第一条边
    while(p)
    {
        if(visit[p->adjvex])==0    //若当前结点未被访问过,则往深处继续访问
        {
            dfs(G,p->adjvex)
        }
        p=p->nextarc;    //否则检测下一个与v邻接的结点
    }    //执行到这一步时,所有与v邻接的结点已经检查完毕,该层dfs也执行到了最后一句,自动返回上一层递归
}
void DFS(Graph *G)
{
    for(int i=0;i<G->n;i++)
        if(visit[i]==0)
            dfs(G,i);
}

 

//天勤上判断图G是否是树的算法,基于dfs
int vc=0,ec=0;    //定义全局变量vc和ec,就不用在函数里面传了
int visit[MAXSIZE]=0;
void dfs(Graph *G,int v)
{
    ArcNode *p;
    visit[v]=1;    //置当前结点为已访问
    vc++;    //结点数自增1
    p=G->adj[v].firstarc;
    while(p)
    {
        if(visit[p->adjvex]==0)    //若当前顶点的邻接点未被访问过,则访问它,并对ec自增1
        {
            ec++;
            dfs(G,p->adjvex);
        }    
        p=p->next;
    }
}

int IsTree(Graph *G)
{
    dfs(G,1);
    if(vc==G->n&&(ec/2)==G->e)    //若vc等于图中顶点数,ec等于图中边数的2倍
        return 1;
    return 0;
}
//由于每次访问顶点ec都会累加上之前所有与访问过的结点关联的边数,因此遍历完之后,相当于ec中的数值是图的度数,(一条边2个度),因此ec/2就是访问过的边数。

Shortest Path

Dijkstra's algorithm (single-source shortest path)

Algorithm idea:

Set up a set of vertices S, and continue to expand this collection as greedy. A vertex belonging to the set S if and only if the source has the shortest path length from the vertex. Initial, S contains only source. Let u G is a vertex of the path through the vertex S of the source to be a special path from the source to only u and u intermediate, and treated with special array dist to record the shortest path length corresponding to each of the current vertex. Dijkstra algorithm for each vertex u is removed from the VS has the shortest special path length, u is added to the set S, while the array dist make the necessary changes. Once V S contains all vertices, dist on the record of the shortest path length between the other from the source vertex to all.

Algorithm Description:

Step 1: Initialization dist [v] = a [s] [v]

             For all vertices v adjacent to opposite s pre [v] = s;

             For other vertex u set pre [u] = 0;

             Table L contains all established pre [v] ≠ vertex v 0 is the initial time @ i.e., L s is the source directly adjacent vertices

Step 2: If empty list L, then the algorithm ends, otherwise, go to Step 3

Step 3: Remove the minimum value of dist i.e. the source vertex v // s latest from the vertex list L

Step 4: For all vertices adjacent to vertex v u opposing dist [u] = min {dist [u], dist [v] + a [v] [u]}

             If dist [u] change, i.e. after the addition of special vertex v path is shorter, the set pre [u] = v; and if not in the list L u, L u is added; go to step 2

//在用邻接矩阵实现的赋权有向图中,单源最短路径问题的Dijkstra算法实现如下
void Dijkstra(int s,WItem dist[],int prev[],Graph G)
{
    int i,j;
    List L=ListInit();    //初始化L表
    if(s<1||s>G->n) Error("Out of bounds")    //处理非法输入
    /*初始化dist,prev和L*/
    for(int i=1;i<=G->n;i++)
    {
        dist[i]=a[s][i];    //顶点i到源点s的初始距离为a[s][i]的值
        if(dist[i]==G->NoEdge) prev[i]=0;    //若顶点i与源点s之间不存在边,则置i的前驱结点为0
        else{prev[i]=s;ListInsert(0,i,L);}    //否则,置i的前驱结点为s,并将顶点i加入L表,参数啥意思?
    }
    dist[s]=0;    //源点s的前驱为0
    /*修改dist和prev*/
    while(!ListEmpty(L))
    {
        /*找L中具有最小dist值得顶点v*/
        /*将顶点v从表L中删除,并修改dist的值*/
        i=ListDelMin(L,dist);    //顶点i是L表中所有已知特殊路径长度的结点中路径最短的
        for(int j=1;j<=G->n;j++)    //遍历所有与顶点i相邻的顶点
        {
            if(G->a[i][j]!=G->NoEdge&&(!prev[j]||dist[j]>dist[i]+G->a[i][j]))    //若边(i,j)存在,且加入结点i之后,结点j到源点s的距离缩短
            {
                /*dist减少*/
                dist[j]=dist[i]+G->a[i][j];    //更新结点j到源点s的特殊路径距离
                /*顶点j插入表L*/
                if(!prev[j]ListInsert(0,j,L))    //结点j的特殊路径长度已知,加入表L
                prev[j]=i;    //更新结点j的前驱
            }
        }
    }
}
//天勤上的写法,我觉得更好理解一些
void Dijkstra(Graph G,int v,int dist[],int path[])    //dist是v到某点vu的距离,path是vu到v最短路径上前驱结点的下标,dist是一维数组,因为将它看作二维数组时,有一维是不变的,下标都是v
{
    int sex[MAXSIZE];    //set记录顶点是否被加入路径,取值为1则为加入,0为未加入

    /*以下是对各数组进行初始化*/
    for(int i=0;i<G.n;i++)
    {
        dist[i]=G.edges[v][i];
        path[i]=v;
        set[i]=0;
    }
    set[v]=1;
    /*初始化完毕*/

    /*以下是找到剩余结点中与已生成的路径距离最短的结点*/
    int min=INF;
    for(int i=0;i<G.n;i++)
    {
        if(set[i]==0&&dist[i]<min)
        {
            u=i;
            min=G.dist[i];
        }
    }
    set[u]=1;
    /*找到该结点后,加入路径中*/

    /*加入顶点u到路径中之后,更新dist和path*/
    for(int i=0;i<G.n;i++)
    {
        if(set[i]==0&&dist[u]+G.edges[u][i]<dist[i])
        {
            dist[i]=dist[u]+G.edges[u][i];
            path[i]=u;    //u为i在路径上的前驱
        }
    }
}

 

Floyd algorithm (shortest path between any two points)

Algorithm idea:

Arranged in a matrix c, initially c [i] [j] = a [i] [j]. Then the c-matrix do n iterations, after the first iteration k, c [i] [J] is a value from vertex i to vertex j, and does not pass through the intermediate path length is greater than the number k of vertices. When doing the k-th iteration in c, c [i] [j] = min {c [i] [k] + c [k] [j], c [i] [j]}, to calculate c [i ] [j] may be compared with the current c [i] [j] and c [i] [k] + c [k] [j] size. Current c [i] [j] value indicates from vertex i to j, the intermediate vertex numbers is not larger than k-1 shortest path length; and c [i] [k] + c [k] [j] denotes the vertex i to k, then from k to j, and does not pass through the intermediate path length is greater than the number of the vertices k.

Two-dimensional array path is used to record the shortest path. When k is the manipulation c [i] [j] to obtain an integer of a minimum value, it is set to p [i] [j] = k. When the path [i] [j] = 0, it represents the shortest path from node i to j is the edge from i to j. After calculating the c [i] [j] value, easily recorded by the path information to find the corresponding shortest path.

void Floyd(WItem **c,int **path,Graph G)
{
    /*初始化c[i][j]*/
    for(int i=1;i<=G->n;i++)
        for(int j=1;j<=G->n;j++)
        {
            if(i==j)
                c[i][j]=0;
            else
            { 
                c[i][j]=G->a[i][j];
                path[i][j]=0;    //path=0表示当前顶点i到顶点j的最短路径就是它们之前的边
            }
        }
    for(int k=1;k<=G->n;k++)
        for(int i=1;i<=G->n;i++)
            for(int j=1;j<G->n;j++)
            {
                if(c[i][k]!=NoEdge&&c[k][j]!=NoEdge&&(c[i][j]==NoEdge||c[i][j]>c[i][k]+c[k][j]))    
                //确保加入的顶点k与顶点i,顶点k与顶点j之前是可达的,当顶点i到顶点j无边或小于顶点i到k加上顶点k到j的距离时,更新顶点i到j的路径长度
                {
                    c[i][j]=c[i][k]+c[k][j];
                    p[i][j]=k;    //加入的点k是使c[i][j]取最小值的数
                }
            }
}

Minimum spanning tree

Prim calculation complexity 0 (n ^ 2)

Algorithm idea:

First set S = {1}, and then, as long as S is a subset of V (that is, not yet contain all the nodes), to be greedy selected as follows: Select the set S point i, VS in the point j, and a [I] [j] the smallest edge and vertex added to the set S j. This process type to the feed S = V up. (Not required to judge whether a loop, because the two sides of the vertices from different sets)

How to find out the condition of i and j, we need to set two arrays closet and lowcost. For vertices j of a VS, Closest [j] is adjacent to a vertex S j in which j and k other adjacent vertices in S are compared with a [j] [closest [j]] <= a [j] [k]. lowcost [j] is the value of a [j] [closest [j]]

Prim during the execution of the algorithm, to find the minimum VS manipulation lowcost vertex j, and select from the array side closest (j, closest [j]), and finally to add the S, j, and closest to the necessary and lowcost modify.

void Prim(WItem *lowcost,int *closest,Graph G)
{
    /*初始化*/
    int *s;    //集合S,值为1表示在集合中,为0表示不在集合中
    s=malloc((G->n+1)*sizeof(int));
    for(int i=1;i<=G->n;i++)
    {
        lowcost[i]=a[1][i];    //先设置顶点1在S中
        closest[i]=1;
        s[i]=0;    //除1以外所有点都不在集合S中
    }
    s[1]=1;    //把顶点1加入集合S
    
    for(int i=1;i<=G->n;i++)    //执行n次
    {
        min=G->NoEdge;    //要找出最小值,先把min设为最大
        j=1;    //j用来记录到集合S距离最短的顶点序号,一开始先设为1作为初始值
        for(int k=2;k<=G->n;k++)    //k=2是因为一开始只有1在S中,V-S要从2开始
        {
            if(lowcost[k]<min)&&(!s[k])    //k到集合S最短距离小于min且k不在集合S中,即k在V-S中
            
            {
                min=lowcost[k];    //更新最小值,j为当前V-S中里S最近的顶点,现在令j为k
                j=k;
            }
        }
        //此时j是到集合S距离最短的顶点序号
        s[j]=1;    //把j加入集合S
        for(int k=2;k<=G->n;k++)    //j加入集合S后,要修改原来的closest和lowcost
        {
            if((G->a[k][j]<lowcost[k])&&(!s[k]))    //若K不在集合S且顶点j和k之间距离小于j加入集合S前顶点k到集合S的最短距离
            {
                //更新closest,lowcost
                closest[k]=j;
                lowcost[k]=G->a[k][j]
;            }
        }
    }
}

//V-S中的顶点为焦点向S中的顶点逐个连线找最短,j是通过k来更新的
//利用closest还能找到最短路径
//while(k>0)
//p=closest[k];
//k=p;

Kruskal's algorithm time complexity O (eloge)

First, the n vertices of G n as an isolated branch communication. All sides by weight from small to large, from the first edges, each side view in ascending order of the right side, when viewing the edge of the k-th (v, w), v and w if endpoints are current apex two different connected components T1 and T2, to use edge (v, w) is connected to the T1 and T2 connected component, and then continue to view the side of the k + 1, or directly access the k + 1 side, the process continues until only one time until the connected component.

The above-described configuration Kruskal minimum spanning tree algorithm requires G See FIG ascending order of weights of all edges. This requires all the G side edges sorted by weight. Structure definition is stored in each edge:

typedef struct edge
{
    int u;
    int v;
    WItem w;
}Edge;

/*函数EDGE(u,v,w)创造一条权威w的边(u,v)*/
Edge EDGE(int u,int v,WItem w)
{
    Edge e;
    e->u=u;
    e->v=v;
    e->w=w;
    return e;
}
/*函数Edges(a,G)抽取图G的所有边到赋权边数组a中,并返回图G的边数*/
int Edges(Edges a[],Graph G)
{
    int k=0;
    for(int i=1;i<=G->n;i++)
        for(int j=1;j<=G->n;j++)
            if(G->a[i][j]!=G->NoEdge)
                a[k++]=EDGE(i,j,G->a[i][j]);
    return k;
}

void Kruskal(Edge mst[],Graph G)
{
    Edge a[maxE];
    UFset U;    //并查集u
    int e=Edges(a,G);    //抽取G的所有边,e为边数
    quicksort(a,0,e-1);    //对边数组a排序,参数意义为对数组a从元素0到元素e-1进行排序
    U=UFinit(G->n);    //初始化并查集U
    for(int i=0,int k=0;i<e&&k<G->n-1;i++)    //k记录循环次数,执行n-1次结束
    {
        int s=UFind(a[i].u,U);    //a[i].u是当前最短边的一个端点,找出它所属的连通分支
        int t=UFind(a[i].v,U);    //a[i].v是当前最短边的另一个端点,找出它所属的连通分支
        if(s!=t)    //若s和t不是同一个连通分支
        {
            mst[k++]=a[i];    //更新最小生成树
            UFunion(s,t,U);    //合并连通分支s和t
        }
    }
    
}

 

Published 35 original articles · won praise 2 · Views 1390

Guess you like

Origin blog.csdn.net/weixin_41001497/article/details/101385955