for循环+递归调用
看递归的时候懂了,看for循环的时候也懂了,看到for循环和递归一起就蒙了,看了一个下午才看懂,通过LeetCode里面的几道题目详细记录一下整体思路。
1、题目描述
给定一个无重复数字的整数数组,求其所有的排列方式。
输入输出样例
输入是一个一维整数数组,输出是一个二维数组,表示输入数组的所有排列方式
Input: [1, 2, 3]
Output: [1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], [3, 1, 2]
思路:
怎样输出所有的排列方式呢?对于每一个当前位置 i,我们可以将其于之后的任意位置交换,然后继续处理位置 i+1,直到处理到最后一位。
为了防止我们每此遍历时都要新建一个子数组储然后继续处理位置 i+1,直到处理到最后一位。为了防止我们每此遍历时都要新建一个子数组储存位置 i 之前已经交换好的数字,我们可以利用回溯法,只对原数组进行修改,在递归完成后再修改回来。
可以将任务划分为多层的子任务,通过for循环实现,所有子任务的处理方式都是通过交换来两个位置的元素,可以通过递归调用实现。
代码:
#include<vector> #include<iostream> #include<algorithm> using namespace std; //辅助函数 void backtracking(vector<int>& nums, int level, vector<vector<int>>& ans) { if (level == nums.size() - 1) { //结束递归调用的条件:当任务不能继续细分时 ans.push_back(nums); return; } for (int i = level; i < nums.size(); ++i) { //子任务(level, i) swap(nums[i], nums[level]); backtracking(nums, level + 1, ans);//在子任务的基础上继续划分子任务(level+1, i),i=level, level+1,...,nums.size()-1 swap(nums[i], nums[level]);//回改节点状态 } } //主函数 vector<vector<int>> permute(vector<int>& nums) { vector<vector<int>> ans; backtracking(nums, 0, ans); return ans; } int main() { vector<int> nums = { 1,2,3 }; vector<vector<int>> result = permute(nums); //输出结果 for (int i = 0; i < result.size(); ++i) { for (int j = 0; j < result[0].size(); ++j) { cout << result[i][j]; } cout << endl; } }
详细的运行情况:
for循环+递归调用将任务先划分为了(level=0, i=0)、(level=0, i=1)、(level=0, i=2)三个子任务,然后对
- (level=0, i=0)继续划分为(level=1, i=1)、(level=1, i=2)两个子任务,
- (level=0, i=1)继续划分为(level=1, i=1)、(level=1, i=2)两个子任务,
- (level=0, i=2)继续划分为(level=1, i=1)、(level=1, i=2)两个子任务。
一直到任务不能再继续划分,满足if条件(level=2),输出该子任务的排序结果,然后返回上一层子任务。(level=1, i=1)继续划分子任务(level=2, i=2),满足if条件,输出排序结果:[1,2,3]。
2、题目描述
给定一个整数 n 和一个整数 k,求在 1 到 n 中选取 k 个数字的所有组合方法。
输入输出样例
输入是两个正整数 n 和 k,输出是一个二维数组,表示所有组合方式
Input: n = 4, k = 2
Output: [[2,4], [3,4], [2,3], [1,2], [1,3], [1,4]]
思路:
类似于排列问题,我们也可以进行回溯。排列回溯的是交换的位置,而组合回溯的是否把当前的数字加入结果中。
代码:
#include<vector> #include<iostream> #include<algorithm> using namespace std; //辅助函数 void backtracking2(vector<vector<int>>& ans, vector<int>& comb, int count, int pos, int n, int k) { if (count == k) { //结束条件 ans.push_back(comb); return; } for (int i = pos; i <= n; ++i) { //for循环设定的子任务count=0赋值为i comb[count] = i; ++count; //递归调用对每个子任务执行同样的操作 backtracking2(ans, comb, count, i + 1, n, k); /* 对于backtracking又有for循环设定的子任务:count=1赋值为i+1 */ --count;//将节点回溯(递归调用结束后自动将count-1,然后继续下一个子任务count=0赋值为i+1) } } //主函数 vector<vector<int>> combine(int n, int k) { vector<vector<int>> ans; vector<int> comb(k, 0); int count = 0; backtracking2(ans, comb, count, 1, n, k); return ans; } int main() { int n = 4, k = 2; vector<vector<int>> result = combine(n, k); for (int i = 0; i < result.size(); ++i) { for (int j = 0; j < result[0].size(); ++j) { cout << result[i][j]; } cout << endl; } }
详细的运行情况:
首先将任务通过for循环划分为四个子任务(count=0, i=1), (count=0, i=2), (count=0, i=3), (count=0, i=4),对于每个子任务,比如(count=0, i=1),使用for循环继续划分为子任务(count=1, i=2),(count=1, i=3),(count=1, i=4)。当任务不能继续划分时(count=2)返回comb,并返回上一个子任务,coun同时减一(回溯)。
可以看出组合问题使用回溯法回溯的是位置,而排序问题回溯的是交换位置。
3、题目描述
给定一个大小为 n 的正方形国际象棋棋盘,求有多少种方式可以放置 n 个皇后并使得她们互不攻击,即每一行、列、左斜、右斜最多只有一个皇后。下图为8皇后的一种解法,为了说明方便,在此只讨论4皇后问题,可以类推到n=8
输入输出样例
输入是一个整数 n,输出是一个二维字符串数组,表示所有的棋盘表示方法。
Input: n = 4
Output:
思路:
类似于在矩阵中寻找字符串,本题也是通过修改状态矩阵来进行回溯。不同的是,我们需要对每一行、列、左斜、右斜建立访问数组,来记录它们是否存在皇后。
本题有一个隐藏的条件,即满足条件的结果中每一行或列有且仅有一个皇后。这是因为我们一共只有 n 行和 n 列,所以如果我们通过对每一行遍历来插入皇后。
代码:
#include<vector> #include<iostream> #include<string> #include<algorithm> using namespace std; //辅助函数 void backtracking4(vector<vector<string>>& ans, vector<string>& board, vector<bool>& column, vector<bool>& ldiag, vector<bool>& rdiag, int row, int n) { if (row == n) { ans.push_back(board); return; } for (int i = 0; i < n; ++i) { //子任务(count=0, i=0) //board[0][0] = 'Q' if (column[i] || ldiag[n - row + i - 1] || rdiag[row + i]) { continue; } board[row][i] = 'Q'; column[i] = ldiag[n - row + i - 1] = rdiag[row + i] = true; //子任务:(count=0, i=0) backtracking4(ans, board, column, ldiag, rdiag, row + 1, n); //回溯:子任务结束后,将节点回调(尝试下一种情况:board[0][1] = 'Q') board[row][i] = '.'; column[i] = ldiag[n - row + i - 1] = rdiag[row + i] = false; } } //主函数 vector<vector<string>> solveNQueens(int n) { vector<vector<string>> ans; if (n == 0) { return ans; } vector<string> board(n, string(n, '.')); //ldiag和rdiag用于初始化左、右斜线向量,n*n矩阵有2*n-1条左、右斜线,且每条斜线的差(左)、和(右)相同 vector<bool> column(n, false), ldiag(2 * n - 1, false), rdiag(2 * n - 1, false); backtracking4(ans, board, column, ldiag, rdiag, 0, n); return ans; } int main() { vector<vector<string>> result = solveNQueens(4); for (int i = 0; i < result.size(); ++i) { for (int j = 0; j < result[0].size(); ++j) { cout << result[i][j]; cout << endl; } cout << endl; } }
运行步骤:
backtracking4(ans, board, column, ldiag, rdiag, 0, n)将任务划分为(count=0, i=0)、(count=0, i=1)、(count=0, i=2)、(count=0, i=3)四个子任务,分别对应board[0][0]='Q'、board[0][1]='Q'、board[0][2]='Q'、board[0][3]='Q',然后将位置对应的所在的行、左斜线、右斜线修改为true,代表已存在Queen,其他的位置如果在这些行或者斜线上时,不能放置Queen。对于四个子任务,使用递归调用,继续划分子任务,直到不能继续划分为止,返回上一级子任务并回溯节点状态。