输入一个字符串,打印出该字符串中字符的所有排列。
例如输入字符串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次, 不论是递归调用的深度, 还是对结果的检查, 枚举次数斗殴远远高于回溯法, 这就是效率的差距吧.