title : 后缀自动机
date : 2021-11-11
tags : ACM,字符串
author : Linno


前置知识

KMP,Trie,AC自动机等字符串基础

DFA(有限状态自动机)

后缀自动机(Suffix automaton ,SAM)

定义

字符串s的SAM是一个接受s的所有后缀的最小DFA(确定性有限自动机)。直观上SAM是给定字符串的所有字串的压缩形式。而构造的时间复杂度和空间复杂度仅为O(n)O(n)O(n)。一个SAM最多有2n-1个节点和3n-4条转移边。

符号定义

endpos(t)为字符串s中t的所有结束位置,endpos的集合即为SAM的状态集

subs(sta):SAM中sta状态所包含的所有子串的集合

mxsub(sta):sta状态所包含的子串中最长的子串

mnsub(sta):sta状态所包含的子串中最短的子串

sz[i]:sta表示的endpos集合大小,即i号点字符串集合在整个串出现次数

nxt(sta):sta遇到的下一个字符集合

link(sta):

性质

状态数:不超过2n-1

转移数:不超过3n-4

DAG性质DAG性质DAG性质:SAM是有向无环图,因此可以用DP计算路径条数。

状态集:所有endpos的集合即为SAM的状态集。

转移函数

nxt(sta)={S[i+1]∣i∈endpos(sta)}nxt(sta)=\{S[i+1]|i\in endpos(sta)\}nxt(sta)={S[i+1]∣i∈endpos(sta)}

trans(sta,c)={x∣mxsub(sta+(c∈subs(x))}trans(sta,c)=\{x|mxsub(sta+(c\in subs(x))\}trans(sta,c)={x∣mxsub(sta+(c∈subs(x))}

后缀链接link

sta状态链接到下一个连续后缀所在的状态,叫做后缀链接。

子串表示法

用一个点的集合以及长度len来描述一个子串

Parent树
引理1

两个非空子串u和w(假设∣u∣≤∣w∣)的endpos相同,当且仅当字符串u是w的后缀。两个非空子串u和w(假设|u|\leq|w|)的endpos相同,当且仅当字符串u是w的后缀。两个非空子串u和w(假设∣u∣≤∣w∣)的endpos相同,当且仅当字符串u是w的后缀。

对于任意一个状态stastasta,任取str∈subs(sta)str\in subs(sta)str∈subs(sta),都有strstrstr是mxsub(sta)mxsub(sta)mxsub(sta)的后缀。

例如:subs(7)={aabbab,abbab,bbab,bab}

任取一个字符串str,str是mxsub(7)=aabbab的后缀。

SAM中一个状态所包含的子串的集合是由长度连续的一系列字符串构成,并且由长到短,依次减少最前面的字符。

引理2

两个非空子串u和w(假设∣u∣≤∣w∣|u|\leq|w|∣u∣≤∣w∣),要么endpos(u)∩endpos(w)=∅endpos(u)\cap endpos(w)=\emptyendpos(u)∩endpos(w)=∅,要么endpos(w)⊆endpos(u),取决于u是否为w的一个后缀endpos(w)\subseteq endpos(u),取决于u是否为w的一个后缀endpos(w)⊆endpos(u),取决于u是否为w的一个后缀

引理3

对于任意状态sta,任取mxsub(sta)的后缀str,若str的长度满足:∣misub(sta)∣≤∣str∣≤∣mxsub(sta)∣,那么str∈substr(sta)对于任意状态sta,任取mxsub(sta)的后缀str,若str的长度满足:\\ |misub(sta)|\le|str|\le|mxsub(sta)|,那么str\in substr(sta) 对于任意状态sta,任取mxsub(sta)的后缀str,若str的长度满足:∣misub(sta)∣≤∣str∣≤∣mxsub(sta)∣,那么str∈substr(sta)

考虑一个endposendposendpos 等价类,将类中的所有子串按长度非递增的顺序排序。每个子串都不会比它前一个子串长,与此同时每个子串也是它前一个子串的后缀。换句话说,对于同一等价类的任一两子串,较短者为较长者的后缀,且该等价类中的子串长度恰好覆盖整个区间 [x,y]。

引理4

所有后缀链接构成一棵根节点为t0t_0t0​的树。

引理5

通过endpos集合构建的树(每个子节点的subset都包含在父节点的subset中)与通过后缀链接link构建的树相同。

构造流程

①令last为添加字符串c前,整个字符串对应的状态,初始last=0。

②创建新的状态cur,len(cur)=len(last)+1,此时link(cur)还未知。

③如果没有字符c的转移,就添加一个状态cur的转移,遍历后缀链接;否则停下并将这个状态标记为p。

④如果没有找到这个状态p,我们就达到了虚拟状态-1,将link(cur)赋值为0并退出。

⑤分类讨论两种状态,要么len(p)+1=len(q)len(p)+1=len(q)len(p)+1=len(q),要么不是。

⑥如果len(p)+1=len(q)len(p)+1=len(q)len(p)+1=len(q),我们只要将link(cur)link(cur)link(cur)赋值为q并退出。否则复制状态q,创建一个新的状态clone,复制除了len以外的所有信息,len(clone)=len(p)+1len(clone)=len(p)+1len(clone)=len(p)+1

⑦将last的值更新为状态cur。

实现

int sz,last; //SAM的大小以及末状态
struct state{ //状态节点int len,link;map<char,int>nxt; //转移的列表
}st[MAXLEN*2]; //因为最多2n-1个状态void sam_init(){ //初始化samst[0].len=0;st[0].link=-1; //-1表示虚拟状态sz++; last=0;
}void sam_extend(char c){ //添加字符cint cur=sz++; //创建新的状态st[cur].len=st[last].len+1;int p=last;while(p!=-1&&st[p].nxt.count(c)){ //遍历后缀链接st[p].nxt[c]=cur;p=st[p].link;}if(p==-1) st[cur].link=0; //没要找到这样的状态else{int q=st[p].nxt[c];if(st[p].len+1==st[q].len) st[cur].link=q;else{int clone=sz++; //复制一个状态st[clone].len=st[p].len+1;st[clone].nxt=st[q].nxt;st[clone].link=st[q].link;while(p!=-1&&st[p].nxt[c]==q){//后缀链接从状态p往回走st[p].nxt[c]=clone;p=st[p].link;}st[q].link=st[cur].link=clone;}}last=cur; //更新last为当前状态
}

检查子串是否出现

在SAM上向父亲跑边,跑完串还未跑到空状态则为原串子串。

后缀数组:跑出sa,然后从最小的后缀开始,一个个往后枚举,记录下当前匹配到的位置,如果匹配不上就下一个后缀,否则位置向后移一位。如果枚举完了后缀还没完全匹配则不是原串子串。

最小循环位移

字符串S+S对应的SAM中寻找最小的长度为|S|的路径。

从初始状态开始,贪心地访问最小的字符即可。

子串第一次出现位置

预处理每个状态第一次出现的位置pos(i)pos(i)pos(i)

只需要每次让pos(np)=len(np)pos(nq)=pos(q)pos(np)=len(np)pos(nq)=pos(q)pos(np)=len(np)pos(nq)=pos(q)就好了

查询答案为pos(i)−∣T∣+1pos(i)-|T|+1pos(i)−∣T∣+1

最短的没有出现的字符串

在SAM上做dp,设dp[i]dp[i]dp[i]表示到点i时的最短长度

如果这个点有不是T中字符的出边,则dp[i]=1dp[i]=1dp[i]=1,否则dp[i]=1+min(i,j,c)∈SAMdp[j]dp[i]=1+min_{(i,j,c)\in SAM}dp[j]dp[i]=1+min(i,j,c)∈SAM​dp[j]

不同子串个数

对字符串S构造后缀自动机。每个S的子串相当于自动机中的一些路径,因此不同子串的个数等于自动机中以t0t_0t0​为起点的不同路径条数。

考虑SAM为DAG,可用动态规划计算不同路径的条数。

令dv为从状态v开始的路径数量,则dv=1+∑w:(v,w,c)∈DAWGdw即dv可以表示为所有v的转移的末端之和。令d_v为从状态v开始的路径数量,则d_v=1+\sum_{w:(v,w,c)\in DAWG} d_w\\即d_v可以表示为所有v的转移的末端之和。令dv​为从状态v开始的路径数量,则dv​=1+∑w:(v,w,c)∈DAWG​dw​即dv​可以表示为所有v的转移的末端之和。

luoguP2408 不同子串个数
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+7;struct State{int len,fa,ch[26];
}st[N];int n,lst=1,cnt=1,ans[N];
string str;inline void ins(int c){ //SAM加点 int p=lst,np=lst=++cnt;st[np].len=st[p].len+1;for(;p&&!st[p].ch[c];p=st[p].fa) st[p].ch[c]=np;if(!p) return (void)(st[np].fa=1);int q=st[p].ch[c];if(st[q].len==st[p].len+1) return (void)(st[np].fa=q);int nq=++cnt;st[nq]=st[q];st[nq].len=st[p].len+1,st[q].fa=st[np].fa=nq;for(;p&&st[p].ch[c]==q;p=st[p].fa) st[p].ch[c]=nq;
}inline int dfs(int x){//在SAM上跑dfs if(ans[x]) return ans[x];for(int i=0;i<26;i++) if(st[x].ch[i]) ans[x]+=dfs(st[x].ch[i])+1;return ans[x];
}signed main(){cin>>n;cin>>str;//memset(ans,0,sizeof(ans));for(int i=0;i<n;i++) ins(str[i]-'a');cout<<dfs(1)<<"\n";return 0;
}

所有不同子串的总长度

与上述做法类似,递推式ansv=∑w:(v,w,c)∈DAWG(dw+answ)ans_v=\sum_{w:(v,w,c)\in DAWG}(d_w+ans_w)ansv​=∑w:(v,w,c)∈DAWG​(dw​+answ​)

luoguP3804 【模板】后缀自动机 (SAM)

所有出现次数不为1的子串的出现次数乘以该子串长度的最大值。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2010000;
char s[N];
int fa[N],ch[N][26],len[N],siz[N];
int lst=1,cnt=1,l,t[N],A[N];
int ans;
void Extend(int c){int f=lst,p=++cnt;lst=p;  //lst为上一个节点,p为当前节点(编号) len[p]=len[f]+1;siz[p]=1;  //len[i]记录i节点的longest,siz[i]记录集合元素大小 while(f&&!ch[f][c]) ch[f][c]=p,f=fa[f]; //创建新的节点并向后退 if(!f) {fa[p]=1;return;} /*把前面的一段没有c儿子的节点的c儿子指向pSituation 1 如果跳到最前面的根的时候,那么把p的parent树上的父亲置为1(初始状态) */int x=ch[f][c],y=++cnt; //y是克隆状态 if(len[f]+1==len[x]) {fa[p]=x;cnt--;return;}/*x表示从p一直跳parent树得到的第一个有c儿子的节点的c儿子Situation 2 如果节点x表示的endpos所对应的字符串集合只有一个字符串,那么把p的parent树父亲设置为xSituation 2 和 Situation 3 本质不同!!!*/len[y]=len[f]+1; fa[y]=fa[x]; fa[x]=fa[p]=y;memcpy(ch[y],ch[x],sizeof(ch[y]));while(f&&ch[f][c]==x) ch[f][c]=y,f=fa[f];/*Situation 3 把x点复制一遍(parent树父亲、儿子),同时len要更新(注意len[x]!=len[f]+1,因为通过加点会使x父亲改变)然后把x点和p点的父亲指向复制点y,再将前面所有本连x的点连向y*/
}
signed main(){//Part 1 建立后缀自动机scanf("%s",s); l=strlen(s);for(int i=l;i>=1;i--) s[i]=s[i-1];s[0]=0;for(int i=1;i<=l;i++) Extend(s[i]-'a');//Part 2 按len从大到小排序(和SA好像啊)后计算答案for(int i=1;i<=cnt;i++) t[len[i]]++;for(int i=1;i<=cnt;i++) t[i]+=t[i-1];for(int i=1;i<=cnt;i++) A[t[len[i]]--]=i;for(int i=cnt;i>=1;i--){ //从小到大枚举,实际上在模拟parent树的DFSint now=A[i];siz[fa[now]]+=siz[now];if(siz[now]>1) ans=max(ans,siz[now]*len[now]);}printf("%lld\n",ans);return 0;
}

字典序第k大子串

字典序第k大的子串对应于SAM中字典序第k大的路径,因此计算每个状态的路径数后,可以很容易的从SAM的根开始找到第k大的路径。预处理O(∣S∣),单词询问O(∣ans∣⋅∣Σ∣)预处理O(|S|),单词询问O(|ans|·|\Sigma|)预处理O(∣S∣),单词询问O(∣ans∣⋅∣Σ∣)

luoguP3975 [TJOI2015]弦论
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2010000;
char s[N];
int fa[N],ch[N][26],len[N],siz[N];
int lst=1,cnt=1,l,t[N],A[N];
int T,K,sum[N];
void Extend(int c){int f=lst,p=++cnt;lst=p;  //lst为上一个节点,p为当前节点(编号) len[p]=len[f]+1;siz[p]=1;  //len[i]记录i节点的longest,siz[i]记录集合元素大小 while(f&&!ch[f][c]) ch[f][c]=p,f=fa[f]; //创建新的节点并向后退 if(!f) {fa[p]=1;return;} int x=ch[f][c],y=++cnt; //y是克隆状态 if(len[f]+1==len[x]) {fa[p]=x;cnt--;return;}len[y]=len[f]+1; fa[y]=fa[x]; fa[x]=fa[p]=y;memcpy(ch[y],ch[x],sizeof(ch[y]));while(f&&ch[f][c]==x) ch[f][c]=y,f=fa[f];
}void Print(int x,int k){ //深搜找到K小子串的位置 if(k<=siz[x]) return;k-=siz[x];for(int i=0;i<26;i++){int R=ch[x][i];if(!R) continue;if(k>sum[R]){k-=sum[R];continue;}putchar(i+'a');Print(R,k);return;}
}signed main(){scanf("%s",s); l=strlen(s);scanf("%d%d",&T,&K); for(int i=l;i>=1;i--) s[i]=s[i-1];s[0]=0;for(int i=1;i<=l;i++) Extend(s[i]-'a');for(int i=1;i<=cnt;i++) t[len[i]]++;for(int i=1;i<=cnt;i++) t[i]+=t[i-1];for(int i=1;i<=cnt;i++) A[t[len[i]]--]=i; //桶排序for(int i=cnt;i>=1;i--) siz[fa[A[i]]]+=siz[A[i]]; //累加子串个数 for(int i=1;i<=cnt;i++) T==0?(sum[i]=siz[i]=1):(sum[i]=siz[i]); //相同串个数 siz[1]=sum[1]=0;for(int i=cnt;i>=1;i--){for(int j=0;j<26;j++){if(ch[A[i]][j]) sum[A[i]]+=sum[ch[A[i]][j]]; //从后往前累加 }}if(sum[1]<K) puts("-1");else Print(1,K),puts("");return 0;
}

子串出现次数

对于每个状态vvv,预处理cntvcnt_vcntv​,使之等于endpos(v)endpos(v)endpos(v)集合的大小。

CF802I Fake News (hard)

T组数据,对字符串s求∑pcnt(s,p)2\sum_pcnt(s,p)^2∑p​cnt(s,p)2,cnt(s,p)表示子串p在s中的出现次数

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+7;int t,n,ans=0;
string str;namespace SAM{map<int,int>ch[N];int lst=1,cnt=1,len[N],fa[N],sz[N];inline void ins(int c){int p=lst,np=++cnt;lst=np;len[np]=len[p]+1;sz[np]=1;for(;!ch[p][c];p=fa[p]) ch[p][c]=np;if(!p) fa[np]=1;else{int q=ch[p][c];if(len[q]==len[p]+1) fa[np]=q;else{int nq=++cnt;len[nq]=len[p]+1;ch[nq]=ch[q];fa[nq]=fa[q];fa[q]=fa[np]=nq;for(;ch[p][c]==q;p=fa[p]) ch[p][c]=nq;} }}inline void clear(){memset(sz,0,sizeof(sz));for(int i=1;i<=cnt;i++) ch[i].clear();memset(len,0,sizeof(len));memset(fa,0,sizeof(fa));lst=cnt=1;}
};
using namespace SAM;vector<int>G[N];
void init(){ans=0;for(int i=1;i<=cnt;i++) G[i].clear();clear();
}void dfs(int u){for(int i=0;i<G[u].size();i++){int v=G[u][i];dfs(v);sz[u]+=sz[v];}ans+=(len[u]-len[fa[u]])*sz[u]*sz[u];
} signed main(){cin>>t;while(t--){init();cin>>str;n=str.length();for(int i=0;i<n;i++) ins(str[i]-'a');for(int i=2;i<=cnt;i++) G[fa[i]].push_back(i);//建立parent tree dfs(1);cout<<ans<<"\n";}return 0;
}

两个串的最长公共子串

对母串建SAM,然后第二个字符串一个个匹配过去。

如果现态p有Si的转移,则len++并跳到下一个字符;否则在parent树上向上跳直到有这个转移为止。

luoguSP1811 LCS - Longest Common Substring
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+7;struct State{int len,fa,ch[26];
}st[N<<1];int lst=1,cnt=1;
string str1,str2;inline void ins(int c){ //SAM加点 int p=lst,np=lst=++cnt;st[np].len=st[p].len+1;for(;p&&!st[p].ch[c];p=st[p].fa) st[p].ch[c]=np;if(!p) return (void)(st[np].fa=1);int q=st[p].ch[c];if(st[q].len==st[p].len+1) return (void)(st[np].fa=q);int nq=++cnt;st[nq]=st[q];st[nq].len=st[p].len+1,st[q].fa=st[np].fa=nq;for(;p&&st[p].ch[c]==q;p=st[p].fa) st[p].ch[c]=nq;
}int calc(int n){int p=1,ans=0,len=0;for(int i=0;i<n;i++){ //子串开始位置 int c=str2[i]-'a';if(st[p].ch[c]) len++,p=st[p].ch[c]; else{for(;p&&!st[p].ch[c];p=st[p].fa); if(p) len=st[p].len+1,p=st[p].ch[c]; //匹配下一个字符 else len=0,p=1; //返回初始状态 }ans=max(ans,len);}return ans;
}signed main(){cin>>str1;cin>>str2;int n1=str1.length(),n2=str2.length();for(int i=0;i<n1;i++) ins(str1[i]-'a');cout<<calc(n2)<<"\n";return 0;
}

多个字符串间的最长公共子串

还是对第一个串建SAM,然后一个一个串处理。在处理每一个串的时候记录当前节点的最大匹配长度,并且记录最大长度的最小值,就是所有串的匹配长度。

SP1812 LCS2 - Longest Common Substring II
#include<bits/stdc++.h>
#define N 2000010
#define inf 0x3f3f3f3f
using namespace std;
int n,lst,tot=1,root=1,num,ans;
int fa[N],len[N],ch[N][30],siz[N][12];
char s[N];
vector<int>G[N];void insert(int c,int id){  //SAM构造 int p=lst,np=lst=++tot;len[np]=len[p]+1,siz[np][id]++; //记录每个节点被多少字符串更新while(p&&!ch[p][c]) ch[p][c]=np,p=fa[p];if(!p) fa[np]=root;else{int q=ch[p][c];if(len[q]==len[p]+1) fa[np]=q;else{int nq=++tot;memcpy(ch[nq],ch[q],sizeof(ch[q]));len[nq]=len[p]+1,fa[nq]=fa[q],fa[q]=fa[np]=nq;while(ch[p][c]==q) ch[p][c]=nq,p=fa[p];}}
}
void dfs(int x){for(int i=0;i<G[x].size();i++){int y=G[x][i];dfs(y);for(int id=1;id<=num;++id)siz[x][id]+=siz[y][id]; //跑DFS处理子串信息 }
}bool check(int p){if(!p) return false;for(int i=1;i<=num;++i) //如果所有字符串都有子串p if(!siz[p][i])  return false;return true;
}void work(){for(int i=2;i<=tot;++i)  G[fa[i]].push_back(i); dfs(root);int p=root,l=0;for(int i=1;i<=n;++i){int c=s[i]-'a';if(check(ch[p][c])) l++,p=ch[p][c];else{while(p&&!check(ch[p][c])) p=fa[p];if(p) l=len[p]+1,p=ch[p][c];else l=0,p=root;}ans=max(ans,l);}
}
signed main(){while(scanf("%s",s+1)!=EOF){n=strlen(s+1);lst=root;num++;for(int i=1;i<=n;++i) insert(s[i]-'a',num); //广义SAM }work();printf("%d\n",ans);return 0;
}

其他例题

每次插入后的不同子串个数

luogu P4070 [SDOI2016]生成魔咒

每次新增字符时,对答案的贡献为∣max(p)∣−∣min(p)∣+1=∣max(p)∣−∣max(p−>link)∣|max(p)|-|min(p)|+1=|max(p)|-|max(p->link)|∣max(p)∣−∣min(p)∣+1=∣max(p)∣−∣max(p−>link)∣,新建的nq节点是没有贡献的。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+7;int n,s[N],ans=0;
namespace SAM{map<int,int>ch[N];int lst=1,cnt=1,len[N],fa[N];inline void ins(int c){int p=lst,np=++cnt;lst=np;len[np]=len[p]+1;for(;!ch[p][c];p=fa[p]) ch[p][c]=np;if(!p) fa[np]=1;else{int q=ch[p][c];if(len[q]==len[p]+1) fa[np]=q;else{int nq=++cnt;len[nq]=len[p]+1;ch[nq]=ch[q];fa[nq]=fa[q];fa[q]=fa[np]=nq;for(;ch[p][c]==q;p=fa[p]) ch[p][c]=nq;} }ans+=len[np]-len[fa[np]];}
};signed main(){cin>>n;for(int i=1;i<=n;i++){cin>>s[i];SAM::ins(s[i]);cout<<ans<<"\n"; }return 0;
}

参考资料

https://www.luogu.com.cn/blog/Kesdiael3/hou-zhui-zi-dong-ji-yang-xie

https://www.bilibili.com/video/BV1wa4y1a7Ag

https://www.bilibili.com/video/BV1ED4y1U7aU

https://www.cnblogs.com/maomao9173/p/10447821.html

https://www.cnblogs.com/mikufun-hzoi-cpp/p/12098738.html

https://www.luogu.com.cn/blog/lhm126/solution-sp1812

【算法竞赛学习笔记】后缀自动机SAM-超经典的字符串问题详解相关推荐

  1. 【算法竞赛学习笔记】Link-Cut-Tree基础-超好懂的数据结构

    titile : Link-Cut-Tree time : 2021-7-21 tags : ACM,数据结构 author : Linno LCT Link-Cut-Tree,中文名为动态树,是一种 ...

  2. [学习笔记]后缀自动机

    解决大部分字符串问题的大杀器 给一下clj课件:戳我 SAM是一个博大精深的算法.虽然没有像网络流家族,Tarjan家族一样拉帮结派,但是自身包含很多方法. 一.前言 字符串常见问题:各种匹配 1.L ...

  3. 【算法竞赛学习笔记】pb_ds-超好懂的数据结构

    title : pb_ds date : 2021-8-21 tags : ACM,数据结构 author : Linno 简介 pb_ds库全称Policy-Based Data Structure ...

  4. 【算法竞赛学习笔记】快速傅里叶变换FFT-数学提高计划

    tilte : 快速傅里叶变换FFT学习笔记 tags : ACM,数论 date : 2021-7-18 简介 FFT(Fast Fourier Transformation),中文名快速傅里叶变换 ...

  5. SLAM学习笔记(二十)LIO-SAM流程及代码详解(最全)

    写在前面 关于安装配置,博客LIO_SAM实测运行,论文学习及代码注释[附对应google driver数据] 我觉得已经写的比较完善了.但是我觉得在注释方面,这位博主写的还不够完善,因此在学习以后, ...

  6. k8s学习笔记(10)--- kubernetes核心组件之controller manager详解

    kubernetes核心组件之controller manager详解 一.Controller Manager简介 1.1 Replication Controller 1.2 Node Contr ...

  7. 【算法竞赛学习笔记】莫队算法-超优雅的暴力算法

    title : 莫队算法 tags : ACM,暴力 date : 2021-10-30 author : Linno 普通莫队 常用操作:分块/排序/卡常/离散化等,直接上板子. luoguP270 ...

  8. 【算法竞赛学习笔记】超好懂的斯坦纳树详解!!!

    title : 斯坦纳树 tags : ACM 图论 date : 2021-6-26 author : Linno 什么是斯坦纳树 给定 n 个点 A1,A2,⋯,An试求连接此n个点,总长最短的直 ...

  9. 【算法竞赛学习笔记】KD-Tree

    title : KD-Tree date : 2022-4-7 tags : ACM,数据结构 author : Linno K-D tree K-D树是在k维欧几里得空间中组织点的数据结构.在算法竞 ...

最新文章

  1. centos7下安装intel Media Server Studio记录
  2. php压制错误的代码,为什么要压制PHP错误?
  3. 制作系统盘,重装新系统。
  4. Javascript 多线程编程​的前世今生
  5. 利用OpenCvSharp处理图片并在winformd的pictureBox中显示
  6. Python面向对象基础一
  7. 用朴素的英语解释9种关键机器学习算法
  8. 【Caffe】利用log文件绘制loss和accuracy(转载)
  9. TextView settextcolor 无效解决方案
  10. FireFox 在新建标签页插入“片段”广告引社区争议
  11. SQL已知现在的某一天求去年的同一天或去年同月份的第一天
  12. JS中的slice和splice
  13. 硬件设计——串联直流稳压电源
  14. DDA划线法(Digital Differenttial Analyzer,数值微分法)
  15. Android状态栏添加快捷开关(Tile)
  16. python 捕捉热键
  17. 元核云赋能银行业,智能双录产品助力银保业务合规高效响应
  18. [转载]人人旗下风车网产品经理的创业失败教训总结
  19. S3C2440的中断体系结构
  20. MDF,LDF格式文件还原数据库

热门文章

  1. Java版星球大战游戏(横向射击)
  2. oracle export utf-8,Linux操作系统下终端乱码的终极解决方案 export LANG=zh_CN.UTF-8 export LANG=en_US...
  3. nuxt 配置局域网访问
  4. 微信小程序购物车和左侧导航栏
  5. 制作CRM管理系统05(客户管理)
  6. Java 界面编程之图片展示
  7. 迷茫吗?30岁之前你一定要做这三件事
  8. Invivoscribe在其位于美国、欧洲和中国的参考实验室首次推出12色流式细胞计数功能
  9. Linux下安装Oracle11G详细流程(只为最好的你)
  10. 《温州传奇》01--温州企业要不要信息化 驴唇也能对上马嘴