欢迎光临
我们一直在努力

P1706 全排列问题

记录159

#include<bits/stdc++.h>
using namespace std;
int path[15];
bool vis[15];
int n;
void dfs(int cnt){
if(cnt>n){
for(int i=1;i<=n;i++) cout<<" "<<path[i];
cout<<"\\n";
return;
}
for(int i=1;i<=n;i++){
if(vis[i]==0){
vis[i]=1;
path[cnt]=i;
dfs(cnt+1);
vis[i]=0;
}
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n;
dfs(1);
return 0;
}


题目传送门https://www.luogu.com.cn/problem/P1706


前言

我是一名专注信奥赛(CSP-J/S、NOIP)的教练。

  • 如果你觉得这篇题解对你有帮助,欢迎点击关注我的CSDN账号,我会持续更新高质量算法解析。
  • 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
  • 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。

 核心解题思路

这道题是一道非常经典的深度优先搜索(DFS)与回溯算法入门题。

  • 问题转化(排列树模型): 生成 1∼n 的全排列,本质上是在构建一棵深度为 nn 的“排列树”。我们在树的每一层(对应排列中的每一个位置),从 1∼n中选择一个还没有被使用过的数字填入。

  • 算法设计(DFS + 状态标记):

    • 使用一个数组 path 来记录当前正在构建的排列序列。
    • 使用一个布尔数组 vis 来记录哪些数字已经被用过了(避免重复)。
    • 每次递归时,枚举 1∼n 的所有数字。如果某个数字没有被用过,就把它放入 path 中,标记为已用,然后进入下一层递归。
    • 当递归深度达到 n 时,说明一个完整的排列已经生成,将其输出。
    • 回溯的关键:从下一层递归返回后,必须将刚才标记为已用的数字重新标记为未用(vis[i] = 0),以便在后续的循环中尝试其他数字。

  • 代码分块详细解释

    1. 全局变量定义

    #include<bits/stdc++.h>
    using namespace std;
    int path[15];
    bool vis[15];
    int n;

    • 详细分析:path 数组用来存放当前正在生成的排列序列;vis 数组(visit的缩写)是一个状态标记数组,vis[i] == 1 表示数字 ii 已经在当前排列中被使用过,0 表示未使用。由于题目保证 n≤9n≤9 ,数组开 15 足够。

    2. 核心逻辑:DFS 搜索与回溯

    void dfs(int cnt){
    if(cnt > n){
    for(int i = 1; i <= n; i++) cout << " " << path[i];
    cout << "\\n";
    return;
    }
    for(int i = 1; i <= n; i++){
    if(vis[i] == 0){
    vis[i] = 1;
    path[cnt] = i;
    dfs(cnt + 1);
    vis[i] = 0; // 回溯:撤销选择,恢复现场
    }
    }
    }

    • 详细分析:这是代码的灵魂,完美体现了回溯法“选择 -> 递归 -> 撤销选择”的三步曲。
      • 递归终止条件:当 cnt > n 时,说明前 nn 个位置都已经填满了数字,一个完整的排列已经生成。此时按照题目要求的“每个数字保留 5 个场宽”(即前面加 4 个空格)输出 path 数组。
      • 枚举与剪枝:在当前位置 cnt,我们尝试枚举 1∼n1∼n 的所有数字。if(vis[i] == 0) 保证了我们只会选择那些尚未被使用的数字。
      • 状态更新与递归:选定数字 i 后,将其标记为已用(vis[i] = 1),存入路径(path[cnt] = i),然后进入下一层 dfs(cnt + 1) 去填充下一个位置。
      • 回溯(恢复现场):当 dfs(cnt + 1) 执行完毕返回时,说明以当前数字 i 为起点的所有排列都已经生成完了。为了尝试下一个数字,我们必须把 i 的状态恢复为未使用(vis[i] = 0),这就是回溯的核心。

    3. 主函数与启动搜索

    int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n;
    dfs(1);
    return 0;
    }

    • 详细分析:读入 nn 后,直接从 dfs(1) 开始,表示从排列的第 1 个位置开始填数。由于我们是从 1 到 n 顺序枚举数字的,所以生成的排列天然就是字典序的。

    核心逻辑总结表

    代码模块核心变量/操作精炼作用解决的痛点
    路径记录 path[cnt] = i 记录当前正在构建的排列序列 保证了在到达叶子节点时,能够完整地输出整个排列
    状态标记 vis[i] = 1 标记数字 i 已被使用 保证了“所产生的任一数字序列中不允许出现重复的数字”
    回溯恢复 vis[i] = 0 撤销对数字 i 的使用标记 使得数字 ii 可以在其他分支中被再次使用,是生成全排列的关键
    字典序保证 for(int i = 1; i <= n; i++) 从小到大枚举数字 保证了输出的排列序列天然符合字典序要求,无需额外排序
    格式化输出 cout << " " << path[i] 每个数字前输出4个空格 完美契合题目“每个数字保留 5 个场宽”的格式要求
    赞(0)
    未经允许不得转载:171主机测评 » P1706 全排列问题
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址