目前可以公開的一些情報
什么是運籌排班算法?
假設一個工廠場景,該工廠下有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. 最后
- N皇后的問題隨着棋盤規則的增加,搜索難度也會大幅提升(NP問題),目前除了量子計算外,解決NP問題的一個手段就是數學武器。
- 局部搜索(Local Search)是解決最優化問題的一種啟發式算法。因為對於很多復雜的問題,求解最優解的時間可能是極其長的。因此誕生了各種啟發式算法來退而求其次尋找次優解,局部搜索就是其中一種。它是一種近似算法(Approximate algorithms)。
局部搜索算法是從爬山法改進而來的。簡單來說,局部搜索算法是一種簡單的貪心搜索算法,該算法每次從當前解的臨近解空間中選擇一個最優解作為當前解,直到達到一個局部最優解。局部搜索從一個初始解出發,然后搜索解的鄰域,如有更優的解則移動至該解並繼續執行搜索,否則返回當前解。
引用鏈接: https://www.cnblogs.com/dengfaheng/p/9245559.html - 后續要加快速度,英文文檔看着還是比較費勁的(單詞都認識、句子可以讀通順,但理解起來就比較困難了),沒多時間了,下周就要開始投入生產了;領導說,這個新項目的難點不是缺人力或者缺時間,而是是否存在有效解的問題,如何快速判斷我們的算法是否值得繼續優化等問題。
- 下一步:
- OPTA怎么選擇其他的算法,目前使用的只有LS
- 怎么快速判斷是否存在解(解空間)
- 怎么優化
- Boot的多線程計算
- 是否可以借助GPU加速