LCT学习笔记
最近自学了一下LCT(Link-Cut-Tree),参考了Saramanda及Yang_Zhe等众多大神的论文博客,对LCT有了一个初步的认识,LCT是一种动态树,可以处理动态问题的算法。对于树分治中的树链剖分,只能处理静态的数据或者在轻重链上的边或点的权值,对于其他动态的处理就毫无办法了。因此我们就需要引入LCT这个东西。那么问题来了,LCT到底是什么呢?我弄了很久总算是理解了LCT,打算总结一下LCT的基本操作。
①浅谈对LCT的初步印象
LCT用来维护动态的森林,以及一些链上操作,是处理节点无序的有根树组成的森林,进行一些列操作(例如合并,剪切,翻转,更新·····)
对于一棵树的操作,我们可以用splay维护;同理,对于一片森林,也可以用多棵splay维护,而LCT就是要把这些森林的splay联系在一起。而LCT的核心就是access操作啦!
看完之后我们知道,LCT和静态的树链剖分很像。怎么说呢?这两种树形结构都是由若干条长度不等的“重链”和“轻边”构成(名字可以不同,大概就是这个意思),“重链”之间由”轻边”连接。就像这样:
可以想象为一棵树被人为的砍成了一段段。
LCT和树链剖分不同的是,树链剖分的链是不会变化的,所以可以很方便的用线段树维护。但是,既然是动态树,那么树的结构形态将会发生改变,所以我们要用更加灵活的维护区间的结构来对链进行维护,不难想到Splay可以胜任。如何分离树链也是保证时间效率的关键(链的数量和长度要平衡),树链剖分的“重儿子”就体现了前人博大精深的智慧。
在这里解释一下为什么要把树砍成一条条的链:我们可以在logn的时间内维护长度为n的区间(链),所以这样可以极大的提高树上操作的时间效率。在树链剖分中,我们把一条条链放到线段树上维护。但是LCT中,由于树的形态变化,所以用能够支持合并、分离、翻转等操作的Splay维护LCT的重链(注意,单独一个节点也算是一条重链)。
这时我们注意到,LCT中的轻边信息变得无法维护。为什么呢?因为Splay只维护了重链,没有维护重链之间的轻边;而LCT中甚至连根都可以不停的变化,所以也没法用点权表示它父边的边权(父亲在变化)。所以,如果在LCT中要维护边上信息,个人认为最方便的方法应该是把边变成一个新点和两条边。这样可以把边权的信息变成点权维护,同时为了不影响,把真正的树上节点的点权变成0,就可以用维护点的方式维护边。
②再谈LCT的各种操作
LCT中用Splay维护链,这些Splay叫做“辅助树“。辅助树以它上面每个节点的深度为关键字维护,就是辅助树中每个节点左儿子的深度小于当前节点的深度,当前节点的深度小于右儿子的深度。
可以把LCT认为是一个由Splay组成的森林,就像这样:(三角形代表一棵Splay,对应着LCT上一条链)
箭头是什么意思呢?箭头记录着某棵Splay对应的链向上由轻边连着哪个节点,可以想象为箭头指向“Splay 的父亲”。但是,Splay的父亲并不记录有这个儿子,即箭头是单向的。同时,每个节点要记录它是否是它所在的Splay的根。这样,Splay构成的森林就建成了。
这个是我的Splay节点最基本的定义:(如果要维护更多信息就像Splay维护区间那样加上更多标记)
1 struct node{ 2 int fa,ch[2]; //父亲和左右儿子。 3 bool reverse,is_root; //区间反转标记、是否是所在Splay的根 4 }T[maxn];
LCT中基本的Splay上操作:
1 int getson(int x){ 2 return x==T[T[x].fa].ch[1]; 3 } 4 void pushreverse(int x){ 5 if(!x)return; 6 swap(T[x].ch[0],T[x].ch[1]); 7 T[x].reverse^=1; 8 } 9 void pushdown(int x){ 10 if(T[x].reverse){ 11 pushreverse(T[x].ch[0]); 12 pushreverse(T[x].ch[1]); 13 T[x].reverse=false; 14 } 15 } 16 void rotate(int x){ 17 if(T[x].is_root)return; 18 int k=getson(x),fa=T[x].fa; 19 int fafa=T[fa].fa; 20 pushdown(fa);pushdown(x); //先要下传标记 21 T[fa].ch[k]=T[x].ch[k^1]; 22 if(T[x].ch[k^1])T[T[x].ch[k^1]].fa=fa; 23 T[x].ch[k^1]=fa; 24 T[fa].fa=x; 25 T[x].fa=fafa; 26 if(!T[fa].is_root)T[fafa].ch[fa==T[fafa].ch[1]]=x; 27 else T[x].is_root=true,T[fa].is_root=false; 28 //update(fa);update(x); //如果维护了信息,就要更新节点 29 } 30 void push(int x){ 31 if(!T[x].is_root)push(T[x].fa); 32 pushdown(x); 33 } 34 void Splay(int x){ 35 push(x); //在Splay到根之前,必须先传完反转标记 36 for(int fa;!T[x].is_root;rotate(x)){ 37 if(!T[fa=T[x].fa].is_root){ 38 rotate((getson(x)==getson(fa))?fa:x); 39 } 40 } 41 }
access操作:
这是LCT最核心的操作。其他所有操作都要用到它。
他的含义是”访问某节点“。作用是:对于访问的节点x,打通一条从树根(真实的LCT树)到x的重链;如果x往下是重链,那么把x往下的重边改成轻边。可以理解为专门开辟一条x到根的路径,由一棵Splay维护这条路径。
access之前:(粗的是重链)
access之后:
access实现的方式很简单:先把x旋转到所在Splay的根,然后把x的右孩子的is_root设为true(此时右孩子对应的是x下方的重链,这样就断开了x和下方的重链)。用y记录上一次的x(初始化y=0),把y接到x的右孩子上,这样就把上一次的重链接到了当前重链一起,同时记得T[y].is_root=false。记录y=x,然后x=T[x].fa,把x上提。重复上面的步骤直到x=0。
实现代码如下:
1 void access(int x){ 2 int y=0; 3 do{ 4 Splay(x); 5 T[T[x].ch[1]].is_root=true; 6 T[T[x].ch[1]=y].is_root=false; 7 //update(x); //如果维护了信息记得更新。 8 x=T[y=x].fa; 9 }while(x); 10 }
mroot操作:
这个操作的作用是把某个节点变成树根(这里的根指的是整棵LCT的根)。加上access操作,就可以方便的提取出LCT上两点之间的路径。提取u到v的路径只需要mroot(u),access(v),然后v所在的Splay对应的链就是u到v的路径。
mroot实现的方式:
由于LCT是Splay组成的森林,所以要把x变成根就只需要让所有Splay的父亲最终指向x所在Splay。所以先access(x),Splay(x),把现在的根和将成为根的x链在一棵Splay中,并转到根即可。但是我们注意到,由于x成为了新的根,所以它和原来的根所在的Splay中深度作为关键字的性质遭到了破坏:新根x应该是Splay中深度最小的,但是之前的操作并不会改变x的深度(也就是目前x依旧是当前Splay中深度最深的)。所以,我们需要把所在的这棵Splay翻转过来。
(粗的是重链,y是原来的根)
翻转前:
翻转后:
这时候x才真正变成了根。
实现代码如下:
1 void mroot(int x){ 2 access(x); 3 Splay(x); 4 pushreverse(x); 5 }
link操作:
这个操作的作用是连接两棵LCT。对于link(u,v),表示连接u所在的LCT和v所在的LCT;
link实现的方式:
很简单,只需要先mroot(u),然后记录T[u].fa=v就可以了,就是把一个Splay森林连到另一个上。
实现代码如下:
void link(int u,int v){ mroot(u); T[u].fa=v; }
cut操作:
这个操作的作用是分离出两棵LCT。
实现代码如下:
1 void cut(int u,int v) 2 mroot(u); //先把u变成根 3 access(v);Splay(v); //连接u、v 4 pushdown(v); //先下传标记 5 T[u].fa=T[v].ch[0]=0; 6 //v的左孩子表示v上方相连的重链 7 //update(v); //记得维护信息 8 }
以上这些就是LCT的基本操作啦。
③例题分析
先来一道简单的入门题
Time Limit: 851MS | Memory Limit: 1572864KB | 64bit IO Format: %lld & %llu |
Description
You are given a tree (an acyclic undirected connected graph) with N nodes, and edges numbered 1, 2, 3...N-1.
We will ask you to perfrom some instructions of the following form:
- CHANGE i ti : change the cost of the i-th edge to ti
or - QUERY a b : ask for the maximum edge cost on the path from node a to node b
Input
The first line of input contains an integer t, the number of test cases (t <= 20). t test cases follow.
For each test case:
- In the first line there is an integer N (N <= 10000),
- In the next N-1 lines, the i-th line describes the i-th edge: a line with three integers a b c denotes an edge between a, b of cost c (c <= 1000000),
- The next lines contain instructions "CHANGE i ti" or "QUERY a b",
- The end of each test case is signified by the string "DONE".
There is one blank line between successive tests.
Output
For each "QUERY" operation, write one integer representing its result.
Example
Input: 1 3 1 2 1 2 3 2 QUERY 1 2 CHANGE 1 3 QUERY 1 2 DONE Output: 1 3
Hint
咦,这道题不是树链剖分的入门题么? 对的,不过,也可以用LCT做(树链剖分只处理静态的树和有关轻重边的动态更新,而LCT就是用来解决动态树问题)
题意:
给定一棵树,给定每条边a,b,w (w权值)
完成两个操作:
1.把第i条边权值改成w
2.查询u到v路径上的最大权值
介绍一下access操作:查询根到u这条路径,并把这条路径更新为偏爱路径(之前的偏爱边可能会有改变)被访问到的u,不再有偏爱儿子。
每次access访问,都会把这条路径上的点用一棵splay维护,splay维护的关键词是点的深度,保证左边的子树比当前点深度小,右边的子树比当前点深度大。
并且这棵树把u作为根节点之后不再有右儿子(u没有偏爱儿子).
而splay要维护的是一条偏爱路径上的点。所有的splay会构成一个森林的集合。
要注意的是,在rotate判断ff的时候,有可能ff的左右儿子都不是f,然而没有进行判断就会跑出神奇的错误。
实现代码如下:
1 #include<cstdio> 2 #include<iostream> 3 #include<cstring> 4 #include<cstdlib> 5 #include<queue> 6 #include<cmath> 7 #include<algorithm> 8 using namespace std; 9 /* 10 LCT的思想类似于轻重链剖分 11 是用偏爱路径划分,用splay维护一条偏爱路径,然后像森林操作一样搞 12 u-v边的权值记录在v点上 13 */ 14 #define maxn 10005 15 int n; 16 int fa[maxn]; 17 struct edge 18 { 19 int v,w; 20 edge *next; 21 edge(int _v,int _w,edge *_next) 22 { 23 v=_v; 24 w=_w; 25 next=_next; 26 } 27 }*head[maxn]; 28 struct node 29 { 30 node *f; 31 node *ch[2]; 32 bool root;//是否是所在辅助树的根节点 33 int cost; 34 int maxcost; 35 36 }tree[maxn],*null,Tnull; 37 void init(node *u) 38 { 39 u->f=u->ch[0]=u->ch[1]=null; 40 u->root=true; 41 u->cost=u->maxcost=0; 42 } 43 void pushup(node *p) 44 { 45 p->maxcost=max(max(p->ch[0]->maxcost,p->ch[1]->maxcost),p->cost); 46 } 47 void rotate(node *u) 48 { 49 node *f=u->f; 50 node *ff=f->f; 51 int d=u==f->ch[1]; 52 53 f->ch[d]=u->ch[d^1]; 54 if(u->ch[d^1]!=null)u->ch[d^1]->f=f; 55 56 u->f=ff; 57 if(ff!=null) 58 { 59 if(ff->ch[0] == f) 60 { 61 ff->ch[0] = u; 62 } 63 else if(ff->ch[1] == f)//一定要用Else If,如果直接用Else会出现错误,因为树本身可能不是二叉树,虽然生成的Splay Tree是 64 { 65 ff->ch[1] = u; 66 } 67 } 68 69 u->ch[d^1]=f; 70 f->f=u; 71 72 pushup(f); 73 pushup(u); 74 if(f->root) 75 { 76 u->root=true; 77 f->root=false; 78 } 79 } 80 void splay(node *u) 81 { 82 if(u==null)return ; 83 while(!u->root) 84 { 85 node *f=u->f; 86 if(f->root) 87 { 88 rotate(u); 89 } 90 else 91 { 92 node *ff=f->f; 93 int d=u==f->ch[1]; 94 int dd=f==ff->ch[1]; 95 if(d==dd)rotate(f); 96 else rotate(u); 97 rotate(u); 98 } 99 } 100 pushup(u); 101 } 102 vector<pair<int,int> >E; 103 char ss[20]; 104 void access(node *u,bool flag) 105 { 106 node *v=null; 107 while(u!=null) 108 { 109 splay(u); 110 if(flag) 111 { 112 if(u->f==null) 113 { 114 printf("%d\n",max(u->ch[1]->maxcost,v->maxcost)); 115 } 116 } 117 u->ch[1]->root=true; 118 u->ch[1]=v; 119 v->root=false; 120 pushup(u); 121 v=u; 122 u=u->f; 123 } 124 } 125 void query(int u,int v) 126 { 127 access(tree+u,0); 128 access(tree+v,1); 129 } 130 void change(int u,int w) 131 { 132 access(tree+u,0); 133 splay(tree+u); 134 tree[u].cost=w; 135 pushup(tree+u); 136 } 137 void bfs() 138 { 139 memset(fa,-1,sizeof(fa)); 140 queue<int >q; 141 q.push(1); 142 fa[1]=0; 143 while(!q.empty()) 144 { 145 int u=q.front(); 146 q.pop(); 147 for(edge *i=head[u];i;i=i->next) 148 { 149 if(fa[i->v]==-1) 150 { 151 fa[i->v]=u; 152 tree[i->v].f=tree+u; 153 tree[i->v].cost=tree[i->v].maxcost=i->w; 154 q.push(i->v); 155 } 156 } 157 } 158 } 159 int main() 160 { 161 int T; 162 scanf("%d",&T); 163 null=&Tnull; 164 init(null); 165 while(T--) 166 { 167 scanf("%d",&n); 168 for(int i=1;i<=n;i++) 169 { 170 head[i]=NULL; 171 init(&tree[i]); 172 } 173 E.clear(); 174 for(int i=1;i<n;i++) 175 { 176 int a,b,c; 177 scanf("%d%d%d",&a,&b,&c); 178 head[a]=new edge(b,c,head[a]); 179 head[b]=new edge(a,c,head[b]); 180 E.push_back(make_pair(a,b)); 181 } 182 bfs(); 183 while(scanf("%s",ss)!=EOF) 184 { 185 if(ss[0]=='D')break; 186 if(ss[0]=='Q') 187 { 188 int u,v; 189 scanf("%d%d",&u,&v); 190 query(u,v); 191 } 192 else if(ss[0]=='C') 193 { 194 int a,w; 195 scanf("%d%d",&a,&w); 196 a--; 197 int u=E[a].first; 198 int v=E[a].second; 199 if(fa[u]==v) 200 { 201 change(u,w); 202 } 203 if(fa[v]==u) 204 { 205 change(v,w); 206 } 207 } 208 } 209 } 210 return 0; 211 }
一道入门题怎么够呢?那就再来一道操作多一点的~~~
Query on The Trees
Time Limit: 10000/5000 MS (Java/Others) Memory Limit: 65768/65768 K (Java/Others)
Total Submission(s): 6250 Accepted Submission(s): 2504
There are N nodes, each node will have a unique weight Wi. We will have four kinds of operations on it and you should solve them efficiently. Wish you have fun!
For each case, the first line contains only one integer N.(1 ≤ N ≤ 300000) The next N‐1 lines each contains two integers x, y which means there is an edge between them. It also means we will give you one tree initially.
The next line will contains N integers which means the weight Wi of each node. (0 ≤ Wi ≤ 3000)
The next line will contains an integer Q. (1 ≤ Q ≤ 300000) The next Q lines will start with an integer 1, 2, 3 or 4 means the kind of this operation.
1. Given two integer x, y, you should make a new edge between these two node x and y. So after this operation, two trees will be connected to a new one.
2. Given two integer x, y, you should find the tree in the tree set who contain node x, and you should make the node x be the root of this tree, and then you should cut the edge between node y and its parent. So after this operation, a tree will be separate into two parts.
3. Given three integer w, x, y, for the x, y and all nodes between the path from x to y, you should increase their weight by w.
4. Given two integer x, y, you should check the node weights on the path between x and y, and you should output the maximum weight on it.
You should output a blank line after each test case.
5
1 2
2 4
2 5
1 3
1 2 3 4 5
6
4 2 3
2 1 2
4 2 3
1 3 5
3 2 1 4
4 1 4
题意:
给定一棵树,维护4个操作:
1.给定u,v,如果不在一棵树上,它们之间连边。
2.给定u,v如果u!=v且在同一棵树上,把u设为整棵树的根,再把v和它的父亲断开。
3.给定u,v,w,如果u,v在同一条树上,把他们之间的点的权值+w
4.给定u,v,如果u,v在同一棵树上,求出他们路径上的最大值。
违法操作输出-1
思路:
连边操作,访问u,把u旋到根,把它左右翻转即可(翻转前splay上u一定没有右节点(没有偏爱儿子),翻转后一定没有左儿子,说明它就是从根到u路径上深度最小的了,就成为了根),把它接到v上,设为整棵树的根:一样的先access(u),splay(u)再rev(u)。
断开:access(u),splay(u),但不翻转,这样u没有右儿子但有左儿子,和左子树断开即可。
对于3、4操作,可以先把u设为根,再access(v)然后找到u-v这条路径上splay的根节点在哪里,信息都记录在那个节点上!
实现代码如下:
1 #include<cstdio> 2 #include<iostream> 3 #include<cstring> 4 #include<cstdlib> 5 #include<algorithm> 6 #include<queue> 7 #include<cmath> 8 using namespace std; 9 const int maxn=300000+20; 10 const int inf=0x3f3f3f3f; 11 struct node 12 { 13 node *f; 14 node *ch[2]; 15 bool rev; 16 int add; 17 int mm; 18 int key; 19 }tree[maxn],*null,*cur; 20 void init() 21 { 22 null=tree; 23 null->f=null->ch[0]=null->ch[1]=null; 24 null->rev=0; 25 null->add=0; 26 null->mm=null->key=-inf; 27 cur=tree+1; 28 } 29 node *newnode(int key) 30 { 31 cur->f=cur->ch[0]=cur->ch[1]=null; 32 cur->mm=cur->key=key; 33 cur->add=0; 34 cur->rev=0; 35 return cur++; 36 } 37 bool isroot(node *x) 38 { 39 return x==null||x->f->ch[0]!=x&&x->f->ch[1]!=x; 40 } 41 void pushup(node *u) 42 { 43 u->mm=max(u->key,max(u->ch[0]->mm,u->ch[1]->mm)); 44 } 45 void pushdown(node *u) 46 { 47 if(u==null)return ; 48 if(u->rev) 49 { 50 swap(u->ch[0],u->ch[1]); 51 if(u->ch[0]!=null)u->ch[0]->rev^=1; 52 if(u->ch[1]!=null)u->ch[1]->rev^=1; 53 u->rev=0; 54 } 55 if(u->add) 56 { 57 if(u->ch[0]!=null) 58 { 59 u->ch[0]->add+=u->add; 60 u->ch[0]->mm+=u->add; 61 u->ch[0]->key+=u->add; 62 } 63 if(u->ch[1]!=null) 64 { 65 u->ch[1]->add+=u->add; 66 u->ch[1]->mm+=u->add; 67 u->ch[1]->key+=u->add; 68 } 69 u->add=0; 70 } 71 } 72 void rotate(node *u) 73 { 74 node *f=u->f; 75 node *ff=f->f; 76 int d=u==f->ch[1]; 77 78 if(u->ch[d^1]!=null)u->ch[d^1]->f=f; 79 f->ch[d]=u->ch[d^1]; 80 81 u->f=ff; 82 if(ff!=null) 83 { 84 if(f==ff->ch[0])ff->ch[0]=u; 85 else if(f==ff->ch[1])ff->ch[1]=u; 86 } 87 88 u->ch[d^1]=f; 89 f->f=u; 90 91 pushup(f); 92 pushup(u); 93 } 94 node *sta[maxn]; 95 int cnt; 96 void splay(node *u) 97 { 98 if(u==null)return ; 99 cnt=1; 100 sta[0]=u; 101 for(node *y=u;!isroot(y);y=y->f) 102 { 103 sta[cnt++]=y->f; 104 } 105 while(cnt)pushdown(sta[--cnt]); 106 while(!isroot(u)) 107 { 108 node *f=u->f; 109 node *ff=f->f; 110 if(isroot(f)) 111 { 112 rotate(u); 113 } 114 else 115 { 116 int d=u==f->ch[1]; 117 int dd=f==ff->ch[1]; 118 if(d==dd)rotate(f); 119 else rotate(u); 120 rotate(u); 121 } 122 } 123 pushup(u); 124 } 125 node *access(node *u) 126 { 127 node *v=null; 128 while(u!=null) 129 { 130 splay(u); 131 v->f=u; 132 u->ch[1]=v; 133 pushup(u); 134 v=u; 135 u=u->f; 136 } 137 return v; 138 } 139 void link(node *u,node *v) 140 { 141 access(u); 142 splay(u); 143 u->rev=1; 144 u->f=v; 145 } 146 bool sam(node *u,node *v) 147 { 148 while(u->f!=null)u=u->f; 149 while(v->f!=null)v=v->f; 150 return u==v; 151 } 152 void changeroot(node *u) 153 { 154 access(u)->rev^=1; 155 } 156 void cut(node *u) 157 { 158 access(u); 159 splay(u);//access+旋转之后左子树的点都比u小,一定会有u的父亲 160 u->ch[0]=u->ch[0]->f=null; 161 pushup(u); 162 } 163 node *getroot(node *u) 164 { 165 access(u); 166 splay(u); 167 while(u->f!=null)u=u->f; 168 splay(u); 169 return u; 170 } 171 int n,m; 172 int det[maxn]; 173 struct edge 174 { 175 int u,v; 176 }E[maxn]; 177 int main() 178 { 179 while(scanf("%d",&n)!=EOF) 180 { 181 init(); 182 for(int i=1;i<n;i++) 183 { 184 scanf("%d%d",&E[i].u,&E[i].v); 185 } 186 for(int i=1;i<=n;i++) 187 { 188 scanf("%d",&det[i]); 189 newnode(det[i]); 190 } 191 for(int i=1;i<n;i++) 192 { 193 link(tree+E[i].u,tree+E[i].v); 194 } 195 scanf("%d",&m); 196 for(int i=1;i<=m;i++) 197 { 198 int k,a,b; 199 scanf("%d",&k); 200 if(k==1) 201 { 202 scanf("%d%d",&a,&b); 203 if(sam(tree+a,tree+b))printf("-1\n"); 204 else 205 { 206 link(tree+a,tree+b); 207 } 208 } 209 else if(k==2) 210 { 211 scanf("%d%d",&a,&b); 212 if(a==b||!sam(tree+a,tree+b))printf("-1\n"); 213 else 214 { 215 changeroot(tree+a); 216 cut(tree+b); 217 } 218 } 219 else if(k==3) 220 { 221 int w; 222 scanf("%d%d%d",&w,&a,&b); 223 if(!sam(tree+a,tree+b))printf("-1\n"); 224 else 225 { 226 changeroot(tree+a); 227 access(tree+b); 228 node *q=getroot(tree+b); 229 q->add+=w; 230 q->mm+=w; 231 q->key+=w; 232 } 233 } 234 else if(k==4) 235 { 236 scanf("%d%d",&a,&b); 237 if(!sam(tree+a,tree+b))printf("-1\n"); 238 else 239 { 240 changeroot(tree+a); 241 access(tree+b); 242 node *q=getroot(tree+b); 243 printf("%d\n",q->mm); 244 } 245 } 246 } 247 printf("\n"); 248 } 249 return 0; 250 }
④习题推荐
我推荐几个LCT的练习题练习下上述的知识点吧!
- BZOJ 2049 SDOI2008洞穴勘探,模板题,只需要link和cut,然后询问连通性。
-
BZOJ 2002 HNOI2010弹飞绵羊,模板题,需要link和询问某点到根的路径长度。
-
BZOJ 3669 NOI2014魔法森林,LCT的综合应用。
相信做完上述例题,你也能对LCT有了个初步的认识吧,加油!
⑤参考资料
作 者:Angel_Kitty
出 处:https://www.cnblogs.com/ECJTUACM-873284962/
关于作者:阿里云ACE,目前主要研究方向是Web安全漏洞以及反序列化。如有问题或建议,请多多赐教!
版权声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
特此声明:所有评论和私信都会在第一时间回复。也欢迎园子的大大们指正错误,共同进步。或者直接私信我
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是作者坚持原创和持续写作的最大动力!
欢迎大家关注我的微信公众号IT老实人(IThonest),如果您觉得文章对您有很大的帮助,您可以考虑赏博主一杯咖啡以资鼓励,您的肯定将是我最大的动力。thx.
我的公众号是IT老实人(IThonest),一个有故事的公众号,欢迎大家来这里讨论,共同进步,不断学习才能不断进步。扫下面的二维码或者收藏下面的二维码关注吧(长按下面的二维码图片、并选择识别图中的二维码),个人QQ和微信的二维码也已给出,扫描下面👇的二维码一起来讨论吧!!!
欢迎大家关注我的Github,一些文章的备份和平常做的一些项目会存放在这里。