图论 模板(1)

版权声明:https://blog.csdn.net/huashuimu2003 https://blog.csdn.net/huashuimu2003/article/details/84426146

最短路(ShortextPath)

Dijkstra

#include<bits/stdc++.h>
using namespace std; 
/*Dijkstra算法*/
int a[3010][3010],d[3010],n,m;
bool v[3010];
void dijkstra()
{
	memset(d,0x3f,sizeof(d));//dist数组 
	memset(v,0,sizeof(v));//节点标记 
	d[1]=0;
	for (int i=1;i<n;i++)
	{//重复进行n-1次 
		int x=0;
		//找到未标记节点中dist最小的 
		for (int j=1;j<=n;j++)
			if (!v[j]&&(x==0||d[j]<d[x]))
				x=j;
		v[x]=1;
		//用全局最小值点X更新其他节点 
		for (int y=1;y<=n;y++)
			d[y]=min(d[y],d[x]+a[x][y]);
	}
}
int main()
{
	cin>>n>>m;
	//构建邻接矩阵 
	memset(a,0x3f,sizeof(a));
	for (int i=1;i<=n;i++)
		for (int j=1;j<=m;j++)
		{
			int x,y,z;
			scanf("%d%d%d",&x,&y,&z);
			a[x][y]=min(a[x][y],z);
		}
	//求单源最短路径 
	dijkstra();
	for (int i=1;i<=n;i++)
		printf("%d\n",d[i]);
}

有向图的Dijkstra算法实现(可输出具体最短路径)

转自http://www.wutianqi.com/?p=1890

/***************************************
* About:    有向图的Dijkstra算法实现
* Author:   Tanky Woo
* Blog:     www.WuTianQi.com
***************************************/
 
#include <iostream>
using namespace std;
 
const int maxnum = 100;
const int maxint = 999999;
 
// 各数组都从下标1开始
int dist[maxnum];     // 表示当前点到源点的最短路径长度
int prev[maxnum];     // 记录当前点的前一个结点
int c[maxnum][maxnum];   // 记录图的两点间路径长度
int n, line;             // 图的结点数和路径数
 
// n -- n nodes
// v -- the source node
// dist[] -- the distance from the ith node to the source node
// prev[] -- the previous node of the ith node
// c[][] -- every two nodes' distance
void Dijkstra(int n, int v, int *dist, int *prev, int c[maxnum][maxnum])
{
	bool s[maxnum];    // 判断是否已存入该点到S集合中
	for(int i=1; i<=n; ++i)
	{
		dist[i] = c[v][i];
		s[i] = 0;     // 初始都未用过该点
		if(dist[i] == maxint)
			prev[i] = 0;
		else
			prev[i] = v;
	}
	dist[v] = 0;
	s[v] = 1;
 
	// 依次将未放入S集合的结点中,取dist[]最小值的结点,放入结合S中。 
	// 一旦S包含了所有V中顶点,dist就记录了从源点到所有其他顶点之间的
	//最短路径长度。 
    // 注意是从第二个节点开始,第一个为源点。 
	for(int i=2; i<=n; ++i)
	{
		int tmp = maxint;
		int u = v;
		// 找出当前未使用的点j的dist[j]最小值。 
		for(int j=1; j<=n; ++j)
			if((!s[j]) && dist[j]<tmp)
			{
				u = j;              // u保存当前邻接点中距离最小的点的号码。 
				tmp = dist[j];
			}
		s[u] = 1;    // 表示u点已存入S集合中。 
 
		//三角形更新,更新dist。 
		for(int j=1; j<=n; ++j)
			if((!s[j]) && c[u][j]<maxint)
			{
				int newdist = dist[u] + c[u][j];
				if(newdist < dist[j])
				{
					dist[j] = newdist;
					prev[j] = u;
				}
			}
	}
}
 
// 查找从源点v到终点u的路径,并输出。 
void searchPath(int *prev,int v, int u)
{
	int que[maxnum];
	int tot = 1;
	que[tot] = u;
	tot++;
	int tmp = prev[u];
	while(tmp != v)
	{
		que[tot] = tmp;
		tot++;
		tmp = prev[tmp];
	}
	que[tot] = v;
	for(int i=tot; i>=1; --i)
		if(i != 1)
			cout << que[i] << " -> ";
		else
			cout << que[i] << endl;
}
 
int main()
{
	//freopen("input.txt", "r", stdin);
	// 各数组都从下标1开始
 
	// 输入结点数
	cin >> n;
	// 输入路径数
	cin >> line;
	int p, q, len;          // 输入p, q两点及其路径长度
 
	// 初始化c[][]为maxint
	for(int i=1; i<=n; ++i)
		for(int j=1; j<=n; ++j)
			c[i][j] = maxint;
 
	for(int i=1; i<=line; ++i)  
	{
		cin >> p >> q >> len;
		if(len < c[p][q])       // 有重边
		{
			c[p][q] = len;      // p指向q
			c[q][p] = len;      // q指向p,这样表示无向图
		}
	}
 
	for(int i=1; i<=n; ++i)
		dist[i] = maxint;
	for(int i=1; i<=n; ++i)
	{
		for(int j=1; j<=n; ++j) 
			printf("%8d", c[i][j]);
		printf("\n");
	}
 
	Dijkstra(n, 1, dist, prev, c);
 
	// 最短路径长度
	cout << "源点到最后一个顶点的最短路径长度: " << dist[n] << endl;
 
	// 路径
	cout << "源点到最后一个顶点的路径为: ";
	searchPath(prev, 1, n);
}

Dijkstra with priority-queue

#include<bits/stdc++.h>
using namespace std; 
/*堆优化*/
const int N=100010,M=1000010;
int head[N],ver[N],edge[N],Next[N],d[N];//顶点 vertex 边 edge 距离 distance
bool v[N];
int n,m,tot;
//大根堆(优先队列),pair的第二维为节点编号
//pair的第一维为dist的相反数(利用相反数变成小根堆) 
priority_queue<pair<int,int> >q;

void add(int x,int y,int z)
{
	ver[++tot]=y,edge[tot]=z,Next[tot]=head[tot],head[x]=tot;
}
void dijkstra()
{
	memset(d,0x3f,sizeof(d));//dist数组
	memset(v,0,sizeof(v));//节点标记
	d[1]=0;
	q.push(make_pair(0,1));
	while (q.size())
	{//取出堆顶 
		int x=q.top().second;
		q.pop();
		if (v[x]) continue;
		v[x]=1;
		//扫描所有出边 
		for (int i=head[x];i;i=Next[i])
		{
			int y=ver[i],z=edge[i];
			if (d[y]>d[x]+z)
			{//更新,把新的二元组插入堆 
				d[y]=d[x]+z;
				q.push(make_pair(-d[y],y));
			}
		}
	}
}
int main()
{
	cin>>n>>m;
	//构建邻接表 
	for (int i=1;i<=m;i++)
	{
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);
	}
	//求单源最短路径 
	dijkstra();
	for (int i=1;i<=n;i++)
		printf("%d\n",d[i]);
}

SPFA

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
const int M=1000010;
int head[N],ver[M],edge[M],Next[M],d[N];
int n,m,tot;
queue<int>q;
bool v[N];
void add(int x,int y,int z)
{
	ver[++tot]=y;
	edge[tot]=z;
	Next[tot]=head[x];
	head[x]=tot;
}
void spfa()
{
	memset(d,0x3f,sizeof(d));//dist数组 
	memset(v,0,sizeof(v));//是否在队列中 
	d[1]=0,v[1]=1;
	q.push(1);
	while(q.size())
	{//取出队头 
		int x=q.front();
		q.pop();
		v[x]=0;
		//扫描所有出边 
		for(int i=head[x];i;i=Next[i])
		{
			int y=ver[i],z=edge[i];
			if(d[y]>d[x]+z)
			{//更新,把新的二元组插入堆 
				d[y]=d[x]+z;
				if(!v[y]) q.push(y) , v[y]=1;
			}
		}
	}
}
int main()
{
	cin>>n>>m;
	//构建邻接表 
	for (int i=1;i<=m;i++)
	{
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);
	}
	//求单源最短路径 
	spfa();
	for (int i=1;i<=n;i++)
		printf("%d\n",d[i]);
}

Bellman-Ford

#include<iostream>
using namespace std;
const int maxnum=100;
const int maxint=99999;
//边 
typedef struct edge
{
	int u,v;//源点,终点 
	int weight;//边的权值 
}Edge;

Edge edge[maxnum];//保存边的权值 
int dist[maxnum];//结点到源点的最小距离
int nodenum,edgenum,source;//结点数,边数,源点
//初始化图 
void init()
{
	//输入结点数,边数,源点 
	cin>>nodenum>>edgenum>>source;
	for (int i=1;i<=nodenum;i++)
		dist[i]=maxint;
	dist[source]=0;
	for (int i=1;i<=edgenum;i++)
	{
		cin>>edge[i].u>>edge[i].v>>edge[i].weight;
		if (edge[i].u==source)//注意这里的设置初始情况 
			dist[edge[i].v]=edge[i].weight;
	}
}

//松弛计算 
void relax (int u,int v,int weight)
{
	if (dist[u]+weight<dist[v])
		dist[v]=dist[u]+weight;
}
bool Bellman_Ford()
{
	for (int i=1;i<nodenum;i++)
		for (int j=1;j<=edgenum;j++)
			relax (edge[j].u,edge[j].v,edge[j].weight);
	bool flag=1;
	//判断是否有负环路 
	for (int i=1;i<=edgenum;i++)
		if (dist[edge[i].u]+edge[i].weight<dist[edge[i].v])
		{
			flag=0;
			break;
		}
	return flag;
} 
int main()
{
	init();
	if (Bellman_Ford())
		for (int i=1;i<=nodenum;i++)
			cout<<dist[i]<<endl;
	return 0;
}

Floyed

/*
*Floyed算法 
*/
#include<bits/stdc++.h>

using namespace std;

/* 
*目标:把图中 任意 点i与j之间的最短距离都求出来
*原理:根据图的传递闭包思想:if(d[i][k]+d[k][i]<d[i][j]) d[i][j]=d[i][k]+d[k][j]
*时间复杂度:O(n*n*n)
*/
//众所周知:只有五行(甚至只有四行)的核心代码
for (int k=1;k<=n;k++)
	for (int i=1;i<=n;i++)
		for (int j=1;j<=n;j++)
			if (dis[i][k]+dis[k][j]<dis[i][j])
					dis[i][j]=dis[i][k]+dis[k][j];
//初始化条件
d[i][j]=0;//自己到自己为0;对角线为0; 
d[i][j]=value;//i与j有直接相连的边; 
d[i][j]=maxint;//i与j无直接相连的边,一般设maxint为:memset 10即可;

/*
*探究:
*1.path[i][j]记录i到j的最短路径中j的前驱顶点,可用于输出最小路径。
*/
//初始化 i到j有边,则
path[i][j]=i;
path[j][i]=j;
//核心
for (int k=1;k<=n;k++)
	for (int i=1;i<=n;i++)
		for (int j=1;j<=n;j++)
			if (d[i][k]+d[k][j]<d[i][j])
			{
				d[i][j]=d[i][k]+d[k][j];
				path[i][j]=path[k][j];
			}
//输出i到j的路线
void dfs (int i,int j)
{
	if (i==j) cout<<i<<' ';
	else if (path[i][j]>0)
	{
		dfs(i,path[i][j]);
		cout<<j<<' ';
	}
}
//2.path[i][j]记录能使i到j的距离变短的节点
//初始化:
path[i][j]=-1;//i与j不通的
path[i][j]=0;//从i到j的最短路径是直接到达的。开始有边的都直接到达。 
path[i][j]>0;//从i到j的最短路径要经过点path[i][j];
//核心:
for (int k=1;k<=n;k++)
	for (int i=1;i<=n;i++)
		for (int j=1;j<=n;j++)
			if (d[i][k]+d[k][j]<d[i][j])
			{
				d[i][j]=d[i][k]+d[k][j];
				path[i][j]=k;
			}
//输出
cout<<i<<' '<<dfs(i,j)<<' '<<j;
void dfs (int i,int j)
{
	if (path[i][j]>0)
	{
		dfs(i,path[i][j]);
		cout<<path[i][j]<<' ';
		dfs(path[i][j],j);
	}
}

第k短路

这是复制大佬的。。。。。。。。。。
https://blog.csdn.net/chty2018/article/details/53258341

#include<bits/stdc++.h>
#define _ 20100
#define INF 1000100000
using namespace std;
struct node
{
	int y,next,v;
}e1[_],e2[_];
struct node2
{
    int f,g,id;
	friend bool operator < (node2 a,node2 b)
	{
        return a.f>b.f;
    }
};
int n,m,k,len1,len2,Link1[_],Link2[_],dist[_],vis[_],ans[_];
priority_queue<node2>Q;
inline int read()
{
   int f=1,num=0;
   char ch=getchar();
   while (ch<'0'||ch>'9') { if (ch=='-') f=-1; ch=getchar(); }
   while (ch>='0'&&ch<='9') num=(num<<1)+(num<<3)+ch-'0', ch=getchar();
   return num*f;
}
void insert(int x,int y,int v)
{
    e1[++len1].next=Link1[x],Link1[x]=len1,e1[len1].y=y,e1[len1].v=v;
    e2[++len2].next=Link2[y],Link2[y]=len2,e2[len2].y=x,e2[len2].v=v;
}
void SPFA()
{
	priority_queue<int,vector<int>,greater<int> >q;
    memset(dist,0x3f,sizeof(dist));
	dist[1]=0,vis[1]=1,q.push(1);
	while(!q.empty())
    {
        int x=q.top();
    	vis[x]=0;
        q.pop();
        for(int i=Link2[x];i;i=e2[i].next)
        {
            int y=e2[i].y;
            if(dist[y]>dist[x]+e2[i].v)
            {
                dist[y]=dist[x]+e2[i].v;
                if(!vis[y]) vis[y]=1,q.push(y);
            }
        }
    }
}
void Astar()
{
    if(dist[n]==INF)  return;
    node2 temp;
	temp.g=0,temp.f=dist[n],temp.id=n;
    Q.push(temp);
    while(!Q.empty())
    {
        temp=Q.top();
		Q.pop();
        if(temp.id==1)
		{
			ans[++ans[0]]=temp.f;
			if(ans[0]>k)
			return;
		}  
        for(int i=Link1[temp.id];i;i=e1[i].next)
    		{
        		node2 opt;
        		opt.g=temp.g+e1[i].v;
        		opt.f=opt.g+dist[e1[i].y];
        		opt.id=e1[i].y;
    			Q.push(opt);
    		}
    }
}
int main()
{
    n=read(),m=read(),k=read();
    for(int i=1;i<=m;i++)
    {
        int x=read(),y=read(),v=read();
        insert(x,y,v);
    }
	SPFA();
    Astar();
	for(int i=1;i<=k;i++)
	{
    	if(!ans[i])  printf("-1\n");
    	else printf("%d\n",ans[i]);
	}
	return 0;
}

差分约束系统

之前刚学差分约束的时候,不太明白,直到刷了几道题,看了些大佬的博客后,才知道,差分约束系统只是名字高深,其实就是建图的方法而已。。。。。。。。
下面是一道板子题:

p1985 糖果

【描述 Description】
幼儿园里有N个小朋友,lxhgww老师现在想要给这些小朋友们分配糖果,要求每个小朋友都要分到糖果。但是小朋友们也有嫉妒心,总是会提出一些要求,比如小明不希望小红分到的糖果比他的多,于是在分配糖果的时候,lxhgww需要满足小朋友们的K个要求。幼儿园的糖果总是有限的,lxhgww想知道他至少需要准备多少个糖果,才能使得每个小朋友都能够分到糖果,并且满足小朋友们所有的要求。
【输入格式 Input Format 】
输入的第一行是两个整数N,K。
接下来K行,表示这些点需要满足的关系,每行3个数字,X,A,B。
如果X=1, 表示第A个小朋友分到的糖果必须和第B个小朋友分到的糖果一样多;
如果X=2, 表示第A个小朋友分到的糖果必须少于第B个小朋友分到的糖果;
如果X=3, 表示第A个小朋友分到的糖果必须不少于第B个小朋友分到的糖果;
如果X=4, 表示第A个小朋友分到的糖果必须多于第B个小朋友分到的糖果;
如果X=5, 表示第A个小朋友分到的糖果必须不多于第B个小朋友分到的糖果;

【输出格式 Output Format 】
输出一行,表示lxhgww老师至少需要准备的糖果数,如果不能满足小朋友们的所有要求,就输出-1。
【样例输入 Sample Input】

5 7
1 1 2
2 3 2
4 4 1
3 4 5
5 4 5
2 3 5
4 5 1
【样例输出 Sample Output】

11
【时间限制 Time Limitation】
1s
【注释 Hint】
【数据范围】
对于30%的数据,保证 N<=100

对于100%的数据,保证 N<=100000

对于所有的数据,保证 K<=100000,1<=X<=5,1<=A, B<=N
【来源 Source】
scoi2011

#include<bits/stdc++.h>
#define _ 999999
#define inf 0x7fffffff
using namespace std;
inline int read()
{
	int f=1,num=0;
	char ch=getchar();
	while (ch<'0'||ch>'9') { if (ch=='-') f=-1; ch=getchar(); }
	while (ch>='0'&&ch<='9') num=(num<<1)+(num<<3)+ch-'0',ch=getchar();
	return num*f;
}
int n,m;
long long ans=0;
int ver[_],edge[_],Next[_],head[_],len;
void add(int x,int y,int z)
{
	ver[++len]=y,edge[len]=z,Next[len]=head[x],head[x]=len;
}
int dist[_],cnt[_];
bool v[_];
queue<int>q;
void SPFA(int s)
{
	for (int i=1;i<=n;++i)
		dist[i]=-inf;
	memset(v,0,sizeof(v));
	dist[s]=0,v[s]=1;
	q.push(s);
	cnt[s]++;
	while (!q.empty())
	{
		int x=q.front();
		q.pop();
		v[x]=0;
		for (int i=head[x];i;i=Next[i])
		{
			int y=ver[i],z=edge[i];
			if (dist[y]<dist[x]+z)
			{
				dist[y]=dist[x]+z;
				if (!v[y])
				{
					q.push(y),v[y]=1;
					if (++cnt[y]>n)
					{
						puts("-1");
						exit(0);
					}
				}
			}
		}
	}
	for (int i=1;i<=n;++i)
		ans+=dist[i];
	printf("%lld\n",ans);
}
int main()
{
	n=read(),m=read();
	for (int i=1;i<=m;++i)
	{
		int z=read(),x=read(),y=read();
		//下面就是所谓的差分约束系统了
		if (z==1) add(x,y,0),add(y,x,0);
		//z=1,表示第A个小朋友分到的糖果必须和第B个小朋友分到的糖果一样多
		else if (z==2)
		{//z=2, 表示第A个小朋友分到的糖果必须少于第B个小朋友分到的糖果
			if (x==y) { puts("-1"); exit(0); }
			add(x,y,1);
		}
		else if (z==3) add(y,x,0);
		//z=3,表示第A个小朋友分到的糖果必须不少于第B个小朋友分到的糖果
		else if (z==4)
		{//z=4,表示第A个小朋友分到的糖果必须多于第B个小朋友分到的糖果
			if (x==y) { puts("-1"); exit(0); }
			add(y,x,1);
		}
		else add(x,y,0);
		//z=5,表示第A个小朋友分到的糖果必须不多于第B个小朋友分到的糖果
	}
	for (int i=n;i>=1;--i)
		add(0,i,1);//虚设源点 
	SPFA(0);
	return 0;
}

最小生成树(MST)

Prim

#include<bits/stdc++.h>
using namespace std;
int a[3010][3010],d[3010],n,m,ans;
bool v[3010];

void prim()
{
	memset(d,0x3f,sizeof(d));
	memset(v,0,sizeof(v));
	d[1]=0;
	for (int i=1;i<n;i++)
	{
		int x=0;
		for (int j=1;j<=n;j++)
			if (!v[j]&&(x==0||d[j]<d[x]))
				x=j;
		v[x]=1;
		for (int y=1;y<=n;y++)
			if (!v[y])
				d[y]=min(d[y],a[x][y]);
	}
}
int main()
{
	cin>>n>>m;
	//构建邻接矩阵 
	memset(a,0x3f,sizeof(a));
	for (int i=1;i<=n;i++)
		a[i][i]=0;
	for (int i=1;i<=m;i++)
	{
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		a[y][x]=a[x][y]=min(a[x][y],z);
	}
	//求最小生成树
	prim();
	for (int i=2;i<=n;i++)
		ans+=d[i];
	cout<<ans<<endl;
	return 0; 
}

Prim with priority-queue

暂时还没有

Kruskal

#include<bits/stdc++.h>
using namespace std;
struct rec
{
	int x,y,z;
}edge[500010];
int fa[100010],n,m,ans,num;
bool operator<(rec a,rec b)
{
	return a.z<b.z;
}
int get (int x)
{
	if (x==fa[x]) return x;
	return fa[x]=get(fa[x]);
}
int main()
{
	cin>>n>>m;
	for (int i=1;i<=m;i++)
		scanf("%d%d%d",&edge[i].x,&edge[i].y,&edge[i].z);
	//按照边排序
	sort(edge+1,edge+m+1);
	//并查集初始化
	for (int i=1;i<=n;i++)
		fa[i]=i;
	//求最小生成树
	for (int i=1;i<=m;i++)
	{
		int x=get(edge[i].x);
		int y=get(edge[i].y);
		if (x==y) continue;
		else
		{
			fa[x]=y;
			++num;
			ans+=edge[i].z;
		}		
		if (num==n-1) break;
	}
	cout<<ans<<endl;
	return 0; 
}

经典例题

p1223 [小数据版]边权差值最小的生成树

【描述 Description】
小 s 最近学了最小生成树,不过聪明的小 s 显然对简单的求最小生成树不感兴趣。现在小 s 想知道,对于一个给定的图,它的所有生成树中,最大边和最小边的边权差最小是多少。
【输入格式 Input Format 】
第一行,两个用空格隔开的整数 N 和 M,分别表示顶点数和边数。
下面 M 行,每行 3 个数 u,v,w,表示 u 和 v 之间有一条权值为 w 的无向边。
【输出格式 Output Format】
一行,一个非负整数,表示最大边和最小边的最小边权差。若图本身不联通,则输出-1
【样例输入 Sample Input】

span1.in
4 5
1 2 3
1 3 5
1 4 6
2 4 6
3 4 7

span2.in

5 10
1 2 9384
1 3 887
1 4 2778
1 5 6916
2 3 7794
2 4 8336
2 5 5387
3 4 493
3 5 6650
4 5 1422
【样例输出 Sample Output】
span1.out
1
span2.out
1686

【时间限制 Time Limitation】
1s
【注释 Hint】
数据范围
20% N<=10
100% 2<=N<=100,0<=M<=3000

#include<bits/stdc++.h>
using namespace std;
int i;
int n,m,fa[100010],ans,cnt=1;
struct rec
{
	int x,y,z;
}edge[100100];
inline int read()
{
	int f=1,num=0;
	char ch=getchar();
	while (ch<'0'||ch>'9') { if (ch=='-') f=-1; ch=getchar(); }
	while (ch>='0'&&ch<='9') { num=(num<<1)+(num<<3)+ch-'0'; ch=getchar(); }
	return num*f;
}
bool operator<(rec a,rec b)
{
	return a.z<b.z;
}
int get(int x)
{
	if (x==fa[x]) return x;
	return fa[x]=get(fa[x]);
}
int main()
{
	n=read(),m=read();
	for (i=1;i<=m;i++)
		edge[i].x=read(),edge[i].y=read(),edge[i].z=read();
	sort(edge+1,edge+m+1);
	ans=edge[m].z;//差值初始化 
	for(int k=1;k<=m;k++)//以k为最小边进行枚举 
	{
		int sum=0;//最小生成树的边数 
		for (i=1;i<=n;i++)//生成树初始化 
			fa[i]=i;
		for (i=k;i<=m;i++)//求最小生成树 
		{
			int x=get(edge[i].x);
			int y=get(edge[i].y);
			if (x==y) continue;
			fa[x]=y;
			sum++;
			if(sum==n-1)//已构成一个最小生成树(从定义出发) 
			{
				ans=min(ans,edge[i].z-edge[k].z);//最大边减去最小边 
				break;
			}
		}
		if(i==m+1)//
			break;
	}
	if(ans==edge[m].z)//ans未经过改变,说明此图未连通 
		cout<<-1<<endl;
	else
		cout<<ans<<endl;
	return 0;
}

猜你喜欢

转载自blog.csdn.net/huashuimu2003/article/details/84426146