前情提要
早在上学期初次看学长写全排列和各种需要数字枚举排列的题解用dfs代替多重循环时,就不时对这种情况下dfs的运用背后原理心存部分疑惑
为什么都是要枚举,dfs能比双重循环就快上近十倍?
例题(牛客NC15128)老子的全排列呢
题目描述
老李见和尚赢了自己的酒,但是自己还舍不得,所以就耍起了赖皮,对和尚说,光武不行,再来点文的,你给我说出来1-8的全排序,我就让你喝,这次绝不耍你,你能帮帮和尚么?
输入描述:
无输出描述:
1~8的全排列,按照全排列的顺序输出,每行结尾无空格。示例1
输入
No_Input
输出
Full arrangement of 1~8
备注:
1~3的全排列 : 1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1题号:NC15128
222ms的题解
#include<stdio.h>
int main (){
const int a=8;
int i,j,k,l,n,m,o,p;
for(i=1;i<=a;i++){
for(j=1;j<=a;j++){
for(k=1;k<=a;k++){
for(l=1;l<=a;l++){
for(n=1;n<=a;n++){
for(m=1;m<=a;m++){
for(o=1;o<=a;o++){
for(p=1;p<=a;p++){
if(i!=j && i!=k && i!=l &&i!=n&&i!=m&&i!=o&&i!=p){
if(j!=k && j!=l &&j!=n&&j!=m&&j!=o&&j!=p){
if(k!=l &&k!=n&&k!=m&&k!=o&&k!=p){
if(l!=n&&l!=m&&l!=o&&l!=p){
if(n!=m&&n!=o&&n!=p){
if(m!=o&&m!=p){
if(o!=p){
printf("%d %d %d %d %d %d %d %d\n",i,j,k,l,n,m,o,p);
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
return 0;
}
是当初刚学c语言头铁用的八重循环加特判(〃´-ω・) 当初因为怎么也想不出第二种方法,就直接循环硬钢了┐(・o・)┌,这种方法的时间复杂度大致为 O(n^n)
44ms的题解
/*输入格式
共一行,包含一个整数 n
输出格式
按字典序输出所有排列方案,每个方案占一行*/
#include<iostream>//经典排序型枚举
#include<stdlib.h>
using namespace std;
#define NUMMAX 9
bool bt[10]={0};
int num[10],n=8;
void dfs(int u){
if(u>n){
for(int i=1;i<n;i++)printf("%d ",num[i]);
printf("%d\n",num[n]);
return;
}
else{
for(int i=1;i<=n;i++){//bt也需要+1
if(!bt[i]){
bt[i]=true;
num[u]=i;
dfs(u+1);
bt[i]=false;
}
}
}
}
int main (){
dfs(1);
return 0;
}
是采用dfs中枚举型排列的方式٩(๑❛ᴗ❛๑)۶,时间复杂度大致为 O(n!)
原因分析和举例证明
原因分析
为什么多重循环是O(n^n)?
很简单的从执行次数最多的操作来看,而多重循环最多的就是循环操作
所以,计算总的迭代次数:
- 最内层循环会执行 n 次。
- 倒数第二层循环会执行 n 次,每次执行时最内层循环都会完整运行。因此,这一层与最内层组合起来会执行 n×n=n^2 次迭代。
- 类似地,倒数第三层循环也会执行 n 次,每次执行时倒数第二层和最内层循环都会完整运行。这样,这三层组合起来会执行 n×n2=n^3 次迭代。
这个模式会一直继续到最外层的循环。因此,所有 n 层循环组合起来会执行 n^n 次迭代。
为什么dfs只用O(n!)?
证明主要基于递归的深度和每一层递归中可能的状态数。
在 DFS 算法中,我们递归地尝试填充排列中的每一个位置,直到所有位置都被填满。对于一个有 n 个元素的排列,我们需要填充 n 个位置。
1. 递归深度:递归的深度是 n,因为我们需要填充 n 个位置。在每一步递归中,我们都向排列中添加一个新的元素。
2. 每一层递归的状态数:在每一层递归中,我们都有 n 个可能的元素可以选择(通过其中的for循环实现)。但是,由于我们使用了一个布尔数组
bt
来跟踪哪些元素已经被使用,所以实际上在每一层递归中,我们只会选择一个尚未被使用的元素(for中的if判断)。因此,在每一层递归中,我们尝试从剩余的未使用元素中选择一个元素来填充当前位置。在最开始的几层递归中,我们有 n 个元素可以选择。但是,随着递归的深入,可用的元素数量逐渐减少。重要的是,虽然可用元素的数量在减少,但算法仍然会尝试所有可能的选择(通过回溯),直到找到一个有效的排列或确定当前路径无效(即所有元素都已尝试且没有有效的排列)。
3. 总的时间复杂度:尽管在每一层递归中可用元素的数量在减少,但算法仍然需要探索所有可能的路径来生成所有排列。由于有 n 个元素,因此总共有 n! 种不同的排列方式。因此,算法需要执行大约 n! 次基本操作(包括递归调用、条件判断、回溯操作等)来生成所有排列。
回溯操作在这里的作用是确保算法在发现当前路径无效时能够及时地返回到上一个状态,并尝试其他可能的路径。虽然这并没有减少需要探索的总路径数(即 n! 种排列),但它使得算法能够更有效地探索这些路径,因为它避免了在不满足条件的情况下继续深入搜索。如果这套dfs没有回溯,就跟多重循环的效率大差不差了,还可能会死循环╭(T_T)
单看时间复杂度分析也许还是不能直观看出两者的差别,下面以1-3全排列两者经过数字范围为例指出差别
以1-3的全排列为例:
多重循环需要经过的数范围如下,
而dfs在bt数组的辅助筛选下只用判断以下范围的数,
如果上图还不能理解,可以去debug看一下bt数组的变化,应该会对dfs回溯和递归有更直观地理解了┐(-。ー;)┌,当初我第一次理解这种dfs时就是看bt数组变化惊叹dfs的回溯之神奇的
感谢你读到这送张镇楼图吧,军爷终于退伍了,回家第一发当然是清凉一夏