優(yōu)化建模之MiniZinc(二) 模型的抽象化 -- 模型與數(shù)據(jù)的分離

模型的抽象化

具體模型與抽象模型

具體模型

在第一篇文章中我們介紹了MiniZinc程序的基本組成部分。

作為復(fù)習(xí),我們可以看一個生產(chǎn)規(guī)劃問題:

我們在邊境和印軍相持,因為不能開槍,我們要生產(chǎn)一批冷兵器來武裝部隊,可選的選項有長矛、劍和錘子;生產(chǎn)武器需要消耗一些資源和工匠的時間,武器對應(yīng)的資源消耗如下:

木材 鐵礦 鐵匠工時 木匠工時
長矛 2.5 0.5 1.0 3.7
0.4 4.2 5.1 1.2
錘子 3.5 4.5 7.0 2.7

我們的資源是有限的:

木材 鐵礦 鐵匠工時 木匠工時
5000.0 2000.0 4000.0 3250.5

每樣武器對應(yīng)一定的戰(zhàn)斗力:

長矛 錘子
12.5 17.0 23.8

我們希望充分利用有限的資源來最大化我們的部隊?wèi)?zhàn)斗力,那么每樣武器需要生產(chǎn)多少件呢?

對應(yīng)的模型如下:

% 混料問題的模型
%--------------------------
% 數(shù)據(jù)部分
enum RESOURCES = {wood, iron, blacksmith_hour, carpenter_hour}; % 需要的資源
enum WEAPONS = {spear, sword, hammer}; % 生產(chǎn)的武器
array[WEAPONS, RESOURCES] of float: consumption = [|2.5, 0.5, 1.0, 3.7
                                                   |0.4, 4.2, 5.1, 1.2
                                                   |3.5, 4.5, 7.0, 2.7|]; % 生產(chǎn)每種武器需要的資源
array[RESOURCES] of float: res_amount = [5000.0, 2000.0, 4000.0, 3250.5]; % 每種資源的數(shù)量
array[WEAPONS] of float: power = [12.5, 17.0, 23.8]; % 每種武器的威力

%--------------------------
% 決策變量
array[WEAPONS] of var int: weapon_amount;

%--------------------------
% 約束
% 每種武器的數(shù)量都必須為非負(fù)數(shù)
constraint forall(w in WEAPONS)(weapon_amount[w] >= 0);
% 武器的資源消耗要不大于最大資源數(shù)量
constraint forall(r in RESOURCES)(
                 (sum(w in WEAPONS)(consumption[w, r] * weapon_amount[w])) <= res_amount[r]);

%--------------------------
% 目標(biāo)函數(shù)
var float: total_power = sum(w in WEAPONS)(power[w] * weapon_amount[w]);
solve maximize total_power;

%--------------------------
% 輸出
output ["Amount of weapon " ++ show(w) ++ " : " ++ show_int(8, weapon_amount[w]) ++ "\n" | w in WEAPONS]
        ++ ["Total power: " ++ show_float(6, 2, total_power)];

通過求解,我們可以得到答案:

Amount of weapon spear :      603
Amount of weapon sword :        0
Amount of weapon hammer :      377
Total power: 16510.10
----------
==========

模型中關(guān)于數(shù)組和一些內(nèi)置函數(shù),可以往下拉到后面看對MiniZinc語法的解釋。

抽象模型

上面這個模型看起來沒有什么問題,求解也很順利。但是在很多情況下,我們建模時會面臨一系列what-if的問題:如果我資源多一些,會怎么樣?如果我增加一些兵器種類供選擇,會怎么樣?

我們需要不斷修改上面的具體模型,來應(yīng)付不斷變動的數(shù)據(jù)。

如果我們不是在求解一個武器生產(chǎn)問題,而是在規(guī)劃我們的期末復(fù)習(xí)計劃呢?我們是不是需要重寫整個模型呢?

在軟件工程的實踐中,設(shè)計模式給我們的重要啟示就是將變動的部分和相對穩(wěn)定的部分分開,對我們的模型來說也是一樣,同一類的生產(chǎn)規(guī)劃模型中有一些共同點,我們可以抽取其想通部分,然后將數(shù)據(jù)單獨分開。

生產(chǎn)規(guī)劃模型

在生產(chǎn)規(guī)劃模型中,我們可以提取一些共同之處:

  • 對給定的資源種類,每種資源都有量的限制
  • 生產(chǎn)每種產(chǎn)品會消耗不同數(shù)量的各類資源
  • 不同產(chǎn)品能帶來不同的收益
  • 通常我們需要最大化收益

根據(jù)這些共同特點,我們可以生成一個抽象的模型(2_2_AbstractProductionModel.mzn):

% 抽象的生產(chǎn)問題模型

%--------------------------
% 數(shù)據(jù)部分
enum RESOURCES; % 用于生產(chǎn)的資源
enum PRODUCTS; % 生產(chǎn)的產(chǎn)品
array[PRODUCTS, RESOURCES] of float: consumption; % 生產(chǎn)每種產(chǎn)品所需要的資源數(shù)量
array[RESOURCES] of float: capacity; % 每種資源的額度
array[PRODUCTS] of float: profit; % 每種產(chǎn)品可以提供的收益

%--------------------------
% 決策變量
array[PRODUCTS] of var int: produce_num;

%--------------------------
% 約束
% 資源消耗少于額度
constraint forall(r in RESOURCES)
        (sum(p in PRODUCTS)(consumption[p, r] * produce_num[p]) <= capacity[r]);
% 每種產(chǎn)品數(shù)量必須為非負(fù)
constraint forall(p in PRODUCTS)(produce_num[p] >= 0);

%--------------------------
% 目標(biāo)函數(shù)
var float: total_profit = sum(p in PRODUCTS)(produce_num[p] * profit[p]);
solve maximize total_profit;

%--------------------------
% 輸出
output ["Amount of product " ++ show(p) ++ " : " ++ show_int(8, produce_num[p]) ++ "\n" | p in PRODUCTS]
        ++ ["Total profit: " ++ show_float(6, 2, total_profit)];

我們可以看到在抽象模型中,沒有包含任何數(shù)據(jù)。這樣我們就分開了模型和數(shù)據(jù),對于同樣一個模型,我們可以用來求解性質(zhì)相同的不同數(shù)據(jù)集了。

為了驗證,我們可以將我們上面的武器生產(chǎn)問題建立一個獨立的數(shù)據(jù)集2_2_WeaponProduction.dzn如下:

RESOURCES= {wood, iron, blacksmith_hour, carpenter_hour};
PRODUCTS = {spear, sword, hammer};
consumption = [|2.5, 0.5, 1.0, 3.7
               |0.4, 4.2, 5.1, 1.2
               |3.5, 4.5, 7.0, 2.7|];
capacity = [5000.0, 2000.0, 4000.0, 3250.5];
profit = [12.5, 17.0, 23.8];

在命令行中運(yùn)行minizinc --solver CBC 2_2_AbstractProductionModel.mzn 2_2_WeaponProduction.dzn指定數(shù)據(jù)集和求解器進(jìn)行求解,得到結(jié)果:

Amount of product spear :      603
Amount of product sword :        0
Amount of product hammer :      377
Total profit: 16510.10
----------
==========

可以看到對于武器生產(chǎn)問題,我們得到的結(jié)果和前面一樣。

自習(xí)時間分配問題

抽象模型的好處在于可以用同一個模型處理一類問題。用之前建立的生產(chǎn)問題模型,我們可以來看看下面的一個時間分配問題:

我們面臨有三門課的期末考試,分別是運(yùn)籌學(xué)、離散數(shù)學(xué)和C語言,對于每種課程的復(fù)習(xí),一種較理想的方案是分別分配一定的上機(jī)復(fù)習(xí)、討論和書本自習(xí),各門課程理想復(fù)習(xí)方案的時間比例如下:

上機(jī) 討論 自習(xí)
運(yùn)籌學(xué) 0.3 0.5 0.2
離散數(shù)學(xué) 0.2 0.4 0.4
C語言 0.6 0.1 0.3

但是由于機(jī)房、討論室和自習(xí)室各自有一定時間限制,我們在考試來之前能夠約到的時間如下:

上機(jī)時間 討論時間 自習(xí)室時間
10.5 7.0 12.0

每花一個小時在不同科目上,我們能夠提高不同的平均GPA:

運(yùn)籌學(xué) 離散數(shù)學(xué) C語言
0.2 0.15 0.1

假設(shè)我們起點很低,這輪復(fù)習(xí)不會將任何科目的GPA刷滿,但是我們希望將GPA盡量刷高一點,那么應(yīng)該怎么分配剩余的復(fù)習(xí)時間呢?

我們發(fā)現(xiàn)這個問題其實也是生產(chǎn)問題的一類,我們并不需要對模型進(jìn)行任何的改動,只需要將數(shù)據(jù)集進(jìn)行修改即可。我們建立一個數(shù)據(jù)文件2_2_SwotUp.dzn放入相應(yīng)的課程、用時和收益:

RESOURCES= {computer_work, discussion_room, desk_work};
PRODUCTS = {operation_research, discrete_math, C_language};
consumption = [|0.3, 0.5, 0.2
               |0.2, 0.4, 0.4
               |0.6, 0.1, 0.3|];
capacity = [10.5, 7.0, 12.0];
profit = [0.2, 0.15, 0.1];

在命令行運(yùn)行minizinc --solver CBC 2_2_AbstractProductionModel.mzn 2_2_SwotUp.dzn進(jìn)行求解:

得到結(jié)果:

Amount of product operation_research :        6
Amount of product discrete_math :        7
Amount of product C_language :       12
Total profit:   3.45
----------
==========

小節(jié)

  • 具體模型中,數(shù)據(jù)及其定義是放在一起的,不能靈活修改,因此只能用來解決特定的問題。

  • 通過將數(shù)據(jù)和模型分開, 并對模型進(jìn)行抽象化,我們可以用同一個模型來應(yīng)對不同規(guī)模的數(shù)據(jù)集,此外這個模型還可以用來解決一類問題,而非僅僅針對某一個特定問題。

  • 除了應(yīng)用更加方便外,定義抽象模型和一個較小的數(shù)據(jù),可以方便我們對復(fù)雜模型進(jìn)行debug。

因此,在建模時,通常建議對模型進(jìn)行抽象化,將模型文件和數(shù)據(jù)文件分開來。

MiniZinc中的一些語法知識

數(shù)組和推導(dǎo)式

在上面的程序中,我們用到了如下語句:

array[WEAPONS, RESOURCES] of float: consumption = [|2.5, 0.5, 1.0, 3.7
                                                   |0.4, 4.2, 5.1, 1.2
                                                   |3.5, 4.5, 7.0, 2.7|]; % 生產(chǎn)每種武器需要的資源

這條語句定義了一個二維數(shù)組consumption,其維度分別為WEAPONSRESOURCES。

數(shù)組是一個比較常用的數(shù)據(jù)結(jié)構(gòu),下面我們會介紹一下數(shù)組的相關(guān)知識:

數(shù)組的聲明

數(shù)組可以是一維或者多維的,在聲明時使用如下格式:

array[index_set1, index_set2,...] of <type>

數(shù)組下標(biāo)的集合可以是枚舉類型或者是某個集合表達(dá)式。

數(shù)組內(nèi)的元素可以是除了數(shù)組以外的任何內(nèi)置類型,也就是說MiniZinc不支持?jǐn)?shù)組嵌套。

數(shù)組的初始化

一維數(shù)組

對于一維數(shù)組,我們可以用列表的方式進(jìn)行初始化,這和大多數(shù)其他語言一樣,比如下面這些例子:

profit = [100, 200, 300];
cost = [0.2, 0.3, 0.5];

二維數(shù)組

MiniZinc中二維數(shù)組的初始化有特定的語法:

  • 起始于符號[|
  • |來分割行
  • 結(jié)束于符號|]

一個例子就是我們上面定義的二維數(shù)組:

consumption = [|2.5, 0.5, 1.0, 3.7
                     |0.4, 4.2, 5.1, 1.2
                             |3.5, 4.5, 7.0, 2.7|];

特別需要注意結(jié)尾,不要忘記|符號。

內(nèi)置函數(shù)初始化任意維度數(shù)組

對于任何維度k不大于6的數(shù)組,我們都可以用內(nèi)置函數(shù)arraykd來進(jìn)行初始化,例如:

profit = array1d(1..3, [100, 200, 300]);
consumption = array2d(1..3, 1..4,
                            [2.5, 0.5, 1.0, 3.7,
                     0.4, 4.2, 5.1, 1.2,
                             3.5, 4.5, 7.0, 2.7]);

分別等價于我們上面的一維和二維數(shù)組初始化方式。

推導(dǎo)式

除了使用內(nèi)置函數(shù)或者列表形式來進(jìn)行初始化意外,MiniZinc也提供了和Python一樣的推導(dǎo)式方式對數(shù)組進(jìn)行初始化。其語法為:

[<expression>| <generator-expr1>, <generator-expr2>,...]

在推導(dǎo)過程中我們也可以進(jìn)行一些檢驗:

[<expression>| <generator-expr1>, <generator-expr2>, ..., where <bool-expr>]

例如

array[int] of int: u = [i + j | i in 1..3, j in 2..5];
array[int] of int: v = [i * j | i in 2..6, j in 3..7 where i * j <= 20];

會生成下面的兩個數(shù)組

[3, 4, 5, 6, 4, 5, 6, 7, 5, 6, 7, 8]
[6, 8, 10, 12, 14, 9, 12, 15, 18, 12, 16, 20, 15, 20, 18]

注意這里因為我們事先不知道數(shù)組的長度,我們用了array[int]來讓求解器自己推導(dǎo)數(shù)組長度。

相應(yīng)的,集合也可以用推導(dǎo)式的方式生成,唯一的不同就是用{}替換[]。

獲得數(shù)組的下標(biāo)

因為在列表推導(dǎo)式的運(yùn)用,我們可能產(chǎn)生不知道列表下標(biāo)范圍的情況。此時我們可以使用內(nèi)置函數(shù)index_set來獲得一維數(shù)組的下標(biāo)范圍。

例如:

array[int] of int: u = [i + j | i in 1..3, j in 2..5];
set of int: k = index_set(u);

這里我們的k會得到范圍1..12。

對于多維數(shù)組,我們需要指定數(shù)組的總維度k和我們想知道具體哪個維度m的下標(biāo)。

MiniZinc中內(nèi)置了一系列函數(shù)index_set_<m>of<k>來幫助我們得到下標(biāo),其中k<=6。例如我們想得到二維數(shù)組第二個維度的下標(biāo),我們就可以使用函數(shù)index_set_2of2。

基礎(chǔ)運(yùn)算

四則運(yùn)算

MiniZinc中支持的整數(shù)運(yùn)算有基礎(chǔ)的四則運(yùn)算加減乘除:+, -, *, div。對于浮點數(shù),除法表示為/

<u>注意這里的整數(shù)除法是div而非/,/在MiniZinc中特指浮點數(shù)的除法</u>,處理整數(shù)時可能得到意料之外的結(jié)果。

除此之外還有我們常用的取余mod,整數(shù)的取余運(yùn)算a mod b會得到和a同號的結(jié)果,也就是說一定有a = b * (a div b) + (a mod b)

內(nèi)置的運(yùn)算函數(shù)

下面給出一些在MiniZinc中常用的內(nèi)置運(yùn)算函數(shù)。

對于數(shù)字:

  • abs(n) - 返回數(shù)字n的絕對值。
  • pow(n, k) - 返回nk次冪。

對于數(shù)組:

  • product(array) -返回數(shù)組array的連乘,注意對于空數(shù)組,product返回1。
  • sum(array) - 返回數(shù)組array的和,對于空數(shù)組,返回0。
  • min(array) - 返回數(shù)組array的最小值,對于空數(shù)組,是未定義行為,會返回一個run-time error。
  • max(array) - 返回數(shù)組array的最大值,對于空數(shù)組,是未定義行為,會返回一個run-time error。
  • has_element(e, array) - 返回一個bool類型,如果array中含有e,返回true否則返回false
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容