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。對於四個子任務,使用遞歸調用,繼續划分子任務,直到不能繼續划分為止,返回上一級子任務並回溯節點狀態。

       


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM