算法学习笔记:回溯法

        回溯法有“通用的解题法”之称。用它可以系统地搜索一个问题的所有解或任一解。回溯法是一个既带有系统性又带有跳跃性的搜索算法。它在包含问题的所有解的解空间树中,按照深度优先的策略,从根节点出发搜索解空间树。算法搜索至解空间树的任一节点时,总是先判断该节点是否肯定不包含问题的解。如果肯定不包含,则跳过对已节点为根后续搜索,逐层向其祖先节点回溯。否则,进入该子树,继续按深度优先的策略进行搜索。回溯法在用来求问题的所有解时,要回溯到根,且根节点的所有子树都已被搜索遍结束。而回溯法在用来求问题的任一解时,只要搜索到问题的一个解就可结束。这种以深度优先的方式系统地搜索问题的解的算法称为回溯法,它适用于解一些组合数较大的问题。

        许多复杂的,规模较大的问题都可以使用回溯法,它是早期的人工智能里使用的算法,借助计算机强大的计算能力帮助我们找到问题的解。

        回溯算法在树和图中有较多的应用,对树形或者图形结构执行一次深度优先遍历,实际上类似枚举的搜索尝试过程,在遍历的过程中寻找问题的解。

        用回溯法解决的问题的所有选项可以形象地用树状结构表示。在某一步有n个可能的选项,那么该步骤可以看成是树状结构中的一个节点,每个选项看成树中节点连接线,经过这些连接线到达该节点的n个子节点。树的叶节点对应着终结状态。如果在叶节点的状态满足题目的约束条件,那么我们找到了一个可行的解决方案。

        如果在叶子节点的状态不满足约束条件,那么只好回溯到它的上一个节点再尝试其他的选项。如果上一个节点所有可能的选项都已经试过,并且不能到达满足约束条件的终结状态,则再次回溯到上一个节点。如果所有节点的所有选项都已经尝试过仍然不能到达满足约束条件的终结状态,则该问题无解。

力扣46、全排列(中等)

        给定一个不含重复数字的数组nums,输出其所有可能的全排列。你可以按任意顺序输出答案。

void Show(int* arr, int len)
{
	for (int i = 0; i < len; i++)
		printf("%d ", arr[i]);
	printf("\n");
}

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
//这个函数用于交换两个整数的值,通过交换两个整数指针所指向的值来实现。
//这意味着它可以直接在原始数组上操作,而不需要返回任何值或修改数组的大小。
//函数接收两个指向整数的指针p1和p2,然后通过一个临时变量tmp来交换这两个指针所指向的值。

void Permutations(int* arr, int k, int n)
{
	if (k == n - 1)
	{
		Show(arr, n);
	}
	else
	{
		for (int i = k; i < n; i++)
		{
			Swap(&arr[k], &arr[i]);
			Permutations(arr, k + 1, n);
			Swap(&arr[k], &arr[i]);//回溯,撤销之前的交换
		}
	}
}
//此函数是生成排列的核心。它接收一个整数数组arr、一个索引k表示当前正在处理的元素位置,以及数组的总长度n。
//基准情况:如果k等于n-1,即已经处理完数组中的所有元素,此时arr表示一个有效的排列,
//因此调用Show函数来显示这个排列。
//递归情况:如果k不等于n-1,则对于从k开始的每个元素,都尝试将其与后面的元素交换,然后递归地调用Permutations来处理剩下的元素(即k+1到n-1)。
//在递归调用之后,需要撤销之前的交换,以便尝试下一个可能的排列。这是通过再次调用Swap来实现的,以确保在回溯时数组状态被正确恢复。

int main()
{
	int arr[] = { 0,1 };
	Permutations(arr, 0, sizeof(arr) / sizeof(arr[0]));

	return 0;
}

C++实现如下:

class Solution {
public:
    vector<vector<string>> solveNQueens(int n) {
        vector<vector<string>>ans;
        vector<int>col(n),on_path(n),diag1(n*2-1),diag2(n*2-1);
        function<void(int)>dfs=[&](int r)
        {
            if(r==n)
            {
                vector<string>board(n);
                for(int i=0;i<n;i++)
                {
                    board[i]=string(col[i],'.')+'Q'+string(n-1-col[i],'.');
                }
                ans.emplace_back(board);
                return;
            }
            for(int c=0;c<n;c++)
            {
                int rc=r-c+n-1;
                if(!on_path[c] && !diag1[r+c] && !diag2[rc])
                {
                    col[r]=c;
                    on_path[c]=diag1[r+c]=diag2[rc]=true;
                    dfs(r+1);
                    on_path[c]=diag1[r+c]=diag2[rc]=false;//恢复现场
                }
            }
        };
        dfs(0);
        return ans;
    }
};

//1、数据结构定义:
//vector<vector<sting>>ans;用于存储所有有效的N皇后解决方案。
//vector<int>col(n),on_path(n),diag1(n*2-1),diag2(n*2-1);
//分别用于记录每一列、当前路径(即当前行)、主对角线和副对角线上是否有皇后。
//col数组用于记录每一行皇后的列位置。
//on_path数组用于标记当前列是否有皇后。
//diag1和diag2数组分别用于标记两个方向的对角线(主对角线和副对角线)上是否有皇后。
//这里使用n*2-1是因为对角线可能跨越的范围最大为从左上角到右下角(或相反),长度为n+n-1。
//2、DFS函数:
//定义一个名为dfs的lambda函数,它接受一个整数r作为参数,表示当前正在尝试放置皇后的行号。
//如果r等于n,说明已经成功地在每一行都放置了一个皇后,没有冲突,因此可以将当前的棋盘状态添加到答案中。
//否则,遍历当前行的每一列c,检查该位置是否可以放置皇后(即不在同一列、不在同一路径上、不在同一主对角线和副对角线上)。
//如果可以放置,就标记这个位置为已占用,并递归地调用dfs(r+1)来尝试放置下一行的皇后。
//递归返回后,需要恢复现场,即将之前标记为已占用的位置重新标记为未占用,以便尝试其他可能的放置方式。
//3、棋盘表示:
//在找到一个有效的N皇后布局时,会构建一个表示该布局的二维字符串向量board。每个字符串表示棋盘的一行,其中'Q'表示皇后,'.'表示空位。
//4、调用DFS:
//从第0行开始调用dfs(0),启动深度优先搜索过程。
//5、返回结果:
//最终,ans中包含了所有有效的N皇后布局,函数返回这个向量。

力扣51、N皇后(困难)

        按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

        n皇后问题研究的是如何将n个皇后放置在n*n的棋盘上,并且使皇后彼此之间不能相互攻击。

        给你一个整数n,输出所有不同的n皇后问题的解决方案。

//N皇后
#define N 8 //皇后数量(数组长度)
void Show(int* arr, int len)//输出
{
	for (int i = 0; i < len; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

//检测当前皇后是否和前面的皇后有冲突,true有冲突,false没有冲突
//cur:当前皇后
bool CheckPos(int* arr, int cur)
{
	for (int i = cur - 1; i >= 0; i--)//前面所有的皇后
	{
		if (arr[cur] == arr[i] || abs(arr[cur] - arr[i]) == cur - i)
			return true;
	}
	return false;
}

//arr:存放每个皇后的位置
//cur:当前处理第几个皇后,从0开始
void Queen(int* arr, int cur)
{
	for (int i = 0; i < N; i++)//当前皇后能存放的所有位置
	{
		arr[cur] = i;//当前皇后存放在i位置
		if (!CheckPos(arr, cur))//检测当前皇后和前面皇后是否冲突
		{
			if (cur == N - 1)//最后一个皇后都没有冲突,则可以
				Show(arr, N);
			else
				Queen(arr, cur + 1);//处理下一个皇后
		}
	}
}

int main()
{
	int arr[N];//保存N皇后的位置
	Queen(arr, 0);//从0下标开始

	return 0;
}
class Solution {
public:
    vector<vector<string>> solveNQueens(int n) {
        vector<vector<string>>ans;
        vector<int>col(n),on_path(n),diag1(n*2-1),diag2(n*2-1);
        function<void(int)>dfs=[&](int r)
        {
            if(r==n)
            {
                vector<string>board(n);
                for(int i=0;i<n;i++)
                {
                    board[i]=string(col[i],'.')+'Q'+string(n-1-col[i],'.');
                }
                ans.emplace_back(board);
                return;
            }
            for(int c=0;c<n;c++)
            {
                int rc=r-c+n-1;
                if(!on_path[c] && !diag1[r+c] && !diag2[rc])
                {
                    col[r]=c;
                    on_path[c]=diag1[r+c]=diag2[rc]=true;
                    dfs(r+1);
                    on_path[c]=diag1[r+c]=diag2[rc]=false;//恢复现场
                }
            }
        };
        dfs(0);
        return ans;
    }
};

//1、数据结构定义:
//vector<vector<sting>>ans;用于存储所有有效的N皇后解决方案。
//vector<int>col(n),on_path(n),diag1(n*2-1),diag2(n*2-1);
//分别用于记录每一列、当前路径(即当前行)、主对角线和副对角线上是否有皇后。
//col数组用于记录每一行皇后的列位置。
//on_path数组用于标记当前列是否有皇后。
//diag1和diag2数组分别用于标记两个方向的对角线(主对角线和副对角线)上是否有皇后。
//这里使用n*2-1是因为对角线可能跨越的范围最大为从左上角到右下角(或相反),长度为n+n-1。
//2、DFS函数:
//定义一个名为dfs的lambda函数,它接受一个整数r作为参数,表示当前正在尝试放置皇后的行号。
//如果r等于n,说明已经成功地在每一行都放置了一个皇后,没有冲突,因此可以将当前的棋盘状态添加到答案中。
//否则,遍历当前行的每一列c,检查该位置是否可以放置皇后(即不在同一列、不在同一路径上、不在同一主对角线和副对角线上)。
//如果可以放置,就标记这个位置为已占用,并递归地调用dfs(r+1)来尝试放置下一行的皇后。
//递归返回后,需要恢复现场,即将之前标记为已占用的位置重新标记为未占用,以便尝试其他可能的放置方式。
//3、棋盘表示:
//在找到一个有效的N皇后布局时,会构建一个表示该布局的二维字符串向量board。每个字符串表示棋盘的一行,其中'Q'表示皇后,'.'表示空位。
//4、调用DFS:
//从第0行开始调用dfs(0),启动深度优先搜索过程。
//5、返回结果:
//最终,ans中包含了所有有效的N皇后布局,函数返回这个向量。

力扣78、子集(中等)

        给你一个整数数组nums,数组中的元素互不相同。返回该数组所有可能的子集。

        不能包含重复的子集。你可能按任意顺序输出解集。

//子集
//arr:数组名,n:数组长度
void SubSet(int* arr, int n)
{
	int maxnum = 1 << n;
	for (int i = 0; i < maxnum; i++)//处理0到2^n  -1之间的数字
	{
		for (int j = 0; j < n; j++)//j表示二进制右数第几位,同时也表示arr中第j号数据
		{
			if ((i & (1 << j)) != 0)//表示当前位不为0,则需要打印相应的字母
			{
				printf("%d ", arr[j]);
			}
		}
		printf("\n");
	}
}

int main()
{
	int arr[] = { 1,2,3 };
	SubSet(arr, sizeof(arr) / sizeof(arr[0]));

	return 0;
}

C++实现如下:

class Solution {
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        vector<vector<int>> v;
        int n=nums.size();      //长度
        for(int i=0;i<(1<<n);i++)//从0~(1000(二进制)-1)
        {
            vector<int>tmp;
            for(int j=0;j<n;j++) //j表示二进制右数第几位,同时也表示arr中第j号数据
            {
                if((i & (1<<j))!=0)//表示当前位不为0,则输出当前数据
                    tmp.push_back(nums[j]);
            }
            v.push_back(tmp);
        }
        return v;
    }
};

//1、初始化:
//首先,创建一个vector<vector<int>>类型的变量v,用于存储所有可能的子集。同时,获取输入数组nums的长度n。
//2、遍历所有可能的子集:
//使用一个从0到(1<<n)的循环来遍历所有可能的子集。
//这里的(1<<n)实际上是2^n的位运算表示,因为n位二进制数可以表示从0到2^n-1的所有整数,
//每个整数都对应数组nums的一个特定子集(通过选择或不选择nums中的每个元素)。
//3、构建子集
//对于每个整数i(从0到2^n-1),通过检查其二进制表示的每一位来确定是否选择nums数组中的对应元素。
//这是通过位运算(i&(i<<j))来实现的,其中j表示当前正在检查的位(从右往左,即数组nums的索引)。
//如果(i&(i<<j))的结果不为0,则表示i的二进制表示中第j位是1,因此应该将nums[j]添加到当前子集tmp中。
//4、存储子集
//完成一个子集tmp的构建后,将其添加到结果集v中。
//5、返回结果:最后,返回包含所有可能子集的v。

        再介绍一个腾讯的面试题

        有一个集合由A~Z这26个字母组成,打印这个集合的所有子集,每个子集一行,写C代码实现,不能使用递归。

//str为A~Z的字母集合,n为需要处理的前n个字符集合,本题n为26,n是为了方便测试
void SubSet(int n)
{
	const char* str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
	int maxnum = 1 << n;   //2^n
	for (int i = 0; i < maxnum; i++)//处理0到2^n   -1之间的数字
	{
		for (int j = 0; j < n; j++)//j表示二进制右数第几位
		{
			if ((i & (1 << j)) != 0)//表示当前位不为0,则需要打印数组相应的字母
			{
				printf("%c ", str[j]);
			}
		}
		printf("\n");
	}
}

int main()
{
	SubSet(3);
	printf("----------------------\n");
	//SubSet(26);//数据太大不好统计测试结果
	return 0;
}

力扣79、单词搜索(中等)

同剑指offer12.矩阵中的路径

//矩阵中的路径
//matrix:字符矩阵,rows:矩阵的行数,cols:矩阵的列数
//row:当前处理行号,col:当前处理的列号
//str:字符串路径,pathLength:当前处理的字符串的第几个字符
//visited:字符是否被访问过标记数组
bool hasPathCore(char* matrix, int rows, int cols, int row, int col,
				const char* str, int* pathLength, bool* visited)
{
	if (str[*pathLength] == '\0')//字符串路径处理完成
		return true;
	bool haspa = false;//从当前开始是否有路径
	if (row >= 0 && row<rows && col >= 0 && col<cols &&
		matrix[row * cols + col] == str[*pathLength] && !visited[row * cols + col])
	{
		//矩阵当前字符和字符串路径当前字符相同
		++(*pathLength);
		visited[row * cols + col] = true;
		//判断下一个点,是否有路径
		haspa = hasPathCore(matrix, rows, cols, row, col - 1, str, pathLength, visited) ||//左
			hasPathCore(matrix, rows, cols, row - 1, col, str, pathLength, visited) ||//上
			hasPathCore(matrix, rows, cols, row, col + 1, str, pathLength, visited) || //右
			hasPathCore(matrix, rows, cols, row + 1, col, str, pathLength, visited);//下
		if (!haspa)//下一个点没有路径,回溯
		{
			--(*pathLength);
			visited[row * cols + col] = false;
		}
	}
	return haspa;
}
//矩阵中的路径
//判断给定的字符串在矩阵中是否有路径
//matrix:字符矩阵
//rows:字符矩阵的行
//cols:字符矩阵的列
//str:需要找的字符串路径
bool hasPath(char* matrix, int rows, int cols, const char* str)
{
	if (matrix == NULL || rows < 1 || cols < 1 || str == NULL)//参数非法
		return false;
	//true:表示当前这个格子已经被访问过,false还没有被访问
	bool* visited = (bool*)calloc(rows * cols, sizeof(bool));//calloc创建的内存默认为0

	int pathLength = 0;//str的下标
	for (int i = 0; i < rows; ++i)//遍历矩阵的所有开始点(每一个点都可能是开始点)
	{
		for(int j=0;j<cols;j++)
			if (hasPathCore(matrix, rows, cols, i, j, str, &pathLength, visited))//判断从当前结点
			{
				free(visited);
				return true;
			}
	}
	free(visited);
	return false;
}

int main()
{
	char matrix[3][4] = { {'a','b','t','g'},{'c','f','c','s'},{'j','d','e','h'} };
	const char* str = "bfce";
	if (hasPath((char*)matrix,3,4,str))
		printf("%s有路\n", str);
	else
		printf("%s没有路\n", str);
	return 0;
}

C++实现如下:

class Solution {
    bool visited[6][6]={false};//题目说明数据范围
public:
    bool hasPath(vector<vector<char>>& board,int i,int j,string word,int&cur)
    {
        if(cur==word.size())//全部比较完成
            return true;
        int m = board.size();//行
        int n = board[0].size();//列
        bool flg = false;//有没有路
        if(0<=i && i<m && 0<=j && j<n && board[i][j]==word[cur] && !visited[i][j])
        {
            visited[i][j]=true;
            ++cur;
            flg = hasPath(board,i,j-1,word,cur)//左
               || hasPath(board,i,j+1,word,cur)//右
               || hasPath(board,i-1,j,word,cur)//上
               || hasPath(board,i+1,j,word,cur);//下
            if(!flg)
            {
                --cur;
                visited[i][j]=false;
            }
        }
        return flg;
    }
    bool exist(vector<vector<char>>& board, string word) {
        int m = board.size();
        int n = board[0].size();
        int cur = 0;
        for(int i=0;i<m;i++)
        {
            for(int j=0;j<n;j++)
            {
                if(hasPath(board,i,j,word,cur))
                    return true;
            }
        }
        return false;
    }
};

//1、全局变量:
//visited数组用于记录棋盘上的ifu是否已经被访问过,避免重复访问和无限循环。
//2、hasPath方法:
//这是一个递归函数,用于在棋盘上搜索给定单词的剩余部分。它接收棋盘board、当前位置(i,j)、
//要搜索的单词word和当前已经匹配的单词长度cur作为参数。
//如果cur等于word.size(),说明单词已经完整匹配,返回true。
//检查当前位置(i,j)是否在棋盘范围内、是否未被访问过、且当前位置的字符与单词的下一个字符匹配。
//如果满足条件,标记当前位置为已访问,并尝试四个方向(左、右、上、下)递归搜索。
//如果在任何方向找到了匹配路径,flg被设置为true并返回。
//如果在所有方向都没有找到匹配路径,则回溯,即将cur减1并将当前位置标记为未访问。
//3、exist方法:
//这是公开的接口函数,用于启动搜索过程。它遍历棋盘上的每个位置,从每个位置开始尝试搜索给定的单词。
//如果hasPath方法返回true,则立即返回true,表示找了匹配路径。
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值