news 2026/9/30 8:29:10

稀疏奖励困境与Hindsight Experience Replay:用事后重标注解锁强化学习新思路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
稀疏奖励困境与Hindsight Experience Replay:用事后重标注解锁强化学习新思路

“hindsight”这个词,我第一次见到它是在一次彻底失败的强化学习项目里。当时我搭了个机械臂仿真环境,任务是推动一个滑块到目标位置,奖励函数写得非常简单:距离小于阈值得1分,否则得0分。跑了整整三天,两万个episode,成功率为零,loss曲线横成一条直线。我当时几乎要放弃,觉得这环境肯定有问题,甚至怀疑仿真器数值精度。

后来读到OpenAI那篇《Hindsight Experience Replay》,论文标题里这个词让我一下愣住了。hindsight,事后聪明,回看往事时我们才知道当初应该怎么做——但文章说的是训练智能体的时候,能不能像人一样,虽然没达成目标,却能从“没达成”的经历里学到点什么。这篇文章我想聊聊这个算法的核心思路、我的复现经验,还有一些在实验里真正踩过的坑。如果你也在做机器人控制、多目标强化学习、或者被稀疏奖励折磨得想摔键盘,这篇应该对你有用。

1. 训练一周零成功率:稀疏奖励问题到底难在哪

1.1 一次真实的挫败经历

先说说我那个失败的实验环境。任务空间是二维平面,机械臂末端要从起点移动到目标点,动作是二维速度指令。我设置的判定条件是欧氏距离小于0.05米算成功。环境看起来简单极了,但我犯了一个“经典”的错误——奖励函数只给出成功/失败两种反馈:

  • 距离小于0.05m:奖励+1
  • 其他情况:奖励0

这个环境的问题在于,动作空间是连续的,机械臂从起点到目标点需要一系列精确的加速度控制。在没有任何梯度信号的情况下,智能体一开始就是瞎转。假设每次随机动作能让末端靠近目标一个微小步长的概率是p,那么一整条轨迹都走对的概率是p的T次方。T是轨迹长度,我设置的是50步,算下来几乎为零。更惨的是,因为成功概率太低,训练过程中一个正样本都采不到,价值网络的loss基本在震荡,策略网络只能朝着随机方向更新。

后来我把奖励改成稠密的负距离奖励,训练立刻就有效果了。但这引出一个问题:我们能不能既保留“稀疏奖励”这种最诚实的任务定义,又让训练跑得动?HER其实就是朝着这个方向去的一个漂亮回答。

1.2 稀疏奖励缺乏的是“中间的台阶”

从强化学习的角度看,奖励稀疏意味着价值函数在大部分状态空间里是平坦的——所有状态的价值都是0,因为没有正样本告诉智能体哪些状态“更接近成功”。价值函数一旦平坦,策略梯度就没有方向,这就像你去爬山但地图上只标了山顶,没有等高线,你根本不知道该往哪个方向迈脚。

有人会想,那给每一步都加个距离惩罚不就行了?行,但这引入了一个更微妙的问题:你设计的距离奖励真的能引导策略学到正确的行为吗?在机械臂推物体这种任务里,直接让机械臂离目标近,可能会导致它推着物体画圈,或者贴着目标来回蹭。我自己就见过一个环境,用距离奖励训练出来的策略非常“鸡贼”,它学会了把物体推到目标附近停下,但一旦目标位置变化,策略立刻失效——因为它学的是“到那个坐标去”,而不是“把物体推到目标点”。奖励塑形虽然能加速收敛,但很容易把策略学歪。HER提供了一个不太一样的思路:不修改奖励函数,而是“修改任务本身”。

1.3 为什么“终于成功了一次”也没用

还有个问题值得单独说说。有人会说,稀疏奖励也可以偶尔成功一次啊,成功一次不就能学到东西了吗?理想很丰满,现实是随机策略在连续动作空间里,可能需要上百万次尝试才能碰到一次成功。就算你运气好碰到了一次,一个孤零零的正样本,对价值函数的更新影响也微乎其微,而且这个正样本对应的目标位置如果以后再也不出现,那这次成功就浪费了。

这就引出了HER想解决的核心矛盾:在一个以“目标”为条件的任务中,每个episode都存在一个“没做到的事”和一个“做到了的事”。传统的强化学习只会记住“我做到了原定目标”,然后把这个episode丢弃;但事实上,这个episode里包含了一条完整的、通往某个状态的轨迹——只是这个状态恰好不是我们设定的目标而已。为什么不把这条轨迹改写成“我完成了另一个任务”?

2. Hindsight的核心思想:把“没有完成任务”改写成“完成了另一个任务”

2.1 goal-conditioned策略:给策略一个“目标输入口”

要理解HER,第一步得先理解什么叫做goal-conditioned策略(目标条件策略)。普通的强化学习策略输入只是状态s,输出动作a;而在多目标设定下,策略输入是状态s和目标g的组合,输出动作a。它的目标是:给定任意目标g,策略都能让智能体从当前状态出发,尽量达成g。

举个例子,机械臂推滑块任务里,状态s是机械臂末端坐标和滑块坐标,目标g是滑块的目标位置。策略需要根据“滑块现在在哪”和“滑块应该到哪”来决定怎么推。这种设定对后续我的训练很重要,因为它让智能体在同一个episode里,可以对不同的目标产生不同的经验。

HER的关键操作就发生在“事后”。打完一个episode后,我们回头看看这条轨迹里智能体实际走过的状态——虽然没有达成原定目标,但它最后到达了某个位置,或者中途经过了某个位置。这些实际到达的位置,是训练中真实发生过的状态转移,不是凭空捏造的。于是我们挑一个“事后目标”来替换掉原来的目标,重新计算这条轨迹的奖励。因为智能体确实到达了事后目标,那么按照稀疏奖励的定义,这条轨迹对“事后目标”就是一条成功的轨迹,奖励为1。

2.2 事后重标注:一次具体的目标替换过程

我用一个具体的例子拆解这个过程。假设原定目标g是把滑块推到坐标(1.0, 0.0),一个episode里智能体执行了50步,但滑块最终停在(0.6, 0.3)。原始轨迹里,几乎每一步的奖励都是0,只有一个最后一步可能是0,反正没有正奖励。

现在用HER做重标注。我们从这条轨迹里取一个“事后目标”,一种最简单的取法是直接用最终状态里的滑块位置作为新目标,g’ = (0.6, 0.3)。然后对整个轨迹重新计算奖励:第t步的滑块位置如果距离(0.6, 0.3)小于0.05,奖励就是1,否则是0。由于轨迹最后确实到达了(0.6, 0.3),所以最后几步的奖励就变成了正数。这条原本“全0奖励”的轨迹,变成了“最后几步有正奖励”的成功轨迹。

这一步操作看似简单,但背后有个非常重要的性质:它不是在凭空捏造经验,而是把真实发生的物理状态转移,重新映射到了一个真实可达的目标上。如果原定目标是(1.0, 0.0)而滑块实际停在(0.6, 0.3),那从(0.6, 0.3)这个状态出发,策略确实已经做到了“达成(0.6, 0.3)这个目标”。所以这条经验对新目标来说是逻辑自洽的、物理真实的。

2.3 为什么这样的“事后聪明”能加速学习

从算法角度看,HER的收益体现在两个层面。

第一,价值函数获得了非零的监督信号。原本平坦的价值函数,因为在重标注后的轨迹里出现了正奖励,开始出现高低起伏。价值网络能学到“离目标更近的状态,价值更高”,这样策略梯度就有了方向,智能体开始被推向“更容易达成目标”的状态空间区域。

第二,探索效率大幅提高。智能体不需要精确地命中某个特定目标,只需要“做过某件事”,这件事就可以作为一个新目标被学习。随着训练的推进,智能体相当于把状态空间里的各种区域都变成了“成功案例”,覆盖面越来越广,价值函数的形状也会越来越准确。

这里我要强调一个容易误解的点:HER并不会让智能体学会欺骗自己。它只是把一条轨迹用在多个目标的训练上,而原始目标(比如(1.0, 0.0))的样本依旧会以一定比例进入经验池。也就是说,HER并不是把所有失败都当成成功,而是给失败轨迹赋予了额外的、与真实物理过程一致的学习价值。这在直觉上很像人学习投篮:你今天没投进,但你发现自己出手后球飞到了篮筐左边一点,你就会对“左边那个位置”这个目标学到点东西,下次你再看到类似的出手角度,会调整得更准确。

3. 手写HER训练流程:从环境定义到代码骨架

3.1 环境设计:以机械臂推动滑块任务为例

下面直接上实战。我以gym风格的环境为例,让你能对照着改。环境的观测分成两部分:状态(observation)和目标(achieved_goal)。

class PusherEnv: def __init__(self): # 状态维度:机械臂末端坐标(2) + 滑块坐标(2) self.obs_dim = 4 # 目标维度:滑块的目标坐标(2) self.goal_dim = 2 # 动作维度:机械臂末端速度指令(2) self.action_dim = 2 self.threshold = 0.05 def reset(self): # 机械臂末端在原点附近,滑块在初始位置,目标随机生成 self.arm_pos = np.array([0.0, 0.0]) self.block_pos = np.array([0.3, 0.2]) self.goal = self._sample_goal() return { "observation": np.concatenate([self.arm_pos, self.block_pos]), "achieved_goal": self.block_pos.copy(), "desired_goal": self.goal.copy(), } def step(self, action): # 简单运动学更新,带一点随机噪声 self.arm_pos = np.clip(self.arm_pos + action * 0.1, -1, 1) # 机械臂接触滑块时将滑块推动 if np.linalg.norm(self.arm_pos - self.block_pos) < 0.1: self.block_pos = np.clip(self.block_pos + action * 0.05, -1, 1) obs = np.concatenate([self.arm_pos, self.block_pos]) achieved = self.block_pos.copy() # 稀疏奖励:距离小于阈值给1,否则给0 reward = 1.0 if np.linalg.norm(achieved - self.goal) < self.threshold else 0.0 done = reward == 1.0 return { "observation": obs, "achieved_goal": achieved, "desired_goal": self.goal.copy(), }, reward, done, {}

这只是个示意实现,实际你不需要太纠结物理精度,重点是观测和目标的分离。我强烈建议你在环境设计阶段就把achieved_goal单独拿出来,因为后面重标注直接依赖它。

3.2 核心数据结构:ReplayBuffer里存的到底是什么

HER需要和off-policy算法配合,所以经验池是核心数据结构。我在实现时采用了一个比较直观的存储方式:每条经验存下当前状态、动作、奖励、下一个状态、以及当时的“目标”。但这里有个关键细节——因为要做重标注,所以我还得多存一份achieved_goal作为“重标注时的素材”。

class HERBuffer: def __init__(self, capacity, her_strategy="future", k_extra=4): self.capacity = capacity self.her_strategy = her_strategy self.k_extra = k_extra self.buffer = [] self.episode_transitions = [] # 暂存当前episode的所有transitions def push_episode(self, episode): # episode是当前完整轨迹的所有transition for t, trans in enumerate(episode): obs_t, act, rew, obs_next, goal = trans # 原始目标保留一份 self._store(obs_t, act, rew, obs_next, goal) # 如果启用HER,补充k_extra个重标注样本 if self.k_extra > 0: self._store_relabeled(episode, t) self.episode_transitions = [] def _store(self, obs, act, rew, obs_next, goal): self.buffer.append((obs, act, rew, obs_next, goal)) if len(self.buffer) > self.capacity: self.buffer.pop(0)

这里有个小设计,我整条轨迹收集完之后再统一push,而不是step一步就存一步。原因很简单:HER的future策略需要从“当前时间步之后”的状态里采样目标,如果逐条存入,后面状态还没出现,就没法做了。

3.3 三种目标替换策略的代码实现

HER论文里讨论了多种事后目标的选择方式,我最常用的有三种。它们的区别在于“从轨迹的哪些状态里采样新目标”:

import random def sample_goal_for_transition(env, episode, t, strategy): # episode: 完整轨迹列表,每个元素是 (obs, act, rew, obs_next, goal, achieved) # t: 当前时间步索引 # strategy: "final" | "random" | "future" if strategy == "final": # 使用轨迹最后状态里的achieved_goal return episode[-1]["achieved_goal"] if strategy == "random": # 从轨迹任意时间步的achieved_goal中随机抽一个 idx = random.randint(0, len(episode) - 1) return episode[idx]["achieved_goal"] if strategy == "future": # 只从当前时间步之后的状态里抽,保证“未来可达” future_indices = range(t + 1, len(episode)) if not future_indices: return episode[t]["achieved_goal"] idx = random.choice(list(future_indices)) return episode[idx]["achieved_goal"] raise ValueError(f"Unknown strategy: {strategy}")

这三种策略里,我最推荐future,因为它有一个非常好的性质:抽取的新目标一定在时间上晚于当前状态,这意味着智能体在当前状态之后确实“经历过”通往新目标的路径,经验在时间因果上是成立的。而random有时会抽到当前状态之前的状态,那个目标其实还没发生过,逻辑上就不太自洽。final则是最简单稳妥的选择,但它损失了轨迹中段大量有价值的状态信息。

3.4 完整训练循环与超参数推荐

核心训练流程我整理成下面这段伪代码,框架是DDPG加HER。你也可以换成SAC,逻辑不变。

def train_her(env, agent, her_buffer, epochs=200, episodes_per_epoch=10): for epoch in range(epochs): for _ in range(episodes_per_epoch): obs_dict = env.reset() episode = [] done = False while not done: obs = obs_dict["observation"] goal = obs_dict["desired_goal"] action = agent.act(obs, goal, noise=0.1) obs_next_dict, reward, done, _ = env.step(action) episode.append({ "observation": obs, "action": action, "reward": reward, "next_observation": obs_next_dict["observation"], "achieved_goal": obs_next_dict["achieved_goal"], # 用s_{t+1}的achieved "desired_goal": goal, }) obs_dict = obs_next_dict her_buffer.push_episode(episode) agent.update(her_buffer.batch_sample(batch_size=256, her_ratio=0.8))

注意几个我实践后觉得很重要的配置:

  • k_extra=4是一个性价比很高的值,论文里也常用4,加更多并不会线性提升效果,反而让经验池里的样本分布偏向“重标注样本”,破坏平衡。
  • 每次采样时,我让80%的batch来自重标注样本,20%来自原始样本。原始样本不能完全没有,否则策略会忘记“原定目标”到底长什么样。
  • batch size建议用256或更大。HER重标注后的样本方差本来就小,小batch反而会让更新偏向噪声。

4. 实战中发现的反直觉现象与调参经验

4.1 future k值:数量比质量重要,但也要适可而止

我第一次复现HER时,以为重标注的“质量”是最重要的,结果实验反复告诉我,数量才是主力。在我那个推滑块环境里,k_extra从1提到4,成功率从12%跳到45%;从4提到8,成功率只到48%;从8提到16,反而掉到43%。为什么会掉?我分析下来是经验池多样性被破坏了——太多的重标注样本让价值函数被“成功之后状态”的分布主导,而真实环境里探索初期到处都是失败状态,两者分布差异太大,策略反而学偏。

所以k_extra默认4基本够用。如果你的任务成功率一直上不去,先别急着加k,去检查一下轨迹长度T是不是太长了,T越长,重标注的“事后目标”离原始目标越远,样本方差就越大。

4.2 观测拼法:把目标直接拼进状态是个大坑

这是另一个非常容易踩的坑。很多强化学习框架习惯把观测拼成一个vector喂给网络。你用HER的时候,如果直接把“当前状态”和“目标状态”拼成一维向量,网络确实也能学,但效果会差不少,尤其是目标维度较多时。原因在于网络很难在这个拼接向量里自动识别出“哪些维度是当前状态、哪些维度是目标”,等它自己学出这个结构,训练已经浪费了大半。

我推荐的做法是:把state和goal分别编码,然后在中间层做融合。最简单的实现方案是用一对输入头:

class Actor(nn.Module): def __init__(self, state_dim, goal_dim, action_dim, hidden=256): super().__init__() self.state_encoder = nn.Sequential(nn.Linear(state_dim, hidden), nn.ReLU()) self.goal_encoder = nn.Sequential(nn.Linear(goal_dim, hidden), nn.ReLU()) # 两个编码结果相加后再进行后续处理 self.fc = nn.Sequential( nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, action_dim), nn.Tanh(), ) def forward(self, state, goal): h = self.state_encoder(state) + self.goal_encoder(goal) return self.fc(h)

这种双编码器结构,能让网络清楚地分别理解“自己在哪里”和“要去哪里”,不混在一起。这个方法让我在相同训练步数下,成功率提升了将近10个百分点。

4.3 HER不是万能药:它最容易失败的三种场景

掌握一个算法最有价值的部分,是知道它什么时候不能用。

第一种是“必须做连续精确操控的长任务”。比如倒水这种需要连续几十步精确配合的任务,重标注一个轨迹之后,即使终点被换成“实际到达的位置”,中间那些步骤对于这个新目标仍然是混乱的,价值函数还是学不到东西。HER更适合“着力点明确”的任务,比如抓取、推动、到达、导航——这些任务的轨迹即使偏离目标,中间状态对未来目标也有清晰相关性。

第二种是目标空间巨大且稀疏的情况。如果目标是一个超高维的图片目标或复杂语义描述,重标注后几乎每条轨迹都对应一个完全不同的目标,经验之间共享的信息量太低,训练效率反而差。

第三种是已经做了复杂的奖励塑形。你想,如果你已经把奖励设计得非常密集且有引导性,再叠加HER的重标注,两种信号可能会打架。奖励塑形告诉策略“靠近目标好”,HER又告诉策略“你现在到的地方也算目标”,两股方向不一致的梯度会让训练震荡。我自己通常的做法是:要么走“稀疏奖励+HER”,要么走“精心塑形奖励”,不混用。

4.4 归一化细节:被忽略的稳定性来源

一个让我印象深刻的bug是,重标注前后的目标分布极不均匀。原始目标在训练开始时集中在环境预设区域,但重标注目标来自实际轨迹覆盖的状态空间,这个覆盖范围一开始很小,随着探索慢慢扩展。如果不对目标做动态归一化,价值网络就会在后期对“突然出现的更大坐标范围”特别不适应。

我踩坑后的固定操作是:使用running mean和running variance对目标做归一化,更新网络时对目标先标准化再输入。这种做法让训练曲线平滑很多。注意这里的归一化统计量要在训练过程中持续更新,不能训练前算一次就固定,因为状态空间的覆盖范围一直在变。

5. Hindsight的认知价值:从算法思想到团队复盘方法

5.1 后见之明偏差为什么既是敌人又是老师

做算法的人通常讨厌“后见之明偏差”——在项目失败后再回头看,总觉得自己当时“本该看到”那些征兆。这种偏差会扭曲记忆,让人低估决策当时的不确定性。但HER算法提供了一个反常识的角度:后见之明不一定是认知上的陷阱,它也可以是一种数据处理策略。算法的运作方式把“事后信息”显式地注入学习过程,却不假装这些信息在事前可得。

团队协作里其实也有类似的机制。我参加过的多数项目复盘,都会陷入两种极端:一种是相互甩锅,把“当初就该知道”挂在嘴边;另一种是一片祥和,说“大家尽力了”。如果用HER的思路来做复盘,会更健康一些:不纠结“当时为什么没做对”,而是问“这次失败的轨迹里,有哪些状态转移是真实的、有价值的?如果把这些状态作为目标,我们的执行过程完成了哪些次目标?”

5.2 把“重标注”用在工程复盘上:一次联调事故的改写法

举个例子。去年我们团队上了一套新的数据采集服务,联调时出了严重事故——某条消费链路的配置项写错,导致大量采集任务重复执行,浪费了两天的计算资源。常规复盘看到的“目标”是“上线成功”,结果是失败。于是讨论变成了“为什么没检查配置”这种归因游戏。

换成HER式的复盘后,我们重新定义了问题:在这次失败轨迹里,我们实际达成了什么?答案是:我们验证了配置变更能顺利推送到消费端、验证了错误配置会被执行器捕获、验证了告警系统能准确定位故障链路。于是这些“次目标”变成了下次改进的参考点——配置推送要做两阶段校验,执行器要增加幂等保护,告警规则要覆盖重复任务场景。这次事故虽然没有达成原始目标,但它实际上帮我们验证了一套“错误注入”的完整路径。这和我们训练机械臂时,把“推到错误位置”重标注为“推到另一个位置的成功样本”在逻辑上一模一样。

5.3 更进一步的启发:从“成不成功”到“接近了多少”

HER的深层价值在于,它把绝对的目标达成标准,转化成了一种相对的可度量信号。这个思路可以延伸到很多工程决策上。比如评估一个方案是否有效,我们总是拿“最终指标涨没涨”来一刀切。但如果按HER的精神,更合理的评估方式是:这个方案虽然没有让指标直接达标,但它把我们带到了哪个状态附近?是不是更接近理想状态了?如果答案是肯定的,那它就值得作为下一步决策的参考。

这个思维转换我实践了一段时间,最大的感受是团队讨论的氛围变了。大家开始更愿意暴露失败的中间状态,而不是憋到成功才拿出来分享。因为你知道“没达到目标”的经验不会被浪费,它会被重标注成“验证了某个子目标”的样本。这不只是心理安慰,这在信息论上是成立的——一条轨迹无论达不达得到目标,它本身就包含了状态空间的拓扑信息和动态转移信息,算法层面HER在利用这些信息,团队层面我们同样应该利用这些信息。

我自己跑HER项目的最大体会是,算法教会我的不只是怎么改经验池,更是一种处理“失败数据”的态度。技术上的重标注需要严谨的设计——目标必须真实可达、样本必须保持分布平衡、时间因果必须成立——但一旦这些约束满足了,那些原本被丢弃的失败轨迹,就成了训练里最宝贵的养料。这种思路放到工作里也一样:复盘时保持技术性的冷静,把“失败项目”当成可重标注的原始轨迹,你会发现团队迭代的速度会提升很多。后来我再遇到稀疏奖励的环境,第一反应已经不是加奖励塑形函数,而是想:这个episode的轨迹里,哪些状态可以作为重标注的新目标?这个思考习惯,就是hindsight这个词带给我的最大收获。

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

COMSOL横波激励仿真全攻略:从物理原理到建模实操

搞过超声仿真或者接触过压电换能器的人应该都听过“横波激励”这个词。刚入行那会儿&#xff0c;我对着COMSOL里那堆物理场接口和边界条件看了好几天&#xff0c;愣是没搞明白怎么让模型里产生一列干净的横波。后来踩了不少坑&#xff0c;翻了无数篇案例文档&#xff0c;才算是…

作者头像 李华
网站建设 2026/9/30 8:26:59

基于WinPcap的ARP数据包解析:绕过以太网帧头实现协议字段提取

简介&#xff1a;面向计算机网络课程设计&#xff0c;这份报告以“解析Ethernet ARP 数据包”为主题&#xff0c;完整呈现了基于WinPcap/PCAP库的网络抓包与解析方案。内容涵盖问题描述、ARP基本原理、概要设计、详细设计及代码实现&#xff0c;包括PCAP_findalldevs、pcap_ope…

作者头像 李华
网站建设 2026/9/30 8:26:41

LeetCode 49 字母异位词分组:哈希key设计是通关关键

如果你刷过LeetCode&#xff0c;尤其是按着“热门100题”列表一路练过去&#xff0c;那第49题《字母异位词分组》大概率是你很早就碰到的又高频又亲民的一道。我第一次刷它的时候&#xff0c;觉得这题不过如此&#xff0c;无非是排序一下、用哈希表存一存。可后来在一次模拟面试…

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

从10G到400G:结构化布线必须重构的链路架构要点

简介&#xff1a;《10G到400G结构化布线指南》是康宁光通信推出的网络基础设施参考文档&#xff0c;面向网络管理员、数据中心运维及技术人员&#xff0c;围绕企业网络从10G向400G演进的实际需求&#xff0c;提供结构化规划、设计与升级路径。整包仅1个PDF文件&#xff0c;大小…

作者头像 李华
网站建设 2026/9/30 8:25:35

LeetCode 5 最长回文子串:从暴力到中心扩展与动态规划

不想花里胡哨&#xff0c;直接说结论&#xff1a; 最长回文子串 这道题&#xff0c;是 LeetCode 第 5 题&#xff0c;也是我刷题生涯中遇到的第一道“标准 DP 入门题”&#xff0c;更是很多人面试时被问到手心冒汗的经典题。它表面上是求一个字符串里的最长回文片段&#xff…

作者头像 李华