news 2026/9/30 12:21:34

线段树状态矩阵求解区间子序列匹配问题(P15532 完整推导)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
线段树状态矩阵求解区间子序列匹配问题(P15532 完整推导)

P15532这题,名字叫《好想大声说爱你》,要不是在MYCOI R1的题单里看到,我差点以为是什么字符串模拟入门的浪漫签到题。点进去之后才发现,核心问题其实是一个很经典的区间子序列判定:给定一个字符串,每次问某个区间内能不能按顺序挑出几个字符组成指定模式串。我一开始也想写暴力扫描,看了眼数据范围之后果断放弃,最后用线段树维护状态矩阵的方式过了。这篇文章就把我的完整推导、代码实现和踩过的坑都写出来,希望能给同样被这题卡住的人一点参考。

这道题的适用范围其实不窄:OIer、ACM选手、甚至准备面试的时候被问到“区间内是否存在某子序列”这类问题,都能直接用上。我会先从最朴素的暴力思路讲起,再一步步走到线段树状态矩阵的写法,保证就算你之前没怎么接触过这类数据结构,也能顺着思路把代码写出来。

1. 题面翻译与两条暴力路线的瓶颈

1.1 把“大声说爱你”翻译成标准题意

题目的大意我整理成下面的形式:给定一个长度为 n 的字符串 S,只包含小写字母。有 q 次询问,每次给一个区间 [l, r],问 S[l..r] 这个子串里,能不能通过删除若干字符、但不改变剩余字符的相对顺序,得到一个等于 "love" 的字符串。如果可以,输出 Yes,否则输出 No。

这个其实就是标准的“子序列匹配”问题,只不过匹配的目标串是固定的 "love"。为什么标题叫《好想大声说爱你》?因为"love"嘛,四个字母正好对应一段告白,出题人把一个看似文艺的场景抽象成了字符串题,这在竞赛里太常见了。

如果原题的数据范围比较小,比如 n 和 q 都在几千以内,那暴力其实就够了。但这类题既然挂在线段树上,数据范围基本不会太友好。我按常规题面来假设 n 和 q 都是 1e5 级别,那么任何单次询问 O(n) 的做法都会直接超时。

1.2 暴力一:每个询问从 l 到 r 扫一遍

最直观的想法是这样:对于一个询问 [l, r],我维护一个指针 idx 初始指向 "love" 的第一个字符 'l'。然后从 l 到 r 扫描 S,如果当前字符正好等于 pat[idx](pat 是 "love",下标从 1 开始),就 idx++,直到 idx = 5 说明四个字符都找到了。

这个思路正确性没问题,但每次询问的代价是 O(n),总复杂度 O(nq)。当 n = q = 1e5 时,1e10 的运算量在任何竞赛环境下都是不可能跑完的。所以这条路走到头了,必须想办法预处理。

我见过不少新手在这里会试图加一些优化,比如先判断区间长度够不够 4,或者在每个询问里先用前缀和看看每个字符出现次数够不够。但这些优化在最坏情况下都不改变 O(nq) 的本质,因为字符出现的顺序才是关键,仅仅统计数量解决不了顺序问题。

1.3 暴力二:预处理 next 数组,只适合静态串

再进一步想,单次询问真正消耗时间的地方,是在区间里反复扫描找下一个需要的字符。如果我能提前知道“在某个位置后面,下一个指定字符出现在哪里”,那一次匹配"love"需要找 4 次字符,不就变成 O(4) 了吗?

这其实就是子序列自动机(也叫 next 数组)的思路。预处理 nxt[i][c] 表示在位置 i 之后(严格大于 i)第一次出现字符 c 的位置。然后对于区间 [l, r],我从 l-1 出发,依次跳向 'l'、'o'、'v'、'e',每次跳到下一个位置,最后看看跳到的位置是否不超过 r。

这个做法对于静态字符串非常优秀:预处理 O(26n),每次询问 O(4),总复杂度 O(26n + 4q),轻松通过。那为什么还需要线段树?因为一旦题目加上单点修改,比如把 S 的某个位置的字符改掉,nxt 数组就需要大范围重构,最坏情况下一次修改就要 O(26n),完全没法接受。

如果你确定这道题没有修改操作,其实用 next 数组就够了,甚至比线段树好写很多。但 P15532 这题的价值就在于让我把线段树的状态矩阵写法完整跑了一遍,所以接下来我重点讲支持修改的做法。

2. “最早结束位置”为什么是贪心最优解

2.1 为什么“越早结束越好”

在进入线段树之前,有一个核心思想必须先想清楚:当我们从左往右匹配一个模式串时,如果存在多种方式匹配到同一个状态(比如已经匹配好了 "lo"),那么“最后一个被用到的字符的位置”越小越好。

道理很简单:后续还剩下 "ve" 两个字符要匹配,而区间向右延伸的范围是固定的。如果前面的匹配结束得越早,留给我继续匹配后面字符的可用区间就越大;反之,如果前面某个字符用得太靠右,可能后面 'v' 和 'e' 的位置虽然存在,却被挤出了当前询问区间。

这个逻辑可以严格证明:假设从状态 x 出发,有两种方式都到达状态 y,且结束位置分别是 p1 和 p2,满足 p1 < p2。那么从状态 y 继续匹配后续字符时,任何在位置 p2 之后能完成的匹配,在 p1 之后也同样能完成,因为 p1 给了你更大的空间。所以无论之后要匹配什么,选择结束位置更小的方案永远不会比更大的方案差。

这就是整个线段树状态设计的基石。我们在线段树每个节点里存的所有“最短结束位置”,本质上都是在做这个贪心决策。

2.2 一次询问的最优走法

站在一次询问的角度,贪心匹配的过程是这样的:初始状态是“已经匹配了 0 个字符”,从 l-1 位置开始。先找第一个 'l' 出现的最早位置 p1,然后从 p1 开始找下一个 'o' 的最早位置 p2,依次类推。

每次找“下一个指定字符的最早位置”,其实就是在多个可行选择中挑结束位置最小的一种。假设我在找 'o',区间里出现了多个 'o',我选最靠左的那个。你可能会担心:选最靠左的 'o',会不会导致后面 'v' 找不到?不会。因为如果最靠左的 'o' 之后的可用区间里都找不到 'v',那换成更靠右的 'o',可用区间只会更短,更不可能找到。所以“最早位置”永远是最优选择。

这个思想反映到线段树上,就是每个节点只用维护最小的结束位置,不需要维护所有可达方案。

2.3 next 数组与子序列自动机的关系

这里顺便说清楚 next 数组和子序列自动机的关系:nxt[i][c] 其实就是在点 i 处遇到字符 c 时应该转移到的下一个节点。对每个位置 i,有 26 条出边,所以它本质上是一个 DFA。每次询问就是在 DFA 上根据区间内的字符序列走,看能不能在限定区间内走到接受状态。

线段树的做法则是把这个 DFA 的信息按区间分块存储,通过合并区间的转移关系来快速回答询问。两者底层逻辑一致,但线段树版本天然支持修改,因为修改一个点只需要更新从叶子到根的一条链。

3. 线段树节点里的状态矩阵:定义与合并规则

3.1 mat[x][y] 到底存什么

这一步是整个做法的灵魂,也是最容易绕晕的地方。我定义每个线段树节点维护一个矩阵 mat,大小为 (M+1) * (M+1),其中 M 是模式串 "love" 的长度,也就是 4。下标从 0 到 M 分别代表“已经匹配了模式串前 i 个字符”的状态。

关键定义来了:对于 x <= y,mat[x][y] 表示在这个节点对应的区间内部,从“已经匹配好了前 x 个字符”的状态出发,允许跳过任意字符并按顺序取字符,至少把状态推进到“已经匹配好了前 y 个字符”,在这个过程中,最后一个被用到的字符在原串中的下标是多少。如果无法做到,值为 INF。

举个例子。假设模式串是 "love",某个节点对应的区间是位置 3 到 6,里面的字符刚好是 l、o、v、e。那么:

  • mat[0][1] = 3,因为从空状态出发,用位置 3 的 'l',就能推进到状态 1;
  • mat[0][2] = 4,因为依次用位置 3 的 'l' 和位置 4 的 'o',最后用到的字符在位置 4;
  • mat[1][4] = 6,因为从已经匹配了 'l' 的状态出发,用位置 4、5、6 的 o、v、e,最后停在位置 6;
  • mat[0][4] = 6,表示完整匹配 "love",最后一个字符在位置 6。

特别注意:当 x = y 时,表示不需要用任何字符就能保持原状态。这时我规定 mat[x][x] 等于“区间左端点 - 1”,代表一个空操作,匹配还没有真正消耗区间里的字符。

3.2 叶子节点的构造规则

对于只有一个字符的叶子节点,构造规则非常简单。假设叶子节点对应原串位置 pos,字符是 c。

第一步,先把所有 mat 值初始化为 INF。

第二步,对于每个状态 x(0 到 M),设置 mat[x][x] = pos - 1,表示空匹配。

第三步,如果 x < M,并且 pat[x + 1] == c,说明当前这个字符 c 可以让状态从 x 推进到 x + 1,于是设置 mat[x][x + 1] = pos。

这里有一点需要注意:模式串中如果出现重复字符,比如模式串是 "aba",那么一个叶子节点上的字符 'a' 同时满足 x = 0 和 x = 1 的情况,因为 pat[1] = 'a' 且 pat[2] = 'a'。我的代码里是用一个循环枚举所有 x,每个满足条件的 x 都会单独设置,所以这种情况天然被覆盖了。

3.3 合并规则:为什么右边决定结束位置

现在是最关键的部分:怎么把左孩子和右孩子的信息合并成当前节点的信息。

假设左孩子区间是 [L, mid],右孩子区间是 [mid + 1, R]。我要计算当前节点的 res.mat[x][y],表示从状态 x 出发,在整个 [L, R] 区间内推进到状态 y 的最早结束位置。

这个目标路径只有两种可能:

第一种,全程都在左孩子区间内完成,也就是没有用到右孩子里的任何字符。这种情况的结果直接取左孩子的 mat[x][y]。

第二种,在左孩子区间内推进到了某个中间状态 z,然后右孩子从状态 z 出发继续推进到最终状态 y。合并时我枚举这个中间状态 z。关键在于,右孩子的所有位置都在左孩子所有位置的右边,所以一旦路径跨过了中线,最终被用到的最后一个字符一定在右孩子区间内。因此整个合并结果的结束位置,就是右孩子从状态 z 推进到状态 y 的结束位置,即右孩子的 mat[z][y],而不需要关心左孩子那一段到底在哪里结束。

只要左孩子的 mat[x][z] 不是 INF(说明左孩子确实能到达状态 z),并且右孩子的 mat[z][y] 不是 INF(说明右孩子能接上),那么整体可达。对多个 z 取最小值即可。

用公式表达就是:

res[x][y] = min( left[x][y], min_{z from x to y, if left[x][z] < INF and right[z][y] < INF} right[z][y] )

为什么 z 的范围是 x 到 y?因为状态只会前进不会后退,左孩子从 x 推进到的中间状态 z 不可能小于 x,也不可能大于最终状态 y。这个范围限制可以省掉不少无效枚举。

合并的代码看起来就是三层循环,但每层最多 M+1 次,M 是 4,所以完全不怕枚举。

4. 完整实现:建树、单点修改、区间查询

4.1 代码设计的几个前置约定

在写代码之前,我先把所有约定说清楚,避免你看代码之后产生混乱。

  • 模式串用字符数组 pat 存,下标从 1 开始,所以 pat[1] = 'l',pat[2] = 'o',pat[3] = 'v',pat[4] = 'e'。M 是 4。
  • 原串 S 也是 1-based,即下标从 1 到 n 存字符。
  • INF 用一个足够大的数,我习惯用 0x3f3f3f3f,方便 memset。
  • 叶子节点里 mat[x][x] = pos - 1,这个值可能是 0(当 pos = 1 时),但没关系,它只表示空匹配,不会和真实字符位置混淆。
  • 查询时,我把答案累积到一个 Node 变量 ans 里,初始时让 ans.mat[i][i] = l - 1,表示从区间左边的虚空位置开始,还没有消耗任何字符。

4.2 完整 C++ 实现

#include <bits/stdc++.h> using namespace std; const int MAXN = 100005; const int M = 4; // "love" 的长度 const int INF = 0x3f3f3f3f; char pat[M + 1] = " love"; // 下标 1..M char s[MAXN]; int n, q; struct Node { int mat[M + 1][M + 1]; void clear() { memset(mat, 0x3f, sizeof(mat)); } } tree[MAXN * 4]; Node mergeNode(const Node& A, const Node& B) { Node C; C.clear(); for (int x = 0; x <= M; ++x) { for (int y = x; y <= M; ++y) { // 整个推进过程都在左区间 A 内完成 if (A.mat[x][y] < INF) { C.mat[x][y] = min(C.mat[x][y], A.mat[x][y]); } // 在 A 内推进到中间状态 z,再由 B 继续推进到 y for (int z = x; z <= y; ++z) { if (A.mat[x][z] < INF && B.mat[z][y] < INF) { C.mat[x][y] = min(C.mat[x][y], B.mat[z][y]); } } } } return C; } void build(int p, int l, int r) { if (l == r) { tree[p].clear(); for (int x = 0; x <= M; ++x) { // 空匹配:不需要字符,结束位置看作左端点前一位 tree[p].mat[x][x] = l - 1; // 当前字符如果能推进状态 x -> x+1 if (x < M && pat[x + 1] == s[l]) { tree[p].mat[x][x + 1] = l; } } return; } int mid = (l + r) >> 1; build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r); tree[p] = mergeNode(tree[p << 1], tree[p << 1 | 1]); } void update(int p, int l, int r, int pos) { if (l == r) { tree[p].clear(); for (int x = 0; x <= M; ++x) { tree[p].mat[x][x] = l - 1; if (x < M && pat[x + 1] == s[l]) { tree[p].mat[x][x + 1] = l; } } return; } int mid = (l + r) >> 1; if (pos <= mid) { update(p << 1, l, mid, pos); } else { update(p << 1 | 1, mid + 1, r, pos); } tree[p] = mergeNode(tree[p << 1], tree[p << 1 | 1]); } void query(int p, int l, int r, int ql, int qr, Node& ans) { if (ql <= l && r <= qr) { ans = mergeNode(ans, tree[p]); return; } int mid = (l + r) >> 1; if (ql <= mid) { query(p << 1, l, mid, ql, qr, ans); } if (qr > mid) { query(p << 1 | 1, mid + 1, r, ql, qr, ans); } } int main() { scanf("%d%d", &n, &q); scanf("%s", s + 1); build(1, 1, n); while (q--) { int op; scanf("%d", &op); if (op == 1) { int pos; char c; scanf("%d %c", &pos, &c); s[pos] = c; update(1, 1, n, pos); } else { int l, r; scanf("%d%d", &l, &r); Node ans; ans.clear(); for (int i = 0; i <= M; ++i) { ans.mat[i][i] = l - 1; } query(1, 1, n, l, r, ans); if (ans.mat[0][M] < INF) { puts("Yes"); } else { puts("No"); } } } return 0; }

4.3 查询初始状态的坑

我在第一次写查询函数的时候,犯了一个比较隐蔽的错误:初始化 ans 的时候,我把所有 ans.mat[i][i] 都设置成了 0。

当时想的是,最开始的空匹配位置当然是 0。但这个值用在不同的查询区间里是有问题的。比如询问 [l, r],如果 l 不是 1,那么用位置 0 之前的空匹配去和一个覆盖 [l, r] 的线段树节点合并,合并逻辑本身还是能工作的,但 mat[i][i] 这个“空匹配结束位置”会被后续合并当成一个可参考的最小值,虽然最终不会影响 mat[0][M] 的实际匹配结果,但语义上不够严谨。

后来我把 ans.mat[i][i] 改成 l - 1,才彻底想通了这件事:空匹配的位置本来就应该是查询区间左端点的前一个位置。这样所有空操作的坐标都跟着当前查询区间走,合并出来的结果在逻辑上才是自洽的。

这个细节如果不注意,在做一些退化数据的时候可能产生奇怪的判断,所以特别提醒一下。

5. 复杂度、正确性验证与两个容易翻车的细节

5.1 复杂度结论

整个数据结构的复杂度可以分成三块看。

建树时,每个叶子节点构造是 O(1) 级别的枚举(M = 4),每个内部节点做一次 mergeNode 操作。mergeNode 的三层循环枚举 x、y、z,每个维度都是 M+1 = 5,所以一次合并大约 125 次操作。总建树复杂度 O(125n),常数很小。

单点修改时,从叶子到根只有一条链,深度 O(log n),每层做一次合并,复杂度 O(M^3 log n)。

区间查询时,线段树最多访问 O(log n) 个被完全覆盖的节点,每访问一个节点做一次合并,所以查询复杂度也是 O(M^3 log n)。再加上初始 ans 的构造 O(M),基本可以忽略。

对于 n = q = 1e5,M = 4 的情况,这个复杂度非常轻松,常数小得可以忽略不计。

5.2 手工跑一个小样例

为了验证这套状态矩阵不是纸面功夫,我手算一个简单的例子。

S = "iloveyou",下标从 1 开始:1 = i,2 = l,3 = o,4 = v,5 = e,6 = y,7 = o,8 = u。

询问区间 [2, 5],也就是子串 "love"。我希望答案是 Yes。

手动构造叶子:

  • 位置 2 是 'l':mat[0][1] = 2,mat[x][x] = 1。
  • 位置 3 是 'o':mat[1][2] = 3。
  • 位置 4 是 'v':mat[2][3] = 4。
  • 位置 5 是 'e':mat[3][4] = 5。

合并前四个叶子后,会存在一条状态链 0 -> 1 -> 2 -> 3 -> 4,最终 mat[0][4] = 5。非 INF,所以输出 Yes。

再看询问区间 [3, 5],也就是 "ove",里面没有 'l'。叶子 4 和 5 只能从状态 2、3 往后推,没有任何节点能把状态 0 推到状态 1。合并之后 mat[0][1]、mat[0][2]、mat[0][3]、mat[0][4] 全是 INF,只有 mat[0][0] 是 2(空匹配)。所以 ans.mat[0][4] = INF,输出 No。

这个例子算完,我对自己的实现就有信心了。

5.3 易错点:空匹配与 INF 的区分

这套代码里有两个容易混淆的概念,我单独拿出来说。

第一个是“空匹配”的位置。叶子节点里 mat[x][x] = l - 1,这个值看起来像是一个真实位置,但它只是虚拟的起点,表示“在这个区间开始之前就已经处于状态 x”。它只用于状态保持,不表示实际用到了哪个字符。所以在判断最终答案的时候,不能只检查 ans.mat[0][M] 是不是 l - 1 这类值,而要看它是不是 INF。一旦 mat[0][M] 非 INF,说明真的完成了一次从状态 0 到状态 M 的推进,这个值是最后一个被用到字符的位置,一定大于等于 l。

第二个容易翻车的点是枚举范围。我在 mergeNode 里用了 z from x to y,这个范围是严格的状态单调性约束。如果你偷懒写成 z from 0 to M,结果不会错,但会多做很多无效判断。更重要的是,如果模式串长度变大,比如变成了 50,那 51 的三层循环就已经是 13 万级别,能省一点是一点。所以 z 的范围最好按照状态单调性收紧。

6. 这类题的变体与可复用的做题思路

6.1 模式串长度不是 4 怎么办

这道题模式串是固定的 "love",所以 M = 4。但如果你遇到模式串长度为 10 甚至 20 的版本,这套矩阵做法会遇到一个问题:合并复杂度 O(M^3 log n) 会随着 M 增长变得不可接受。

我实话说,模式串一旦超过 10,直接的矩阵合并就有点悬了,M = 20 的时候 8000 次内层操作乘上 log n 就很吃力。这时候有两个替代方案。

第一种,如果题目没有修改操作,老老实实用 next 数组的子序列自动机,单次询问 O(M),预处理 O(26n),这才是静态区间的正解。

第二种,如果题目强制修改,可以考虑用 bitset 优化状态转移,或者用分块 + 预处理小块转移矩阵,把合并复杂度从 O(M^3) 降到接近 O(M^2 / 64)。但这就已经进入比较偏的优化领域了,竞赛里出现频率不高。

所以个人建议:看到 M 很小(比如 1 到 4),直接用线段树矩阵;M 中等,评估修改频率和查询频率;M 很大,优先找静态做法或其他性质。

6.2 只问最长可匹配前缀的做法

有时候题目会从“能不能匹配完整模式串”变成“最长能匹配到模式串的前几位”。这个改法其实更简单。

在得到了查询区间的 ans 矩阵之后,我不需要检查 ans.mat[0][M],而是从大到小枚举一个状态 p,看 ans.mat[0][p] 是不是 INF。第一个非 INF 的 p 就是最长可匹配前缀长度。

这个思路在面试题里很有用,比如“给定一个文本串,在某个子区间内,最多能匹配上 pattern 的前多少个字符”。用这套线段树矩阵,答案是现成的。

6.3 这道题留给我的经验

最后说说我做题的具体体会。P15532 这题让我深刻意识到一个问题:字符串题不一定非要用字符串算法,尤其是“区间查询 + 区间合并”的味道出来之后,线段树几乎是本能反应。关键在于,怎么把“匹配进行到哪一步”压缩成一个可合并的状态。

“已经匹配了模式串前 i 个字符”就是一个非常自然的状态,而两个区间合并时,只需要考虑左边区间把状态推进到哪个中间状态、右边区间能否接着推进。只要抓住了这个本质,矩阵里存的是什么就一目了然了。

如果你在考场上一时想不起线段树矩阵的合并细节,我建议你先把“状态机”这三个字写在草稿纸上,再写下状态 x 表示“前缀匹配长度”,然后从叶子节点开始推。这种方式能帮你避免直接把左孩子和右孩子的 mat 相加这类常见错误。

这题之后,我又拿这套模板去练了好几个类似的区间子序列问题,稳定性和正确性都很好。如果你也在刷字符串数据结构题,强烈建议把这道题吃透,它背后代表了一整类“区间匹配状态合并”的题目,值得反复写几遍。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 12:21:09

OpenClaw 小龙虾 AI|Windows3.1.0 一键部署本地 AI 智能体实操教程

OpenClaw 小龙虾 AI&#xff1a;Windows 一键部署本地 AI 智能体实操教程 适配版本&#xff1a;Windows 3.1.0 / Mac 2.7.9 核心特点&#xff1a;可视化图形界面、自动配置运行环境、内置全部依赖组件&#xff0c;支持 28 万 Tokens 额度 Windows 3.1.0 下载地址&#xff1a;ht…

作者头像 李华
网站建设 2026/9/30 12:19:55

Rocky Linux 9 + VMware Workstation 安装配置全指南

1. 为什么选Rocky Linux 9 VMware Workstation&#xff1f;这不是随便搭的环境 我从2014年开始用VMware做Linux实验环境&#xff0c;前前后后搭过CentOS、RHEL、Ubuntu、Debian、AlmaLinux&#xff0c;也踩过无数坑——比如某次升级后网卡驱动突然消失&#xff0c;再比如快照回…

作者头像 李华
网站建设 2026/9/30 12:18:18

FPGA时序约束与收敛实战:从XDC编写到违例修复

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

作者头像 李华
网站建设 2026/9/30 12:17:02

SpringMVC 6升级实战:javax迁移、拦截器与路径匹配避坑指南

先交代一下背景。我手头一个老项目&#xff0c;Spring Boot 2.7.9 Spring Framework 5.3.x&#xff0c;跑了三年多&#xff0c;一直稳如老狗。因为团队整体要切 JDK 17 和新的基础设施&#xff0c;我被迫把 SpringMVC 一路升到 6.1&#xff08;对应 Spring Boot 3.2&#xff0…

作者头像 李华
网站建设 2026/9/30 12:17:01

TensorFlow生产级部署核心:SavedModel、tf.function与可观测性

1. 这不是“又一个深度学习框架”&#xff1a;TensorFlow 的真实定位与误用重灾区 很多人第一次听说 TensorFlow&#xff0c;是在某篇“2024年最值得学的AI框架”榜单里&#xff0c;和 PyTorch 并列排在前两位&#xff1b;也有人是在安装时卡在 pip install tensorflow 命令…

作者头像 李华
网站建设 2026/9/30 12:16:38

HER事后经验回放:破解强化学习稀疏奖励难题的工程实践

1. 先搞懂为什么要引入“事后聪明”1.1 强化学习中的一个老大难&#xff1a;稀疏奖励做强化学习的同学&#xff0c;十有八九都会被同一个问题折磨过&#xff1a;稀疏奖励。举个例子&#xff0c;你让一个机械臂去抓取桌面上的方块&#xff0c;只有把方块送到指定位置才给1奖励&a…

作者头像 李华