在算法题的刷题过程中,你是否遇到过这样的场景:
- 动态规划数组明明逻辑对了,却总是报
IndexError: list index out of range? - 循环遍历字符串/数组时,要么漏掉最后一个元素,要么多走一步?
- 调试了半小时,最后发现只是少写了一个
+1?
如果这些问题你都遇到过,那么这篇博客就是为你准备的。我们将从核心原则 、常见场景 、实战步骤 三个维度,彻底讲透算法题中数组/循环范围的 +1 判断逻辑,帮你避开90%的维度错误。
一、核心判断原则(先记住这句话)
判断是否需要 +1,本质是看你的"计数/状态定义"是否包含"边界情况",主要分为两种核心场景:
| 场景类型 | 判断依据 | 是否需要 +1 |
|---|---|---|
| 空状态型 | 状态变量表示"前 i 个元素/字符"(包含 i=0,即"空"的情况) |
✅ 必须 +1 |
| 左闭右开型 | 循环/切片遵循"左闭右开"规则(如 Python 的 range、列表切片),需要覆盖到最后一个元素 |
✅ 必须 +1 |
接下来我们逐个场景拆解,结合经典算法题和代码对比,让你彻底理解。
二、场景一:空状态型 +1(动态规划重灾区)
这是算法题中最常见的 +1 场景,90%的字符串/数组类动态规划题都需要它 ,核心原因是:动态规划的"初始状态"通常是"空"。
1. 什么是"空状态"?
假设我们定义状态 dp[i][j]:
- 如果
i表示"s的第i个字符"(索引从0开始),那么i的范围是0~m-1,没有"空字符串"的概念; - 如果
i表示"s的前i个字符",那么i的范围是0~m,其中i=0对应"前0个字符"(即空字符串),这就是"空状态"。
动态规划几乎都用"前 i 个"的定义,因为"空状态"是推导后续状态的基础(比如空字符串匹配空正则、空字符串转换成另一个空字符串的编辑距离为0)。
2. 经典例子1:正则表达式匹配(LeetCode 10)
状态定义
dp[i][j]:表示 s 的前 i 个字符和 p 的前 j 个字符是否匹配。
i的取值:0,1,2,...,m(m是s的长度,i=0对应空s)j的取值:0,1,2,...,n(n是p的长度,j=0对应空p)
数组维度判断
i有m+1个取值,j有n+1个取值;- 所以
dp数组的维度必须是(m+1) × (n+1)。
错误 vs 正确代码对比
python
# 错误代码:少了 +1,无法处理空状态,会报索引越界
m, n = len(s), len(p)
dp = [[False] * n for _ in range(m)] # 维度是 m×n,没有 i=0/j=0
dp[0][0] = True # 直接报错:索引超出范围
# 正确代码:维度 +1,覆盖空状态
m, n = len(s), len(p)
dp = [[False] * (n + 1) for _ in range(m + 1)] # 维度是 (m+1)×(n+1)
dp[0][0] = True # 空字符串匹配空正则,正确初始化
3. 经典例子2:编辑距离(LeetCode 72)
状态定义
dp[i][j]:表示将 s 的前 i 个字符转换成 p 的前 j 个字符所需的最小操作数。
i=0:空s转换成p的前j个字符,需要j次插入操作;j=0:s的前i个字符转换成空p,需要i次删除操作。
数组维度
同样需要 (m+1) × (n+1),否则无法初始化 i=0 和 j=0 的边界情况。
4. 空状态型 +1 的判断步骤
遇到动态规划题,按这3步走:
- 明确状态定义 :你的
i/j是"前i个"还是"第i个"?
→ 优先用"前i个",因为方便处理空状态; - 确定取值范围 :
i需要从 0 到m吗?
→ 如果需要(通常都需要),则取值个数是m+1; - 推导数组维度 :数组长度 = 取值个数 →
m+1。
三、场景二:左闭右开型 +1(循环/切片常见坑)
这种场景和动态规划无关,而是由编程语言的语法规则决定的------Python、Java等语言的循环、切片通常遵循"左闭右开"原则(即包含起始索引,不包含结束索引)。
1. 什么是"左闭右开"?
以 Python 为例:
range(a, b):生成的整数是a, a+1, ..., b-1,不包含b;s[a:b]:取字符串s的第a到第b-1个字符,不包含b。
如果我们需要覆盖到最后一个元素,就必须在结束索引上 +1。
2. 经典例子1:统计 1~n 的和
需求
计算从 1 到 n(包含 n)的所有整数的和。
错误 vs 正确代码对比
python
n = 10
# 错误代码:range(1, n) 只到 9,漏掉了 10
sum_wrong = 0
for i in range(1, n):
sum_wrong += i
print(sum_wrong) # 输出 45,错误(正确结果是 55)
# 正确代码:range(1, n+1) 包含 10
sum_correct = 0
for i in range(1, n + 1):
sum_correct += i
print(sum_correct) # 输出 55,正确
3. 经典例子2:遍历字符串的所有字符
需求
遍历字符串 s 的每一个字符并打印。
错误 vs 正确代码对比
python
s = "abc"
# 错误代码:range(len(s)-1) 只到 1,漏掉了最后一个字符 'c'
print("错误遍历:")
for i in range(len(s) - 1):
print(s[i]) # 只打印 'a' 和 'b'
# 正确代码1:range(len(s)) 到 len(s)-1,刚好覆盖所有字符
print("正确遍历1:")
for i in range(len(s)):
print(s[i]) # 打印 'a'、'b'、'c'
# 正确代码2:如果需要遍历"前 i 个字符"(i 从 1 到 len(s)),则需要 +1
print("正确遍历2(前 i 个字符):")
for i in range(1, len(s) + 1):
print(s[:i]) # 打印 'a'、'ab'、'abc'
4. 左闭右开型 +1 的判断步骤
遇到循环/切片时,按这2步走:
- 明确你的目标范围 :你需要包含的最后一个索引/值是什么?
→ 比如统计 1~n,最后一个值是 n; - 调整结束索引:因为左闭右开,结束索引 = 目标最后一个值 + 1。
四、新手避坑:两种 +1 的混淆与区分
很多新手会把"空状态型 +1"和"左闭右开型 +1"搞混,我们用一个表格清晰区分:
| 维度 | 空状态型 +1 |
左闭右开型 +1 |
|---|---|---|
| 原因 | 状态定义包含"前0个"(空) | 语法规则是左闭右开 |
| 常见场景 | 动态规划(正则匹配、编辑距离、LCS) | 循环(range)、切片(s[:]) |
| 例子 | dp = [[False]*(n+1) for _ in range(m+1)] |
range(1, n+1) |
不 +1 的后果 |
索引越界、无法初始化空状态 | 漏掉最后一个元素 |
五、实战演练:用判断步骤解决一道题
我们以**最长公共子序列(LeetCode 1143)**为例,完整走一遍判断流程:
题目
给定两个字符串 text1 和 text2,返回它们的最长公共子序列的长度。
步骤1:明确状态定义
dp[i][j]:表示 text1 的前 i 个字符和 text2 的前 j 个字符的最长公共子序列长度。
→ 用"前 i 个",需要包含 i=0/j=0(空字符串的最长公共子序列长度为0)。
步骤2:确定取值范围
i的取值:0,1,2,...,m(m是text1的长度)j的取值:0,1,2,...,n(n是text2的长度)
→ 取值个数分别是m+1和n+1。
步骤3:推导数组维度
dp 数组的维度是 (m+1) × (n+1)。
步骤4:循环范围(左闭右开型)
i需要从 0 到m,所以range(m+1);j需要从 0 到n,所以range(n+1)。
最终代码框架
python
def longestCommonSubsequence(text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
# 空状态型 +1:数组维度 (m+1)×(n+1)
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 左闭右开型 +1:range(m+1) 覆盖 0~m
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
六、总结:记住这两点,再也不踩 +1 的坑
- 动态规划优先想"空状态" :如果状态是"前
i个",数组维度必须+1; - 循环/切片注意"左闭右开" :如果要包含最后一个元素,结束索引必须
+1。
算法题中的 +1 看似是小细节,实则是逻辑严谨性的体现。只要你在写代码前多花10秒钟,按我们的步骤判断一下状态定义和目标范围,就能轻松避开这个高频坑。