我是靠谱客的博主 平淡小兔子,最近开发中收集的这篇文章主要介绍枚举字符串的排列, 八皇后,枚举&回溯2种解法,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

输入一个字符串,打印出该字符串中字符的所有排列。
例如输入字符串abc,则打印出由字符a、b、c所能排列出来的所有字符串abc, acb , bac、bca, cab cba。


由于字符串的长度不同, 使用for循环嵌套枚举是不行的, 需要使用递归才能处理不定长度的字符串.

需要把字符全排列, 每个字符都可能出现在第一位置, 可以看成  完整字符 =  单个字符 + 剩余字符的全排列 , 剩余字符的全排列 = 单个字符 + (剩余字符-单个字符)的全排列, .....  这样下去几次之后,剩余字符会越来越少, 当剩余字符没有时, 说明全排列已经完成了.
现在说一个例子 ,
ABCD的全排列 = A + BCD的全排列;  ABCD的全排列 = B + ACD的全排列;   ABCD的全排列 = C + ABD的全排列;   ABCD的全排列 = D + ABC的全排列;  
BCD的全排列 = B + CD的全排列;   BCD的全排列 = C + BD的全排列;  BCD的全排列 = D + BC的全排列; 
CD的全排列 = C + D;  CD的全排列 = D + C ;

经过这样分解子问题 , 就可以写出递归的代码了.

- (void)viewDidLoad {
    [super viewDidLoad];

    [self logAllString];
    
}

// 枚举字符串的所有组合
- (void)logAllString {
    
    NSString * str = @"ABCD";
    
    // 把str的每个字符加入到数组中
    NSMutableArray<NSString *> * array = [NSMutableArray arrayWithCapacity:str.length];
    for (int i = 0; i<str.length; i++) {
        [array addObject:[str substringWithRange:NSMakeRange(i, 1)]];
    }
    [self __logString:@"" withLeftArray:array];
    
}
/// 每次都是 原始字符串+剩下的没有处理的数组
- (void)__logString:(NSString *)str withLeftArray:(NSMutableArray *)leftArray{
    
    if (leftArray.count==0) {
        static int count = 1;
        NSLog(@"%@  第%d种",str,count++);
        return;
    }

    for (NSString * oneChar in leftArray) {
        // 从剩余的数组中移除本次添加的oneChar
        NSMutableArray * copyArray = [leftArray mutableCopy];
        NSString * result = [NSString stringWithFormat:@"%@%@",str,oneChar];
        [copyArray removeObject:oneChar];
        [self __logString:result withLeftArray:copyArray ];

    }
    
}


好, 有了字符的全排列, 就可以用枚举法尝试着做一下八皇后问题了 

经典算法之八皇后问题

在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问一共有多少种摆法?  
这个问题被众多神级人物的研究过, 大数学家高斯认为一共有76种摆法,1854年在柏林的象棋杂志上不同的作者发表了共计40种不同的见解,后来还有人利用图论的方法得出共有92种摆法。
而如今,通过我们的计算机以及编程语言我们可以轻松的解决这个问题。一共有92种.

最直接的也是最容易想到的一种解法便是暴力法,我们可以在8×8的格子中任选8个皇后,选定后看是否满足任意两个皇后都不处于同行同列同斜线的条件,若满足则累计满足条件的方案。学习过排列组合的我们发现64取8这个数字达到了40亿,显然是令人难以接受的。
但我们根据这个条件,我们可以人为地做出一些选择,比如根据条件我们可知每行每列最多都只能有一个皇后,这样可以在一定程度上缩减问题的规模。在第一行的某一列选择放置一个皇后,共有8种不同的选择,而第二行只能选择剩下的7列,也就是7种选择,剩下每一行的选择都会递减1,那么总共可供选择的方案有8的阶乘种, 也就是8!=40320,已经是一种远优于暴力解法的解法,最起码对计算机来说, 有执行的可能性了.

我们在看下是怎么转成8!的 , 根据题目, 可以知道 , 每个皇后不能是同行同列 , 我们可以对0~7进行全排列 , 把0~7的组合,看成棋盘上皇后的位置, 位置看成第几行,位置上的值看成第几列, 来个例子73025164,  比如说7, 7本身的下标理解为对应的行, 7这个数组看成列, 如此7就可以理解为第0行,第7列, queen[0][7] , 在比如说3, 就是对应的queen[1][3] , ......... 4就是queen[7][4] , 

如此一来 ,我们对0~7就行全排列, 共有8!种可能, 在验证每个能否满足条件, 就可以解出此问题了.

验证能否满足条件, 横着的行 , 不需要处理了,因为每一行就只有一个,  竖着的列, 也不需要处理, 因为0~7没有重复,所以列也不会重复.  就是斜着的 , 需要处理. 
斜向上的, 比如说(6,0),(5,1)(4,2)(3,3)(2,4)(1,5),(6,0) , 观察后发现 这些 行+列 都是一个固定值
斜向下的,,比如(0,2),(1,3),(2,4),(3,5),(4,6),(5,7) 观察后发现, 这些 行-列 或者 列-行都是一个固定值,

因此, 比如在(5,1)处有一个皇后, 就验证其他位置是否有  行+列 == 6 , 行-列==4的值, 如果有一个,说明这个方案不行,换下一个方案.

so, 总结下思路, 
1. 把八皇后问题转成0~7字符串的全排列, 字符串所在的下标+字符串对应的值,可以定位到皇后的位置, queen[下标][字符串的值],
2. 有了字符串的全排列之后, 逐一验证字符串是否满足八皇后的条件, 如果满足, 输出对应的 字符串+转后的棋盘.             

上代码

- (void)viewDidLoad {
    [super viewDidLoad];

    [self eightQueen1];
}

// 八皇后问题
// 在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法?
// 总共有92种
// 使用枚举
- (void)eightQueen1 {
    
    // 8! = 40320
    NSString * str = @"01234567";
    
    // 把str的每个字符加入到数组中
    NSMutableArray<NSString *> * array = [NSMutableArray arrayWithCapacity:str.length];
    for (int i = 0; i<str.length; i++) {
        [array addObject:[str substringWithRange:NSMakeRange(i, 1)]];
    }
    
    [self __logString:@"" withLeftArray:array];

}

/// 每次都是 原始字符串+剩下的没有处理的数组
- (void)__logString:(NSString *)str withLeftArray:(NSMutableArray *)leftArray{
    
    if (leftArray.count==0) {
        static int count = 1;
//        NSLog(@"%@  第%d种",str,count++);
        [self chectQueenSafeWithString:str];
        return;
    }

    for (NSString * oneChar in leftArray) {
        // 从剩余的数组中移除本次添加的oneChar
        NSMutableArray * copyArray = [leftArray mutableCopy];
        NSString * result = [NSString stringWithFormat:@"%@%@",str,oneChar];
        [copyArray removeObject:oneChar];
        [self __logString:result withLeftArray:copyArray ];

    }
    
}
/// 把0~7的组合,看成棋盘上皇后的位置, 位置看成第几行,位置上的值看成第几列, 比如47261530 , 6,可以理解为在queen[3][6]
- (BOOL)chectQueenSafeWithString:(NSString *)str {
    
    int queen[8] = {0,0,0,0, 0,0,0,0};
    for (int i = 0; i<str.length; i++) {
        NSString * oneChar = [str substringWithRange:NSMakeRange(i, 1)];
        queen[i] = oneChar.intValue;
    }
    // 横着,不需要检查,因为每行都是只有一个元素
    // 竖着,也不要检查,因为是对0~7的枚举,每个数字只出现了一次

    // 对每一个位置的(i,queen[i])进行检查
    for (int i = 0; i<8; i++) {
        
        int section = i ;
        int row = queen[i];
        
        // 检查对角线,
        for (int j = 0; j<8; j++) {
            
            if (j == section) {
                continue;
            }
            // 检查斜向上对角线 , section+row是一个固定值
            if (j+queen[j] == section+row) {
                return NO;
            }
            // 检查斜向下对角线 , section-row是一个固定值
            if (j-queen[j] == section-row) {
                return NO;
            }
        }
        
    }
    
    static int solution = 1;
    NSLog(@"第%d种解决方案 %@",solution++,str);
    for (int i = 0; i<8; i++) {

        NSString * logStr = @"00000000";
        logStr = [logStr stringByReplacingCharactersInRange:NSMakeRange(queen[i], 1) withString:@"1"];
        NSLog(@"%@",logStr);

    }
    
    return YES ;
    
}


有了枚举的方法,  自然要看下正规的回溯法时如何解决的. 

回溯法,又被称为“试探法”。解决问题时,每进行一步,都是抱着试试看的态度,如果发现当前选择并不是最好的,或者这么走下去肯定达不到目标,立刻做回退操作重新选择。这种走不通就回退再走的方法就是回溯法。

回溯法从问题本身出发,寻找可能实现的所有情况。和穷举法的思想相近,不同在于穷举法是将所有的情况都列举出来以后再一一筛选,而回溯法在列举过程如果发现当前情况根本不可能存在,就停止后续的所有工作,返回上一步进行新的尝试。

回溯算法是尝试搜索算法中最基本的一种算法,其采用了一种“走不通就掉头”的思想,作为控制结构。在使用回溯算法解决问题中每向前走一步都有很多路径需要选择,但当没有决策信息或决策信息不充分时,只好尝试某一路径向下走,直至走到一定程度后得知此路不通时,再回溯到上一步尝试其他路径;当然在尝试成功时,则问题得解而算法结束。

 

递归是从问题的结果出发,例如求 n!,要想知道 n!的结果,就需要知道 n*(n-1)! 的结果,而要想知道 (n-1)! 结果,就需要提前知道 (n-1)*(n-2)!。这样不断地向自己提问,不断地调用自己的思想就是递归。

回溯和递归唯一的联系就是,回溯法可以用递归思想实现。
 

回溯法与树的遍历
       使用回溯法解决问题的过程,实际上是建立一棵“状态树”的过程。例如,在解决列举集合{1,2,3}所有子集的问题中,对于每个元素,都有两种状态,取还是舍,所以构建的状态树为:

       回溯法的求解过程实质上是前序遍历“状态树”的过程。树中每一个叶子结点,都有可能是问题的答案。图 1 中的状态树是满二叉树,得到的叶子结点全部都是问题的解。

        在某些情况下,回溯法解决问题的过程中创建的状态树并不都是满二叉树,因为在试探的过程中,有时会发现此种情况下,再往下进行没有意义,所以会放弃这条死路,回溯到上一步。在树中的体现,就是在树的最后一层不是满的,即不是满二叉树,需要自己判断哪些叶子结点代表的是正确的结果。


八皇后问题是使用回溯法解决的典型案例。算法的解决思路是:

从棋盘的第一行开始,从第一个位置开始,依次判断当前位置是否能够放置皇后,判断的依据为:
同该行之前的所有行中皇后的所在位置进行比较,如果在同一列,或者在同一条斜线上(斜线有两条,为正方形的两个对角线),都不符合要求,继续检验后序的位置。
如果该行所有位置都不符合要求,则回溯到前一行,改变皇后的位置,继续试探。
如果试探到最后一行,所有皇后摆放完毕,则直接打印出 8*8 的棋盘。最后一定要记得将棋盘恢复原样,避免影响下一次摆放。

理论知识就是这样, 看具体的实现吧 
 

- (void)viewDidLoad {
    [super viewDidLoad];

    [self eightQueen2];

}

// 八皇后问题
// 在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法?
// 总共有92种
// 棋盘
int queen[8][8] = {
    {0,0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0,0},
    
    {0,0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0,0},
};
- (void)eightQueen2 {
    // 从第0行开始寻找
    [self fineNext:0];
}

// 在这一行寻找
- (void)fineNext:(NSInteger)section {

    /// 遍历到8, 说明前面的都可以了
    if (section == 8) {
        static int solution = 1;
        NSLog(@"第%d种解决方案",solution++);
        for (int i = 0; i<8; i++) {
            NSString * str = @"";
            for (int j = 0; j<8; j++) {
                str = [str stringByAppendingFormat:@"%d",queen[i][j]];
            }
            NSLog(@"%@",str);
        }
        
        return;
    }
    
    for (int i = 0; i<8; i++) {
        
        // 如果这个位置可以放,就继续找下一行; 不可以放,继续循环
        if ([self isEightQueenSafe:section row:i]) {
            queen[section][i] = 1;
            [self fineNext:section+1];
            // 回溯的关键,这一种不论是找成功还是失败,都把棋盘恢复原状
            queen[section][i] = 0;
        }
    }
    
    
}


/// 检查皇后的位置是否安全
- (BOOL)isEightQueenSafe:(NSInteger)section row:(NSInteger)row {
    
    // 横着的row不用检查,因为每一行只有一个值
    
    // 检查竖着的section
    for (int i = 0; i<8; i++) {
        if (queen[i][row] == 1) {
            return NO ;
        }
    }
    
    // 检查斜向上对角线, section+row是一个固定值, chectSection+chectRow==section+row
    for (int i = 0; i<8; i++) {
        
        NSInteger chectSection = i;
        NSInteger chectRow = section+row-i;
        if (chectRow<0 || chectRow>7) {
            continue;
        }
        
        if (queen[chectSection][chectRow] == 1) {
            return NO ;
        }
        
    }
    
    
    // 检查斜向下对角线, row-section 是固定值,chectRow-chectSection==
    for (int i = 0; i<8; i++) {
         
         NSInteger chectSection = i;
         NSInteger chectRow = row-section+i;
         if (chectRow<0 || chectRow>7) {
             continue;
         }
         
         if (queen[chectSection][chectRow] == 1) {
             return NO ;
         }
         
     }
    
    return YES;
}


现在手上有了2套方案, 自然要比较下2者的优劣了.

枚举法的话,  枚举出所有的结果有8!种, 验证情况是否合法,需要8^2次, 所以需要 8! * 8^2=258 0480次, 验证解决需要大约258W次. 

回溯法的话, 这个有点不好计算, 一顿搜索之后, 也没有找到答案, 索性2个代码都有,各跑100次看看执行时间对比.

在都循环100次的情况下, 枚举大约用时22S, 而回溯在0.07S,   有点不敢相信, 回溯的算法跑10000次试试,

10000次的情况下, 回溯用时6.4S,  好吧, 看来回溯算法的时间复杂度远远小于枚举, 相差300多倍.

回溯算法, 总共尝试调用findNext了2057次,  检查是否安全isEightQueenSafe调用了15720次, 

枚举算法 在枚举出结果的过程中, 枚举递归调用了10 9601次, 枚举结果的检查需要40320次, 不论是递归调用的深度, 还是对结果的检查, 枚举次数斗殴远远高于回溯法, 这就是效率的差距吧.

最后

以上就是平淡小兔子为你收集整理的枚举字符串的排列, 八皇后,枚举&回溯2种解法的全部内容,希望文章能够帮你解决枚举字符串的排列, 八皇后,枚举&回溯2种解法所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(80)

评论列表共有 0 条评论

立即
投稿
返回
顶部