初學OptaPlanner-03- 調包LocalSearch求解N皇后問題


目前可以公開的一些情報

什么是運籌排班算法?

假設一個工廠場景,該工廠下有100個工人需要排班,全排列會有100!約等於 \(10^{158}\)種結果,此類問題也屬於NP問題;粗略計算一個地球年不到\(10^8\)秒,假設每秒可以運算\(10^{10}\)種情況,也需要\(10^{140}\)個地球年來跑完全部的結果集,進而得到全局最優解。
除此之外,考慮三班倒,有五條生產線,另外需要考慮約束:連上限制、休息限制、工種限制、生產計划限制、熟練度限制、請假換班、加班、公平性考慮等約束考慮;假設為每個個體受限於10種約束,100個人,會有1000個約束考慮,這時,再來人工來手動排班難度極大。

運籌學是一門通過數學統籌方法來嘗試解決此類NP問題的學科,可以通過一些數學手段來縮減求解問題的規模,減少搜索范圍;其主要分支有:線性規划、非線性規划、整數規划、幾何規划、大型規划、動態規划、圖論、網絡理論、博弈論、決策論、排隊論、存貯論、搜索論等。。

OptaPlanner的初步作用?

通過啟發式算法,諸如暴力搜索、元啟發式算法、進化算法、蟻群算法、粒子群算法、分支界定算法等搜索算法來找到一個局部近似解;使用懲罰項來構建約束,在規定的可以接受的時間范圍,通過盡量降低懲罰分數,來得到一個較為最優的解。

原文地址

https://docs.optaplanner.org/7.45.0.Final/optaplanner-docs/html_single/index.html#nQueens

01. N皇后問題是啥?

約束項:

  • 給出一個N * N的棋盤,問最多可以擺放幾個國際象棋中的皇后,他們互不攻擊。
  • Queue可以攻擊任意橫向、縱向、兩個對角線方向上的棋子。

開始照着葫蘆畫瓢~

葫蘆見上期文章

02.初始化子狀態類、輸入輸出域的類、約束打分規則類、測試類

構建最小的子狀態類 Queen

@Data
@PlanningEntity
@NoArgsConstructor
public class Queen {
    /**
     * 固定字段
     */
    private Long id;

    /**
     * 列坐標
     */
    @PlanningVariable(valueRangeProviderRefs = "colIdxRange")
    private Integer colIdx;

    /**
     * 行坐標
     */
    @PlanningVariable(valueRangeProviderRefs = "rowIdxRange")
    private Integer rowIdx;

    public Queen(int i) {
        id = (long) i;
    }

    @Override
    public String toString() {
        return String.format("Queen[id=%d: %d,%d]", id, colIdx, rowIdx);
    }

    public Integer getLeftDiagonalConflictCheck() {
        return colIdx + rowIdx;
    }

    public Integer getRightDiagonalConflictCheck() {
        return colIdx - rowIdx;
    }
}

構建輸入輸出域

@Data
@NoArgsConstructor
@AllArgsConstructor
@PlanningSolution
public class ChessBoard {

    @ValueRangeProvider(id = "colIdxRange")   // 對應 @PlanningVariable下的id
    @ProblemFactCollectionProperty         // 輸入 不變
    private List<Integer> colIdxInitList;

    @ValueRangeProvider(id = "rowIdxRange")   // 對應 @PlanningVariable下的id
    @ProblemFactCollectionProperty         // 輸入 不變
    private List<Integer> rowIdxInitList;

    /**
     * 輸入時: 無
     * <p>
     * 輸出時:
     * 輸出結果存儲在在 col/row idx
     */
    @PlanningEntityCollectionProperty        // 輸出  結果域  (在計算過程中會一直進行嘗試,直到嘗試到最優解)
    private List<Queen> queenList;

    @PlanningScore
    private HardSoftScore score;

    @Override
    public String toString() {
        return "ChessBoard{" +
                ",\n colIdxInitList=" + colIdxInitList +
                ",\n rowIdxInitList=" + rowIdxInitList +
                ",\n\n queenList=" + queenList +
                ",\n score=" + score +
                '}';
    }

    /**
     * 繪制棋盤
     */
    public void paintChessBoard(int n) {
        System.out.println("打印棋盤,最后一行第1個點為(0,0),12點方向為x軸正軸方向\n");
        for (int i = n - 1; i >= 0; i--) {
            for (int j = 0; j < n; j++) {
                boolean flag = false;
                for (Queen queen : queenList) {
                    if (queen.getRowIdx() == i && queen.getColIdx() == j) {
                        System.out.print(" * ");
                        flag = true;
                    }
                }
                if (!flag) {
                    System.out.print(" - ");
                }
            }
            System.out.print("\n");
        }
    }
}

約束(懲罰項)類 : 四種不合法的類型

public class QueueConstraintProvider implements ConstraintProvider {
    @Override
    public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
        return new Constraint[]{
                // Only hard Constraint

                rowConflictCheck(constraintFactory),
                colConflictCheck(constraintFactory),
                // 加上左右對角線
                leftDiagonalConflictCheck(constraintFactory),
                rightDiagonalConflictCheck(constraintFactory),
        };
    }


    private Constraint rowConflictCheck(ConstraintFactory constraintFactory) {
        return constraintFactory.from(Queen.class)
                .join(Queen.class,
                        Joiners.equal(Queen::getRowIdx),
                        Joiners.lessThan(Queen::getId)
                )
                .penalize("行約束", HardSoftScore.ONE_HARD);
    }


    private Constraint colConflictCheck(ConstraintFactory constraintFactory) {
        return constraintFactory.from(Queen.class)
                .join(Queen.class,
                        Joiners.lessThan(Queen::getId),
                        Joiners.equal(Queen::getColIdx)
                )
                .penalize("列約束", HardSoftScore.ONE_HARD);
    }


    private Constraint leftDiagonalConflictCheck(ConstraintFactory constraintFactory) {
        return constraintFactory.from(Queen.class)
                .join(Queen.class,
                        Joiners.lessThan(Queen::getId),
                        Joiners.equal(Queen::getLeftDiagonalConflictCheck)
                )
                .penalize("右對角線 約束", HardSoftScore.ONE_HARD);
    }

    private Constraint rightDiagonalConflictCheck(ConstraintFactory constraintFactory) {
        return constraintFactory.from(Queen.class)
                .join(Queen.class,
                        Joiners.lessThan(Queen::getId),
                        Joiners.equal(Queen::getRightDiagonalConflictCheck)
                )
                .penalize("左對角線 約束", HardSoftScore.ONE_HARD);
    }

}

測試類

/**
 * 記得保持在同一啟動類的目錄下
 */
@SpringBootTest
class NQueenOptaplannerApplicationTests {

    @Resource
    private SolverManager<ChessBoard, UUID> nQueensPuzzleSolverManager;

    /**
     * 01  N皇后測試
     */
    @Test
    void testNQueensPuzzle() {
        int n = 5;
        int queenNum = 4;

        /////////////////////////////////////////////////////////////////////
        List<Integer> colIdxInitList = new ArrayList<>();
        List<Integer> rowIdxInitList = new ArrayList<>();
        List<Queen> queenList = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            colIdxInitList.add(i);
            rowIdxInitList.add(i);
        }

        for (int i = 0; i < queenNum; i++) {
            queenList.add(new Queen(i));
        }

        ChessBoard problem = new ChessBoard(colIdxInitList, rowIdxInitList, queenList, null);

        UUID problemId = UUID.randomUUID();
        // Submit the problem to start solving
        SolverJob<ChessBoard, UUID> solverJob = nQueensPuzzleSolverManager.solve(problemId, problem);
        ChessBoard solution;
        try {
            // Wait until the solving ends
            solution = solverJob.getFinalBestSolution();
        } catch (InterruptedException | ExecutionException e) {
            throw new IllegalStateException(">>>>>Solving failed.", e);
        }
        System.out.println(solution);
        System.out.println("\n\n");
        solution.paintChessBoard(n);
    }
}

03. 測試

測試N=5,Queen=4時,是否存在對應的合法局面

默認使用的LocalSearch算法,跑了35917輪, 找到一個合理解, 返回; 打印如下

打印棋盤,最后一行第1個點為(0,0),12點方向為x軸正軸方向

 -  -  *  -  - 
 -  -  -  -  - 
 -  *  -  -  - 
 -  -  -  *  - 
 *  -  -  -  - 

測試N=5,QueenNum=5時,是否存在對應的合法局面

默認使用的LocalSearch算法,跑了36689輪, 找到一個合理解, 返回; 打印如下

( Solving ended: time spent (5000), best score (0hard/0soft), score calculation speed (14519/sec), phase total (2), environment mode (REPRODUCIBLE).)

打印棋盤,最后一行第1個點為(0,0),12點方向為x軸正軸方向

 -  -  *  -  - 
 -  -  -  -  * 
 -  *  -  -  - 
 -  -  -  *  - 
 *  -  -  -  - 

測試N=10,QueenNum=10時,是否存在對應的合法局面

Local Search phase (1) ended: time spent (5000ms), best score (-1hard/0soft), score calculation speed (12470/sec), step total (32526).
這時不存在對應的合法局面, 找到的局部最優解為-1hard/0soft,意思是違反了一個硬約束,打印局部最優解如下:

打印棋盤,最后一行第1個點為(0,0),12點方向為x軸正軸方向

 -  -  -  -  -  -  -  -  -  * 
 -  -  *  -  -  -  -  -  -  - 
 -  -  -  -  -  -  *  -  -  - 
 -  -  -  *  -  -  -  -  -  - 
 -  -  -  -  -  -  -  *  -  - 
 -  -  -  -  -  -  -  *  -  - 
 -  -  -  -  *  -  -  -  -  - 
 *  -  -  -  -  -  -  -  -  - 
 -  -  -  -  -  *  -  -  -  - 
 -  -  -  -  -  -  -  -  *  - 

測試N=10,QueenNum=10時,加大搜索時間到30s,再次嘗試:

更改application.properties即可:

# The solver runs only for 5 seconds to avoid a HTTP timeout in this simple implementation.
# It's recommended to run for at least 5 minutes ("5m") otherwise.
optaplanner.solver.termination.spent-limit=30s
logging.level.org.optaplanner=debug

再次嘗試,找到一個最優解 (0hard/0soft),沒有違反任何約束

DefaultLocalSearchPhase : Local Search phase (1) ended: time spent (30000), best score (0hard/0soft), score calculation speed (14415/sec), step total (236831).

打印棋盤,最后一行第1個點為(0,0),該點的12點方向為x軸正軸方向————
*  -  -  -  -  -  -  -  -  - 
 -  -  -  -  -  -  -  -  -  * 
 -  -  -  -  -  -  *  -  -  - 
 -  -  -  -  *  -  -  -  -  - 
 -  -  -  -  -  -  -  *  -  - 
 -  *  -  -  -  -  -  -  -  - 
 -  -  -  -  -  -  -  -  *  - 
 -  -  *  -  -  -  -  -  -  - 
 -  -  -  -  -  *  -  -  -  - 
 -  -  -  *  -  -  -  -  -  - 

04. 最后

  1. N皇后的問題隨着棋盤規則的增加,搜索難度也會大幅提升(NP問題),目前除了量子計算外,解決NP問題的一個手段就是數學武器。
  2. 局部搜索(Local Search)是解決最優化問題的一種啟發式算法。因為對於很多復雜的問題,求解最優解的時間可能是極其長的。因此誕生了各種啟發式算法來退而求其次尋找次優解,局部搜索就是其中一種。它是一種近似算法(Approximate algorithms)。

    局部搜索算法是從爬山法改進而來的。簡單來說,局部搜索算法是一種簡單的貪心搜索算法,該算法每次從當前解的臨近解空間中選擇一個最優解作為當前解,直到達到一個局部最優解。局部搜索從一個初始解出發,然后搜索解的鄰域,如有更優的解則移動至該解並繼續執行搜索,否則返回當前解。

    引用鏈接: https://www.cnblogs.com/dengfaheng/p/9245559.html
  3. 后續要加快速度,英文文檔看着還是比較費勁的(單詞都認識、句子可以讀通順,但理解起來就比較困難了),沒多時間了,下周就要開始投入生產了;領導說,這個新項目的難點不是缺人力或者缺時間,而是是否存在有效解的問題,如何快速判斷我們的算法是否值得繼續優化等問題。
  4. 下一步:
    • OPTA怎么選擇其他的算法,目前使用的只有LS
    • 怎么快速判斷是否存在解(解空間)
    • 怎么優化
    • Boot的多線程計算
    • 是否可以借助GPU加速


免責聲明!

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



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