1. 程式人生 > >賞月齋源碼共享計劃 第三期

賞月齋源碼共享計劃 第三期

遞歸 ins 試圖 efficient true return 們的 param 不同

/**
 * 需求描述:
 * 有一塊N*M像素格的畫板,初始狀態空白,用‘X’表示
 * 繪畫規則為:每次選擇一條斜線 
 * 如果斜線斜率為1,則選擇一段格子,都塗為藍色,用‘B’表示
 * 如果斜線斜率為-1,則選擇斜線中的一段格子,塗成黃色,用‘Y’表示
 * 一個格子既塗成藍色又塗成黃色,則變成綠色,用‘G’表示
 * 已知一幅作品,求最少需要多少次操作完成這幅畫
 * *****************************************
 * 輸入:正整數N,M
 * 畫作:N行長度為M的字符串
 * 
 *
 * */
# include <stdio.h>
char str[60][60];
int m, n;

void dfs_Y(int x, int y){ //斜率-1,塗黃色Y
    if (x >= 0 && x < n && y >=0 && y < m && (str[x][y] == ‘Y‘ || str[x][y] == ‘G‘)){
        if(str[x][y] == ‘G‘){
            str[x][y] = ‘B‘;
        }
        else
        {
            str[x][y] = ‘X‘;
        }
        dfs_Y(x - 1, y - 1);
        dfs_Y(x + 1, y + 1);
    }

    return;
}

void dfs_B(int x, int y){  //斜率1,塗藍色B
    if(x >= 0 && x < n && 0 <= y && y <m && (str[x][y] == ‘B‘ || str[x][y] == ‘G‘)){
        if (str[x][y] == ‘G‘){
            str[x][y] = ‘Y‘;
        }
        else
        {
            str[x][y] = ‘X‘;
        }
        dfs_B(x + 1, y - 1);
        dfs_B(x - 1, y + 1);
    }

    return;
}

int main(void){
    int cnt;
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++)
    {
        scanf("%s", str[i]);
    }
    cnt = 0;
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < m; j++)
        {
            if (str[i][j] == ‘Y‘)
            {
                dfs_Y(i, j);
                cnt++;
            }
            else if (str[i][j] == ‘B‘)
            {
                dfs_B(i, j);
                cnt++;
            }
            else if (str[i][j] == ‘G‘)
            {
                dfs_Y(i, j);
                str[i][j] = ‘B‘;
                dfs_B(i, j);
                cnt += 2;
            }
        }
        }
    printf("%d\n", cnt);  //無論初始str是什麽圖案,執行完計算過程之後str全變成‘X‘
    
    //測試用:
    // for (int i = 0; i < 4; i++)
    // {
    //     for (int j = 0; j < 4; j++)
    //     {
    //         printf("%c", str[i][j]);
    //     }
    //     printf("\n");
    // }

    return 0;
}

  


回溯是啥

用爬山來比喻回溯,好比從山腳下找一條爬上山頂的路,起初有好幾條道可走,當選擇一條道走到某處時,又有幾條岔道可供選擇,只能選擇其中一條道往前走,若能這樣子順利爬上山頂則罷了,否則走到一條絕路上時,只好返回到最近的一個路口,重新選擇另一條沒走過的道往前走。如果該路口的所有路都走不通,只得從該路口繼續回返。照此規則走下去,要麽找到一條到達山頂的路,要麽最終試過所有可能的道,無法到達山頂。
回溯是一種窮舉,但與brute force有一些區別,回溯帶了兩點腦子的,並不多,brute force一點也沒帶。
第一點腦子是回溯知道回頭;相反如果是brute force,發現走不通立刻跳下山摔死,換第二條命從頭換一條路走。

第二點腦子是回溯知道剪枝;如果有一條岔路上放了一坨屎,那這條路我們不走,就可以少走很多不必要走的路。

還有一些愛混淆的概念:遞歸,回溯,DFS。
回溯是一種找路方法,搜索的時候走不通就回頭換路接著走,直到走通了或者發現此山根本不通。
DFS是一種開路策略,就是一條道先走到頭,再往回走一步換一條路走到頭,這也是回溯用到的策略。在樹和圖上回溯時人們叫它DFS。
遞歸是一種行為,回溯和遞歸如出一轍,都是一言不合就回到來時的路,所以一般回溯用遞歸實現;當然也可以不用,用棧。
以下以回溯統稱,因為這個詞聽上去很文雅。

識別回溯

判斷回溯很簡單,拿到一個問題,你感覺如果不窮舉一下就沒法知道答案,那就可以開始回溯了。

一般回溯的問題有三種:

  1. Find a path to success 有沒有解

  2. Find all paths to success 求所有解

    • 求所有解的個數

    • 求所有解的具體信息

  3. Find the best path to success 求最優解

理解回溯:給一堆選擇, 必須從裏面選一個. 選完之後我又有了新的一組選擇. This procedure is repeated over and over until you reach a final state. If you made a good sequence of choices, your final state is a goal state; if you didn‘t, it isn‘t.

回溯可以抽象為一棵樹,我們的目標可以是找這個樹有沒有good leaf,也可以是問有多少個good leaf,也可以是找這些good leaf都在哪,也可以問哪個good leaf最好,分別對應上面所說回溯的問題分類。
good leaf都在leaf上。good leaf是我們的goal state,leaf node是final state,是解空間的邊界。

對於第一類問題(問有沒有解),基本都是長著個樣子的,理解了它,其他類別迎刃而解:

boolean solve(Node n) {
    if n is a leaf node {
        if the leaf is a goal node, return true
        else return false
    } else {
        for each child c of n {
            if solve(c) succeeds, return true
        }
        return false
    }
}

請讀以下這段話以加深理解:
Notice that the algorithm is expressed as a boolean function. This is essential to understanding the algorithm. If solve(n) is true, that means node n is part of a solution--that is, node n is one of the nodes on a path from the root to some goal node. We say that n is solvable. If solve(n) is false, then there is no path that includes n to any goal node.

還不懂的話請通讀全文吧:Backtracking - David Matuszek

關於回溯的三種問題,模板略有不同,
第一種,返回值是true/false。
第二種,求個數,設全局counter,返回值是void;求所有解信息,設result,返回值void。
第三種,設個全局變量best,返回值是void。

第一種:

boolean solve(Node n) {
    if n is a leaf node {
        if the leaf is a goal node, return true
        else return false
    } else {
        for each child c of n {
            if solve(c) succeeds, return true
        }
        return false
    }
}

第二種:

void solve(Node n) {
    if n is a leaf node {
        if the leaf is a goal node, count++, return;
        else return
    } else {
        for each child c of n {
            solve(c)
        }
    }
}

第三種:

void solve(Node n) {
    if n is a leaf node {
        if the leaf is a goal node, update best result, return;
        else return
    } else {
        for each child c of n {
            solve(c)
        }
    }
}

題目

八皇後 N-Queens

問題

1.給個n,問有沒有解;
2.給個n,有幾種解;(Leetcode N-Queens II)
3.給個n,給出所有解;(Leetcode N-Queens I)

解答

1.有沒有解

怎麽做:一行一行的放queen,每行嘗試n個可能,有一個可達,返回true;都不可達,返回false.

邊界條件leaf:放完第n行 或者 該放第n+1行(出界,返回)

目標條件goal:n行放滿且isValid,即目標一定在leaf上

helper函數:
boolean solve(int i, int[][] matrix)
在進來的一瞬間,滿足property:第i行還沒有被放置,前i-1行放置完畢且valid
solve要在給定的matrix上試圖給第i行每個位置放queen。

public static boolean solve1(int i, List<Integer> matrix, int n) {
    if (i == n) {
        if (isValid(matrix))
            return true;
        return false;
    } else {
        for (int j = 0; j < n; j++) {
            matrix.add(j);
            if (isValid(matrix)) {    //剪枝
                if (solve1(i + 1, matrix, n)) 
                    return true;
            }
            matrix.remove(matrix.size() - 1);
        }
        return false;
    }
}
2.求解的個數

怎麽做:一行一行的放queen,每行嘗試n個可能。這回因為要找所有,返回值就沒有了意義,用void即可。在搜索時,如果有一個可達,仍要繼續嘗試;每個子選項都試完了,返回.

邊界條件leaf:放完第n行 或者 該放第n+1行(出界,返回)

目標條件goal:n行放滿且isValid,即目標一定在leaf上

helper函數:
void solve(int i, int[][] matrix)
在進來的一瞬間,滿足property:第i行還沒有被放置,前i-1行放置完畢且valid
solve要在給定的matrix上試圖給第i行每個位置放queen。
這裏為了記錄解的個數,設置一個全局變量(static)int是比較efficient的做法。

public static void solve2(int i, List<Integer> matrix, int n) {
    if (i == n) {
        if (isValid(matrix))
            count++;
        return;
    } else {
        for (int j = 0; j < n; j++) {
            matrix.add(j);
            if (isValid(matrix)) {    //剪枝
                solve2(i + 1, matrix, n); 
            }
            matrix.remove(matrix.size() - 1);
        }
    }
}
3.求所有解的具體信息

怎麽做:一行一行的放queen,每行嘗試n個可能。返回值同樣用void即可。在搜索時,如果有一個可達,仍要繼續嘗試;每個子選項都試完了,返回.

邊界條件leaf:放完第n行 或者 該放第n+1行(出界,返回)

目標條件goal:n行放滿且isValid,即目標一定在leaf上

helper函數:
void solve(int i, int[][] matrix)
在進來的一瞬間,滿足property:第i行還沒有被放置,前i-1行放置完畢且valid
solve要在給定的matrix上試圖給第i行每個位置放queen。
這裏為了記錄解的具體情況,設置一個全局變量(static)集合是比較efficient的做法。
當然也可以把結果集合作為參數傳來傳去。

public static void solve3(int i, List<Integer> matrix, int n) {
    if (i == n) {
        if (isValid(matrix))
            result.add(new ArrayList<Integer>(matrix));
        return;
    } else {
        for (int j = 0; j < n; j++) {
            matrix.add(j);
            if (isValid(matrix)) {    //剪枝
                solve3(i + 1, matrix, n); 
            }
            matrix.remove(matrix.size() - 1);
        }
    }
}

優化

上面的例子用了省空間的方法。
由於每行只能放一個,一共n行的話,用一個大小為n的數組,數組的第i個元素表示第i行放在了第幾列上。

Utility(給一個list判斷他的最後一行是否和前面沖突):

public static boolean isValid(List<Integer> list){
    int row = list.size() - 1;
    int col = list.get(row);
    for (int i = 0; i <= row - 1; i++) {
        int row1 = i;
        int col1 = list.get(i);
        if (col == col1)
            return false;
        if (row1 - row == col1 - col)
            return false;
        if (row1 - row == col - col1)
            return false;
    }
    return true;
    
}


遞歸:就是出現這種情況的代碼: (或者說是用到了棧)

解答樹角度:在dfs遍歷一棵解答樹

優點:結構簡潔

缺點:效率低,可能棧溢出

遞歸的一般結構:

  1. void f()
  2. {
  3. if(符合邊界條件)
  4. {
  5. ///////
  6. return;
  7. }
  8. //某種形式的調用
  9. f();
  10. }



回溯:遞歸的一種,或者說是通過遞歸這種代碼結構來實現回溯這個目的。回溯法可以被認為是一個有過剪枝的DFS過程。

解答樹角度:帶回溯的dfs遍歷一棵解答樹

回溯的一般結構:

  1. void dfs(int 當前狀態)
  2. {
  3. if(當前狀態為邊界狀態)
  4. {
  5. 記錄或輸出
  6. return;
  7. }
  8. for(i=0;i<n;i++) //橫向遍歷解答樹所有子節點
  9. {
  10. //擴展出一個子狀態。
  11. 修改了全局變量
  12. if(子狀態滿足約束條件)
  13. {
  14. dfs(子狀態)
  15. }
  16. 恢復全局變量//回溯部分
  17. }
  18. }



BFS和DFS是相似。

BFS(顯式用隊列)

DFS(隱式用棧)(即遞歸)

當然,對於DFS,用遞歸可能會造成棧溢出,所以也可以更改為顯示棧。

BFS:典型例題:P101 對於二叉樹的層次遍歷,P108對於圖的走迷宮最短路徑

  1. 將(起始)首節點加入隊列: q.push(head);
  2. 標記首節點已經被訪問: isvisited[head]=true;
  3. 以下自動反應: while(!q.empty())
  4. {
  5. int temp=q.front();
  6. q.pop();
  7. 訪問temp,並標記temp已被訪問過,將temp的子相關節點加入隊列
  8. q.push(temp相關節點);
  9. }


DFS:典型例題:P107黑白圖像

格式:將所有節點遍歷一遍,在遍歷每個節點是,DFS的遍歷該節點相關的所有節點

  1. void dfs(int x, int y)
  2. {
  3. if(!mat[x][y] || vis[x][y]) return; // 曾經訪問過這個格子,或者當前格子是白色
  4. vis[x][y] = 1; // 標記(x,y)已訪問過
  5. dfs(x-1,y-1); dfs(x-1,y); dfs(x-1,y+1);
  6. dfs(x-1,y); dfs(x,y+1);
  7. dfs(x+1,y-1); dfs(x+1,y); dfs(x+1,y+1); // 遞歸訪問周圍的八個格子
  8. }
  9. 主循環:
  10. for(int i = 1; i <= n; i++)
  11. for(int j = 1; j <= n; j++)
  12. if(!vis[i][j] && mat[i][j])
  13. {
  14. count++;
  15. dfs(i,j);
  16. } // 找到沒有訪問過的黑格


Ref:

http://www.cnblogs.com/HectorInsanE/archive/2010/11/09/1872656.html

賞月齋源碼共享計劃 第三期