算法题避坑指南:数组/循环范围的 `+1` 到底什么时候加?

在算法题的刷题过程中,你是否遇到过这样的场景:

  • 动态规划数组明明逻辑对了,却总是报 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,...,mms 的长度,i=0 对应空 s
  • j 的取值:0,1,2,...,nnp 的长度,j=0 对应空 p
数组维度判断
  • im+1 个取值,jn+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=0s 的前 i 个字符转换成空 p,需要 i 次删除操作。
数组维度

同样需要 (m+1) × (n+1),否则无法初始化 i=0j=0 的边界情况。

4. 空状态型 +1 的判断步骤

遇到动态规划题,按这3步走:

  1. 明确状态定义 :你的 i/j 是"前 i 个"还是"第 i 个"?
    → 优先用"前 i 个",因为方便处理空状态;
  2. 确定取值范围i 需要从 0 到 m 吗?
    → 如果需要(通常都需要),则取值个数是 m+1
  3. 推导数组维度 :数组长度 = 取值个数 → 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. 明确你的目标范围 :你需要包含的最后一个索引/值是什么?
    → 比如统计 1~n,最后一个值是 n;
  2. 调整结束索引:因为左闭右开,结束索引 = 目标最后一个值 + 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)**为例,完整走一遍判断流程:

题目

给定两个字符串 text1text2,返回它们的最长公共子序列的长度。

步骤1:明确状态定义

dp[i][j]:表示 text1 的前 i 个字符和 text2 的前 j 个字符的最长公共子序列长度。

→ 用"前 i 个",需要包含 i=0/j=0(空字符串的最长公共子序列长度为0)。

步骤2:确定取值范围

  • i 的取值:0,1,2,...,mmtext1 的长度)
  • j 的取值:0,1,2,...,nntext2 的长度)
    → 取值个数分别是 m+1n+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 的坑

  1. 动态规划优先想"空状态" :如果状态是"前 i 个",数组维度必须 +1
  2. 循环/切片注意"左闭右开" :如果要包含最后一个元素,结束索引必须 +1

算法题中的 +1 看似是小细节,实则是逻辑严谨性的体现。只要你在写代码前多花10秒钟,按我们的步骤判断一下状态定义和目标范围,就能轻松避开这个高频坑。

相关推荐
liliangcsdn1 小时前
基于似然比的显著图可解释性方法的探索
人工智能·算法·机器学习
骇城迷影1 小时前
代码随想录:二叉树篇(中)
数据结构·c++·算法·leetcode
期末考复习中,蓝桥杯都没时间学了2 小时前
力扣刷题23
算法·leetcode·职场和发展
菜鸡儿齐2 小时前
leetcode-子集
算法·leetcode·深度优先
今儿敲了吗2 小时前
28| A-B数对
数据结构·c++·笔记·学习·算法
Desirediscipline2 小时前
#include<limits>#include <string>#include <sstream>#include <iomanip>
java·开发语言·前端·javascript·算法
Felven2 小时前
B. Luntik and Subsequences
算法
菜鸡儿齐2 小时前
leetcode-括号生成
算法·leetcode·职场和发展
fs哆哆2 小时前
在VB.NET中,随机打乱列表顺序的算法与方法
算法·.net