最长定差子序列 图解题解
这道题到底在问什么
- arr
- [1,5,7,8,5,3,4,2,1]
- difference
- -2
- 输出
- 4
- 解释
- 选 7,5,3,1(每步 -2),长度 4
先想最直接的笨办法
公差固定让前驱唯一,所以每个数只做一次哈希查 + 一次写,整体 O(n)。这正是比 1027(公差要枚举,O(n²))简单的地方。(动画第 29 步)
最优解:为什么这么做
一句话答案:LeetCode 1218 最长定差子序列:公差 difference 已给定,用哈希表 dp[v]=dp[v−difference]+1 记以值 v 结尾的链长。公差固定让前驱唯一,每个数只查一次,时间 O(n)、空间 O(n)。
定差子序列这道题到底要挑出什么
给数组 arr 和固定整数 difference,在 arr 里找最长的子序列,要求相邻两项差恰好是 difference,可跳着取但保持原顺序。以 arr=[1,5,7,8,5,3,4,2,1]、difference=−2 为例,隔着别的数挑出 7、5、3、1,每步减 2,长度 4 就是答案。公差直接给死,问题就是沿固定步长最长能串几个数。
为什么每个数往后凑一条链会 O(n²)
顺手的想法是拿每个数当起点,反复往后找「当前值加 difference」凑一条链。但每个起点都要往后扫一遍,n 个起点就是 O(n²),多条链还反复重算。普通最长递增子序列(LIS,最长的逐个变大的子序列)也一样:每个位置都要往前扫所有更小的数,逃不掉 O(n²)。反过来做:扫到一个数,只回头看它的前一项在不在。
dp 的下标为什么用数值而不用数组位置
定义 dp[v] 表示以数值 v 结尾的最长定差子序列有多长,下标是数值本身、不是它在数组里的位置。道理在公差固定:v 接在谁后面唯一确定——前一项 p 满足 v − p = difference,即 p = v − difference,只有这一个值能当前驱。既然「v 接谁」只由数值决定、和排第几无关,就把每个值当键塞进哈希表(拿「值」一步查到记录、不挨个找的表),一步查到前驱。公差不定的题前驱不唯一,只好用 dp[下标][公差] 两维硬记。
前驱查得到和查不到各怎么办
转移式(由前面算好的值推出当前值的式子)就一行:dp[v]=dp.get(v−difference,0)+1。扫到 v,查前驱 v−difference 当前的 dp 值:查得到就在那条链上长度加一;查不到,v 自己当开头,长度是 0+1=1(不是 1+1,最易写错)。从左往右扫为什么天然对:前驱在 v 左边出现过就早写进表、查得到;没出现就另起一条,继承的一定是左边那条链,顺序自动满足。还有个坑:同一个值再次出现直接覆盖旧 dp。
拿题面这组数把 dp 一路查出来
difference=−2 时前驱是 v+2,逐个扫。前四个数 1、5、7、8 的前驱 3、7、9、10 都不在表里,各自 dp=1。再遇值 5 查 dp[7]=1,dp[5]=2,链 7→5;值 3 查 dp[5]=2,dp[3]=3,链 7→5→3;值 4 前驱 6 不在,dp[4]=1;值 2 查 dp[4]=1,dp[2]=2,短旁支;末尾值 1 查 dp[3]=3,dp[1] 覆盖成 4,主链 7→5→3→1 拼齐。扫完取最大值 4,对上示例。
复杂度为什么是 O(n),difference 为 0 会不会崩
数组扫一遍,每个数一次哈希查加一次写、均摊 O(1)(均摊=偶尔慢一下、平均每次仍是常数),时间 O(n);dp 表最多存 n 个不同值,空间 O(n),赢在前驱唯一。两个边界:前驱方向别算反,前一项是 v−difference 不是 v+difference,公差为负时前驱反而比 v 大;difference=0 时前驱是 v 自己,相同值接成一条链,[1,1,1] 答案是 3,代码不用特判。
▶ 动画逐步走查(共 29 步)——想跟着动画一帧帧对照就展开
- 3可以,但那样对每个起点都要往后扫一遍,最坏 O(n²)。我们想要一遍扫完。换个角度想:与其往后找,不如往回看「我的前一项在没在」。
- 4对每个数当起点,反复在后面找「当前值 + difference」,凑出一条链。多个起点的链会大量重算,整体 O(n²) 甚至更糟。我们要把「往后找」翻转成「往回查一次」。
- 5注意这里 dp 的下标是数值本身,不是数组位置。因为公差固定,一个数 v 能接到谁后面是唯一确定的——只能接在值为 v − difference 的那个数后面。
- 6扫到 v 时,去查它的前驱 v−difference 当前的 dp 值:查得到就 +1 接上去,查不到就说明 v 是新链的开头(长度 1)。前驱要是已经在 v 左边出现过就在表里、查得到;还没出现就查不到、让 v 自己开新链——所以从左往右扫天然满足顺序。下面用 difference = −2,前驱就是 v+2。
- 7arr 长度 9 · difference = −2这排是原数组,格子下方小数字是下标。公差固定为 −2,意味着 v 只能接在值 v+2 的数后面。下面下方会出现一张哈希表 dp{值→长度},随扫描一格格长出来。
- 8答案链 7→5→3→1提前剧透答案:绿色的 7、5、3、1(下标 2、4、5、8)隔着别的数取出来,两两差 −2。接下来看 dp 怎么从左到右把这条链一节一节接出来。
- 9查 dp[1+2]=dp[3]第一个数 值1(下标0)。先算前驱 1+2=3,去 dp 表里查——表还空着,查不到 3。
- 101 是开头,dp[1]=1没查到前驱 → 1 自己是开头,dp[1]=1。dp 表里新增一行「1 → 1」(绿色高亮的就是刚写入的)。
- 11查 dp[5+2]=dp[7]值5(下标1)。前驱 7,但 7 在更右边还没扫到,dp 表里查不到。
- 125 暂为开头,dp[5]=1查不到 7 → 5 暂时也是开头,dp[5]=1。表里多一行「5 → 1」。
- 13查 dp[7+2]=dp[9]值7(下标2)。前驱 9 不存在。别小看现在的 7,它正是答案链的头。
- 147 开头,dp[7]=1写入 dp[7]=1,新增「7 → 1」。记牢这行——后面 5、3、1 会一节节接到它后面。
- 15查 dp[8+2]=dp[10]值8(下标3)。前驱 10 不存在,8 是个对答案没用的旁支。
- 168 开头,dp[8]=1dp[8]=1,照记「8 → 1」。算法一视同仁,不管是不是答案链。
- 17查 dp[5+2]=dp[7]=1又见 值5(下标4)。前驱 7 这次命中了(蓝色高亮那行 dp[7]=1)! 数组里下标2 的 7 也点亮了——5 能接到它后面。
- 18dp[5] 由 1 升到 2dp[5] = 1+1 = 2。原来的「5 → 1」被覆盖成「5 → 2」(同一个值只留最长)。链 7→5 拼成长度 2。
- 19查 dp[3+2]=dp[5]=2值3(下标5)。前驱 5 命中,且 dp[5] 已经是 2(那条 7→5 链)! 3 能接到这条更长的链后面。
- 20dp[3]=3dp[3] = 2+1 = 3,新增「3 → 3」。链变成 7→5→3,长度 3。一节一节接出来了。
- 21查 dp[4+2]=dp[6]值4(下标6)。前驱 6 从没出现过,又是一条新开头。
- 22dp[4]=1dp[4]=1,新增「4 → 1」。4 也是旁支,对最长链没贡献。
- 23查 dp[2+2]=dp[4]=1值2(下标7)。前驱 4 命中,dp[4]=1。2 能接到 4 后面,但这只是条短旁支。
- 24dp[2]=2dp[2] = 1+1 = 2,新增「2 → 2」。这条 4→2 旁支长度才 2,比不过主链的 3。
- 25查 dp[1+2]=dp[3]=3最后的 值1(下标8)! 前驱 3 命中,而 dp[3]=3(就是那条 7→5→3)! 数组里下标5 的 3 也点亮——1 要接到主链尾巴。
- 26dp[1] 由 1 升到 4dp[1] = 3+1 = 4! 开头那行「1 → 1」被覆盖成「1 → 4」。主链 7→5→3→1 终于拼成长度 4!
- 27答案 = max(dp 所有值) = 4全扫完了,看 dp 表里的最大值:4(来自 dp[1])。它对应的链就是高亮的 7→5→3→1,下标 2、4、5、8。和示例答案 4 完全对上。
- 287 → 5 → 3 → 1,每步 −2回放这条绿链:下标 2、4、5、8 的 7、5、3、1,跳过中间的数,公差恒为 −2。dp 正是靠「每个数只查唯一前驱 v−difference」一节一节接出来的——一遍扫完,干净利落。
- 29公差固定让前驱唯一,所以每个数只做一次哈希查 + 一次写,整体 O(n)。这正是比 1027(公差要枚举,O(n²))简单的地方。
- 32因为公差固定时,「v 该接谁」只由数值决定(接值为 v−difference 的那个),和它在哪个下标无关。用数值当键,查前驱一步到位。1027 公差不定,才要 dp[下标][公差] 两维。
- 34i=4 值5 误查 5−2=3如果把前驱错算成 v−difference 又把 difference 当 +2,扫到 5 时会去查 dp[3](不存在),结果 5 接不到前面的 7,主链断在第一节,答案错成 1。正确是查 5−(−2)=7,命中 dp[7]=1 才接得上。
⚠️ 容易写错的地方
✗ 错:查前驱算成 v + difference
✓ 对:查 v − difference
前一项 p 满足 v − p = difference,故 p = v − difference
✗ 错:前驱不存在时按 1 接
✓ 对:按 0 接(再 +1)
查不到说明前面没有这条链,v 自己当长度 1,是 0+1 不是 1+1
✗ 错:同一个值的 dp 不覆盖、另存一份
✓ 对:直接覆盖(只留最长)
值相同的数后出现的能继承更长前驱,旧值无用
完整代码(Python / C++ / Java)
Python
class Solution:
def longestSubsequence(self, arr, difference):
dp = {} # dp[v]=以值v结尾的最长长度
ans = 0
for v in arr:
dp[v] = dp.get(v - difference, 0) + 1 # 接前驱
if dp[v] > ans:
ans = dp[v]
return ansC++
class Solution {
public:
int longestSubsequence(vector<int>& arr, int difference) {
unordered_map<int,int> dp; // dp[v]
int ans = 0;
for (int v : arr) {
dp[v] = dp[v - difference] + 1; // 接前驱
ans = max(ans, dp[v]);
}
return ans;
}
};Java
class Solution {
public int longestSubsequence(int[] arr, int difference) {
Map<Integer,Integer> dp = new HashMap<>(); // dp[v]
int ans = 0;
for (int v : arr) {
int prev = dp.getOrDefault(v - difference, 0);
dp.put(v, prev + 1); // 接前驱
ans = Math.max(ans, dp.get(v));
}
return ans;
}
}复杂度
时间复杂度
O(n)
数组扫一遍,每个数做一次哈希查 + 一次写,均摊 O(1)
空间复杂度
O(n)
dp 哈希表最多存 n 个不同的值
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最长定差子序列 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
和 LC1027 最长等差数列有什么本质区别?+
1027 的公差没给,要对每一对下标算出公差、用 dp[下标][公差] 两维哈希记,复杂度 O(n²)。本题 difference 直接给定,一个数的前驱 v − difference 唯一,dp 只需一维、按数值索引,一遍扫就是 O(n)。固定公差是能降到一维的关键。
为什么从左到右扫一遍就能保证子序列的先后顺序?+
扫到 v 时,它的前驱 v − difference 如果存在,一定在更早的位置已经被处理过、dp 值已经写好。后出现的 v 继承的是左边那条链,位置天然排在前面,所以不用额外判下标,顺序就是对的。
difference 为 0 或数组里有重复元素会出问题吗?+
不会。difference=0 时前驱是 v 自己,相同的值依次接成一条长链,[2,2,2] 答案就是 3;重复值靠「同一个值只留最长、后者覆盖」自然处理,代码不用任何特判。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最长定差子序列 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。