尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
算法竞赛树结构进阶:从LCA到树链剖分与虚树实战
1. 从线性到层级为什么树结构是算法竞赛的分水岭刚接触算法竞赛那会儿我对数据结构的认知基本停留在数组和链表上觉得能用一维数组解决的问题何必搞那么复杂。直到遇到一道题给出一棵树每次询问两个节点之间的路径上权值最大的边或者要求动态修改某个节点的子树内所有节点的值。用数组硬做时间复杂度直接爆炸超时超到怀疑人生。那时候我才真正意识到树这种非线性结构是算法竞赛从入门到进阶的一道硬门槛。树结构之所以重要是因为它天然适合表达层级关系和递归性质。现实世界中有大量数据本身就是树形的文件系统的目录结构、公司组织架构、网页的DOM树、表达式语法树等等。在算法竞赛中很多问题表面上看起来和图论、动态规划、字符串处理相关但核心解法往往依赖于对树结构的深刻理解。比如树形DP、树上倍增、树链剖分、虚树、点分治这些高级技巧都是建立在树的基础之上的。这篇文章主要面向已经掌握基本数据结构数组、链表、栈、队列、哈希表和基础图论图的存储、DFS、BFS的算法竞赛学习者。我会从树的存储方式讲起逐步深入到LCA、树链剖分、树上差分等核心技巧结合我自己的踩坑经验把每个知识点的为什么和怎么做都掰开揉碎讲清楚。如果你正在准备区域赛或者提高组级别的比赛这篇文章应该能帮你把树结构这块硬骨头啃下来。2. 树的存储与遍历看似简单坑却不少2.1 三种存储方式的取舍与实战选择树的存储方式直接决定了后续算法的实现难度和常数大小。我见过太多人因为存储方式选错导致代码写得又长又容易出错。常见的存储方式有三种邻接矩阵、邻接表和链式前向星。邻接矩阵用二维数组g[u][v]表示节点u和v之间是否有边。优点是实现简单判断两点是否相邻是O(1)的。缺点是空间复杂度O(n²)当n超过5000的时候基本就爆内存了。而且遍历某个节点的所有邻居需要O(n)的时间效率很低。我一般只在n≤1000且需要频繁判断两点是否相邻的情况下才用它。邻接表用vectorint g[N]存储每个节点的邻居。优点是空间复杂度O(nm)遍历邻居的效率高。缺点是vector的动态扩容会带来一定的常数开销而且在某些对缓存不友好的场景下性能不如链式前向星。不过对于绝大多数题目来说邻接表已经足够好了代码可读性也高。链式前向星用数组模拟链表通过head[]、to[]、nxt[]三个数组来存储。优点是空间紧凑、缓存友好、常数小适合边数很大的场景。缺点是实现稍微复杂一点需要手动管理边的编号。我在写需要卡常数的题目时通常会优先选择链式前向星。// 链式前向星的标准写法 int head[N], to[M], nxt[M], w[M], cnt 0; void addEdge(int u, int v, int weight) { to[cnt] v; w[cnt] weight; nxt[cnt] head[u]; head[u] cnt; } // 遍历u的所有邻居 for (int i head[u]; i; i nxt[i]) { int v to[i]; // 处理边(u, v) }注意链式前向星的边数组大小要开到最大边数的两倍无向图因为每条无向边要存两次。我见过不少人因为这个原因RE查了半天才发现是数组开小了。2.2 DFS序把树拍扁成一维数组的艺术DFS序是树结构中最基础也最强大的工具之一。它的核心思想是对树进行一次DFS记录每个节点第一次被访问的时间戳dfn[u]和离开时的时间戳或者子树大小siz[u]。这样一棵子树在DFS序中就对应了一段连续的区间。这个性质极其有用。比如要把某个子树内所有节点的值加上x在树上做需要遍历整棵子树复杂度O(siz)。但有了DFS序之后子树对应区间[dfn[u], dfn[u]siz[u]-1]直接用树状数组或线段树做区间修改即可复杂度降到O(log n)。int dfn[N], siz[N], timer 0; void dfs(int u, int fa) { dfn[u] timer; siz[u] 1; for (int i head[u]; i; i nxt[i]) { int v to[i]; if (v fa) continue; dfs(v, u); siz[u] siz[v]; } } // 子树u对应的区间就是[dfn[u], dfn[u] siz[u] - 1]我刚开始学DFS序的时候总觉得这东西没什么了不起的不就是个时间戳吗。后来做了一道题给一棵树支持三种操作——单点修改、子树修改、查询子树和。用DFS序加上树状数组代码不到50行就写完了而且跑得飞快。从那以后DFS序就成了我处理子树问题的首选工具。2.3 欧拉序与RMQLCA问题的经典解法欧拉序是另一种把树拍扁的方式。它的定义是在DFS的过程中每次进入一个节点或者回溯到一个节点时都把该节点加入序列。这样得到的序列长度是2n-1。欧拉序有一个重要性质任意两个节点u和v的LCA就是欧拉序中u第一次出现的位置和v第一次出现的位置之间深度最小的那个节点。基于这个性质LCA问题就转化成了RMQ区间最小值查询问题。用ST表预处理可以在O(1)的时间内回答每次LCA查询预处理复杂度O(n log n)。这是LCA问题的最优解法之一。int euler[N 1], dep[N], first[N], timer 0; int st[N 1][20], lg[N 1]; void dfs(int u, int fa, int d) { dep[u] d; first[u] timer; euler[timer] u; for (int i head[u]; i; i nxt[i]) { int v to[i]; if (v fa) continue; dfs(v, u, d 1); euler[timer] u; } } void buildST() { lg[1] 0; for (int i 2; i timer; i) lg[i] lg[i 1] 1; for (int i 1; i timer; i) st[i][0] euler[i]; for (int j 1; (1 j) timer; j) for (int i 1; i (1 j) - 1 timer; i) { int x st[i][j - 1], y st[i (1 (j - 1))][j - 1]; st[i][j] dep[x] dep[y] ? x : y; } } int lca(int u, int v) { int l first[u], r first[v]; if (l r) swap(l, r); int k lg[r - l 1]; int x st[l][k], y st[r - (1 k) 1][k]; return dep[x] dep[y] ? x : y; }提示欧拉序的长度是2n-1所以ST表的第二维要开到log2(2n)左右。我一般直接开20对于n≤10^5的题目足够了。如果n更大需要开到21或22。3. 树上倍增与LCA从暴力到优雅的进化3.1 倍增思想的核心逻辑倍增是一种非常巧妙的思想任何整数都可以表示成若干个2的幂次之和。基于这个性质我们可以在O(log n)的时间内完成很多看似需要O(n)的操作。在树上倍增最经典的应用就是求LCA和求第k级祖先。先来看暴力求LCA的做法先把两个节点中较深的那个往上跳到和另一个节点相同的深度然后两个节点一起往上跳直到它们的父节点相同。这个做法在最坏情况下比如一条链是O(n)的遇到多次询问就会超时。倍增的优化思路是不一步一步跳而是按2的幂次跳。预处理出每个节点向上跳2^j步后到达的祖先up[u][j]。这样跳任意步数都可以拆分成若干个2的幂次每次跳的复杂度是O(log n)。int up[N][20], dep[N]; void dfs(int u, int fa) { dep[u] dep[fa] 1; up[u][0] fa; for (int j 1; j 20; j) up[u][j] up[up[u][j - 1]][j - 1]; for (int i head[u]; i; i nxt[i]) { int v to[i]; if (v fa) continue; dfs(v, u); } } int lca(int u, int v) { if (dep[u] dep[v]) swap(u, v); int diff dep[u] - dep[v]; for (int j 0; j 20; j) if (diff (1 j)) u up[u][j]; if (u v) return u; for (int j 19; j 0; j--) if (up[u][j] ! up[v][j]) { u up[u][j]; v up[v][j]; } return up[u][0]; }这段代码我写过不下几十遍但每次写还是要注意几个细节。第一up数组的第二维大小要开到log2(n)1对于n10^5开到17就够了但为了保险我一般开20。第二在求LCA的时候第二个循环必须从大到小枚举j因为我们要尽可能跳得远。如果从小到大枚举可能会跳过LCA。3.2 倍增求第k级祖先的两种写法求第k级祖先就是求节点u向上跳k步后到达的节点。用倍增可以做到O(log n)。写法很简单把k二进制拆分对应位为1就跳。int kthAncestor(int u, int k) { for (int j 0; j 20; j) if (k (1 j)) u up[u][j]; return u; }但这里有一个坑如果k大于u的深度那么u会跳到0假设0是虚拟根节点。所以在调用之前最好先判断一下k是否小于等于dep[u]。如果题目保证k合法那就不用判断。还有一种更高效的求第k级祖先的方法叫做长链剖分可以在O(1)的时间内回答每次询问预处理O(n)。不过长链剖分的实现比较复杂我一般只在题目明确要求O(1)查询的时候才会用。对于大多数题目倍增的O(log n)已经足够了。3.3 倍增在树形DP中的妙用倍增不仅仅可以用来求LCA还可以用来优化树形DP。比如有一类问题每个节点有一个权值要求从根节点出发每次可以向下走任意步求满足某种条件的最长路径。这类问题如果直接做树形DP状态转移可能涉及子树内所有节点复杂度O(n²)。但如果用倍增预处理出每个节点向上跳2^j步的信息就可以在O(n log n)的时间内解决。我印象比较深的一道题是给一棵树每个节点有一个颜色每次询问从u到v的路径上有多少种不同的颜色。这道题用倍增加上状态压缩把颜色集合压成一个64位整数就可以做。虽然正解是树上莫队或者树链剖分但倍增的做法在n≤50000的时候也能过而且代码短很多。4. 树链剖分把树上路径问题变成区间问题4.1 重链剖分的核心原理树链剖分Heavy-Light Decomposition简称HLD是处理树上路径问题的终极武器。它的核心思想是把树划分成若干条重链使得任意一条路径都可以拆分成O(log n)条重链上的连续区间。这样树上路径的修改和查询就可以转化成线段树上的区间操作。重链剖分的关键概念有三个重儿子、重链和轻儿子。重儿子是指子树大小最大的那个儿子重链是从一个节点出发一直沿着重儿子走到底形成的链。轻儿子就是除了重儿子之外的其他儿子。重链剖分有两个重要性质第一从任意节点到根节点的路径上最多经过O(log n)条重链。第二每条重链上的节点在DFS序中是连续的。基于这两个性质我们可以把路径查询拆分成若干段重链上的区间查询。int siz[N], son[N], fa[N], dep[N]; int top[N], dfn[N], rnk[N], timer 0; void dfs1(int u, int f) { fa[u] f; dep[u] dep[f] 1; siz[u] 1; son[u] 0; for (int i head[u]; i; i nxt[i]) { int v to[i]; if (v f) continue; dfs1(v, u); siz[u] siz[v]; if (siz[v] siz[son[u]]) son[u] v; } } void dfs2(int u, int tp) { top[u] tp; dfn[u] timer; rnk[timer] u; if (son[u]) dfs2(son[u], tp); for (int i head[u]; i; i nxt[i]) { int v to[i]; if (v fa[u] || v son[u]) continue; dfs2(v, v); } }第一次DFS求出每个节点的子树大小、重儿子、父节点和深度。第二次DFS按照优先访问重儿子的顺序进行给每个节点分配DFS序并记录每个节点所在重链的顶端节点。4.2 路径查询与修改的模板实现有了树链剖分之后路径查询的模板就非常固定了。核心逻辑是不断把所在重链顶端深度较大的那个节点往上跳直到两个节点在同一条重链上。void pathUpdate(int u, int v, int val) { while (top[u] ! top[v]) { if (dep[top[u]] dep[top[v]]) swap(u, v); segUpdate(dfn[top[u]], dfn[u], val); u fa[top[u]]; } if (dep[u] dep[v]) swap(u, v); segUpdate(dfn[u], dfn[v], val); } int pathQuery(int u, int v) { int res 0; while (top[u] ! top[v]) { if (dep[top[u]] dep[top[v]]) swap(u, v); res segQuery(dfn[top[u]], dfn[u]); u fa[top[u]]; } if (dep[u] dep[v]) swap(u, v); res segQuery(dfn[u], dfn[v]); return res; }这段代码看起来简单但有几个细节容易出错。第一在跳链的时候要先比较dep[top[u]]和dep[top[v]]而不是比较dep[u]和dep[v]。第二跳完之后u要更新为fa[top[u]]而不是top[u]。第三最后在同一条重链上的时候要比较dep[u]和dep[v]让深度较小的那个作为区间的左端点。注意树链剖分处理的是边权还是点权会影响最后一步的处理方式。如果是点权直接查询[dfn[u], dfn[v]]即可。如果是边权通常要把边权下放到深度较大的节点上最后查询[dfn[u]1, dfn[v]]假设u是LCA。这个细节我踩过坑当时调了一个多小时才发现问题。4.3 树链剖分与线段树的配合技巧树链剖分本身不复杂复杂的是和线段树的配合。线段树需要支持区间修改和区间查询而且修改和查询的操作可能有很多种比如区间加、区间乘、区间赋值、区间求和、区间求最大值等。我一般会把线段树封装成一个结构体这样代码更清晰也不容易出错。struct SegTree { int sum[N 2], lazy[N 2]; void pushUp(int rt) { sum[rt] sum[rt 1] sum[rt 1 | 1]; } void pushDown(int rt, int l, int r) { if (lazy[rt]) { int mid (l r) 1; sum[rt 1] lazy[rt] * (mid - l 1); sum[rt 1 | 1] lazy[rt] * (r - mid); lazy[rt 1] lazy[rt]; lazy[rt 1 | 1] lazy[rt]; lazy[rt] 0; } } void build(int rt, int l, int r, int a[]) { if (l r) { sum[rt] a[l]; return; } int mid (l r) 1; build(rt 1, l, mid, a); build(rt 1 | 1, mid 1, r, a); pushUp(rt); } void update(int rt, int l, int r, int ql, int qr, int val) { if (ql l r qr) { sum[rt] val * (r - l 1); lazy[rt] val; return; } pushDown(rt, l, r); int mid (l r) 1; if (ql mid) update(rt 1, l, mid, ql, qr, val); if (qr mid) update(rt 1 | 1, mid 1, r, ql, qr, val); pushUp(rt); } int query(int rt, int l, int r, int ql, int qr) { if (ql l r qr) return sum[rt]; pushDown(rt, l, r); int mid (l r) 1, res 0; if (ql mid) res query(rt 1, l, mid, ql, qr); if (qr mid) res query(rt 1 | 1, mid 1, r, ql, qr); return res; } };把线段树封装好之后树链剖分的代码就变得非常简洁了。我建议在比赛前准备好这套模板比赛的时候直接抄能省不少时间。5. 树上差分与虚树进阶技巧的实战应用5.1 树上差分的两种形式树上差分是处理路径修改、单点查询或者单点修改、子树查询问题的利器。它的核心思想和数组差分类似只是把差分数组搬到了树上。树上差分有两种常见形式点差分和边差分。点差分用于处理路径上所有节点的权值修改。假设要把u到v路径上所有节点的权值加上x设l lca(u, v)那么只需要做以下操作diff[u] x; diff[v] x; diff[l] - x; diff[fa[l]] - x;最后对整棵树做一次DFS把子节点的diff值累加到父节点上就能得到每个节点最终的权值。边差分用于处理路径上所有边的权值修改。边差分和点差分的区别在于边权通常下放到深度较大的节点上所以修改的时候不需要减去fa[l]diff[u] x; diff[v] x; diff[l] - 2 * x;提示边差分最后累加的时候节点u的diff值代表的是u到父节点那条边的权值。根节点的diff值没有意义因为根节点没有父边。我刚开始学树上差分的时候总是搞混点差分和边差分的公式。后来总结了一个口诀点差分减两个边差分减两个x。点差分是diff[l] - x和diff[fa[l]] - x边差分是diff[l] - 2 * x。记住这个口诀之后就再也没搞混过。5.2 虚树压缩无用节点的艺术虚树是一种用于处理多次询问、每次询问涉及少量关键节点的树上问题的技巧。它的核心思想是只保留关键节点以及它们之间的LCA把其他无关节点压缩掉。这样原本O(n)规模的树就被压缩成了O(k)规模的虚树k是关键节点的数量。虚树的构建过程分为三步第一把关键节点按照DFS序排序。第二用栈维护一条从根到当前节点的链。第三依次加入每个关键节点如果当前节点和栈顶节点的LCA不在栈中就弹出栈顶元素并连边。vectorint vt[N]; // 虚树的邻接表 int stk[N], top; bool cmp(int a, int b) { return dfn[a] dfn[b]; } void buildVirtualTree(vectorint nodes) { sort(nodes.begin(), nodes.end(), cmp); stk[top 1] 1; // 假设1是根节点 vt[1].clear(); for (int u : nodes) { if (u 1) continue; int l lca(u, stk[top]); if (l ! stk[top]) { while (dfn[stk[top - 1]] dfn[l]) { vt[stk[top - 1]].push_back(stk[top]); top--; } if (stk[top] ! l) { vt[l].clear(); vt[l].push_back(stk[top]); stk[top] l; } } vt[u].clear(); stk[top] u; } while (top 1) { vt[stk[top - 1]].push_back(stk[top]); top--; } }虚树的代码看起来有点绕但核心逻辑其实很简单维护一条从根到当前节点的链每次加入新节点的时候把不在链上的部分弹出并连边。我建议在写虚树之前先把DFS序和LCA的模板准备好这样虚树的代码就只是简单的组合。虚树的经典应用场景是给一棵树每次询问给出k个关键节点要求计算这些关键节点之间的某种信息比如两两距离之和、最小生成树等。如果每次询问都遍历整棵树复杂度是O(nq)肯定超时。但用虚树把规模压缩到O(k)之后总复杂度就变成了O(sum(k) log n)可以轻松通过。5.3 点分治处理树上路径计数问题的利器点分治是另一种处理树上路径问题的技巧主要用于统计满足某种条件的路径数量。它的核心思想是每次选取树的重心作为分治中心统计经过重心的路径然后递归处理各个子树。点分治的复杂度是O(n log n)因为每次分治之后子树的大小至少减半。这个性质保证了分治的层数是O(log n)的。int siz[N], mx[N], root, total; bool vis[N]; void getRoot(int u, int fa) { siz[u] 1; mx[u] 0; for (int i head[u]; i; i nxt[i]) { int v to[i]; if (v fa || vis[v]) continue; getRoot(v, u); siz[u] siz[v]; mx[u] max(mx[u], siz[v]); } mx[u] max(mx[u], total - siz[u]); if (mx[u] mx[root]) root u; } void divide(int u) { vis[u] true; // 统计经过u的路径 for (int i head[u]; i; i nxt[i]) { int v to[i]; if (vis[v]) continue; // 处理子树v total siz[v]; root 0; getRoot(v, u); divide(root); } }点分治的难点不在于分治本身而在于如何统计经过重心的路径。不同的题目有不同的统计方法常见的有用桶记录距离、用双指针统计满足条件的路径、用树状数组维护信息等。我建议先做几道模板题比如求树上距离不超过k的点对数量熟悉了之后再挑战更复杂的题目。注意点分治的getRoot函数中mx[root]的初始值要设成一个很大的数否则第一次调用的时候root不会被更新。我一般把mx[0]设成INF这样第一次比较的时候一定能更新。6. 常见问题与排查技巧实录6.1 树结构题目的调试 checklist树结构的题目有一个特点代码量大、细节多、容易写挂。我总结了几个调试树结构题目的常用技巧希望能帮你少走弯路。第一先写暴力对拍。树结构的题目往往可以用O(n²)的暴力做法拿到部分分先写一个暴力程序然后随机生成小数据和正解对拍。这样能快速发现逻辑错误。第二检查数组大小。树结构题目中边数组要开到2n线段树要开到4n倍增数组要开到log2(n)1。我见过太多人因为数组开小导致RE或者WA。第三检查递归深度。如果树是一条链递归深度可能达到n。对于n10^5的题目系统栈可能会溢出。解决办法是手动开大栈空间在代码开头加#pragma comment(linker, /STACK:102400000,102400000)或者把DFS改成非递归写法。第四检查LCA的边界情况。如果u和v是同一个节点LCA就是它本身。如果u是v的祖先LCA就是u。这些边界情况在写代码的时候要特别注意。6.2 树链剖分常见错误速查表错误现象可能原因排查方法WA on test 2跳链时比较的是dep[u]而不是dep[top[u]]检查while循环中的比较条件WA on test 3最后一步没有比较dep[u]和dep[v]检查最后一段区间的左右端点TLE线段树没有加懒标记检查update函数是否有pushDownRE线段树数组开小了确认数组大小是否为4nWA边权下放后没有处理LCA边权问题查询区间应为[dfn[u]1, dfn[v]]死循环跳链时u没有更新为fa[top[u]]检查u的更新语句这张表是我自己踩坑之后总结的基本上涵盖了树链剖分90%的错误。每次写挂的时候对照这张表检查一遍通常都能找到问题。6.3 倍增LCA的常数优化技巧倍增LCA虽然好写但常数比较大。如果题目对时间要求很紧可以尝试以下几个优化技巧。第一把up数组的第二维放在第一维。也就是用up[j][u]而不是up[u][j]。这样在访问的时候内存是连续的缓存命中率更高。对于n10^5、询问次数10^5的题目这个优化能带来20%左右的性能提升。第二预处理log数组。在求LCA的时候如果需要用到log值提前预处理出来避免重复计算。第三用链式前向星代替vector。链式前向星的遍历速度比vector快不少尤其是在边数很大的时候。第四如果询问次数不多可以用欧拉序ST表。虽然预处理复杂度是O(n log n)但每次查询是O(1)的。如果询问次数远大于n这个做法会比倍增更快。6.4 树上差分与虚树的实战心得树上差分和虚树是我在区域赛中经常用到的两个技巧。这里分享几个实战心得。树上差分的核心是差分数组的累加顺序。一定要在DFS回溯的时候累加也就是先递归子节点再把子节点的diff值加到父节点上。如果顺序反了结果就会出错。虚树的核心是栈的维护。在加入新节点之前要先计算它和栈顶节点的LCA然后把栈中所有深度大于LCA的节点弹出并连边。这个过程需要仔细处理边界情况尤其是当LCA已经在栈中的时候。还有一个细节虚树构建完成之后要记得清空虚树的邻接表。因为虚树是多次询问共用的如果不清空下一次询问就会受到上一次的影响。我一般会在构建虚树之前把涉及到的节点的邻接表清空。7. 从模板到实战如何把树结构技巧用活学完了这些树结构技巧之后最重要的就是刷题。我建议按照以下顺序进行专项训练。第一阶段基础巩固。做10道左右的LCA模板题、5道DFS序模板题、5道树上差分模板题。这个阶段的目的是把模板写熟做到不看板子也能默写出来。第二阶段综合应用。做20道左右的树链剖分题目涵盖点权、边权、路径查询、子树查询等不同变种。这个阶段的目的是学会根据题目条件灵活调整模板。第三阶段进阶挑战。做10道左右的虚树题目、10道点分治题目、5道树形DP倍增优化题目。这个阶段的目的是掌握高级技巧能够独立分析问题并选择合适的解法。第四阶段套题训练。找一些包含树结构题目的区域赛套题限时训练。这个阶段的目的是提高实战能力学会在压力下快速写出正确的代码。我个人在训练的时候有一个习惯每做完一道题都会把这道题的核心思路和踩过的坑记录在一个文档里。时间长了这个文档就成了我自己的“错题本”。比赛前翻一翻能避免很多重复错误。最后分享一个小心得树结构的题目代码量往往很大但核心逻辑其实很简单。写代码的时候先把框架搭好再逐步填充细节。遇到错误的时候不要急着改代码先静下心来把逻辑理一遍往往能更快找到问题。我在区域赛的时候有一道树链剖分的题目调了一个半小时才发现是线段树的pushDown写错了。如果当时能冷静下来从头检查一遍可能二十分钟就能找到问题。
RELATED

相关推荐

蓝桥杯Python组备赛全攻略:题型拆解、高频考点与避坑指南

蓝桥杯Python组备赛全攻略:题型拆解、高频考点与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/13 1:21:40
STM32F105的USB BULK批量传输实战:从虚拟串口到高速可靠通信

STM32F105的USB BULK批量传输实战:从虚拟串口到高速可靠通信

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/13 1:21:40
智能家电语音模块设计:WT2003H喇叭盒的工程实践与解耦逻辑

智能家电语音模块设计:WT2003H喇叭盒的工程实践与解耦逻辑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/13 1:21:40
MORE NEWS

更多资讯

📰

Dinic最大流算法详解:分层图、多路增广与当前弧优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

ReactDOMRoot源码解析:从createRoot到render的完整链路

做React源码分析&#xff0c;很多人一上来就啃scheduler调度器或者fiber架构&#xff0c;结果被lane、workLoop这些概念劝退。我自己的经验是&#xff0c;不如先从ReactDOMRoot入手——这是每个React应用真正的起点&#xff0c;你天天写的createRoot(...).render(<App/>)…

📰

MySQL学生选课系统源码实战:表结构设计与事务并发控制

简介&#xff1a;这份资源是面向高校计算机相关专业学生与毕业设计开发者的一套MySQL学生智能选课系统完整资料&#xff0c;包含源码、数据库与配套论文&#xff0c;适合作为课程设计、毕业设计或SSM框架练手项目。系统围绕学生、教师与课程之间的信息交互展开&#xff0c;重点…

📰

MySQL学生智能选课系统:数据库设计、事务锁与高并发避坑指南

简介&#xff1a;这份资源是面向高校计算机相关专业学生与毕业设计开发者的一套MySQL学生智能选课系统完整资料&#xff0c;包含源码、数据库与配套论文&#xff0c;适合作为课程设计、毕业设计或Java Web入门练手项目。系统围绕学生选课场景设计&#xff0c;覆盖课程浏览、教师…

📰

CPU指令执行五阶段硬件原理与流水线设计

1. 这不是普通笔记&#xff0c;是理解CPU如何“思考”的底层地图你有没有想过&#xff0c;当你敲下gcc hello.c&#xff0c;再运行./a.out&#xff0c;那一行行C代码究竟是怎么变成屏幕上跳出来的“Hello World”的&#xff1f;它没长腿&#xff0c;不会自己跑&#xff1b;它没…

📰

PCA-LSTM多变量时间序列预测:从高维特征压缩到时序建模实战

既然标题已经这么直白了&#xff0c;我也就不绕弯子。PCA-LSTM预测模型&#xff0c;说白了就是先让PCA把高维特征“挤一挤”&#xff0c;去掉冗余和噪声&#xff0c;只留下最能代表原始数据变化的主成分&#xff0c;然后再把这条压缩后的时间序列喂给LSTM去学长期依赖&#xff…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬