回溯中如何判断下一次递归应传 ind + 1 还是 i + 1?
下一次递归传什么,取决于当前选择后还剩什么需要处理。排列每次填好一个位置,所以传 ind + 1;回文分割每次选取区间 [ind, i],所以传 i + 1。
原问题最后一句把两者写反了,下面以代码中的调用为准。
排列:ind 是待填位置,i 是候选元素的位置
在这段交换式排列代码中,进入 set(ind, nums, ans) 时:
[0, ind)中的位置已经确定,在当前递归分支内保持不变。[ind, nums.length)中的元素还没有安排好。- 本层递归负责确定位置
ind应该放哪个元素。
循环变量 i 用来枚举候选元素。执行:
swap(i, ind, nums);
原本位于 i 的候选元素就被放到了位置 ind。这一步确定了一个位置,所以下一个待填位置始终是 ind + 1:
for (int i = ind; i < nums.length; i++) {
swap(i, ind, nums);
set(ind + 1, nums, ans);
swap(i, ind, nums);
}
例如,nums = [1, 2, 3],当前 ind = 0,选择 i = 2,交换后的数组是:
[3, 2, 1]
此时只确定了位置 0 放 3,位置 1、2 还需要排列,所以应调用 set(1, nums, ans)。
如果改为 set(i + 1, nums, ans),这里就会调用 set(3, nums, ans),直接触发终止条件。程序会把尚未处理的位置当作已经完成,从而漏掉排列。
递归返回后的第二次 swap 用来恢复现场,让循环继续尝试其他候选元素。
回文分割:ind 是未处理后缀的起点,i 是当前片段的终点
进入 partition(ind, ans, ds, s) 时,ds 中的片段已经连续覆盖了前缀 [0, ind)。接下来要分割的是从 ind 开始的后缀。
本层循环尝试不同的结束位置 i,检查区间 [ind, i] 是否为回文:
if (ispalindrome(s, ind, i)) {
ds.add(s.substring(ind, i + 1));
partition(i + 1, ans, ds, s);
ds.remove(ds.size() - 1);
}
Java 的 substring(ind, i + 1) 包含起点、不包含终点,因此选中的字符下标是 ind 到 i。这些字符已经使用过,下一次递归就从 i + 1 开始。
例如,s = "aab",当前 ind = 0,选择 i = 1:
已选片段:"aa"
已覆盖下标:0、1
剩余部分:"b"
下一次起点:2
此时应调用 partition(2, ans, ds, s)。如果传入 ind + 1,起点会变成 1,下标 1 的字符就会被重复使用,产生重叠的分割。
当选择单个字符,即 i == ind 时,i + 1 和 ind + 1 恰好相同;选取更长的片段时,两者就不同了。
如何自己推导递归参数?
写递归调用前,可以先回答三个问题:
- 函数参数表示什么?是待填的位置、剩余区间的起点,还是其他状态?
- 本次选择完成了多少工作?是填好一个位置,还是消耗了一段连续区间?
- 下一层还需要处理什么?根据剩余任务更新参数。
这两段代码的状态变化如下:
| 问题 | 当前参数的含义 | 本次选择完成的工作 | 下一次参数 |
|---|---|---|---|
| 交换式排列 | ind 是待填位置 | 确定位置 ind 的元素 | ind + 1 |
| 回文分割 | ind 是未处理后缀的起点 | 使用字符区间 [ind, i] | i + 1 |
常见的子集枚举也会使用 i + 1。如果当前选择了下标 i 的元素,下一层只考虑它后面的元素,就能按下标递增的顺序构造子集,避免重复枚举同一组下标。允许重复使用元素的问题则可能继续从 i 开始,具体取决于题目约束和状态定义。
代码使用时需要注意什么?
这些片段省略了结果收集和辅助函数。原始回文分割片段末尾还多了一个右花括号,整理为可运行代码时需要修正。到达终止条件后,应把当前结果的副本加入 ans;例如回文分割使用:
if (ind == s.length()) {
ans.add(new ArrayList<>(ds));
return;
}
保存副本可以避免后续回溯修改已经收集的结果。排列也需要在叶子节点复制当前数组内容。另外,如果输入数组含有重复值,这段交换式排列代码可能生成重复排列,需要另行处理去重。
备注:内容仅供参考。
