最长的斐波那契子序列的长度 图解题解
这道题到底在问什么
- 输入
- arr=[1,2,3,4,5,6,7,8]
- 输出
- 5 (子序列 [1,2,3,5,8])
- 输入
- arr=[1,3,7,11,12,14,18]
- 输出
- 3 (如 [1,11,12])
先想最直接的笨办法
开局:左边是严格递增的数组,右边哈希表记录每个「值 → 下标」。接下来枚举末尾两个数 arr[j](中间数,蓝指针)和 arr[i](末尾数,紫指针),j 在 i 前面,逐对找前驱。(动画第 4 步)
最优解:为什么这么做
一句话答案:LeetCode 873 最长的斐波那契子序列的长度:哈希表存每个数的下标,双层循环枚举末尾两数 arr[i]、arr[j],前驱 arr[i]−arr[j] 若在表里且更靠前,就把这两数结尾的 dp 接长一位。时间、空间均 O(n²)。
最长的斐波那契子序列,到底要在数组里找出什么
给一个严格递增的正整数数组 arr,要在里面挑一条最长的『斐波那契式』子序列。子序列指按原顺序、可跳着挑出的若干个数;斐波那契式要求长度至少为 3,且从第三个数起每数都等于前面两数之和。题面 arr=[1,2,3,4,5,6,7,8] 答案是 5,对应 [1,2,3,5,8]。
为什么把斐波那契子序列一条条列出来会算爆
数组里每个数选或不选,子序列就有 2ⁿ 条,逐条验是不是斐波那契式,n 才几十就数不完。斐波那契式有个硬性质:头两数一旦定死,后面每项由『前两项之和』唯一确定。可就算只枚举开头两数往后推,仍是 O(n²)(大 O,衡量运算量),每对还要往后核对、同段链反复重算。突破口是反过来:盯住末尾两数往前找前驱(前驱,就是斐波那契链里排在这两数前头的那个数)。
为什么要拿 arr[i]、arr[j] 两个下标才定得下状态
要复用子问题,先给它一个标识。斐波那契式的下一项由前两项决定,只记结尾是哪个数不够:同一个结尾数,前面搭的倒数第二个数不同,要求的前驱也不同。所以状态要锁住末尾两数:定义 dp[i][j] 为以中间数 arr[j]、末尾数 arr[i] 收尾的最长斐波那契式子序列长度(i、j 是下标,j 在前 i 在后)。dp 是动态规划的表(以 arr[i]、arr[j] 结尾的斐式链长算一次、存下复用);只用一个下标,两条链会挤进同一格,转移就错。
定了末尾两数,前驱为什么正好是 arr[i]−arr[j]
末尾两数是中间的 arr[j] 和最后的 arr[i],要在前头接的前驱须满足『前驱 + arr[j] = arr[i]』,值就是 arr[i] − arr[j]。剩下只问它在不在数组里、下标排不排在 arr[j] 前面,哈希表(『值 → 下标』一步可查)正合用。设前驱下标为 k,只要它在表里且 k 小于 j,就把以 arr[k]、arr[j] 结尾的那段接长一位:dp[i][j] = dp[j][k] + 1,否则自成长度 2(参考代码里 dp 表叫 f、哈希表叫 d)。
拿题面示例把这条最长链亲手接一遍
拿题面的 arr=[1,2,3,4,5,6,7,8] 走一遍,先把每个『值 → 下标』存进哈希表,相邻两数起手长度 2。中间数 2、末尾数 3:前驱 = 3 − 2 = 1,下标 0 靠前,把 1、2 那段接成 3。中间数 3、末尾数 5:前驱 = 5 − 3 = 2 靠前接成 4。中间数 5、末尾数 8:前驱 = 8 − 5 = 3 靠前接成 5。反例中间数 2、末尾数 4:前驱 = 4 − 2 = 2 虽在表里,下标恰是中间数 2 自己、不靠前,作废。枚举完最长 5,对应 [1,2,3,5,8]。
前驱明明在哈希表里,为什么这一对还是接不上
最容易栽的一处,是只顾查前驱在不在、忘了查位置。前驱下标必须严格排在中间数前面,也就是 k 小于 j;漏掉这关,会把前驱恰是中间数自己、或排在后头的情况当成合法,凑出不存在的链。另一处是别用二分找前驱:数值不按前驱排,二分没着力点。长度不足 3 返回 0。复杂度:枚举末尾两数 O(n²) 对,每对靠哈希把找前驱压到一步,空间 O(n²)。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这套「固定末尾两数、往前找前驱 t = arr[i] − arr[j]、查哈希接长一位」,下面每一帧都在套它。
- 4开局:左边是严格递增的数组,右边哈希表记录每个「值 → 下标」。接下来枚举末尾两个数 arr[j](中间数,蓝指针)和 arr[i](末尾数,紫指针),j 在 i 前面,逐对找前驱。
- 5固定中间数 arr[1]=2(蓝)和末尾数 arr[2]=3(紫)。要在它们前面接一个数,必须满足 前驱 + 2 = 3,所以前驱 t = 3 − 2 = 1。去哈希表查 1 在不在。
- 6哈希里命中 1,它在下标 0(绿),而且 0 排在 1 前面,顺序合法。于是把以 (1,2) 结尾的那段长度 2 接长一位:dp(2,3) = 3。比旧的最长还长,ans 刷新成 3。
- 7固定中间数 arr[1]=2(蓝)和末尾数 arr[3]=4(紫)。要在它们前面接一个数,必须满足 前驱 + 2 = 4,所以前驱 t = 4 − 2 = 2。去哈希表查 2 在不在。
- 8哈希里虽然有 2,但它在下标 1(红),并不排在中间数 arr[1] 的前面,凑不成「t, 2, 4」这种从左到右的合法顺序,这对作废,dp 保持长度 2。
- 9固定中间数 arr[2]=3(蓝)和末尾数 arr[3]=4(紫)。要在它们前面接一个数,必须满足 前驱 + 3 = 4,所以前驱 t = 4 − 3 = 1。去哈希表查 1 在不在。
- 10哈希里命中 1,它在下标 0(绿),而且 0 排在 2 前面,顺序合法。于是把以 (1,3) 结尾的那段长度 2 接长一位:dp(3,4) = 3。没超过当前最长 ans=3,ans 不变。
- 11固定中间数 arr[1]=2(蓝)和末尾数 arr[4]=5(紫)。要在它们前面接一个数,必须满足 前驱 + 2 = 5,所以前驱 t = 5 − 2 = 3。去哈希表查 3 在不在。
- 12哈希里虽然有 3,但它在下标 2(红),并不排在中间数 arr[1] 的前面,凑不成「t, 2, 5」这种从左到右的合法顺序,这对作废,dp 保持长度 2。
- 13固定中间数 arr[2]=3(蓝)和末尾数 arr[4]=5(紫)。要在它们前面接一个数,必须满足 前驱 + 3 = 5,所以前驱 t = 5 − 3 = 2。去哈希表查 2 在不在。
- 14哈希里命中 2,它在下标 1(绿),而且 1 排在 2 前面,顺序合法。于是把以 (2,3) 结尾的那段长度 3 接长一位:dp(3,5) = 4。比旧的最长还长,ans 刷新成 4。
- 15固定中间数 arr[3]=4(蓝)和末尾数 arr[4]=5(紫)。要在它们前面接一个数,必须满足 前驱 + 4 = 5,所以前驱 t = 5 − 4 = 1。去哈希表查 1 在不在。
- 16哈希里命中 1,它在下标 0(绿),而且 0 排在 3 前面,顺序合法。于是把以 (1,4) 结尾的那段长度 2 接长一位:dp(4,5) = 3。没超过当前最长 ans=4,ans 不变。
- 17固定中间数 arr[1]=2(蓝)和末尾数 arr[5]=8(紫)。要在它们前面接一个数,必须满足 前驱 + 2 = 8,所以前驱 t = 8 − 2 = 6。去哈希表查 6 在不在。
- 18去哈希表查 6,整个数组里根本没有这个数,说明 2 和 8 前面接不上合法的前驱,这一对只能停在长度 2。
- 19固定中间数 arr[2]=3(蓝)和末尾数 arr[5]=8(紫)。要在它们前面接一个数,必须满足 前驱 + 3 = 8,所以前驱 t = 8 − 3 = 5。去哈希表查 5 在不在。
- 20哈希里虽然有 5,但它在下标 4(红),并不排在中间数 arr[2] 的前面,凑不成「t, 3, 8」这种从左到右的合法顺序,这对作废,dp 保持长度 2。
- 21固定中间数 arr[3]=4(蓝)和末尾数 arr[5]=8(紫)。要在它们前面接一个数,必须满足 前驱 + 4 = 8,所以前驱 t = 8 − 4 = 4。去哈希表查 4 在不在。
- 22哈希里虽然有 4,但它在下标 3(红),并不排在中间数 arr[3] 的前面,凑不成「t, 4, 8」这种从左到右的合法顺序,这对作废,dp 保持长度 2。
- 23固定中间数 arr[4]=5(蓝)和末尾数 arr[5]=8(紫)。要在它们前面接一个数,必须满足 前驱 + 5 = 8,所以前驱 t = 8 − 5 = 3。去哈希表查 3 在不在。
- 24哈希里命中 3,它在下标 2(绿),而且 2 排在 4 前面,顺序合法。于是把以 (3,5) 结尾的那段长度 4 接长一位:dp(5,8) = 5。比旧的最长还长,ans 刷新成 5。
- 25枚举完所有末尾两数对,最长的一条是绿色这 5 个:1、2、3、5、8。它们环环相扣,1+2=3、2+3=5、3+5=8,正好是斐波那契式,答案就是 5。灰掉的 4 没被选进这条链。
⚠️ 容易写错的地方
✗ 错:前驱下标 k 不要求在 j 前面
✓ 对:必须 k 在 j 前面(k 小于 j)
三个数下标要从左到右严格递增,才是合法子序列
✗ 错:用遍历或二分找前驱
✓ 对:哈希表 O(1) 直接查值→下标
严格递增不代表要二分,哈希查更快更简单
✗ 错:答案忘了「至少 3 个数」
✓ 对:dp 从 2 起算,最终若没接长过就返回 0
长度 2 不算斐波那契,ans 初值 0、只在成功接长时才更新
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def lenLongestFibSubseq(self, arr: List[int]) -> int:
n = len(arr)
f = [[0] * n for _ in range(n)]
d = {x: i for i, x in enumerate(arr)}
for i in range(n):
for j in range(i):
f[i][j] = 2
ans = 0
for i in range(2, n):
for j in range(1, i):
t = arr[i] - arr[j]
if t in d and (k := d[t]) < j:
f[i][j] = max(f[i][j], f[j][k] + 1)
ans = max(ans, f[i][j])
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int lenLongestFibSubseq(vector<int>& arr) {
int n = arr.size();
int f[n][n];
memset(f, 0, sizeof(f));
unordered_map<int, int> d;
for (int i = 0; i < n; ++i) {
d[arr[i]] = i;
for (int j = 0; j < i; ++j) {
f[i][j] = 2;
}
}
int ans = 0;
for (int i = 2; i < n; ++i) {
for (int j = 1; j < i; ++j) {
int t = arr[i] - arr[j];
auto it = d.find(t);
if (it != d.end() && it->second < j) {
int k = it->second;
f[i][j] = max(f[i][j], f[j][k] + 1);
ans = max(ans, f[i][j]);
}
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int lenLongestFibSubseq(int[] arr) {
int n = arr.length;
int[][] f = new int[n][n];
Map<Integer, Integer> d = new HashMap<>();
for (int i = 0; i < n; ++i) {
d.put(arr[i], i);
for (int j = 0; j < i; ++j) {
f[i][j] = 2;
}
}
int ans = 0;
for (int i = 2; i < n; ++i) {
for (int j = 1; j < i; ++j) {
int t = arr[i] - arr[j];
Integer k = d.get(t);
if (k != null && k < j) {
f[i][j] = Math.max(f[i][j], f[j][k] + 1);
ans = Math.max(ans, f[i][j]);
}
}
}
return ans;
}
}复杂度
时间
O(n²)
枚举所有末尾两数对,每对查哈希 O(1)
空间
O(n²)
dp 表 n×n,外加 O(n) 哈希表
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最长的斐波那契子序列的长度 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么状态非得用两个下标,只记一个结尾数不行吗?+
斐波那契式的下一项等于前两项之和,只知道子序列的最后一个数,没法确定还能往下接谁:同一个结尾数,前面搭的倒数第二个数不同,要求的前驱也不同。所以状态必须把末尾两个数一起锁住,dp[i][j] 记的就是『以中间数 arr[j]、末尾数 arr[i] 收尾』这件事,这样『前两项定下一项』的递推才落得下去。只记一个数,等于把两条不同的链混进同一个格子,转移就没法写对。
这题和求斐波那契数 LeetCode 509 是一回事吗?+
不是。509 是给定项号、顺着 F(n)=F(n−1)+F(n−2) 把整串数往后加,答案是一个确定的数;本题反过来,给你一排数,问能从里面挑出多长的一段满足『每项等于前两项之和』。两者都带着『前两项之和』这条规则,但 509 在生成数列、本题在数组里做子序列搜索加动态规划,难点完全不同:本题的关键是用两个下标定状态、用哈希表快速回查前驱。真正和本题同款的是 LeetCode 1027 最长等差数列:一样用双下标定状态、用哈希表回查前驱(那里回查的是等差的公差)。
时间能不能压到比 O(n²) 更快?+
一般压不动。枚举末尾两数这一步天然是 O(n²) 对,而每对已经靠哈希表把找前驱降到一步,没有多余开销可省。想更快得换整套思路,实现复杂得多;对本题 n ≤ 1000 的规模,O(n²) 已经绰绰有余。面试里把这一版写清、讲明白两个下标定状态和哈希查前驱,比硬凑更优复杂度更稳。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最长的斐波那契子序列的长度 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。