Alpha-Beta剪枝的原理的深入理解(無圖預警)

Satar07發表於2023-12-26

轉載請註明 原文連結 :https://www.cnblogs.com/Multya/p/17929261.html

考慮一個樹:

一棵樹上只有葉子節點有值,有確定的根節點的位置

根據層數來劃分葉子節點和根節點之間的連結節點

偶數層上的值取子節點的最大值,奇數取最小

因為葉子節點上的值確定,在有這麼個規則之後整棵樹上所有節點就定下來了吧

現在我遮住全部葉子節點,讓你透過開啟儘量少次數葉子節點,確定根節點的值

我們透過alpha-beta 剪枝來實現

確定的事情:

  • 一個節點上的值必定是長在它身上的所有葉子的值中的一個
  • max{ a, min{b,x} } 如果b比a小,無論x取什麼,結果都是a
  • min{ a, max{b,x} } 如果b比a大,無論x取什麼,結果都是a

為什麼? 我們放慢這個思考過程看看背後的邏輯

我們用一個區間來表示這個算式最後的結果的範圍,下界是a,上界是b

我們知道計算最大最小的過程,其實就是一個單邊的區間不斷根據新的值重新整理的過程:

假設計算max{4,5,1}

第一個數是4暫定是表示式的值

然後4確定下界是4的區間,這個區間希望得到一個在這個區間內的數重新整理區間下界和更新表示式的值。5在這個區間內,所以它能重新整理表示式和更新區間下界

1不在這個區間內,所以它不能更新表示式和區間。所以表示式是最後更新狀態下的5,計算正確

重新整理區間的操作也可以用求交集來實現,這樣的話就省了判斷的那一步,然後結果也可以用最後的下界來確定

所以可以變成這樣:

求區間(4,+ \(\infty\) )∩(5,+ \(\infty\) )∩(1,+ \(\infty\) )的下界

這樣我們就實現了用區間來求最大(最小)的功能

再看max{ 4, min{3,1} }

第一個數是4暫定是表示式的值

然後4確定下界是4的區間,這個區間希望得到一個在這個區間內的數重新整理區間下界和更新表示式的值

這個區間希望表示式min{3,1}得到一個大於4的數重新整理區間下界和更新表示式的值(這個過程區間原封不動傳遞下去)

先計算表示式min{3,1} 第一個數是3,然後5確定表示式在小於3的區間內,希望得到一個小於5的值重新整理表示式

但是這個區間內所有的數都不在上個表示式期望的大於4的數區間內(區間不重合),也就是這個表示式所有可能的值都不能重新整理上個表示式的值,所以跳過計算這個子表示式

由於上個表示式所有數遍歷完了,最後更新的數是4,所以表示式值為4

我們再來看完全用區間來實現的方法:(【】內計算得到一個數)所有都是閉區間哈,意會就行

求(4,+ \(\infty\) )∩ ( min{3,1} ,+ \(\infty\) )的下界

即求(4,+ \(\infty\) )∩ (【(- \(\infty\) ,3)∩ (- \(\infty\) ,1)的上界】,+ \(\infty\) )的下界

我們定義空區間的上界是正無窮,下界是負無窮,這樣做的理由是使空區間對結果不產生任何貢獻(因為都是取交集)

然後是關鍵的一步:根據上面的啟發,我們把前面得到的區間套一層殼,也就是:

等價為求(4,+ \(\infty\) )∩ (【(4,+ \(\infty\) )∩【(- \(\infty\) ,3)∩ (- \(\infty\) ,1)的上界】的上界】,+ \(\infty\) )的下界

這個結果不變。因為【(4,+ \(\infty\) )∩【(- \(\infty\) ,3)∩ (- \(\infty\) ,1)的上界】的上界】的結果不是空集的話對上界沒有影響,是空集的話沒有貢獻。

可以等價為(4,+ \(\infty\) )∩ (【(4,+ \(\infty\) )∩(- \(\infty\) ,3)∩ (- \(\infty\) ,1)的上界】,+ \(\infty\) )的下界,因為取交集,先後沒有影響。

此時可以先透過判斷(4,+ \(\infty\) )∩(- \(\infty\) ,3)是空集來提前結束求值,得到最後區間(4,+ \(\infty\)

像上面這樣,如果把傳遞給子表示式期望的區間和子表示式結果的區間看成一回事的話,那就是alpha-beta剪枝的邏輯。回到最開始的那個樹。這個區間能被固定在每一個節點身上,表示這個節點的狀態如果要重新整理這個節點的值,要求新輸入的值的區間範圍。如果這個節點從未被重新整理,那麼這個節點的值就不會產生任何貢獻來重新整理上一個表示式的值。如果這個區間是一個空區間,那麼所有的值都不能重新整理這個節點的值,那麼就沒有必要繼續給這個節點輸入值了。

觀察區間動向的話會發現有關區間的操作有以下幾種:

  • 把值改寫為區間
  • 區間取交集
  • 取區間一端的數傳遞回去
  • 把一個區間傳遞給子表示式內提前取交集

再看max{ 4, min{3,1} },這次我們加上樹的形狀和葉子節點遮擋的特性

自行畫圖:有4 3 1三個節點,一個根節點,兩個連結節點,三個葉子節點

開始。開啟葉子4,更新所連線的父節點

這裡將每個節點區間初始化為全體實數,因為是MAX層,重新整理這個節點的區間為(4,+ \(\infty\)

傳遞(4,+ \(\infty\) )給連結3和1的連結節點(此時我們還不知道3和1)

連結節點開啟葉子3,因為是MAX層,產生區間(- \(\infty\) ,3)

原來已經有集合了,取交集為空集,提前結束運算,不做任何貢獻,不傳遞值回去

這樣就完成了任務,只開啟了4和3就知道了根節點的值是4

如果把前面的值用負號在min層取反,那麼所有的層的操作邏輯都變成一樣的了

alpha-beta剪枝的演演算法的程式碼:

//意義:目前棋盤的最值評分 分數的正負取決於一開始的isBlackNow
int abSearch(int floor, int alpha, int beta, bool isBlackNow, Coord &searchResult) {
    int tmpScore, moveCount = 0;
    Coord tempSearchResult{};
    //最佳化1
    std::vector<ScoreCoord> possibleMove = generatePossibleMove(isBlackNow);
    for (auto &now: possibleMove) {
		//最佳化2
        moveCount++;
        if (moveCount > 8) break; //只搜尋前8個可能的落子點
        int x = now.coord.x, y = now.coord.y;
        m_map[x][y] = isBlackNow ? BLACK_CHESS : WHITE_CHESS;
        //最佳化3
        if (someoneWin({x, y})) {//如果有人贏了 必定是下這個子的人贏了
            searchResult = {x, y};
            tmpScore = evaluateAll(isBlackNow);//返回這個局面最高的得分,也就是贏局的分數
            m_map[x][y] = NO_CHESS;
            return tmpScore;
        }
        //單層搜尋
        if (floor == 1) {//如果只看這一步子 那就是這一步子所有可能的得分中的最大值
            tmpScore = evaluateAll(isBlackNow);
            m_map[x][y] = NO_CHESS;
            if (tmpScore > alpha) {
                alpha = tmpScore;
                searchResult = {x, y};
            }
            continue;
        }
        tmpScore = -abSearch(floor - 1, -beta, -alpha, !isBlackNow, tempSearchResult);//不然得分就是我下了之後的對方的所能得到的最高分取負
        m_map[x][y] = NO_CHESS;
        if (tmpScore >= beta) {
            return beta;
        }
        if (tmpScore > alpha) {//取對方盡所有努力後得到最大值中的最小的一個 取負值後變成最大的一個
            alpha = tmpScore;
            searchResult = {x, y};
        }
    }
    return alpha;
}

抽象出來的虛擬碼:

//意義:目前棋盤的最值評分 分數的正負取決於一開始的isBlackNow
int abSearch(int floor, int alpha, int beta, bool isBlackNow, Coord &searchResult) {
    possibleMove = generatePossibleMove();
    for (auto &now: possibleMove) {
	    downOneStep();
        if (someoneWin()) {//如果有人贏了 必定是下這個子的人贏了
            saveSearchResult();
            restoreLastStep();
            return evaluateScore();
        }
        //單層搜尋
        if (floor == 1) {//如果只看這一步子 那就是這一步子所有可能的得分中的最大值
            tmpScore = evaluateScore();
            restoreLastStep();
            if (tmpScore > alpha) {
                alpha = tmpScore;
                saveSearchResult();
            }
            continue;
        }
        tmpScore = -abSearch(floor - 1, -beta, -alpha, !isBlackNow, tempSearchResult);//不然得分就是我下了之後的對方的所能得到的最高分取負
        restoreLastStep();
        if (tmpScore >= beta) {
            return beta;
        }
        if (tmpScore > alpha) {//取對方盡所有努力後得到最大值中的最小的一個 取負值後變成最大的一個
            alpha = tmpScore;
            saveSearchResult();
        }
    }
    return alpha;
}

相關文章