📚 算法笔记:P1706 全排列问题 (DFS 基础)
1. 题目描述
输出 1 ∼ N 1 \sim N 1∼N 的所有全排列,要求每个数字占 5 个场宽,排列按字典序从小到大输出。
2. 核心代码 (C++ 版本)
C++
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll N;
ll ans[15]; // 记录当前位置存了哪个数字
bool used[15]; // 标记数字 i 是否已被使用
void dfs(int position)
{
// 1. 递归出口:当位置超过 N 时,说明 N 个坑位已填满
if(position > N)
{
for(int i = 1; i <= N; i++)
{
cout << setw(5) << ans[i]; // 核心格式控制
}
cout << "\n";
return; // 功成身退,回溯到上一层
}
// 2. 尝试在当前位置填入数字 i
for(int i = 1; i <= N; i++)
{
if(!used[i]) // 只有没用过的数字才能填入
{
ans[position] = i; // 填入数字
used[i] = true; // 标记为已占用
dfs(position + 1); // 递归进入下一个位置
used[i] = false; // 【核心回溯】:撤销标记,释放数字
}
}
}
void solve()
{
if(!(cin >> N)) return;
dfs(1);
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int _ = 1;
while(_--)
{
solve();
}
return 0;
}
3. 核心考点与注意事项
- DFS 递归模型:本题体现了 DFS 最经典的思想------"不撞南墙不回头"。通过递归不断深入,直到触发出口条件。
- 回溯机制 (Backtracking) :
used[i] = false;是整段代码的灵魂。它保证了在完成一种排列并返回后,之前使用的数字能被释放,从而参与到其他分支的排列中。 - 状态维护 :
position:记录递归的深度(即当前的坑位)。i:记录横向的选择范围(即手里的数字)。
- 格式要求 :
setw(5)是iomanip库中的函数(万能头已包含),用于满足题目严格的场宽要求。
🕵️ 深度拆解:第二层工人的"工作日志"
假设 N = 3 N=3 N=3,第二层工人的任务是:填好第二个坑位(
ans[2])。第一阶段:尝试数字 2
- 开始循环 :工人看手里有哪些牌。数字 1 1 1 被第一层拿走了,数字 2 2 2 还没人用。
- 填坑 :他在第二个坑里填入
2(ans[2] = 2),并标记used[2] = true。- 派发任务 :他大喊一声:"第三层,剩下的交给你了!",然后调用
dfs(3)。- 原地待命 :此时,第二层工人的程序暂停 在了
dfs(3)这一行,他进入了漫长的等待。第二阶段:回火(Backtracking)
- 任务返回 :过了一会儿,第三层跑完回来了(也就是
1 2 3已经打印完了)。- 苏醒 :第二层工人"苏醒"过来,接着执行
dfs(3)下面的代码。- 撤销操作 :他执行
used[2] = false。这意味着他把数字2从坑里拿了出来,重新放回手里。这一步极其关键,因为它让数字 2 重新变回了"可用状态"。第三阶段:开启新分支(i 变成 3)
- 继续循环 :因为他还在
for循环里,执行完刚才那两行后,i++发生了。- 寻找下一张牌 :现在
i变成了3。- 检查可用性 :他发现数字 3 3 3 也没被用过(
used[3]是false)。- 新的尝试:
- 他在第二个坑里填入新的数字:
ans[2] = 3。- 标记
used[3] = true。- 再次大喊:"第三层,我又来了!",调用
dfs(3)。
4. 易错点回顾 (My Mistakes)
1. return 位置导致的"截断"错误
- 错误经历 :曾将
return放在if块之外,导致函数刚进入就直接结束,无法进入下方的for循环。- 教训 :在 DFS 中,
return通常只出现在递归出口(Base Case)中,代表当前路径搜索完毕。2. 回溯的必要性理解
- 反思 :如果不写
used[i] = false,数字被用过一次后就永远失效,最终只能输出1 2 3 ... N这一种结果,无法生成其他排列。