程序员运筹优化

优化建模之MiniZinc(三) 全局约束 -- 以解数独为例

2020-11-22  本文已影响0人  ChaoesLuol

数独问题中约束的表达

数独是一个经典的问题,在计算机科学中,我们通常使用回溯法来进行求解,但是对于规模稍大的数独问题,回溯法的设计是相当复杂的。这里我们也可以建立一个数学模型,让求解器帮我们来得到答案。

我们从数独网站上找一个困难级别的题目,如下图所示:

3_1 Sudoku.jpg

根据数独的要求,我们可以归纳出以下约束:

直接表达的约束

对于第一个约束,一种可行的表达方式是先定义一个辅助数组completed表示填充之后每个位置的数字

forall(i, j in COLS where i < j)(completed[1, i] != completed[1, j]),这样就可以约束第1行中任意两个元素不相等。

依次对第2,3,...,9行进行同样的约束,我们就保证了每一行中不出现重复的数字。

按照同样的思路对列进行约束,这样的话一共我们需要写18条约束语句,才能完成我们上面短短一句自然语言所表达的约束。

这个思路并不难,约束的表达也好理解,唯一的问题就是太过繁琐。我们知道编程中有一个重要的原则叫做DRY,也就是Don't Repeat Yourself。在建模中也是同样,我们是不是能够找到更加简洁的表达上面约束的方式呢?答案当然是可以的,就是利用MiniZinc中的全局约束

forall(i in ROWS) alldifferent(completed[i, ..]); 就可以替代我们上面的9条语句,表达每行中所有元素不同的约束。这里的alldifferent是MiniZinc中定义的全局约束中的一个。

使用全局约束

模型文件3_1_Sudoku.mzn

% 建模求解数独问题

%--------------------------
% 数据
int: nrow;
int: ncol;
set of int: rows = 1..nrow;
set of int: cols = 1..ncol;
array[rows, cols] of int: grid; % 用0表示当前棋盘上空的位置

%--------------------------
% 决策变量
array[rows, cols] of var 0..9: fill; % 0表示当前棋盘上已经填充的位置,其他数字表示应该填充的数字
array[rows, cols] of var 1..9: completed; % 建模辅助变量

%--------------------------
% 约束
% 0. 约束fill数组和completed数组的关系
constraint forall(i in rows, j in cols)(completed[i, j] = fill[i, j] + grid[i, j]);
% 1. 保证grid中已经填好数字的位置在fill中为0
constraint forall(i in rows, j in cols)(grid[i, j] != 0 -> fill[i, j] = 0);
% 2. 保证每行都是1-9,每列都是1-9
include "alldifferent.mzn";
constraint forall(i in rows)(alldifferent(completed[i, ..]));
constraint forall(j in cols)(alldifferent(completed[.., j]));
% 3. 保证每个 3x3的小块中放入的都是不同的数字
constraint forall(i in 0..((nrow div 3) - 1), j in 0..((ncol div 3) - 1))
                (alldifferent([completed[3*i + u, 3*j + v] | u in 1..3, v in 1..3]));

%--------------------------
% 目标函数
solve satisfy;

%--------------------------
% 输出
output ["\(completed[i, j])" ++ if ((j mod ncol) = 0) then "\n" else " " endif  |i in rows, j in cols];

数据文件3_1_Sudoku.dzn

nrow = 9;
ncol = 9;
grid = [|0, 0, 3, 6, 0, 0, 0, 0, 7
        |0, 0, 0, 0, 0, 0, 0, 1, 5
        |0, 5, 0, 0, 7, 0, 6, 8, 0
        |0, 0, 9, 8, 0, 0, 0, 0, 0
        |1, 0, 2, 0, 0, 7, 0, 0, 8
        |0, 0, 0, 1, 0, 2, 0, 0, 0
        |0, 4, 0, 0, 0, 0, 0, 0, 0
        |7, 0, 0, 0, 0, 0, 3, 0, 9
        |8, 0, 1, 0, 0, 4, 2, 0, 0|];

我们可以得到填好的数独结果:

9 1 3 6 8 5 4 2 7
6 8 7 4 2 3 9 1 5
2 5 4 9 7 1 6 8 3
4 7 9 8 5 6 1 3 2
1 6 2 3 4 7 5 9 8
5 3 8 1 9 2 7 6 4
3 4 5 2 6 9 8 7 1
7 2 6 5 1 8 3 4 9
8 9 1 7 3 4 2 5 6
----------

全局约束

MiniZinc中的全局约束

MiniZinc中预置了一系列的约束供我们使用。

所谓的”全局约束“,指的是约束本身不限制变量的具体维度,也就是说对于任意大小的变量,只要其类型符合要求,就都可以套用,从这个角度来说,这种约束是”全局“的。例如alldifferent([<set>]),并不会在意传入的set到底维度如何,包含3个变量或者10个变量,都可以进行统一处理。

而从另外一个角度来说,”全局约束“也是在许多问题中会出现的共性的约束,这也是其全局性的一种体现。

”全局约束“其实是预先实现的一些谓词(Predicate),后面我们更加深入学习时会看到什么叫做谓词以及如何使用谓词。

全局约束的引入 -- include语句

利用语句include "alldifferent.mzn";,可以在模型中导入alldifferent全局约束;用include "globals.mzn"可以在模型中导入所有全局约束。

和C/C++中一样include "<filename>"会在MiniZinc的环境变量中临时添加一条路径,使得程序编译和链接模型时能够在添加的路径中查找相应的文件。

除了添加全局约束以外,我们还可以像C/C++中一样,使用include来将较大的模型化整为零,分成功能单一便于理解和修改的部分。

为什么需要全局约束

为什么需要使用全局约束呢?

常用的全局约束

下面列出了一些建模中常用的全局约束,更多的全局约束可以参考MiniZinc的reference manual,当然在后面的系列文章里我们也会接触到更多的全局约束。

上一篇 下一篇

猜你喜欢

热点阅读