CSP2019前夕整理一下模板,顺便供之后使用
1. 数据结构
1.1. 虚树
描述:
给定树上的\(k\)个关键点,构建出一棵虚树,只有关键点和任意两个关键点的LCA会被保留,且原树上的祖先关系和虚树上祖先关系保持一致。可以证明虚树最多有\((2k-1)\)个点。
把所有关键点按DFS序排序,用一个栈维护动态加点连边即可。时间复杂度\(O(k\log n)\).
注意事项:
一定要处理好最先加入栈中的点(所有点的LCA).
代码:addedge0_(u,v)
表示在\(u\)和\(v\)之间连边,边权根据实际情况确定。
bool cmp(int x,int y) {return dfn[x]<dfn[y];} void build() { sort(ky+1,ky+kyn+1,cmp); rt = ky[1]; for(int i=2; i<=kyn; i++) rt = LCA(rt,ky[i]).first; tp = 1; stk[tp] = rt; n0++; kid[n0] = rt; isky[ky[1]] = true; for(int i=(rt==ky[1]?2:1); i<=kyn; i++) { int u = ky[i],lca = LCA(stk[tp],u).first; if(lca==stk[tp]) {tp++; stk[tp] = u; n0++; kid[n0] = u; isky[u] = true;} else { while(tp>1 && dep[stk[tp-1]]>dep[lca]) {addedge0_(stk[tp],stk[tp-1]); tp--;} addedge0_(stk[tp],lca); tp--; if(stk[tp]!=lca) { tp++; stk[tp] = lca; n0++; kid[n0] = lca; } tp++; stk[tp] = u; n0++; kid[n0] = u; isky[u] = true; } } while(tp>1) { addedge0_(stk[tp],stk[tp-1]); tp--; } }
1.2. 左偏树
描述:
可并堆的一种实现,支持插入、删除、合并、取最小值等堆操作,单个操作时间复杂度均不超过\(O(\log n)\).
代码:
int merge(int u,int v) { if(u==0||v==0) return u+v; if(val[u]>val[v]) swap(u,v); son[u][1] = merge(son[u][1],v); if(dis[son[u][1]]>dis[son[u][0]]) {swap(son[u][1],son[u][0]);} dis[u] = dis[son[u][1]]+1; return u; } void insert(int u,int x) { siz++; val[siz] = x; rtn[u] = merge(rtn[u],siz); } int popnode(int u) { return merge(son[u][0],son[u][1]); }
1.3. KD树
暂无
2. 字符串
2.1. 后缀数组
描述:
倍增法求后缀数组,时间复杂度\(O(n\log n)\).sa[i]
: 排在第\(i\)位的后缀rk[i]
: 后缀\(i\)的排名h[i]
: 后缀\(i\)与其前一名的后缀的LCP。height[i]
: 第\(i\)名与其前一名的后缀的LCP.
注意:
(1) w1
处不可令h[1]=1
然后从\(2\)开始循环。
代码:
namespace SA { int height[N+3],h[N+3],tmp[N+3],st[lgN+2][N+3],buc[N+3]; void buildSA() { int *x = rk,*y = tmp; for(int i=1; i<=S; i++) buc[i] = 0; for(int i=1; i<=n; i++) buc[x[i]=a[i]]++; for(int i=1; i<=S; i++) buc[i] += buc[i-1]; for(int i=n; i>=1; i--) sa[buc[x[i]]--] = i; int p = 0,s = S; for(int j=1; p<n; j<<=1) { p = 0; for(int i=n-j+1; i<=n; i++) y[++p] = i; for(int i=1; i<=n; i++) {if(sa[i]>j) y[++p] = sa[i]-j;} for(int i=1; i<=s; i++) buc[i] = 0; for(int i=1; i<=n; i++) buc[x[y[i]]]++; for(int i=1; i<=s; i++) buc[i] += buc[i-1]; for(int i=n; i>=1; i--) sa[buc[x[y[i]]]--] = y[i]; p = 1; swap(x,y); x[sa[1]] = 1; for(int i=2; i<=n; i++) x[sa[i]] = y[sa[i]]==y[sa[i-1]]&&y[sa[i]+j]==y[sa[i-1]+j]?p:++p; s = p; } for(int i=1; i<=n; i++) rk[sa[i]] = i; for(int i=1; i<=n; i++) //w1 { h[i] = h[i-1]==0?0:h[i-1]-1; while(i+h[i]<=n && sa[rk[i]-1]+h[i]<=n && a[i+h[i]]==a[sa[rk[i]-1]+h[i]]) {h[i]++;} } for(int i=1; i<=n; i++) height[i] = h[sa[i]]; for(int i=1; i<=n; i++) st[0][i] = height[i]; for(int j=1; j<lgN; j++) { for(int i=1; i+(1<<j)-1<=n; i++) {st[j][i] = min(st[j-1][i],st[j-1][i+(1<<j-1)]);} } } int querymin(int l,int r) { int g = lg2[r-l+1]; int *adr = st[g]; return min(adr[l],adr[r-(1<<g)+1]); } int LCP(int x,int y) { if(x==y) return n-x+1; if(rk[x]>rk[y]) swap(x,y); return querymin(rk[x]+1,rk[y]); } } using SA::buildSA; using SA::LCP;
2.2. 扩展KMP
描述:
求出一个串的每个后缀与整个串的LCP. 与Manacher算法极其类似,时间复杂度\(O(n)\).z[i]
: 后缀\(i\)与母串的LCP长度。
代码:
void Z_box() { int mx = 0,mxid = 0; z[1] = m; for(int i=2; i<=m; i++) { if(a[i]!=a[1]) {z[i] = 0;} else if(i>mx) {z[i] = 1;} else {z[i] = min(z[i-mxid+1],mx-i+1);} while(i+z[i]<=m && a[i+z[i]]==a[z[i]+1]) {z[i]++;} if(i+z[i]-1>mx) {mx = i+z[i]-1; mxid = i;} } }
2.3. Manacher算法
描述:
求出以每个位置为中心的最长回文子串,时间复杂度\(O(n)\).
注意事项:
(1) 不要把\(2n+1\)写成\(n\).
代码:
void manacher() { for(int i=n; i>=1; i--) a[2*i] = a[i]; for(int i=1; i<=2*n+1; i+=2) a[i] = '#'; int mxid = 1,mx = 1; p[1] = 1; for(int i=2; i<=2*n+1; i++) { if(i>mx) {p[i] = 1;} else {p[i] = min(p[2*mxid-i],mx-i+1);} while(i-p[i]>0 && i+p[i]<=2*n+1 && a[i+p[i]]==a[i-p[i]]) {p[i]++;} if(i+p[i]-1>mx) {mx = i+p[i]-1; mxid = i;} } }
2.4. 后缀自动机
描述:
增量法构造后缀自动机,时间复杂度\(O(n)\).
注意广义后缀自动机必须对Trie树进行BFS建立才能保证复杂度为节点数,否则复杂度退化为所有叶子节点总深度。
注意事项:
(1) 不要忘记初始化三个变量。
代码:
void init() { siz = lstpos = rtn = 1; } void insertchar(char ch) { int p = lstpos,np; siz++; np = lstpos = siz; len[np] = len[p]+1; sz[np]++; for(; p && son[p][ch]==0; p=fa[p]) {son[p][ch] = np;} if(p==0) {fa[np] = rtn;} else { int q = son[p][ch]; if(len[q]==len[p]+1) {fa[np] = q;} else { siz++; int nq = siz; len[nq] = len[p]+1; memcpy(son[nq],son[q],sizeof(son[q])); fa[nq] = fa[q]; fa[q] = fa[np] = nq; for(; p && son[p][ch]==q; p=fa[p]) {son[p][ch] = nq;} } } }
2.5. 回文自动机
描述:
一个字符串本质不同的回文子串个数不超过\(n\). 回文自动机上一个节点代表一个回文子串。
增量法构造回文自动机,时间复杂度\(O(n)\).
代码:
void initPAM() { siz = 1; fail[0] = fail[1] = 1; len[0] = 0; len[1] = -1; lstpos = 1; } void insertchar(int id) { int p = lstpos; while(a[id-1-len[p]]!=a[id]) {p = fail[p];} if(!son[p][a[id]]) { siz++; int u = siz,v = fail[p]; while(a[id-1-len[v]]!=a[id]) {v = fail[v];} fail[u] = son[v][a[id]]; len[u] = len[p]+2; son[p][a[id]] = u; } lstpos = son[p][a[id]]; }
2.6. Lyndon分解
描述:
若一个串的最小循环表示为它本身,则称作Lyndon串。
将一个串划分为若干Lyndon串,称作Lyndon划分。一个串的Lyndon划分方案唯一。
Lyndon划分的Duval算法,时间复杂度\(O(n)\).
代码:
暂无
2.7. 最小循环表示
暂无
3. 图论
3.1. Tarjan算法
3.1.1. 强连通分量
描述:
Tarjan求有向图的强连通分量。构建DFS树,统计每个点的DFS时间戳(dfn[u]
)以及其所到达的时间戳最小的点(low[u]
), 若后者为该点本身,则求出一个极大强连通分量。时间复杂度\(O(n+m)\).
注意事项:
注意要从每个未遍历的点出发进行遍历。
代码:
void tarjan(int u) { cnt++; dfn[u] = cnt; low[u] = cnt; ins[u] = true; tp++; sta[tp] = u; for(int i=fe0[u]; i; i=e0[i].nxt) { if(!dfn[e0[i].v]) {tarjan(e0[i].v); low[u] = min(low[u],low[e0[i].v]);} else if(ins[e0[i].v]) low[u] = min(low[u],dfn[e0[i].v]); } if(low[u]==dfn[u]) { num++; ca[num] = a[u]; while(sta[tp]!=u) { ins[sta[tp]] = false; clr[sta[tp]] = num; ca[num] += a[sta[tp]]; tp--; } ins[u] = false; clr[u] = num; tp--; } }
3.1.2. 点双连通分量
描述:
Tarjan算法求无向图的点双连通分量。构建DFS树,low[u]
的定义改为该点经过至多一条非树边到达的最小的时间戳,若某个点\(u\)存在至少一个儿子\(v\)满足\(low[v]\ge dfn[u]\), 则\(u\)是割点。时间复杂度\(O(n+m)\).
点双连通分量一定是边双连通分量,割边的两个端点一定是割点。
代码:
(建圆方树)
namespace Graph { const int N = 1e6; const int M = 4e6; struct Edge { int v,nxt; } e[(M<<1)+3]; int fe[N+3]; int fa[N+3]; int dfn[N+3],low[N+3],stk[N+3]; int n,en,cnt,tp; void addedge(int u,int v) { en++; e[en].v = v; e[en].nxt = fe[u]; fe[u] = en; } void Tarjan(int u) { cnt++; dfn[u] = low[u] = cnt; tp++; stk[tp] = u; for(int i=fe[u]; i; i=e[i].nxt) { int v = e[i].v; if(v==fa[u]) continue; if(!dfn[v]) { fa[v] = u; Tarjan(v); low[u] = min(low[u],low[v]); if(low[v]>=dfn[u]) { Tree::n++; Tree::addedge(u,Tree::n); Tree::addedge(Tree::n,u); while(tp>0) { Tree::addedge(Tree::n,stk[tp]); Tree::addedge(stk[tp],Tree::n); tp--; if(stk[tp+1]==v) {break;} } } } else {low[u] = min(low[u],dfn[v]);} } } void buildTree() { Tree::n = n; for(int i=1; i<=n; i++) Tree::a[i] = 1; for(int i=1; i<=n; i++) { if(!dfn[i]) {Tarjan(i);} } } }
3.1.3. 边双连通分量
描述:
把点双连通分量的\(\ge\)改成\(\gt\)即可。
代码:
略。
3.2. 欧拉回路
3.2.1. 无向图欧拉回路
描述:
若无向图连通且度数为奇数的点个数不超过\(2\), 则存在欧拉回路。DFS时记录回溯的路径,即可构造出一条欧拉回路,时间复杂度\(O(m)\).
注意事项:
(1) 一定要使用自杀式遍历,否则时间复杂度退化为平方级。
代码:
namespace Undirected { struct Edge{int v,nxt;} e[M+2]; int fe[N+2]; int dgr[N+2]; int ans[(N<<1)+2]; bool vis[M+2]; int n,m,en,tp; void addedge(int u,int v) { en++; e[en].v = v; e[en].nxt = fe[u]; fe[u] = en; dgr[u]++; } void dfs(int u) { for(int i=fe[u]; i; i=fe[u]) { fe[u] = e[i].nxt; if(vis[(i+1)>>1]) continue; vis[(i+1)>>1] = true; dfs(e[i].v); tp++; ans[tp] = (i&1) ? ((i+1)>>1) : -((i+1)>>1); } } void solve() { scanf("%d%d",&n,&m); for(int i=1; i<=m; i++){int x,y;scanf("%d%d",&x,&y);addedge(x,y);addedge(y,x);} for(int i=1; i<=n; i++) { if(dgr[i]&1){printf("NO");return;} } tp = 0; dfs(e[1].v); if(tp<m) {printf("NO"); return;} printf("YES\n"); for(int i=tp; i>=1; i--) { printf("%d ",ans[i]); } } }
3.2.2. 有向图欧拉回路
描述:
有向图中每个点入度等于出度是存在欧拉回路的必要条件。依然可以通过记录回溯路径的方法构造欧拉回路。时间复杂度\(O(m)\).
代码:
namespace Directed { struct Edge{int v,nxt;} e[M+2]; int fe[N+2]; int ind[N+2]; int oud[N+2]; int ans[(N<<1)+2]; bool vis[M+2]; int n,m,en,tp; void addedge(int u,int v) { en++; e[en].v = v; e[en].nxt = fe[u]; fe[u] = en; oud[u]++; ind[v]++; } void dfs(int u) { for(int i=fe[u]; i; i=fe[u]) { fe[u] = e[i].nxt; if(vis[i]) continue; vis[i] = true; dfs(e[i].v); tp++; ans[tp] = i; } } void solve() { scanf("%d%d",&n,&m); tp = 0; for(int i=1; i<=m; i++) { int x,y; scanf("%d%d",&x,&y); addedge(x,y); } for(int i=1; i<=n; i++) { if(ind[i]!=oud[i]) {printf("NO"); return;} } dfs(e[1].v); if(tp<m) {printf("NO"); return;} printf("YES\n"); for(int i=tp; i>=1; i--) printf("%d ",ans[i]); } }
4. 数论
4.1. 扩展欧几里得算法
描述:
求解不定方程\(ax+by=\gcd(a,b)\). 时间复杂度\(O(\log(a+b))\).
代码:
llong exgcd(llong a,llong b,llong &x,llong &y) { if(b==0) {x = 1,y = 0; return x;} llong nx,ny; llong ret = exgcd(b,a%b,nx,ny); x = ny; y = nx-a/b*ny; return ret; }
4.2. 扩展中国剩余定理
描述:
求解同余方程组,模数可以不互质。具体做法是将模数不互质的方程进行合并。
代码:
暂无
4.3. 扩展BSGS
暂无
4.4. 二次剩余之Cipolla算法
暂无
5. 多项式
暂无