回溯算法及典型例题

大多是使用回溯算法的题目都符合以下条件:

  • 输出可以看作一个n元组(x1,x2,…,xn),例如八皇后问题。
    问题的解可以表示为n元组:(x1,x2,…,xn),xi∈Si,Si为有穷集合。(x1,x2,…,xn)具有完备性,即(x1,x2,…,xn)是合理的,则(x1,x2,…,xi) (i<n)一定合理
  • 元组需要满足一些约束条件。

回溯算法是在状态空间树上跳跃式地进行深度优先搜索,即用判定函数考察x[k]的取值,如果x[k]是合理的就搜索以x[k]为根节点的子树,如果x[k]取完了所有的值,便回溯到x[k-1]。
个人认为回溯算法和暴力法有很大的相似度,但是回溯算法会在每次构造解的过程中进行剪枝。整个选择过程可以构成一棵状态空间树,树的根可以代表查找解之前的初始状态。大多数情况下可以用DFS的方法来生成状态空间树。如果当前结点是有希望的,可以继续深度遍历,否则回溯到父母节点。
首先来看一道经典例题

一.八皇后问题

1.问题描述
在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问一共有多少种摆法。

2.问题分析
回溯法是求解皇后问题最经典的方法。算法的思想在于如果一个皇后选定了位置,那么下一个皇后的位置便被限制住了,下一个皇后需要一直找直到找到安全位置,如果没有找到,那么便要回溯到上一个皇后,那么上一个皇后的位置就要改变,这样一直递归直到所有的情况都被举出。
首先思考一下,输出的元组中每一个分量,分开来看,在八皇后问题中,每一个分量都有八种可能,那么在每一层递归中我们都要穷举这八种可能,然后根据条件来判断该种结果是否合理。再者,因为不能在同一列,也为了避免重复,下一个皇后只能在上一个皇后的下面。这样思考问题就迎刃而解了。

#include<iostream>
using namespace std;

#define MAX 8;
//回溯法
int chessboard[8][8] = {
    
     0 };
int num = 0;
bool check(int x, int y)  //
{
    
     
	for (int i = 0; i < x; i++) {
    
      //检查同列是否有皇后
		if (chessboard[i][y] == 1)
			return false;
		if (x - 1 - i >= 0 &&y-1-i>=0&&chessboard[x - 1 - i][y - 1 - i] == 1) //检查左斜线
			return false;
		if (x - 1 - i >= 0 &&y+1+i<=7&& chessboard[x-1-i][y+1+i] == 1)  //检查右斜线
			return false;
	}
	
	return true;
}

void show() {
    
    
//	++num;
	for (int i = 0; i < 8; i++) {
    
    
		for (int j = 0; j < 8; j++) {
    
    
			cout << chessboard[i][j];
		}
		cout << endl;
	}
}

void putQueen(int x) {
    
    
	int i;
	for (i = 0; i < 8; i++) {
    
    
		for (int j = 0; j < 8; j++) chessboard[x][j] = 0;
		if (check(x,i)) {
    
    
			chessboard[x][i] = 1;
			if (x == 7) {
    
    
				cout << "第" << num + 1 << "种解法" << endl;
				show();
				cout << endl;
				++num;
			}	
			else {
    
    
				putQueen(x + 1);			
			}			
		}
	}
}

int main()
{
    
    
	putQueen(0);
	cout << num << endl;
}
二.二进制手表

1.问题描述
二进制手表顶部有 4 个 LED 代表 小时(0-11),底部的 6 个 LED 代表 分钟(0-59)。每个 LED 代表一个 0 或 1,最低位在右侧。

例如,上面的二进制手表读取 “3:25”。
给定一个非负整数 n 代表当前 LED 亮着的数量,返回所有可能的时间。

2.问题分析
仔细想想,这和八皇后问题其实是一个套路。解题方法参考了一个大佬的文章。首先我们来个映射

0-3号表示时,后面的4-9号表示分,然后从10盏灯中选择n盏灯点亮。

#include<iostream>
#include<vector>
#include<string>
#include<unordered_map>
using namespace std;

vector<string>res;
unordered_map<int, int> timeHash = {
    
     {
    
    0,1},{
    
    1,2},{
    
    2,4},{
    
    3,8},{
    
    4,1},{
    
    5,2},{
    
    6,4},{
    
    7,8},{
    
    8,16},{
    
    9,32} };
void backtrack(int num, int start, pair<int, int> & time)
{
    
    
	if (num == 0)
	{
    
    
		if (time.first > 11 || time.second > 59)//判断合法性
			return;
		string temp_hour = to_string(time.first);
		string temp_minute = to_string(time.second);
		if (temp_minute.size() == 1)//如果minute只有一位要补0
			temp_minute.insert(0, "0");
	//	res.push_back(temp_hour + ":" + temp_minute);//构造格式
		cout << temp_hour << ":" << temp_minute << endl;
		return;
	}

	for (int i = start; i < 10; i++)
	{
    
    
		if (time.first > 11 || time.second > 59)
			continue;
		pair<int, int>store = time;//保存状态
		if (i < 4)
			time.first += timeHash[i];
		else
			time.second += timeHash[i];
		backtrack(num - 1, i + 1, time);//进入下一层,注意下一层的start是i+1,即从当前灯的下一盏开始
		time = store;//恢复状态
	}
}
void readBinaryWatch(int num) {
    
    
	pair<int, int>time(0, 0);//初始化时间为0:00
	backtrack(num, 0, time);
	//return res;
}

int main()
{
    
    
	readBinaryWatch(2);
}

但是需要特别注意的是,在这个题目中需要有状态的恢复,即我们所说的回溯到状态空间树中的父结点的状态,然后在对其进行改变,所以在我们递归到下一层时,首先应该保存一下上一层的状态。

猜你喜欢

转载自blog.csdn.net/qq_43406565/article/details/108216655
今日推荐