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,直到处理到最后一位。为了防止我们每此遍历时都要新建一个子数组储存位置 之前已经交换好的数字,我们可以利用回溯法,只对原数组进行修改,在递归完成后再修改回来。

       可以将任务划分为多层的子任务,通过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: 


思路:       

       类似于在矩阵中寻找字符串,本题也是通过修改状态矩阵来进行回溯。不同的是,我们需要对每一行、列、左斜、右斜建立访问数组,来记录它们是否存在皇后。

       本题有一个隐藏的条件,即满足条件的结果中每一行或列有且仅有一个皇后。这是因为我们一共只有 行和 列,所以如果我们通过对每一行遍历来插入皇后。

代码: 

#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。对于四个子任务,使用递归调用,继续划分子任务,直到不能继续划分为止,返回上一级子任务并回溯节点状态。

       

posted @ 2021-09-02 16:09  ChangYuanD  阅读(2456)  评论(1编辑  收藏  举报