2024年7月

我以为用docker搭建一个rabbitMq集群会非常简单,但是结果却出乎意料,我花了差不多两个半天才搞定。这还是依赖了AI的协助,否则难度不敢想象。

我的环境是Mac上的OrbStack。用了Kimi + 文心一言 + ChatGPT + Claude,还是Kimi价值最大。

back and forth 的过程就不讲了,这里直接说一下正确步骤。

创建docker-compose文件

既然是集群,肯定要用到docker-compose了。

建一个文件夹,比如叫rabbit3.
在里面创建docker-compose.yml:

version: '3'
services:
  rabbitmq1:
    image: rabbitmq:3.6-management
    hostname: rabbitmq1
    environment:
      - RABBITMQ_ERLANG_COOKIE='secretcookie'
      - RABBITMQ_DEFAULT_USER=guest
      - RABBITMQ_DEFAULT_PASS=guest
      - AUTOCLUSTER_TYPE=docker
      - AUTOCLUSTER_DISCOVERY_NODE=rabbitmq1
    ports:
      - "5672:5672"
      - "15672:15672"
    volumes:
      - ./data/rabbitmq1:/var/lib/rabbitmq
    networks:
      - rabbitmq-cluster

  rabbitmq2:
    image: rabbitmq:3.6-management
    hostname: rabbitmq2
    environment:
      - RABBITMQ_ERLANG_COOKIE='secretcookie'
      - RABBITMQ_DEFAULT_USER=guest
      - RABBITMQ_DEFAULT_PASS=guest
      # - RABBITMQ_CLUSTER_FORMATION_PEER_DISCOVERY=docker
      - AUTOCLUSTER_TYPE=docker
      - AUTOCLUSTER_DISCOVERY_NODE=rabbitmq1
    volumes:
      - ./data/rabbitmq2:/var/lib/rabbitmq
    networks:
      - rabbitmq-cluster
    ports:
      - "15673:15672"
      - "5673:5672"
    depends_on:
      - rabbitmq1
    links:
      - rabbitmq1

  rabbitmq3:
    image: rabbitmq:3.6-management
    hostname: rabbitmq3
    environment:
      - RABBITMQ_ERLANG_COOKIE='secretcookie'
      - RABBITMQ_DEFAULT_USER=guest
      - RABBITMQ_DEFAULT_PASS=guest
      # - RABBITMQ_CLUSTER_FORMATION_PEER_DISCOVERY=docker
      - AUTOCLUSTER_TYPE=docker
      - AUTOCLUSTER_DISCOVERY_NODE=rabbitmq1
    volumes:
      - ./data/rabbitmq3:/var/lib/rabbitmq
    networks:
      - rabbitmq-cluster
    ports:
      - "15674:15672"
      - "5674:5672"
    depends_on:
      - rabbitmq1
    links:
      - rabbitmq1



networks:
  rabbitmq-cluster:
    external: true

最后那里是
external: true
是因为我自己把网络提前创建好了。如果没有的话就把最后一行删掉就行。
单独创建网络的命令是
docker network create rabbitmq-cluster

创建集群

image

先执行
docker-compose up -d
启动容器。启动以后访问本地的15672端口,用guest账户登陆。正常应该能看到一个节点在运行。
image
然后进入第一个节点的控制台,分别执行

rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app

进入第二个和第三个节点的控制台,执行

rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@rabbitmq1
rabbitmqctl start_app

完了以后控制台自动刷新应该能看到三个节点了
image

敢惹我?后悔也晚了

集群助手

很多资料说可以用其他途径实现,我尝试了一些,大部分没尝试。
比如文心一言说可以使用docker 镜像
everestoss/rabbitmq-cluster-discovery
来自动搭建集群。但是我怎么也搜不到这个镜像。

docker mirror

本来我配置了阿里的mirror,但是怎么拉也拉不下新镜像。知乎了半天找到一个
https://registry.dockermirror.com
,配到orbStack上替换了阿里的,速度很快。但是几个小时后也不能用了。什么情况?

控制台没法登陆

可能由于我之前测试了太多其他版本的rabbitMq,集群启动后
http://localhost:15672/
竟然打不开。准确说是打开了啥也看不到,F12也看不到报错。最后还是靠GPT3.5说清空浏览器缓存可以了。

其他版本

我之前是下载了3.12的镜像的,但是
rabbitmqctl
用不了。后来试了3.13也是一样的问题。3.7和3.8的镜像创建了容器连启动都启动不了,说数据库schema不对。最后还是用了3.6。

卷数据不能留

每次创建容器之前,上一次的卷一定要删掉。我是映射到文件夹里的data目录的,开始没删发现会影响新容器。所以上面说的3.8和3.9的schema不对有可能就是这个原因。

理论

我们需要一个数据结构维护树上的问题,仿照序列上的问题,我们需要一个方法快速的刻画出信息。

比如说线段树就通过分治的方式来通过将一个区间划分成
\(\log n\)
个区间并刻画出这
\(\log n\)
个区间的信息。

然后我们考虑把这个东西放到树上类比。你发现线段树上每个非叶节点都有两个儿子,那么你划分树上信息的方式应当也满足这个性质,也就是说把树划分成联通子图的过程中,应当每次合并两个联通子图,同时线段树只有两个端点(维护的是区间)所以这个联通子图应当只有两个界点(与其他联通子图公用的点,这里认为维护的是边集信息,联通子图间边集不交)。

接下来我们把一个划分出的联通子图称为簇。初始每条边都是一个簇。
在合并簇的过程中合并出新的簇也会以一条边的形式出现在树上用于下一次合并。

合并两个簇

分为两种。

compress

我们将一个二度点缩掉,或者说将对于两条相邻的边,若他们的公共点度数为
\(2\)
那么就把这两条边合并。合并出的新簇包含原来两个簇的边集,我们记这样的点是 C 点。

rake

对于一个度数为
\(1\)
的点
\(u\)
,其唯一的边为
\((u,v)\)
我们将
\((u,v)\)
这条边代表的簇与点
\(v\)
任意一个其他的边代表的簇合并。

最后不难发现的是只会剩下一条边。而合并的过程建出一棵树就是 Top tree。

静态构建

实际上根据全局平衡二叉树的建法可以给出一个
\(2 \times \log n\)
树高的构建方法。

首先按全局平衡二叉树的方法对原树划分,然后轻儿子先把所有簇收缩后 rake 到虚父亲上,对于一条轻儿子全部 rake 完成的重链再用全局平衡二叉树对重链的划分方式把重链上所有边 compress 成一条然后向上递归。

不过对于有多个轻儿子的点显然是有问题的。所以对于多个轻儿子在按照重量选取带权中点,每次按照中点分治,两个分治区间内的轻儿子 rake 成一条在 rake 到一起,还是重量平衡的,所以树高
\(2 \times \log n\)

按照这种方法建树,需要
\(2\)
倍空间。

维护点信息

对于每个簇我们维护的信息不包括界点信息,在合并信息的时候再把删除的界点的信息带上,那么你发现修改点只需要在他被删掉的簇时修改在一路爬上去就行了,时间复杂度是
\(O(\log n)\)

应用

动态 dp

维护矩阵(线性变换)

ABC351G

对于一个联通子图来说,若其中只有一个叶子的值不确定,我们可以把这个联通子图的根的 dp 值写成关于不确定叶子的值的线性变换形式,对于这道题目而言就是
\(y = k \times x + b\)
的一次函数形式。

更进一步地化简,因为
\(dp_{u} = a_{u} + \prod dp_v\)
,而
\(a_u\)
是一个很简单的项,所以不妨对于每个联通子图表示出
\(\prod dp_v = k \times dp_{x} + b\)
这里
\(x\)
表示联通子图中尚未确定的那个点的
\(dp\)
值。

#pragma GCC optimize("Ofast")
#include<bits/stdc++.h>
const int mod = 998244353;
//#define lowbit(x) (x&(-x))
using namespace std;
namespace IO{
    const int SIZE=1<<21;
    static char ibuf[SIZE],obuf[SIZE],*iS,*iT,*oS=obuf,*oT=oS+SIZE-1;
    int qr;
    char qu[55],c;
    bool f;
    #define getchar() (IO::iS==IO::iT?(IO::iT=(IO::iS=IO::ibuf)+fread(IO::ibuf,1,IO::SIZE,stdin),(IO::iS==IO::iT?EOF:*IO::iS++)):*IO::iS++)
    #define putchar(x) *IO::oS++=x,IO::oS==IO::oT?flush():0
    #define flush() fwrite(IO::obuf,1,IO::oS-IO::obuf,stdout),IO::oS=IO::obuf
    #define puts(x) IO::Puts(x)
    template<typename T>
    inline void read(T&x){
        for(f=1,c=getchar();c<48||c>57;c=getchar())f^=c=='-';
        for(x=0;c<=57&&c>=48;c=getchar()) x=(x<<1)+(x<<3)+(c&15);
        x=f?x:-x;
    }
    template<typename T>
    inline void write(T x){
        if(!x) putchar(48); if(x<0) putchar('-'),x=-x;
        while(x) qu[++qr]=x%10^48,x/=10;
        while(qr) putchar(qu[qr--]);
    }
    inline void Puts(const char*s){
        for(int i=0;s[i];++i)
            putchar(s[i]);
        putchar('\n');
    }
    struct Flusher_{~Flusher_(){flush();}}io_flusher_;
}
using IO::read;
using IO::write;
const int maxn = 4e5+114;
struct node{
	int u,v,id;
	int k,b;
	char type;
	//u 在上面 v 在下面
}cluster[maxn];
int n,m,a[maxn];
int pos[maxn],fa[maxn],ls[maxn],rs[maxn];
char type[maxn];//P 是边点 C 是 compress 点 R 是 rake 点
int root=1;//根簇
void compress(node x,node y,node &w){
	//x 在上面 y 在下面
	w.u=x.u;
	w.v=y.v;
	w.k=1ll*x.k*y.k%mod;
	w.b=(1ll*x.k*y.b%mod+1ll*x.k*a[x.v]%mod+x.b)%mod;
	pos[x.v]=w.id;
	fa[x.id]=fa[y.id]=w.id;
	ls[w.id]=x.id;
	rs[w.id]=y.id;
	//cout<<"compress"<<w.u<<" "<<w.v<<" "<<w.ans<<'\n';
	w.type='C';
	root=w.id;
}
void rake(node x,node y,node &w){
	//把 x rake 到 y 上
	w.u=x.u;
	w.v=y.v;
	w.k=1ll*y.k*(1ll*x.k*a[x.v]%mod+x.b)%mod;
	w.b=1ll*y.b*(1ll*x.k*a[x.v]%mod+x.b)%mod;
	pos[x.v]=w.id;
	fa[x.id]=fa[y.id]=w.id;
	ls[w.id]=x.id;
	rs[w.id]=y.id;
	//cout<<"rake"<<w.u<<' '<<w.v<<' '<<w.ans<<'\n';
	w.type='R';
	root=w.id;
}
void update(int u){
    if(u==0) return ;
    if(cluster[u].type=='C'){
        compress(cluster[ls[u]],cluster[rs[u]],cluster[u]);
        update(fa[u]);
    }else{
        rake(cluster[ls[u]],cluster[rs[u]],cluster[u]);
        update(fa[u]);
    }
}
vector<int> E[maxn];
int father_pos[maxn];//一个点到其父亲的边的簇编号
int father[maxn];
int son[maxn],sz[maxn],tot;
vector<int> st[maxn];//重链上的点存到链顶
void dfs1(int u){
	sz[u]=1;
	for(int v:E[u]){
		if(v==father[u]) continue;
		father[v]=u;
		father_pos[v]=++tot;
		cluster[tot].u=u,cluster[tot].v=v,cluster[tot].id=tot,cluster[tot].k=1,cluster[tot].b=0;
		dfs1(v);
		if(sz[v]>sz[son[u]]) son[u]=v;
		sz[u]+=sz[v];
	}
}
void dfs2(int u,int tp){
	st[tp].push_back(u);
	if(son[u]!=0) dfs2(son[u],tp);
	for(int v:E[u]){
		if(v==father[u]||v==son[u]) continue;
		dfs2(v,v);
	}
}
vector<int> vec[maxn];
vector<int> pre[maxn];
int solve(int l,int r,int u){
    if(l>r) return 0;
	if(l==r) return father_pos[vec[u][l]];
	int L=l,R=r;
	while(L+1<R){
		int mid=(L+R)>>1;
		if((pre[u][mid]-pre[u][l-1])*2<=(pre[u][r]-pre[u][l-1])) L=mid;
		else R=mid;
	}
	int mid=L;
	int lson=solve(l,mid,u);
	int rson=solve(mid+1,r,u);
	int res=++tot;
	cluster[tot].id=tot;
	rake(cluster[lson],cluster[rson],cluster[res]);
	return res;
}
int calc(int l,int r,int u){
    if(l>r) return 0;
    if(l==r) return father_pos[vec[u][l]];
	int L=l,R=r;
	while(L+1<R){
		int mid=(L+R)>>1;
		if((pre[u][mid]-pre[u][l-1])*2<=(pre[u][r]-pre[u][l-1])) L=mid;
		else R=mid;
	}
	int mid=L;
	int lson=calc(l,mid,u);
	int rson=calc(mid+1,r,u);
	int res=++tot;
    cluster[tot].id=tot;
	compress(cluster[lson],cluster[rson],cluster[res]);
	return res;
}
void dfs3(int u){
	for(int x:st[u]){
        if(son[x]==0) continue;
		pre[x].push_back(0);
		vec[x].push_back(0);
		for(int v:E[x]){
			if(v!=son[x]&&v!=father[x]){
				dfs3(v);
				//收缩 (x,v) 一个簇
				vec[x].push_back(v);
			}
		}
		//在对这些轻儿子簇按中点分治的方法合并起来
		for(int i=1;i<=vec[x].size()-1;i++){
			pre[x].push_back(pre[x][i-1]+sz[vec[x][i]]);
		}
		int rt=solve(1,vec[x].size()-1,x);
		if(rt!=0){
		    tot++;
		    cluster[tot].id=tot;
            rake(cluster[rt],cluster[father_pos[son[x]]],cluster[tot]);
            father_pos[son[x]]=tot;//rake 到重链上
		}
	}
	vec[u].clear();
	pre[u].clear();
	pre[u].push_back(0);
	vec[u].push_back(0);
	for(int x:st[u]){
		vec[u].push_back(x);
	}
	for(int i=1;i<=vec[u].size()-1;i++){
		pre[u].push_back(pre[u][i-1]+sz[father[vec[u][i]]]-sz[vec[u][i]]);
	}
	if(u!=1) father_pos[u]=calc(1,vec[u].size()-1,u);//把重链上的边 compress 成一条
	else father_pos[u]=calc(2,vec[u].size()-1,u);
	E[u].clear();
	E[u].push_back(father[u]);
	return ;
}
int sum;
int main(){
    read(n);
    read(m);
    for(int i=2;i<=n;i++){
        int p;
        read(p);
        E[p].push_back(i);
        E[i].push_back(p);
    }
    for(int i=1;i<=n;i++) read(a[i]);
    dfs1(1);
    dfs2(1,1);
    dfs3(1);
    while(m--){
        int x,v;
        read(x);
        read(v);
        a[x]=v;
        update(pos[x]);
        write(((1ll*cluster[root].k*a[cluster[root].v]+cluster[root].b)+a[cluster[root].u])%mod);
        putchar('\n');
    }
	return 0;
}

分治计算贡献

洛谷 P3781 切树游戏

注意到 Top tree 本身可以是一种树分治。

按照 Top tree 维护点集的套路,我们不将上下界点的异或值计入状态贡献,但是还是要开 dp 数组记录上下界点选或不选的 dp 值。

定义
\(F_i,G_i,D_i,Z_i\)
分别表示簇内选出一些点(不能选一个界点,可以选一个其他节点或者两个界点)且只包含上界点,只包含下界点,同时包含上下界点,上下界点均不包含的答案。有如下转移式(我们均认为将簇
\(x,y\)
合并为簇
\(w\)
):

对于一个 compress 节点:

\(F_{w,i} = \sum_{j \oplus k = i \oplus a_{v}} D_{x,j} \times F_{y,k} + F_{x,i} + D_{x,{i \oplus a_v}}\)

\(G_{w,i} = \sum_{j \oplus k = i \oplus a_v} G_{x,j} \times D_{y,k} + G_{y,i} + D_{y,{i \oplus a_v}}\)

\(D_{w,i} = \sum_{j \oplus k = i \oplus a_v} D_{x,j} \times D_{y,k}\)

\(Z_{w,i} = \sum_{j \oplus k = i \oplus a_v} G_{x,j} \times F_{y,k} + G_{x,i \oplus a_v} + F_{y,i \oplus a_v} + \left[i = a_v \right]\)

对于一个 rake 节点:

\(F_{w,i} = \sum_{j \oplus k = i} F_{x,j} \times F_{y,k} + \sum_{j \oplus k = i \oplus a_v} D_{x,j} \times F_{y,k} + F_{x,i} + F_{y,i} + D_{x,i \oplus a_v}\)

\(G_{w,i} = G_{y,i}\)

\(D_{w,i} = \sum_{j \oplus k = i} F_{x,j} \times D_{y,k} + \sum_{j \oplus k = i \oplus a_v} D_{x,j} \times D_{y,k} + D_{y,i}\)

\(Z_{u,i} = Z_{x,i} + G_{x,i \oplus a_v} + Z_{y,i}\)

然后你发现转移的瓶颈是异或卷积以及异或平移下标,第一个可以将原 dp 数组转变为 fwt 处理后的形式,第二个则可以这么解决:

\(a_{i \oplus C} = \sum_{j \oplus k = i} a_i \times \left[k = C \right] = a \otimes \left[i = C \right]\)

从而转变为异或卷积形式用 fwt 处理后的数组解决。

#include<bits/stdc++.h>
#define int long long
const int mod = 10007;
using namespace std;
const int maxn = 6e4+114;
const int maxv = 128;
struct node{
	int u,v,id;
    int f[maxv],g[maxv],d[maxv],z[maxv];
	char type;
}cluster[maxn];
int tran[maxv][maxv];
int n,m;
int a[maxn];
int pos[maxn],fa[maxn],ls[maxn],rs[maxn];
int root=1;
void compress(node x,node y,node &w){
	w.u=x.u;
	w.v=y.v;
    for(int i=0;i<maxv;i++){
        w.f[i]=((x.d[i]*y.f[i]*tran[a[x.v]][i] + x.f[i] + x.d[i]*tran[a[x.v]][i])%mod);
        w.g[i]=((x.g[i]*y.d[i]*tran[a[x.v]][i] + y.g[i] + y.d[i]*tran[a[x.v]][i]%mod)%mod);
        w.d[i]=((x.d[i]*y.d[i]*tran[a[x.v]][i])%mod);
        w.z[i]=((x.g[i]*y.f[i]*tran[a[x.v]][i] + x.g[i]*tran[a[x.v]][i] + y.f[i]*tran[a[x.v]][i] + tran[a[x.v]][i]+x.z[i]+y.z[i])%mod);
    }
	pos[x.v]=w.id;
	fa[x.id]=fa[y.id]=w.id;
	ls[w.id]=x.id;
	rs[w.id]=y.id;
	w.type='C';
	root=w.id;
}
void rake(node x,node y,node &w){
	//把 x rake 到 y 上
	w.u=x.u;
	w.v=y.v;
	for(int i=0;i<maxv;i++){
        w.f[i]=((x.f[i]*y.f[i] + x.d[i]*y.f[i]*tran[a[x.v]][i] + x.f[i] + y.f[i] + x.d[i]*tran[a[x.v]][i])%mod);
        w.g[i]=((y.g[i])%mod);
        w.d[i]=((x.f[i]*y.d[i] + x.d[i]*y.d[i]*tran[a[x.v]][i] + y.d[i])%mod);
        w.z[i]=((x.z[i] + x.g[i]*tran[a[x.v]][i] + y.z[i] + tran[a[x.v]][i])%mod);
	}
	pos[x.v]=w.id;
	fa[x.id]=fa[y.id]=w.id;
	ls[w.id]=x.id;
	rs[w.id]=y.id;
	w.type='R';
	root=w.id;
}
void update(int u){
    if(u==0) return ;
    if(cluster[u].type=='C'){
        compress(cluster[ls[u]],cluster[rs[u]],cluster[u]);
        update(fa[u]);
    }else{
        rake(cluster[ls[u]],cluster[rs[u]],cluster[u]);
        update(fa[u]);
    }
}
vector<int> E[maxn];
int father_pos[maxn];
int father[maxn];
int son[maxn],sz[maxn],tot;
vector<int> st[maxn];
int F[maxv],G[maxv],D[maxv],Z[maxv];
void dfs1(int u){
	sz[u]=1;
	for(int v:E[u]){
		if(v==father[u]) continue;
		father[v]=u;
		father_pos[v]=++tot;
		cluster[tot].u=u,cluster[tot].v=v,cluster[tot].id=tot;
		for(int i=0;i<maxv;i++) cluster[tot].f[i]=F[i],cluster[tot].g[i]=(G[i]),cluster[tot].d[i]=(D[i]),cluster[tot].z[i]=(Z[i]);
		dfs1(v);
		if(sz[v]>sz[son[u]]) son[u]=v;
		sz[u]+=sz[v];
	}
}
void dfs2(int u,int tp){
	st[tp].push_back(u);
	if(son[u]!=0) dfs2(son[u],tp);
	for(int v:E[u]){
		if(v==father[u]||v==son[u]) continue;
		dfs2(v,v);
	}
}
vector<int> vec[maxn];
vector<int> pre[maxn];
int solve(int l,int r,int u,char type){
    if(l>r) return 0;
	if(l==r) return father_pos[vec[u][l]];
	int L=l,R=r;
	while(L+1<R){
		int mid=(L+R)>>1;
		if((pre[u][mid]-pre[u][l-1])*2<=(pre[u][r]-pre[u][l-1])) L=mid;
		else R=mid;
	}
	int mid=L;
	int lson=solve(l,mid,u,type);
	int rson=solve(mid+1,r,u,type);
	int res=++tot;
	cluster[tot].id=tot;
	if(type=='R') rake(cluster[lson],cluster[rson],cluster[res]);
	else compress(cluster[lson],cluster[rson],cluster[res]);
	return res;
}
void dfs3(int u){
	for(int x:st[u]){
        if(son[x]==0) continue;
		pre[x].push_back(0);
		vec[x].push_back(0);
		for(int v:E[x]){
			if(v!=son[x]&&v!=father[x]){
				dfs3(v);
				vec[x].push_back(v);
			}
		}
		for(int i=1;i<=vec[x].size()-1;i++){
			pre[x].push_back(pre[x][i-1]+sz[vec[x][i]]);
		}
		int rt=solve(1,vec[x].size()-1,x,'R');
		if(rt!=0){
		    tot++;
		    cluster[tot].id=tot;
            rake(cluster[rt],cluster[father_pos[son[x]]],cluster[tot]);
            father_pos[son[x]]=tot;
		}
	}
	vec[u].clear();
	pre[u].clear();
	pre[u].push_back(0);
	vec[u].push_back(0);
	for(int x:st[u]){
		vec[u].push_back(x);
	}
	for(int i=1;i<=vec[u].size()-1;i++){
		pre[u].push_back(pre[u][i-1]+sz[father[vec[u][i]]]-sz[vec[u][i]]);
	}
	if(u!=1) father_pos[u]=solve(1,vec[u].size()-1,u,'C');
	else father_pos[u]=solve(2,vec[u].size()-1,u,'C');
	E[u].clear();
	E[u].push_back(father[u]);
	return ;
}
void fwt_xor(int *Q,int x=1){
    for(int o=2,k=1;o<=maxv;o<<=1,k<<=1){
        for(int i=0;i<maxv;i+=o)
            for(int j=0;j<k;j++) Q[i+j]=(Q[i+j]+Q[i+j+k])%mod,Q[i+j+k]=(Q[i+j]+mod+mod-Q[i+j+k]-Q[i+j+k])%mod,Q[i+j]=Q[i+j]*x%mod,Q[i+j+k]=Q[i+j+k]*x%mod;
    }
}
int mx;
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    D[0]=1;
    fwt_xor(F);
    fwt_xor(G);
    fwt_xor(D);
    fwt_xor(Z);
    for(int i=0;i<maxv;i++){
        tran[i][i]=1;
        fwt_xor(tran[i]);
    }
    cin>>n>>mx;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    for(int i=2;i<=n;i++){
        int u,v;
        cin>>u>>v;
        E[u].push_back(v);
        E[v].push_back(u);
    }
    dfs1(1);
    dfs2(1,1);
    dfs3(1);
    cin>>m;
    while(m--){
        string opt;
        cin>>opt;
        if(opt=="Change"){
            int x,y;
            cin>>x>>y;
            a[x]=y;
            update(pos[x]);
        }else{
            int k;
            cin>>k;
            for(int i=0;i<maxv;i++) F[i]=cluster[root].f[i],G[i]=cluster[root].g[i],D[i]=cluster[root].d[i],Z[i]=cluster[root].z[i];
            fwt_xor(F,(mod+1)/2);
            fwt_xor(G,(mod+1)/2);
            fwt_xor(D,(mod+1)/2);
            fwt_xor(Z,(mod+1)/2);
            cout<<(F[k^a[cluster[root].u]]+G[k^a[cluster[root].v]]+D[k^a[cluster[root].u]^a[cluster[root].v]]+Z[k]+(k==a[cluster[root].u])+(k==a[cluster[root].v]))%mod<<'\n';
        }
    }
    return 0;
}

邻域上的 dp

能维护子树链的 dp 的结构不少,看看邻域上的 dp 的维护。

P8498 树上邻域数点

找到最大的被给定邻域覆盖的簇,它在 Top tree 上的子树内的簇同样被完全覆盖,提示我们可以处理出每个邻域的信息,而由于它的父亲没有被完全覆盖,所以
\(d < sz_{fa}\)
,并且在这个簇外的邻域形如以界点为根的子树中深度不超过
\(d - dis_{u,v}\)
的所有点的深度信息,而这个深度显然小于
\(sz_{fa}\)
,并且对于一个父亲簇需要处理的界点只有
\(4\)
个,而 Top tree 的树高为
\(\log n\)
代表其子树大小和为
\(O(n \log n)\)
级别。总之,如果我们有办法求出这
\(O(n \log n)\)
个信息,就有可能做到快速合并。

由于状态总量很少,考虑 dp。

我们定义
\(dp_{u,0/1,i}\)
表示簇
\(u\)
的上界点与下界点在其簇内的
\(i\)
邻域信息,显然有
\(i \leq sz_u\)
,所以在构建 Top tree 时可以在 rake 与 compress 时暴力合并,总合并量级还是
\(O(n \log n)\)
的。同时在这个过程中顺便维护出每个簇所有边集与以其两个界点为点集的信息,

然后考虑换根 dp。

定义
\(g_{u,0/1,i}\)
表示簇
\(u\)
的上界点与下界点在簇外的
\(i\)
邻域信息,我们在知道
\(g_{u,0/1,i}\)

\(f_{ls_{u},0/1.i}\)
后可以
\(O(sz_{u})\)
地求出
\(g_{u,0/1,i}\)
这个状态数位
\(O(sz_u)\)
个的 dp 数组。那么便从上到下的 dp 求解出所有
\(g_{u,0/1,i}\)

#ifndef CIRCLE_H
#define CIRCLE_H
#include<vector>
struct info{
  unsigned val;
  unsigned poi[2];
};
const info emptyinfo=info{0,(unsigned)-1,(unsigned)-1};
info MR(info a,info b);
info MC(info a,info b);
void init(int T,int n,int q,std::vector<int>dad,std::vector<info>ve,int M);
bool isempty(info a);
info ask(int x,int d);
#endif
#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e5+114;
vector<int> E[maxn];
info edge[maxn];//点 i 到其父亲的信息
struct node{
	int u,v,id,dis;//包括界点
	int len,maxu,maxv;//维护直径
	vector<info> fu,fv,gu,gv;//子树内距离两个界点 k 邻域信息/子树外距离两个界点 k 邻域信息 第一个处理到自己簇大小 第二个处理到父亲簇大小
	info all;//整个簇的信息
 	char type;
	//u 在上面 v 在下面
}cluster[maxn];
int pos[maxn],fa[maxn],ls[maxn],rs[maxn];//pos 表示每个点所在的最小簇
char type[maxn];//P 是边点 C 是 compress 点 R 是 rake 点
int root=1;//根簇
info R(info a,info b){
    if(isempty(a)==true) return b;
    if(isempty(b)==true) return a;
    return MR(a,b);
}
info C(info a,info b){
    if(isempty(a)==true) return b;
    if(isempty(b)==true) return a;
    return MC(a,b);
}
info queryf(node &u,int p,char type){
    if(p>=(int)u.fu.size()) p=(int)u.fu.size()-1;
    if(p<0) return emptyinfo;
    else return (type=='u'?u.fu[p]:u.fv[p]);
}
info queryg(node &u,int p,char type){
    if(p>=(int)u.gu.size()) p=(int)u.gu.size()-1;
    if(p<0) return emptyinfo;
    else return (type=='u'?u.gu[p]:u.gv[p]);
}
void compress(node &x,node &y,node &w){
	//x 在上面 y 在下面
	w.u=x.u,w.v=y.v;
    w.len=max(max(x.len,y.len),x.maxv+y.maxu);
    w.maxu=max(x.maxu,x.dis+y.maxu);
    w.maxv=max(y.maxv,y.dis+x.maxv);
    w.dis=x.dis+y.dis;
    w.all=C(x.all,y.all);
    w.fu.push_back(emptyinfo);
    w.fv.push_back(emptyinfo);
    for(int i=1;i<=w.len;i++){
        w.fu.push_back(C(queryf(x,i,'u'),queryf(y,i-x.dis,'u')));
        w.fv.push_back(C(queryf(x,i-y.dis,'v'),queryf(y,i,'v')));
    }
	fa[x.id]=fa[y.id]=w.id;
	ls[w.id]=x.id;
	rs[w.id]=y.id;
	w.type='C';
	root=w.id;
}
void rake(node &x,node &y,node &w){
	//把 x rake 到 y 上
	w.u=y.u,w.v=y.v;
	w.len=max(max(x.len,y.len),y.maxu+x.maxu);
    w.maxu=max(x.maxu,y.maxu);
    w.maxv=max(y.maxv,x.maxu+y.dis);
	w.dis=y.dis;
	w.all=R(y.all,x.all);
	w.fu.push_back(emptyinfo);
	w.fv.push_back(emptyinfo);
	for(int i=1;i<=w.len;i++){
        w.fu.push_back(R(queryf(y,i,'u'),queryf(x,i,'u')));
        w.fv.push_back(R(queryf(y,i,'v'),queryf(x,i-y.dis,'u')));
    }
	fa[x.id]=fa[y.id]=w.id;
	ls[w.id]=x.id;
	rs[w.id]=y.id;
	w.type='R';
	root=w.id;
}
int father_pos[maxn];//一个点到其父亲的边的簇编号
int father[maxn];
int son[maxn],sz[maxn],tot,dep[maxn];
int top[maxn];
vector<int> st[maxn];//重链上的点存到链顶
void dfs1(int u){
	sz[u]=1;
	for(int v:E[u]){
        dep[v]=dep[u]+1;
        father[v]=u;
		father_pos[v]=++tot;
		pos[u]=pos[v]=tot;
		cluster[tot].u=u,cluster[tot].v=v,cluster[tot].id=tot,cluster[tot].dis=1,cluster[tot].len=1,cluster[tot].maxu=1,cluster[tot].maxv=1,cluster[tot].all=edge[v],cluster[tot].fu.push_back(emptyinfo),cluster[tot].fu.push_back(edge[v]),cluster[tot].fv.push_back(emptyinfo),cluster[tot].fv.push_back(edge[v]);
		dfs1(v);
		if(sz[v]>sz[son[u]]) son[u]=v;
		sz[u]+=sz[v];
	}
}
void dfs2(int u,int tp){
    top[u]=tp;
	st[tp].push_back(u);
	if(son[u]!=0) dfs2(son[u],tp);
	for(int v:E[u]){
		if(v==son[u]) continue;
		dfs2(v,v);
	}
}
int LCA(int u,int v){
    while(top[u]!=top[v]){
        if(dep[top[u]]<dep[top[v]]) swap(u,v);
        u=father[top[u]];
    }
    if(dep[u]<dep[v]) swap(u,v);
    return v;
}
int dis(int u,int v){
    return dep[u]+dep[v]-2*dep[LCA(u,v)];
}
vector<int> vec[maxn];
vector<int> pre[maxn];
int solve(int l,int r,int u){
	if(l==r) return father_pos[vec[u][l]];
	int L=l,R=r;
	while(L+1<R){
		int mid=(L+R)>>1;
		if((pre[u][mid]-pre[u][l-1])*2<=(pre[u][r]-pre[u][l-1])) L=mid;
		else R=mid;
	}
	int mid=L;
	int lson=solve(l,mid,u);
	int rson=solve(mid+1,r,u);
	int res=++tot;
	cluster[tot].id=tot;
	rake(cluster[lson],cluster[rson],cluster[res]);
	return res;
}
int calc(int l,int r,int u){
    if(l==r) return father_pos[vec[u][l]];
	int L=l,R=r;
	while(L+1<R){
		int mid=(L+R)>>1;
		if((pre[u][mid]-pre[u][l-1])*2<=(pre[u][r]-pre[u][l-1])) L=mid;
		else R=mid;
	}
	int mid=L;
	int lson=calc(l,mid,u);
	int rson=calc(mid+1,r,u);
	int res=++tot;
    cluster[tot].id=tot;
	compress(cluster[lson],cluster[rson],cluster[res]);
	return res;
}
void dfs3(int u){
	for(int x:st[u]){
        if(son[x]==0) continue;
		pre[x].push_back(0);
		vec[x].push_back(0);
		for(int v:E[x]){
			if(v!=son[x]){
				dfs3(v);
				//收缩 (x,v) 一个簇
				vec[x].push_back(v);
			}
		}
		//在对这些轻儿子簇按中点分治的方法合并起来
		for(int i=1;i<=(int)vec[x].size()-1;i++){
			pre[x].push_back(pre[x][i-1]+sz[vec[x][i]]);
		}
		if(vec[x].size()>=2){
            int rt=solve(1,(int)vec[x].size()-1,x);
            if(rt!=0){
                tot++;
                cluster[tot].id=tot;
                rake(cluster[rt],cluster[father_pos[son[x]]],cluster[tot]);
                father_pos[son[x]]=tot;//rake 到重链上
            }
		}
	}
	vec[u].clear();
	pre[u].clear();
	pre[u].push_back(0);
	vec[u].push_back(0);
	for(int x:st[u]){
		vec[u].push_back(x);
	}
	for(int i=1;i<=(int)vec[u].size()-1;i++){
		pre[u].push_back(pre[u][i-1]+sz[father[vec[u][i]]]-sz[vec[u][i]]);
	}
	if(u!=1) father_pos[u]=calc(1,(int)vec[u].size()-1,u);//把重链上的边 compress 成一条
	else father_pos[u]=calc(2,(int)vec[u].size()-1,u);
	E[u].clear();
	E[u].push_back(father[u]);
	return ;
}
void DP(int u){
    if(ls[u]==0) return ;
    if(cluster[u].type=='C'){
        cluster[ls[u]].gu.push_back(emptyinfo);
        cluster[ls[u]].gv.push_back(emptyinfo);
        for(int i=1;i<=cluster[u].len;i++) cluster[ls[u]].gu.push_back(queryg(cluster[u],i,'u')),cluster[ls[u]].gv.push_back(C(queryf(cluster[rs[u]],i,'u'),queryg(cluster[u],i-cluster[rs[u]].dis,'v')));
        cluster[rs[u]].gu.push_back(emptyinfo);
        cluster[rs[u]].gv.push_back(emptyinfo);
        for(int i=1;i<=cluster[u].len;i++) cluster[rs[u]].gu.push_back(C(queryf(cluster[ls[u]],i,'v'),queryg(cluster[u],i-cluster[ls[u]].dis,'u'))),cluster[rs[u]].gv.push_back(queryg(cluster[u],i,'v'));
    }else{
        cluster[ls[u]].gu.push_back(emptyinfo);
        cluster[ls[u]].gv.push_back(emptyinfo);
        for(int i=1;i<=cluster[u].len;i++) cluster[ls[u]].gu.push_back(R(queryg(cluster[u],i,'u'),C(queryf(cluster[rs[u]],i,'u'),queryg(cluster[u],i-cluster[rs[u]].dis,'v')))),cluster[ls[u]].gv.push_back(emptyinfo);
        cluster[rs[u]].gu.push_back(emptyinfo);
        cluster[rs[u]].gv.push_back(emptyinfo);
        for(int i=1;i<=cluster[u].len;i++) cluster[rs[u]].gu.push_back(R(queryg(cluster[u],i,'u'),queryf(cluster[ls[u]],i,'u'))),cluster[rs[u]].gv.push_back(queryg(cluster[u],i,'v'));
    }
    DP(ls[u]);
    DP(rs[u]);
    //默认将界点 u 的簇外信息合并上自己簇的信息
    for(int i=0;i<=cluster[u].len;i++){
        cluster[ls[u]].gu[i]=C(cluster[ls[u]].gu[i],cluster[ls[u]].all);
        cluster[rs[u]].gu[i]=C(cluster[rs[u]].gu[i],cluster[rs[u]].all);
    }
}//Top tree 上换根 dp
char check(node &u,int p){
    if(p==u.u) return 'u';
    else return 'v';
}
info ask(int u, int d){
    if(d==0) return emptyinfo;
    int now=pos[u];
    while(cluster[fa[now]].len<d) now=fa[now];
    return C(queryg(cluster[now],d-dis(cluster[now].u,u),'u'),queryg(cluster[now],d-dis(cluster[now].v,u),'v'));
}
void init(int T, int n, int q, vector<int> FA, vector<info> e, int M){
    for(int i=1;i<n;i++){
        E[FA[i-1]].push_back(i+1);
        edge[i+1]=e[i-1];
    }
    dfs1(1);
    dfs2(1,1);
    dfs3(1);
    DP(root);
    cluster[root].len=n;
    return ;
}

其他应用

待补充。

主题:

aspnetcorewebapi项目,提交到gitlab,通过jenkins(gitlab的ci/cd)编译、发布、推送到k8s。

关于gitlab、jenkins、k8s安装,都是使用docker启动服务。

首先新建一个项目,为了方便浏览就把swaggerr非开发环境不展示去掉

下面就是需要准备Dockerfile和k8s.yaml文件,这里不应该用net5,过时了。

FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS baseCOPY ./app
WORKDIR
/app
EXPOSE
5000/tcp
ENV ASPNETCORE_URLS http:
//*:5000/ ENV TZ=Asia/Shanghai

# Work around
forbroken dotnet restore
ADD http:
//ftp.us.debian.org/debian/pool/main/c/ca-certificates/ca-certificates_20210119_all.deb . RUN dpkg -i ca-certificates_20210119_all.deb

# soft link
RUN ln
-snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN ln
-s /lib/x86_64-linux-gnu/libdl-2.24.so /lib/x86_64-linux-gnu/libdl.so

# install System.Drawing native dependencies
RUN apt
-getupdate \&& apt-get install -y --allow-unauthenticated \
ca
-certificates \&& update-ca-certificates \
libgdiplus \
&& rm -rf /var/lib/apt/lists/*ENTRYPOINT ["dotnet", "autopubtest.dll"]

apiVersion: apps/v1
kind: Deployment
metadata:
name: {deployName}
labels:
app: {deployName}
namespace: defaultspec:
replicas:
2selector:
matchLabels:
app: {deployName}
template:
metadata:
labels:
app: {deployName}
spec:
nodeSelector:
group: web
containers:
-name: {containerName}
image: {imageRegistry}
/{imageName}:{imageTag}
volumeMounts:
- name: config-volume
mountPath:
/app/appsettings.Production.json
subPath: appsettings.Production.json
env:
-name: ASPNETCORE_ENVIRONMENT
value: Production
ports:
- containerPort: 5000volumes:- name: config-volume
configMap:
name: autopubtest
-config
imagePullSecrets:
- name: docker-secret---kind: Service
apiVersion: v1
metadata:
name: {serviceName}
labels:
app: {serviceName}
namespace: defaultspec:
selector:
app: {deployName}
ports:
-name: {serviceName}
port:
5000protocol: TCP
targetPort:
5000

这里需要注意的是configMap的name是我们需要再K8S里面建的appsettings.环境.json文件

  configMap:
          name: autopubtest-config

一切准备就绪,本地需要有docker环境,就能验证dockerfile是否有报错,我本地是dockerdesktop。

下面就先把代码提交到gitlab,我是用develop自建分支,而且我用的是http

这里gitlab

v17.1.1

有一个问题就是默认会把容器的id当成请求的ip地址,通过git 的git或者http拉取代码这里都会有问题,进入gitlab的容器内部找到 /etc/gitlab/gitlab.rb找到external_url注释掉的一行,改下你实际的地址和端口就行。

这里稍微提一下gitlab的ci/cd,本篇主要是jenkins。

gitlab安装完默认密码存放在 /etc/gitlab/initial_root_password ,默认用户root

networks:指定唯一,在服务器中新建一个networks,方便一个网段通信,如果是分开的服务器就是用ip或者其他。
register runner的时候手敲,ip指定gitlab容器的内网ip,查看命令 docker inspect docker容器id,类似这样的,下面提示就是成功注册一个runner
Registering runner... succeeded                     runner=
gitlab-runner register \--url http://gitlab的docker的ip \
  --registration-token gitlab runners中的token \--executor docker \--description "My Docker Runner"\--docker-image "alpine:latest"
这里是安装gitlab和gitlab-runner的docker-compose.yml 
version: '3.3'services:
gitlab:
image: gitlab
/gitlab-ce:latest
container_name: gitlab
ports:
- "80:80"networks:- my-network

gitlab
-runner:
image: gitlab
/gitlab-runner:latest
container_name: gitlab
-runner
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- my-network

networks:
my
-network:
driver: bridge

只要在项目中新增.gitlab-ci.yml,再把类似jenkins的shell操作放到文件中就可以了。这里有一个测试的文件,tags很重要,注册runner的时候指定需要的,再在文件中配置了,就会按照流程。

stages:          # List of stages forjobs, and their order of execution-build-test-deploy

build
-job: # This job runs inthe build stage, which runs first.
stage: build
tags: # Add the tags here
-docker-linux
script:
- echo "Compiling the code..." - echo "Compile complete."unit-test-job: # This job runs inthe test stage.
stage: test # It only starts when the job
inthe build stage completes successfully.
tags: # Add the tags here
-docker-linux
script:
- echo "Running unit tests... This will take about 60 seconds." - sleep 60 - echo "Code coverage is 90%"lint-test-job: # This job also runs inthe test stage.
stage: test # It can run at the same time
as unit-test-job (inparallel).
tags: # Add the tags here
-docker-linux-fast
script:
- echo "Linting code... This will take about 10 seconds." - sleep 10 - echo "No lint issues found."deploy-job: # This job runs inthe deploy stage.
stage: deploy # It only runs when
*both* jobs inthe test stage complete successfully.
tags: # Add the tags here
-docker-linux-fast
environment: production
script:
- echo "Deploying application..." - echo "Application successfully deployed."

上面仅仅只是一个测试完整流程文件,不涉及docker打包操作,需要docker打包的话runner就需要安装,安装模式有几种,自行查资料。

下面介绍jenkins的操作

这里提一提,通过git拉取代码,需要在jenkins的容器内部生成.ssh的公钥私钥,公钥添加到gitlab的ssh中,私钥就放到jenkins的全局变量中,Credentials就可以选择你的验证方式了。

下面的选择会影响你拉取代码,第一个设置你有可能需要在jenkins容器内部拉取一次代码,最后一个设置可以通过http拉。

下面继续:

这里我有三个步骤,编译,发布,K8S拉取镜像

第一部分

#
!/bin/bash

echo
"========== 当前 Branch: $GIT_BRANCH =========="echo"========== Commit Hash: $GIT_COMMIT =========="cd $WORKSPACE/src/autopubtest/dotnet restoreif [ -d $WORKSPACE/publish ]; then
rm
-rf $WORKSPACE/publish
fi

dotnet publish
-c Release -o $WORKSPACE/publish --no-restoreif [ $? -ne 0]; then
echo
"!!!!!!!!!!编译失败!!!!!!!!!!"exit1 elseecho"<<<<<<<<<<编译成功>>>>>>>>>>"fi


第二部分

#
!/bin/bash
ob
=`echo $jobName|tr 'A-Z' 'a-z'|cut -d '.' -f 2`
imageName
="autopubtest-api"#imageTag=$TagName
imageTag
=`echo "$GIT_COMMIT" | cut -b1-8`
pushRegistry
=镜像仓库/项目名

echo
"========== 开始构建镜像 =========="docker login-u 仓库账号 -p 仓库密码 $pushRegistry
cd $WORKSPACE
/publish
docker build
--rm -t $imageName:$imageTag -f $WORKSPACE/docker/Dockerfile .if [ $? -ne 0]; then
echo
"!!!!!!!!!!镜像构建失败!!!!!!!!!!"exit1 elseecho"<<<<<<<<<<镜像构建成功 $imageName:$imageTag>>>>>>>>>>"fi


docker tag $imageName:$imageTag $pushRegistry
/$imageName:$imageTag
docker push $pushRegistry
/$imageName:$imageTagif [ $? -ne 0]; then
echo
"!!!!!!!!!!镜像发布失败!!!!!!!!!!"exit1 elseecho"<<<<<<<<<<镜像发布成功 $imageName:$imageTag>>>>>>>>>>"fi

docker rmi $imageName:$imageTag
docker rmi $pushRegistry
/$imageName:$imageTag


第三部分

projectName
="autopubtest-api"#imageTag=$TagName
imageTag
=`echo "$GIT_COMMIT" | cut -b1-8`
deployName
=$projectName
serviceName
=$projectName
containerName
=$projectName
imageName
=$projectName
git_message
=`git log --format=format:%s -1${GIT_COMMIT}`
pullRegistry
=仓库地址/项目名

cat $WORKSPACE
/docker/k8s.yaml | sed 's|'extensions/v1beta1'|'apps/v1'|g; s|{imageRegistry}|'$pullRegistry'|g; s|{imageName}|'$imageName'|g; s|{imageTag}|'$imageTag'|g; s|{deployName}|'$deployName'|g; s|{serviceName}|'$serviceName'|g; s|{containerName}|'$containerName'|g' > $WORKSPACE/docker/k8s.value
sed
-i '/^---/,$d' $WORKSPACE/docker/k8s.value

kubectl apply
-f $WORKSPACE/docker/k8s.valueif [ $? -ne 0]; then
echo
"!!!!!!!!!!更新失败,Deployment $deployName 可能不存在,尝试创建该Deployment!!!!!!!!!!"kubectl create-f $WORKSPACE/docker/k8s.value
fi

构建的日志就略过,这里使用的是harbor仓库,注需要注意,docker login需要登陆harbor的仓库,在harbor主机host通过ip地址映射一个随意取名的域名,不要用ip,否则触发https安全检查。

jenkins的第三步,会触发k8s去pull仓库镜像。关于jenkins和k8s的关联就是把k8s主机的config文件拷贝到jenkins的 ./var/jenkins_home/root/.kube/config

当K8S拉取镜像后,服务正常启动。

配置字典里新建autopubtest的appsettings.Production.json文件,该名称需要与k8s。yaml的对应起来autopubtest-config

新建下面的服务

下面就能正常使用接口了

前言

大家好,我是
山里看瓜
,一个有三年开发经验的前端搬砖仔。本期想跟大家分享一下我的一些经历以及我是怎么从一个编程课次次都差点挂科的学渣一步步通过自学到找到还算理想的前端开发工作的。

希望有类似经历或想法的同学可以从中看到或明白一些什么。当然我不是个多优秀的人,只能说从曾经的我到现在,我自己看来还算过得去,自己认定的一些事我也都去实现了。

“中等偏上”的我

别人的一生璀璨精彩,而我这一生可以说是平庸。从小学开始我就都是不会受老师批评也不会拿奖的那种孩子,成绩没有很差也没有很好。
“中等偏上”
是我一直以来的标签,这也不知不觉让我养成了一种
不想争也不颓废
的状态。

7e20d919b039479c9b0ac8dc5e06011f.gif

上初中不是尖子班,但又是普通班中的尖子班(除了尖子班我们班成绩最好),在班里我不是前三名,但从来没跌出过前十五(虽然我们前三也没多少分)。初中我们在镇上上学,尖子班目标大多考进市里重点高中,而我们班很多人的目标只是县里的所谓重点高中,市里县里都叫一中,但两者可谓天差地别,一个高考一本是保底,一个一本是顶点。

而我,当时脑回路可谓及其清奇,
比上不足又不想比下
,毅然决然选了一个不是市里不是县里的高中上学(在所谓的开发区,离家贼远),至今想不通当时是为何会想到这么个地方,但这选择真莫名符合我的性格。我的见识真的不多,但是每次到这种稍重要的选择时刻,
我的脑子里好像住着一个不属于自己的一小部分的自我,有时会莫名跳脱,冒出些与别人甚至是与自己不是那么相似的想法,
最后高中我成了我们班上为数不多考上大学的人,其他要好的朋友大多选择二战。我就这样带着我高中的中等偏上进入大学。

大学时期的混子

不知道大家有没有跟我一样的感受,
生来好像就注定会离家越来越远
。我大学在离家一千多公里外的地方上学,我还清晰记得第一次去上学,当时一个人坐的火车,30多个小时的硬座,火车上有很多送孩子上学的父母,也好象是有同校的同学,我当时淡定沉静得可怕,没有任何不适没有任何不开心甚至是没有任何高昂或低沉的情绪。到如今我唯一清楚的记得的是我把每一节能走的车厢来回一共走了23遍。扯远了扯远了~

大学干得最多的事就是打游戏,其次就是打篮球。当时才拥有自己的电脑,就好像把那个高中包夜玩电脑的劲带了过来,一天就猛玩,作业从来都是课前补,上课总是打瞌睡,甚至一些课还偷偷玩手机游戏。一到下午或者周末,大多是出去打球,电脑可以说是几乎没用来编程。

5d1972fb209d4ac39c4c5f479668e86a.gif

大一到大三这三年,几乎没怎么专心搞学习,每学期能堪堪不挂科基本可以说是全靠考前熬夜恶补,平时玩得有多欢,考前就有多拼,主修的编程课也很少用心学,当时是感兴趣的,一开始也敲得还行,但慢慢的随着越来越放纵和没有人约束,所有的兴趣都被每天玩乐的即时快感所代替。而且编程这条路是没有捷径的,只有靠一步一个脚印和不断地学习积累才能走得更远,这导致我对所有的编程课也越来越没兴趣,知道后期考试全靠死记硬背,根本不理解为什么要这么写。

临近毕业的迷茫

我们大学的专业课到基本在大三这年学完,最后的一年是参加各种活动、选修补学分,以及完成自己的毕业设计。当然对于我们这种菜鸟来说,这毕设真的让我们脱层皮,各种找人问,各种查资料。很直观的说明了我们高中班主任说的那句话:
“学习上欠的债,迟早都是要还的。”

7ff913bf9c734aecbe1f2c0cc97edbee.gif

通过这次毕设,也让我捡起来了一点点对编程的兴趣。加上这一年需要实习,看着很多同学都有自己清晰的目标和去处,有的考研,有的考编,有的拿了好几家公司的offer在纠结要去哪里。像我们这样的,一部分是在迷茫中不知道以后的路在哪里,剩下的去了那听起来高大上的“管培生”——就是那个比厂里打螺丝工资还低的岗位。

960871f235204883bdc8343264a2789c.gif

而当时的我,没有任何技能傍身,专业课也没学好,真完全不知道以后要做啥。之前跟朋友吹牛说:咱学计算机的,以后就去拉网线吧,实在不行就去送外卖,黄袍加身。你别说,你还真别说,外卖我送过,干了半个月,收入七八百,最后付了租车押金一千二,倒亏五百。然后去宽带公司拉过网线(也算是专业相关了),我当时不可能想到我之后会是个程序员,那时候在我眼里,这是只有那种学霸才能干的工作。

努力只要开始就不会晚:自学前端

确定方向,规划学习路线

直到大四下学期,我依然每天混日子,不知道以后会做什么,甚至有过进厂打螺丝的想法。转变是在一次跟我朋友酒后聊天,当时他问我以后想做什么,想成为什么样的人,我当时愣住了五秒,脑海里闪过的是这几年我的不务正业,我这时候是真的后悔啊,想着我要是一开始就好好学就好了。

当时朋友的一句话对我影响很大:
“不要为了过去的事后悔,过去没法改变,努力只要开始就不会晚”

006c1edf6ead4e31a111d1d5c2df729e.gif

当晚喝得很醉,但是这句话在第二天甚至到今天,我依然深深的记在了心里。中午起床后,我开始思考:我想做什么?我是学编程的,我对编程一开始是非常有兴趣的,只是后来荒废了。—— 所以我很快想清楚确定了,我以后要当程序员。

当时同学中比较多的是干java的,而我是那种喜欢有视觉即时反馈刺激的人,或者说一开始我不是很喜欢跟数据打交道的人。所以我在想,还有那些职业岗位我能做,除了大学的专业课,我至今觉得大学里最重要的课是最后一年的大学生就业指导课,当时我们的老师给我说了一个概念:面向面试学习。我真的很感谢当时的老师,虽然没有直接教会我什么技能,但是让我懂得了这个我现在依然觉得很重要的方法。

我开始上boss直聘搜索程序员的相关岗位,java、c++、大数据... 直到我看到前端开发这个岗位,我才确定这是我想做的职业,我没有任何过硬的专业技能,所以一开始我只看岗位职责,看这个岗位需要做什么。我觉得前端的岗位描述非常符合我想从事职业的特点。

c6fc11b3ea1b4de5b9e6bcadb5db34cc.gif

确定岗位之后,就是看怎么才能够找到相关工作。这就用到我前边说的面向面试学习,我看了大概二十来个前端岗位招聘的要求,看他需要掌握哪些技能,我把它们按照这些岗位出现的次数排序。然后就是找一些别人总结的学习路线,这其中有两个前期对我影响比较大的大佬:
程序员鱼皮和技术胖
。我自己的学习过程中有参考过这两个大佬分享的学习路线,然后综合自己总结的招聘要求上的技能,整理出了一份符合自己的学习路线。
知道并明确方向这在一开始非常重要

坚持不懈,只要出发了总能到达

过程其实总结下来很简单:坚持、坚持、还是™的坚持。在这段学习期间中午几乎没有午休过,游戏更是没玩,为了担心自己控制不住,我的王者账号被自己注销,所有游戏都被我卸载。晚上更是每天学习到一两点。因为有方向有路线,我按部就班一点一点的学习前端知识,慢慢完善自己的技能。这半年多的时间我的努力程度是我高考时期的数倍。终于,我能独立写出一些网上的项目。大多数常用知识点我都能说上一些,于是我开始准备简历准备面试。

痛苦面经,在失败中总结提升自己

然而第一次前端面试就给了我一个大大的耳光,我被面试官虐得体无完肤,一开始还能答得上来几个,慢慢的甚至自己知道的都说的一塌糊涂,关于业务关于项目更是说得一言难尽。

54b0e9c35ef040afab24dd59355ff2b7.gif

然后我开始反思自己,是哪里做得不够好。我重新完善我的简历,开始准备面试自我介绍,每次面试我都会偷偷录下自己的面试过程,事后针对面试说到我不会的点去加强学习,就这样知道面试第十次,是的第十次。我终于被通知一面过了,后续约了我进行二面。虽然最后这个公司没有拿到offer,但真的是向前迈进了一大步。

成功上岸,第一份前端工作

再往后免了大概五六家,我比以前更加自信,面试过程部署对答如流,也是基本能答上来,对业务项目这些都更了解也更说得清楚,以前是会做但说不上来。这半个月期间,我拿到了三家公司的offer,虽然工资都不高。但是这种自己的努力换来了成果的感觉,真的比什么都痛快。最后也是综合选择了一家小公司,这就是我的第一份前端工作,在这家公司我也学到了很多。

fd3e33a49f6b4275b8538df6897bb1c2.gif

一些感想和建议

我能走到今天,成为一名程序员,我觉得跟哔哩哔哩关系很大,我想说,b站真的是个学习网站,只要是你想学的你都能找到相关教学视频(当然这更适合新手,大佬都是看书)。然后我看的第一个前端视频是pink老师的前端视频,我觉得真的有趣且通俗易懂,入门推荐真的。

这个世界上其实大部分人还没有到那种需要拼天赋的程度,大家都是普通人,只要你想,别人能做的你也能做。这是我一直相信的。

然后就是:
很多事,努力真的会有回报。

写在后面

你做了很多事,到后来你回头看看,好像所有发生的事都是恰好,恰好你当初那么做了,恰好你做了那个选择。所以如果你有什么目标有什么想法,不用管它有多难,
先做,先让自己动起来
。等有一天你回头看,会说:这也就这样嘛,没多难啊。

你有什么样的编程学习经历呢,可以分享评论区一起聊聊~

技术背景

关于拉氏图的更多介绍,可以参考下
这篇博客
,这里简单引述一部分内容:

Ramachandran plot(拉氏图)是由G. N. Ramachandran等人于1963年开发的,用来描述蛋白质结构中氨基酸残基二面角
\(\psi\)

\(\phi\)
是否在合理区域的一种可视化方法。同时也可以反映出该蛋白质的构象是否合理。

思路是比较简单的,就是找到一个蛋白质主链中的C,C
\(_{\alpha}\)
,N,O这几种原子,然后计算对应共价键的二面角即可。计算相关内容可以参考我之前写过的这两篇文章:
AlphaFold2中的残基刚体表示


使用numpy计算分子内坐标
。这里我们就简单的做一个Python的脚本,可以用来读取pdb文件,生成对应的拉氏图,用来判断对应的蛋白质构象是否合理,也可以用来比较两个同类蛋白之间的构型差异。

脚本与使用方法

先把下面这个Python脚本文件保存到本地为
rplot.py

# rplot.py
""" 
README
    Run this script with command:
    ```bash
    $ python3 rplot.py --input ../tutorials/pdb/case5.pdb --levels 8 --sigma 0.2 --grids 30
    ```
Requirements
    hadder, numpy, matplotlib
"""
try:
    from hadder.parsers import read_pdb
except ImportError:
    import os
    os.system('python3 -m pip install hadder --upgrade')
    from hadder.parsers import read_pdb
import matplotlib.pyplot as plt
import numpy as np

import argparse
parser = argparse.ArgumentParser()

parser.add_argument("--input", help="Set the input pdb filename. Absolute file path is recommended.")
parser.add_argument("--levels", help="Contour levels.", default='8')
parser.add_argument("--sigma", help="KDE band width.", default='0.2')
parser.add_argument("--grids", help="Number of grids.", default='30')

args = parser.parse_args()
pdb_name = args.input
num_levels = int(args.levels)
sigma = float(args.sigma)
num_grids = int(args.grids)

def plot(fig, X, Y, Z, num_levels=8):
    plt.xlabel(r'$\psi$')
    plt.ylabel(r'$\phi$')
    levels = np.linspace(np.min(Z), np.max(Z), num_levels)
    fc = plt.contourf(X, Y, Z, cmap='Greens', levels=levels)
    plt.colorbar(fc)
    return fig

def torsion(crds, index1, index2, index3, index4):
    crd1 = crds[index1]
    crd2 = crds[index2]
    crd3 = crds[index3]
    crd4 = crds[index4]
    vector1 = crd1 - crd2
    vector2 = crd4 - crd3
    axis_vector = crd3 - crd2
    vec_a = np.cross(vector1, axis_vector)
    vec_b = np.cross(vector2, axis_vector)
    cross_ab = np.cross(vec_a, vec_b)
    axis_vector /= np.linalg.norm(axis_vector, axis=-1, keepdims=True)
    sin_phi = np.sum(axis_vector * cross_ab, axis=-1)
    cos_phi = np.sum(vec_a * vec_b, axis=-1)
    phi = np.arctan(sin_phi / cos_phi)
    return phi

def psi_phi_from_pdb(pdb_name):
    pdb_obj = read_pdb(pdb_name)
    atom_names = pdb_obj.flatten_atoms
    crds = pdb_obj.flatten_crds
    c_index = np.where(atom_names=='C')[0]
    n_index = np.where(atom_names=='N')[0]
    ca_index = np.where(atom_names=='CA')[0]
    psi = torsion(crds, n_index[:-1], ca_index[:-1], c_index[:-1], n_index[1:])
    phi = torsion(crds, c_index[:-1], n_index[1:], ca_index[1:], c_index[1:])
    return psi, phi

def gaussian2(x1, x2, sigma1=1.0, sigma2=1.0, A=0.5):
    return np.sum(A*np.exp(-0.5*(x1**2/sigma1**2+x2**2/sigma2**2))/np.pi/sigma1/sigma2, axis=-1)

def potential_energy(position, psi, phi, sigma1, sigma2):
    # (A, )
    psi_, phi_ = position[:, 0], position[:, 1]
    # (A, R)
    delta_psi = psi_[:, None] - psi[None]
    delta_phi = phi_[:, None] - phi[None]
    # (A, )
    Z = -np.log(gaussian2(delta_psi, delta_phi, sigma1=sigma1, sigma2=sigma2, A=2.0)+1)
    return Z

psi_grids = np.linspace(-np.pi, np.pi, num_grids)
phi_grids = np.linspace(-np.pi, np.pi, num_grids)
grids = np.array(np.meshgrid(psi_grids, phi_grids)).T.reshape((-1, 2))
psi, phi = psi_phi_from_pdb(pdb_name)
Z = potential_energy(grids, psi, phi, sigma, sigma).reshape((psi_grids.shape[0], phi_grids.shape[0])).T
X,Y = np.meshgrid(psi_grids, phi_grids)

fig = plt.figure()
plt.title('Ramachandran plot for {}'.format(pdb_name.split('/')[-1]))
plot(fig, X, Y, Z, num_levels=num_levels)
plt.plot(psi, phi, '.', color='black')
plt.show()

然后加载一个本地pdb文件:

$ python rplot.py case2.pdb

生成图像效果如下:

关于这个脚本还有一些常量可以配置:

$ python3 rplot.py --help
usage: rplot.py [-h] [--input INPUT] [--levels LEVELS] [--sigma SIGMA]
                [--grids GRIDS]

optional arguments:
  -h, --help       show this help message and exit
  --input INPUT    Set the input pdb filename. Absolute file path is
                   recommended.
  --levels LEVELS  Contour levels.
  --sigma SIGMA    KDE band width.
  --grids GRIDS    Number of grids.

例如说,我们可以把KDE中高斯波包的sigma值配置的更小一点(默认值为0.2),这样图像显示的会更集中,还可以把levels调多一点(默认值为8),这样显示的等高线会更密一些:

$ python3 rplot.py --input ../tutorials/pdb/case2.pdb --sigma 0.1 --levels 10

效果如下:

总结概要

这里我提供了一个用于画拉氏图的Python脚本源代码,供大家免费使用。虽然现在也有很多免费的平台和工具可以用,但很多都是黑箱,有需要的开发者可以直接在这个脚本基础上二次开发,定制自己的拉氏图绘制方法。

版权声明

本文首发链接为:
https://www.cnblogs.com/dechinphy/p/rplot.html

作者ID:DechinPhy

更多原著文章:
https://www.cnblogs.com/dechinphy/

请博主喝咖啡:
https://www.cnblogs.com/dechinphy/gallery/image/379634.html

参考链接

  1. https://blog.csdn.net/MCANDML/article/details/80672174