枚举字符串的排列, 八皇后,枚举&回溯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次, 不论是递归调用的深度, 还是对结果的检查, 枚举次数斗殴远远高于回溯法, 这就是效率的差距吧.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值