算法应用公式(一)回溯法

回溯法在生活中其实是十分常见的,比如在9*9数独游戏中,我们根据同行、同列,同块中不能出现重复数字的原则补充未完成的表格,当有依据地推理出所有可以填写的空格时,经常会遇到某些不确定的格子,这些格子总是具有多选1的情况,此时,我们需要首先代入一个选项,并继续推理下去,当同行同列不重复的原则出现矛盾时,我们就可以发现我们之前代入的选项是错的,由此排除,并代入其他可能的选项继续尝试,这就是一次回溯的过程。

算法概念

回溯法是一种选优搜索法,又称为试探法,主要流程为按照某种约束或条件逐步执行搜索过程,从而达到我们所需的目标,当约束无法满足或目标无法达成时,就退回到某一步重新选择,也就是回溯的过程。

回溯法的核心在于约束、行为、目标三个方面。比如,在前文提到的数独游戏中,“同列同行同块不重复”的原则就是回溯法的约束,“下一个空白格子中可以填写1-9的数字”就是搜索的行为,“填满整张表格”就是我们要达到的目标,每次我们填写一个格子就是一次搜索过程。

回溯法的本质是递归,之所以能够使用递归就是因为回溯法的每一步搜索都是向前的,且遵循相同的搜索原则,同时达到目标或目标无法达成的场景完成了对递归过程结束的约束。由此,只要我们将一个问题判定为可回溯的问题,我们就能够很快地写出回溯算法的实现。

算法应用公式

回溯法 = 行为(逐个xxxxx) + 约束(xxx应该xxxx) + 目标(最终xxxx)

回溯代码 = 约束检查函数 + 目标截止的行为递归函数

经典应用场景

八皇后问题

问题场景

八皇后问题是回溯法的经典场景,问题描述如下:在8*8的国际象棋棋盘上摆放八个皇后,使其不能互相攻击,也就是说任意两个皇后都不能出现在同一行、同一列、同一斜线上,有多少种摆法?(92)

解法

前文提到,回溯法的核心在于约束、搜索方向、目标三方面。

在八皇后问题中,由于每行每列不能有超过1个皇后,而8个皇后想要放在8行的棋盘中,则每行有且只有一个皇后,所以我们只要逐一确定每一行皇后应该摆放在哪一列,当所有皇后均摆放完成,我们便可以得到摆放情况了。

由此,我们可以得到如下的信息:对于八皇后问题,约束就是“每一行、列、斜线上有且只有1个皇后”,行为就是“确定下一行放皇后的位置”,目标则是“完成8枚棋子的摆放”,每次达到目标后统计达成次数即可。

代码编写思路

我们一步步来讲解如何完成代码编写:

A. 约束检查函数 check

第一步选择约束部分编写,是因为约束部分通常包含问题中所需的各种关键值,可以帮助我们尽早确定需要的变量。八皇后问题中,约束为

  1. 同行只有一个皇后
  2. 同列只有一个皇后
  3. 同斜线最多只有一个皇后

我们的行为是逐行确定皇后位置,所以每一行一定只有一个皇后,易知约束函数首先需要的两个参数是行号int line和列号int row;此时,为了检查第二条约束,我们需要判断当前列与其之前所有行上是否有重复的皇后,所以,我们还需要一个int board[8]来保存每一行中皇后的列号;已有列号数据,检查第三条约束自然也是很简单

inline bool check(int line, int row, int board[]){ // 约束
    for(int i = 0; i < line; ++i){
    	int existRow = board[i];
    	if(existRow == row){ // 列是否重复
    		return false;
    	}
    	else if(line-i == abs(row-existRow)){ // 斜线是否重复
    		return false;
    	}
    }
    return true;
}
B. 行为递归函数 placeQueen

在参数设定上,首先,八皇后问题的行为是逐行确定皇后的位置,所以,要有一个参数存储当前搜索到的行号;其次,综合约束函数,还需要board[8]存储列信息;最后,题目要求是计算出方案数量,我们还需要一个num用来统计结果数。

作为一个递归函数,placeQueen需要有一个截止设定,也就是我们的目标——完成所有皇后拜访,也就是说,当前行号要大于棋盘行号

if(line > 7){ // 目标
    num++;
    cout << "right!" << endl;
    return ;
}

行为中的主要环节就是确定这一行中可以摆放的位置,所以对当前行的8个位置逐一进行约束检查,满足则允许摆放,并进入下一行。

注意:当允许摆放时,board[8]的对应值要修改,操作完成后为了防止偏差要修改回来

    for(int i = 0; i < 8; ++i){
    	if(check(line, i, board)){
    		board[line] = i;
    		placeQueen(line+1, board, num); // 行为
                board[line] = 8;
    	}
    }

代码

#include <iostream>
#include <cmath>
using namespace std;

inline bool check(int line, int row, int board[]){ // 约束
    for(int i = 0; i < line; ++i){
    	int existRow = board[i];
    	if(existRow == row){
    		return false;
    	}
    	else if(line-i == abs(row-existRow)){
    		return false;
    	}
    }
    return true;
}

void placeQueen(int line, int board[], int& num){
    if(line > 7){ // 目标
        num++;
        return ;
    }
    for(int i = 0; i < 8; ++i){
    	if(check(line, i, board)){
    		board[line] = i;
    		placeQueen(line+1, board, num); // 行为
                board[line] = 8;
    	}
    }
}

int main(){
    int num = 0;
    int board[8];
    for(int i = 0; i < 8; ++i){
        board[i] = 0;
    }
    placeQueen(0, board, num);
    cout << num << endl;
    return 0;
}

数独问题

问题场景

一个大的99表格是由9个33的方格组合而成的。对于每一个33的表格,都各自需要填入1-9这9个不同的数,同时使得这99的表格的每一行以及每一列上没有重复的数字,已知方格的一部分,请将表格填满。

解法

同理,我们还是要分析数独问题的三个方面,填写数独时,我们通常会是不断用排除法填写确定的格子,不确定的格子再依次尝试可能的选项,对于代码实现我们当然不可以这样做,而是对于81个格子按照顺序依次进行填写,如果已有预先给定的值则继续处理下一个,当我们发现某个尝试的值产生矛盾时,回溯到最近的回溯点。三个要素的含义前文已经给出,这里对其进行进一步说明:

  1. 约束:同列、同行、同块的9个格子均由1-9这9个数字构成
  2. 行为:对每一行的每一个格子进行赋值,一行完成后从下一行的第一个开始,如果该格子已有数据则选择跳过
  3. 目标:最后右下角的格子已经有数据填充

代码

这里直接给出代码

#include <iostream>
using namespace std;

bool check(int line, int row, int value, int board[][9]){
	for(int i = 0; i < 9; ++i){
		if(board[line][i] == value){
			return false;
		}
		if(board[i][row] == value){
			return false;
		}
	}
	line /= 3;
	row /= 3;
	for(int i = line*3; i < line*3+3; ++i){
		for(int j = row*3; j < row*3+3; ++j){
			if (board[i][j] == value){
				return false;
			}
		}
	}
	return true;
}

void printSudoku(int board[][9]){
	for(int i = 0; i < 9; ++i){
		for(int j = 0; j < 9; ++j){
			cout << board[i][j] << " ";
		}
		cout << endl;
	}
	cout << "success" << endl;
}

void setNum(int line, int row, int board[][9]){
	if(line>8){
		printSudoku(board);
		return;
	}
	bool succ = false;
	if(board[line][row] == 0){
		for(int i = 1; i < 10; ++i){
			if(check(line, row, i, board)){
				board[line][row] = i;
				if(row < 8){
					setNum(line, row+1, board);
				}
				else{
					setNum(line+1, 0, board);
				}
				board[line][row] = 0;
			}
		}
	}
	else{
		if(row < 8){
			setNum(line, row+1, board);
		}
		else{
			setNum(line+1, 0, board);
		}	
	}
	
}

int main(){
	int board[9][9] = {
		{3, 0, 8, 0, 0, 0, 6, 0, 0},
		{0, 0, 0, 0, 7, 4, 3, 0, 8},
		{7, 1, 0, 6, 0, 0, 0, 2, 0},
		{0, 6, 7, 5, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 4, 0, 2, 6, 9},
		{9, 0, 0, 0, 8, 0, 0, 1, 0},
		{0, 5, 0, 3, 2, 0, 0, 4, 0},
		{0, 7, 0, 0, 0, 5, 1, 0, 0},
		{0, 0, 0, 4, 0, 1, 5, 3, 0}
	};
	
    setNum(0, 0, board);
    return 0;	
}

猜你喜欢

转载自www.cnblogs.com/suata/p/12669899.html