1. AC 自动机 ACAM

1.1. 简介

AC 自动机用于解决多模式串匹配问题,例如求多个模式串在文本串中的出现次数。显著地,它的应用实际上非常广泛。

借助 KMP 的思想,我们对 Trie 树上的每个节点构造其失配指针 \(fail_i\),指向对于当前字符串的最长后缀(其他(前缀)作为当前串后缀的最长的一个),显著地,每个节点的失配指针都指向一个更短的状态。当这样的后缀不存在时,失配指针会指向表示空串的根节点。

考虑如何构建 \(fail_i\):

根据每个节点的失配指针都指向一个更短的状态这个性质,考虑用 BFS 解决 \(fail_i\) 的构建,对于当前节点 \(now\) 来说,假设深度较小的节点都已经被处理完了。

现在假设当前节点 \(i\) 由 \(fa_i\) 经过字符 \(ch\) 转移过来,使 \(fail_i\leftarrow trans(fail_{fa_i},ch)\),若不存在 \(fail_{fa_i}\) 通过 \(ch\) 转移到的某一节点,则尝试使 \(fail_i\leftarrow trans(fail_{fail_{fa_i}},ch)\)。直到 \(fail\) 指向根节点,说明根本不存在合法前缀,我们使 \(fail_i\leftarrow rt\)。

特殊地,若不存在 \(trans(fa,ch)\) 这个转移方式,则直接令 \(trans(fa,ch)\leftarrow trans(fail_{fa_i},ch)\)。

1.2. 常见技巧

1.2.1 fail 树的性质

构建的 \(fail\) 指针会形成一棵树,称为 fail 树。这不是废话吗。

  1. fail 树为一颗有根树,可以进行树剖等树上操作。
  2. 对于节点 \(p\) 与其对应字符串 \(t_p\),对于任意一个子树内节点 \(q\),都有 \(t_p\) 是 \(t_q\) 的后缀。逆命题亦成立。
  3. 设 \(cnt_p\) 表示作为 \(t_p\) 后缀的字符串数量。若无重复串,则 \(cnt_p\) 为树上节点 \(p\) 到根节点上字符串节点数量。

1.2.2 应用

ACAM 可以与 DP 结合,在自动机中进行 DP。

1.3. 例题

\(\color{blueviolet}{P5357}\)

时间瓶颈在于每次跳 \(tail\) 的重复访问,考虑经过每个点时记录权值,最后一起统计,可以采用 DFS 或者拓扑排序。

$\text{Code}$:
#include<bits/stdc++.h>
#define LL long long
#define UN unsigned
using namespace std;
//--------------------//
//IO
inline int rd()
{
int ret=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-f;ch=getchar();}
while(ch>='0'&&ch<='9')ret=ret*10+ch-'0',ch=getchar();
return ret*f;
}
//--------------------//
const int N=2e6+5,Ch=30; int n;
char str[N];
int id[N],ans[N];
struct Edge
{
int to,nex;
}edge[N];
int etot,head[N];
void add(int from,int to)
{
edge[++etot]={to,head[from]};
head[from]=etot;
return;
}
struct ACAM
{
struct Trie_Node
{
int nex[Ch];
int fail,flag,cnt;
}t[N];
int tot,fcnt;
void insert(char *s,int temp)
{
int now=0,len=strlen(s+1);
for(int i=1;i<=len;i++)
{
if(!t[now].nex[s[i]-'a'+1])
t[now].nex[s[i]-'a'+1]=++tot;
now=t[now].nex[s[i]-'a'+1];
}
if(!t[now].flag)
t[now].flag=++fcnt;
id[temp]=t[now].flag;
return;
} void get_fail()
{
queue<int>q;
for(int i=1;i<=26;i++)
{
if(t[0].nex[i])
q.push(t[0].nex[i]);
}
while(!q.empty())
{
int now=q.front();
q.pop();
for(int to,i=1;i<=26;i++)
{
to=t[now].nex[i];
if(!to)
{
t[now].nex[i]=t[t[now].fail].nex[i];
continue;
}
t[to].fail=t[t[now].fail].nex[i];
q.push(to);
}
}
return;
}
void get_ans(char *s)
{
int len=strlen(s+1),now=0;
for(int i=1;i<=len;i++)
{
now=t[now].nex[s[i]-'a'+1];
t[now].cnt++;
}
return;
}
void build()
{
for(int i=1;i<=tot;i++)
add(t[i].fail,i);
return;
}
void DFS(int now)
{
//printf("%d %d\n",now,t[now].cnt);
for(int to,i=head[now];i;i=edge[i].nex)
{
to=edge[i].to;
DFS(to);
t[now].cnt+=t[to].cnt;
}
if(t[now].flag)
ans[t[now].flag]=t[now].cnt;
return;
}
}A;
//--------------------//
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%s",str+1),A.insert(str,i);
A.get_fail();
scanf("%s",str+1);
A.get_ans(str);
A.build();
A.DFS(0);
for(int i=1;i<=n;i++)
printf("%d\n",ans[id[i]]);
return 0;
}

2. 后缀自动机 SAM

2.1. 简介

2.1.1. 基本定义与结论

SAM 一般用于在线性时间内解决如下问题:

  • 在一个字符串中求另一字符串出现的位置。
  • 一个字符串的本质不同的子串个数。

SAM 的定义:一个长为 \(n\) 的字符串 \(s\) 的SAM 是一个接受 \(s\) 的所有后缀最小的有限状态自动机。

较为人话的说法:

  • SAM 是一张有向无环图。每个节点为一个状态,边则为状态间的转移
  • 存在一个源点 \(t_0\),称作初始状态,其他状态均可从 \(t_0\) 出发到达。
  • 每个转移(边)对应一个字符(一条路径表示一个字符串),从一个状态(节点)出发的转移均不同。
  • 从初始状态 \(t_0\) 出发,最终转移到一个终止状态,则此径代表的字符串一定是原字符串 \(s\) 的一个后缀,\(s\) 的每个后缀都可以用一条这种路径表示。
  • 满足上述条件的自动机中,SAM 的节点数量最少

SAM 的较为重要的一条性质:

  • 初始状态出发到任意状态的路径与串 \(s\) 的所有子串(本质不同)一一对应

接下来给出一些定义和符号表示:

  • \(c_{p,q}\):转移 \(p\to q\) 代表的字符
  • \(\mathrm{st}(p,c)\):状态 \(p\) 经过字符 \(c\) 转移所到达的状态
  • \(\mathrm{endpos}(t)\):字符串 \(t\) 在原字符串中所有结束位置的集合
  • 等价类:对于 \(\mathrm{endpos}\) 集合相同的子串,我们将它们划分为一个等价类,作为一个状态
  • \(\mathrm{ep}(p)\):状态 \(p\) 所对应的 \(\mathrm{endpos}\) 集合
  • \(\mathrm{substr}(p)\):状态 \(p\) 所表示的所有子串的集合
  • \(\mathrm{longest}(p)\):状态 \(p\) 所表示的所有子串中,长度最长的那一个子串
  • \(\mathrm{shortest}(p)\):状态 \(p\) 所表示的所有子串中,长度最短的那一个子串
  • \(\mathrm{len}(p)\):状态 \(p\) 所表示的所有子串中,长度最长的那一个子串的长度
  • \(\mathrm{minlen}(p)\):状态 \(p\) 所表示的所有子串中,长度最短的那一个子串的长度

方便理解,我们再次整理上述定义。SAM 的每个状态对应一个等价类,即 \(\mathrm{endpos}\) 集合相同的子串所组成的状态。具体地,我们给出例子。假设现有串 \(s=\texttt{"abcab"}\),则 \(\mathrm{endpos}(\texttt{"ab"})=\mathrm{endpos}(\texttt{"b"})={2,5}\),而串 \(\texttt{"ab"},\texttt{"b"}\) 便属于统一等价类。

\(\mathrm{longest}(p),\mathrm{shortest}(p),\mathrm{len}(p),\mathrm{minlen}(p)\) 则描述了状态 \(p\) 对应的字符串集合 \(\mathrm{substr}(p)\) 中最长、最短的字符串以及它们的长度。

下面介绍两个结论:

  • 对于两个非空字符串 \(x,y\)(\(|x|\leq |y|\)),要么有 \(\mathrm{endpos}(x)\subseteq \mathrm{endpos}(y)\),要么有 \(\mathrm{endpos}(x)\cup\mathrm{endpos}(y)=\varnothing\)
  • 对于一个状态 \(p\),其包含的字符串长度连续,且较短者是较长者的后缀。

两个结论的证明过程并不复杂,简单思考也可以感性理解,在这里不给出具体证明,具体可参考 Alex_Wei 的博客或者 OI-Wiki,链接见参考资料部分。

总结以上两个性质有:

  • 对于一个子串 \(t\) 的所有后缀,其 \(endpos\) 集合的大小随着后缀长度减小而单调不降,并且较大的集合包含较小的集合。简单定性分析,当后缀越短时,约束条件越宽松,出现位置更可能多。

根据上面的性质,我们给出 SAM 中最核心的定义:

  • 后缀链接 \(\mathrm{link}(p)\):对于所有 \(\mathrm{ep}(p)\subseteq\mathrm{ep}(q)\),\(\mathrm{link}(p)\) 指向 \(\mathrm{len}(q)\) 最大的那个 \(q\)。

稍微直观一点理解我们仍然用一个例子,假设现有串 \(s=\texttt{"babcab"}\)

我们假设状态 \(p\) 对应着我们的字符串集合 \(\{\texttt{"cab"}\}\),对应有 \(\mathrm{ep}(p)=\{6\}\)。

状态 \(a\) 对应字符串集合 \(\{\texttt{"ab"}\}\),对应有 \(\mathrm{ep}(a)=\{3,6\}\)。

状态 \(b\) 对应字符串集合 \(\{\texttt{"b"}\}\),对应有 \(\mathrm{ep}(b)=\{1,3,6\}\)。

根据刚才的定义,现有 \(\mathrm{ep}(p)\subseteq\mathrm{ep}(a),\mathrm{ep}(p)\subseteq\mathrm{ep}(b)\),且只有 \(a,b\) 两状态的 \(\mathrm{ep}\) 包含状态 \(p\) 的 \(\mathrm{ep}\),又有 \(\mathrm{len}(a)>\mathrm{len}(b)\),所以 \(\mathrm{link}(p)\) 应指向状态 \(a\)。

再次重复一下 \(\mathrm{link}(p)\) 的意义,它指向了状态 \(p\) 的所有后缀状态中(最长)长度最大的那个,易知 \(\mathrm{len}(\mathrm{link}(p))+1=\mathrm{minlen}(p)\)。

对于后缀链接有这样一条性质:

  • 所有后缀链接形成一颗以 \(t_0\) 为根的树。(\(t_0\) 是我们最开始定义的初始状态,它包含了空串。)

显著的,对于任意状态(除了 \(t_0\)),沿着后缀链接移动总会达到一个 \(\mathrm{len}\) 更短的状态,直到 \(t_0\)。

后缀链接构成的树本质上是 \(\mathrm{endpos}\) 集合构成的一棵树,我们一般称为 Parent 树。

2.1.2. 关键结论

这部分主要摘自 Alex_wei 的博客,理解构建 SAM 的过程需要理解此部分的结论。如果你能较为直观地理解 Parent 树,那么这部分的结论都很显然,大部分证明请参考 Alex_wei 的博客。

第一组结论:

  • 从任意状态 \(p\) 出发通过后缀链接跳转到 \(t_0\) 的路径,所有路径上的状态 \(q\) 的 \([\mathrm{minlen}(q),\mathrm{len}(q)]\) 无交集,并且范围随着在 Parent 上的深度减小而减小,并且他们的并集形成一个连续区间 \([0,\mathrm{len}(p)]\)。
  • 从任意状态 \(p\) 出发通过后缀链接跳转到 \(t_0\) 的路径,所有路径上的状态 \(q\) 的 \(\mathrm{substr}(q)\) 的并集为 \(\mathrm{longest}(p)\) 的所有后缀

第二组结论:

  • 有任意状态 \(p\) 使得有从 \(p\) 到 \(q\) 的转移,对于 \(\forall t_p\in \mathrm{substr}(p)\),有 \(t_p+c_{p,q}\in\mathrm{substr}(q)\)。
  • 对于 \(\forall t_q\in\mathrm{substr}(q)\),存在且只存在一个状态 \(p\) 使得有从 \(p\) 到 \(q\) 的转移,并且 \(\exist t_p\in \mathrm{substr}(p)\) 使得 \(t_p+c_{p,q}=t_q\)。

第三组结论:

  • 不存在从状态 \(p\) 到状态 \(q\) 的转移使得 \(\mathrm{len}(p)+1>\mathrm{len}(q)\)。
  • 存在唯一状态 \(p\),有 \(p\) 到 \(q\) 的转移使得 \(\mathrm{len}(p)+1=\mathrm{len}(q)\)。
  • 存在唯一状态 \(p\),有 \(p\) 到 \(q\) 的转移使得 \(\mathrm{minlen}(p)+1=\mathrm{minlen}(q)\)。

在给出第四组结论之前,我们先给出两个定义:

  • \(\mathrm{maxtrans}(q)\):有 \(p\) 到 \(q\) 的转移使得 \(\mathrm{len}(p)+1=\mathrm{len}(q)\) 的唯一 \(p\)。
  • \(\mathrm{mintrans}(q)\):有 \(p\) 到 \(q\) 的转移使得 \(\mathrm{minlen}(p)+1=\mathrm{minlen}(q)\) 的唯一 \(p\)。

第四组结论:

  • 对于转移 \(p\to q\),一定有 \(p\) 在 Parent 树上为 \(\mathrm{maxtrans}(q)\) 或其祖先。
  • 对于转移 \(p\to q\),一定有 \(p\) 在 Parent 树上为 \(\mathrm{mintrans}(q)\) 或其子树内节点。
  • 对于转移 \(p\to q\),所有这样的 \(p\) 在 Parent 树上构成了一条深度递减链,即 \(\mathrm{mintrans}(q)\to\mathrm{maxtrans}(q)\)。

并不难理解,考虑到 Parent 树的定义以及性质,一条从上到下的链中字符串长度连续并且都为链底长串的子串。

2.1.3 SAM 的构建

至此为止,我们可以用以上的所有性质来构建 SAM 了。

我们考虑在前缀串 \(s[1,i-1]\) 的 SAM 基础上插入当前字符更新整个 SAM。

设上一状态(目前已插入的前缀所在状态)为 \(las\),当前状态为 \(cur\),状态总数为 \(tot\)。初始时 \(las,cnt\) 均为 \(1\),即我们设初始状态 \(t_0=1\)。

我们使先新建编号为 \(cur\) 赋值,\(cur\) 表示的是以当前插入字符结尾前缀的状态,然后令 \(p\leftarrow las\),\(p\) 表示我们现在更新到的节点。

考虑转移边的处理,我们将 \(p\) 沿着 Parent 树向上跳,可以保证的是每到一个节点都是 \(s[1,i-1]\) 的后缀,所以我们要更新其向 \(s_i\) 的转移,若其没有此转移,我们就为其新建出边,并继续沿着 Parent 向上跳转。直到我们到一个节点存在此转移,说明再往上的节点都有此转移,就不必再更新了。

接下来考虑 Parent 树边的构建,我们分三种情况讨论。


情况一:

不存在一个 \(p\) 有以 \(s_i\) 的转移。

这种情况存在且只存在于 \(s_i\) 这个字符从未被加入过,我们令 \(\mathrm{link}(cur)\leftarrow t_0\) 即可。


情况二:

存在 \(p\) 有以 \(s_i\) 的转移,令 \(q=\mathrm{st}(p,s_i)\),且 \(\mathrm{len}(p)+1=\mathrm{len}(q)\)。

我们设 \(las\to t_0\) Parent 树上的路径 \(p\) 的前一个状态有 \(p'\),并且 \(p'\) 已经新建了 \(s_i\) 的转移到 \(cur\),根据 Parent 性质有 \(\mathrm{minlen}(cur)=\mathrm{len}(p')+1=(\mathrm{len}(p)+1)+1=\mathrm{len}(q)+1\),根据定义令 \(\mathrm{link}(cur)\leftarrow q\)。


情况三:

存在 \(p\) 有以 \(s_i\) 的转移,令 \(q=\mathrm{st}(p,s_i)\),且 \(\mathrm{len}(p)+1\not=\mathrm{len}(q)\)。

当 \(\mathrm{len}(p)+1\not=\mathrm{len}(q)\),只能存在 \(\mathrm{len}(p)+1<\mathrm{len}(q)\)。状态 \(q\) 中有一部分是无法转移到我们当前状态 \(cur\) 的,可以理解为 \(\mathrm{substr}(q)\) 不全为 \(\mathrm{substr}(cur)\) 的后缀,因为在 \(q\) 中存在以除 \(p\) 之外的状态转移过来的部分。我们考虑将 \(q\) 分为小于等于 \(\mathrm{len}(p)+1\) 和大于 \(\mathrm{len}(p)+1\) 的两部分,并新建状态 \(cl\) 存储小于等于 \(\mathrm{len}(p)+1\) 的部分。对于继承我们需要进行以下操作:

  • \(cl\) 保存所有 \(q\) 向外的转移。
  • \(\mathrm{len}(cl)\) 应等于 \(\mathrm{len}(p)+1\)。
  • \(\mathrm{link}(cl)\) 应等于原来的 \(\mathrm{link}(q)\)。
  • 对于 Parent 树上 \(p\to t\) 路径上的状态,我们也应该将原指向 \(q\) 的转移指向 \(cl\)。
  • \(\mathrm{link}(q),\mathrm{link}(cur)\) 应等于 \(cl\)。

在构建完 Parent 树边后,我们使 \(las\leftarrow cur\),退出构建即可。

2.1.4. SAM 的时空间限制

对于 SAM 构造、使用时间复杂度为线性的证明略复杂,仍是可参考 Alex_Wei 的博客或者 OI-Wiki。

因为每次加入字符最多新建两个节点,所以空间应当开双倍,特殊地,当字符集很大时,可以用 map 维护转移。

2.2. 常用技巧

2.2.1. 求本质不同子串个数

考虑每个子串存在且只存在于一个状态,考虑计数所有状态中的子串总和,对于每个状态 \(i\) 有答案 \(\sum\mathrm{len}(i)-\mathrm{len}(\mathrm{link}(i))\)。

另一种考虑方式,SAM 上一条路径对应一个子串,对于每个状态求一下以此状态结尾的路径数量,最后求和,可以考虑用拓扑排序。(相当于转化为所有前缀的后缀个数。)

2.2.2. 解决匹配串问题

较为简单的,直接在 SAM 上各种跑,失配就跳 Parent,与 KMP 的思想相似。

2.2.3. 求 \(\mathrm{endpos}\) 集合大小

每加入一个新状态 \(cur\),为其计数器打上 \(1\),SAM 构建好后求一下 Parent 子树内计数器和即可。

2.3. 例题

\(\color{blueviolet}{P3804}\)

SAM 板子。

$\text{Code}$:
#include<bits/stdc++.h>
#define LL long long
#define UN unsigned
using namespace std;
//--------------------//
//IO
inline int rd()
{
int ret=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-f;ch=getchar();}
while(ch>='0'&&ch<='9')ret=ret*10+ch-'0',ch=getchar();
return ret*f;
}
//--------------------//
const int N=1e6+5,N2=2e6+5; struct Edge
{
int to,nex;
}edge[N2];
int etot,head[N2];
void add(int from,int to)
{
edge[++etot]={to,head[from]};
head[from]=etot;
return;
} char s[N];
struct SAM
{
struct SAM_Node
{
int nex[30];
int len,fa;
int cnt;
}a[N2];
int las=1,tot=1;
void insert(char ch)
{
int it=ch-'a'+1,p=las;
int cur=++tot;
a[cur].len=a[las].len+1,las=cur,a[cur].cnt=1;
while(!a[p].nex[it]&&p)
a[p].nex[it]=cur,p=a[p].fa;
if(!p)
{
a[cur].fa=1;
return;
}
int q=a[p].nex[it];
if(a[p].len+1==a[q].len)
{
a[cur].fa=q;
return;
}
int cl=++tot;
a[cl]=a[q],a[cl].cnt=0,a[cl].len=a[p].len+1;
a[cur].fa=a[q].fa=cl;
while(a[p].nex[it]==q&&p)
a[p].nex[it]=cl,p=a[p].fa;
return;
}
void build()
{
for(int i=1;i<=tot;i++)
add(a[i].fa,i);
return;
}
LL ans=0;
void DFS(int now)
{
for(int to,i=head[now];i;i=edge[i].nex)
{
to=edge[i].to;
DFS(to);
a[now].cnt+=a[to].cnt;
}
if(a[now].cnt!=1)
ans=max(ans,1LL*a[now].cnt*a[now].len);
return;
}
}S;
//--------------------//
int main()
{
scanf("%s",s+1);
int len=strlen(s+1);
for(int i=1;i<=len;i++)
S.insert(s[i]);
S.build();
S.DFS(1);
printf("%lld",S.ans);
return 0;
}

参考资料

\(\mathcal{Alex\_Wei's\ Blog}\)

\(\mathcal{OI-Wiki}\)

「Note」字符串方向 - 自动机相关的更多相关文章

  1. 【LOJ】#3095. 「SNOI2019」字符串

    LOJ#3095. 「SNOI2019」字符串 如果两个串\(i,j\)比较\(i < j\),如果离\(a_{i}\)最近的不同的数是\(a_{k}\),如果\(j < k\)那么\(i ...

  2. 「JSOI2015」字符串树

    「JSOI2015」字符串树 传送门 显然可以树上差分. 我们对于树上每一条从根出发的路径都开一 棵 \(\text{Trie}\) 树,那么我们就只需要在 \(\text{Trie}\) 树中插入一 ...

  3. LOJ 3055 「HNOI2019」JOJO—— kmp自动机+主席树

    题目:https://loj.ac/problem/3055 先写了暴力.本来想的是 n<=300 的那个在树上暴力维护好整个字符串, x=1 的那个用主席树维护好字符串和 nxt 数组.但 x ...

  4. LG2444/BZOJ2938 「POI2000」病毒 AC自动机

    问题描述 LG2444 BZOJ2938 I \(\mathrm{AC}\)自动机 \(\mathrm{AC}\)自动机是一种多模式串匹配算法,本萌新今天刚学了它qwq 约定在构造\(\mathrm{ ...

  5. 「NOTE」常系数齐次线性递推

    要不是考到了,我还没发现这玩意我不是很会-- # 前置 多项式取模: 矩阵快速幂. # 常系数齐次线性递推 描述的是这么一个问题,给定数列 \(c_1,c_2,\dots,c_k\) 以及数列 \(f ...

  6. 「Foundation」字符串

    一.Foundation框架中一些常用的类 字符串型: NSString:不可变字符串 NSMutableString:可变字符串 集合型: 1)NSArray:OC不可变数组  NSMutableA ...

  7. 【LOJ】#2278. 「HAOI2017」字符串

    题解 好神仙的题啊 感觉转二维平面能想到,算重复情况的方法真想不到啊 通过扒stdcall代码获得的题解QAQQQQ 我们先把\(p_i\)正串反串建出一个AC自动机来 然后我们把s串放在上面跑匹配, ...

  8. 「Python」字符串操作内置函数

    目录: capitalize casefold center count encode decode endswith expandtabs find format format_map index ...

  9. 「Redis」字符串

    原文链接:https://www.changxuan.top/?p=1109 简介 Redis 中自定义的字符串结构. 字符串是 Redis 中最常用的一种数据类型,在 Redis 中专门封装了一个字 ...

  10. 「SNOI2019」字符串

    题目 看起来非常一眼啊,我们完全可以\(std::sort\)来解决这歌问题 于是现在的问题转化成了比较函数怎么写 随便画一下就会发现前面的好几位是一样的,后面的好几位也是一样,只需要比较中间的一段子 ...

随机推荐

  1. python 打开yaml文件提示Empty test suite.

    网上方案: 我自己: 将test改为其他名称开头即可 读取成功

  2. MySQL REPLACE INTO语句

    介绍 在向表中插入数据时,我们经常会:首先判断数据是否存在:如果不存在,则插入:如果存在,则更新. 但在 MySQL 中有更简单的方法,replace into(insert into 的增强版),当 ...

  3. 通过局域网访问连接 vite 或 Django 之类的项目

    博客地址:https://www.cnblogs.com/zylyehuo/ step1 将 vite 或 Django 类的项目启动 ip 设置为 0.0.0.0:端口 step2 查询本机电脑在当 ...

  4. Pydantic Mixin:构建可组合的验证系统体系

    title: Pydantic Mixin:构建可组合的验证系统体系 date: 2025/3/22 updated: 2025/3/22 author: cmdragon excerpt: Pyda ...

  5. 模型即产品?从应用角度看AI产品发展趋势

    提供AI咨询+AI项目陪跑服务,有需要回复1 在ChatGPT发布后的两年里,AI应用层的发展可以用一个词来评价不温不火,到去年年初时甚至有些疲软的情况.其原因有三点: 第一,算力不足,模型API响应 ...

  6. 元模型对AI的哲学意义:让机器真正"懂"世界

    元模型对AI的哲学意义:让机器真正"懂"世界 (用日常语言和比喻解释) 1. 传统AI像"死记硬背的学生" 问题:现在的ChatGPT就像背了无数词典的人,能对 ...

  7. 面试题:Linux 系统基础 (二)

    Linux系统中的定时任务有哪些类型,它们是如何配置的? Linux系统中的定时任务主要有两种类型:Cron作业和at作业. 1.Cron作业: 使用crontab命令配置和管理. 配置周期性执行的任 ...

  8. 如何学习SLAM(超级全面)

    如何学习SLAM(超级全面) 由于SLAM是一个错综复杂的研究领域,涉及到非常多的关键技术.这里先讲讲学习方法论,然后对一些关键性概念(包括SLAM.ROS.SLAM移动机器人)进行分析,最后给出典型 ...

  9. 全网最详细的CM311-1A魔百和刷Armbian教程

    CM311-1A魔百和搭载了晶晨S905L3A芯片(实质上是S905X2的定制版本,两者在性能上并无显著差异).然而,遗憾的是,关于这款设备的网络教程相对较少,导致我在自学过程中遇到了不少挑战和障碍. ...

  10. STM32串口缓冲区

    在嵌入式开发中,外设通信(如UART.SPI.I2C)的数据接收常面临两大挑战:不定时.不定量数据的实时处理和高频率数据流下的稳定性保障.传统的轮询方式效率低下,而中断驱动的接收逻辑又容易因处理延迟导 ...