初学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

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{" +
                ",
 colIdxInitList=" + colIdxInitList +
                ",
 rowIdxInitList=" + rowIdxInitList +
                ",

 queenList=" + queenList +
                ",
 score=" + score +
                '}';
    }

    /**
     * 绘制棋盘
     */
    public void paintChessBoard(int n) {
        System.out.println("打印棋盘,最后一行第1个点为(0,0),12点方向为x轴正轴方向
");
        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("
");
        }
    }
}

约束(惩罚项)类 : 四种不合法的类型

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("

");
        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加速

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注