本人咸鱼一条,参加过19年数学建模,当时在B题和C题之间选择,最后还是选择了C题(其实是B题不会写)。看着去年九月参加比赛的教室,今年也坐着三人一组的建模小队,查资料、分析数据、编程,触景生情(我又老了一岁),心血来潮也看了看今年的建模题目,类型还是和去年一样,三道题目B题的游戏模拟走出沙漠让我(游戏迷)眼前一亮,闲着无聊就拿这一题做一做。
真是越来越罗嗦了,废话少说,我直接上我B题的思路吧!
#######简陋的分界线########
下面均是我自己的看法和观点,是对是错我也不知道(无辜)。我只对第一问的第一关进行求解(第二关也可以求,本人太懒,不想根据图构建关联矩阵)。
根据假设2和假设3,玩家的路径可以由村庄、矿场、起点和终点,以及它们之间的最短路径确定。
例如:玩家的路线可以是:起点1——line(1,12)——矿山12——line(12,27)——终点27
最短路劲(可以调用包求得,这里就不解释了)
12 1
Path 1 (8.0): 12 -> 14 -> 16 -> 17 -> 21 -> 23 -> 24 -> 25 -> 1
15 1
Path 1 (6.0): 15 -> 9 -> 21 -> 23 -> 24 -> 25 -> 1
15 12
Path 1 (2.0): 15 -> 14 -> 12
27 1
Path 1 (3.0): 27 -> 26 -> 25 -> 1
27 12
Path 1 (5.0): 27 -> 21 -> 9 -> 15 -> 13 -> 12
27 15
Path 1 (3.0): 27 -> 21 -> 9 -> 15
最短天数矩阵
最短天数矩阵(不考虑天气)此处使用递归生成树结构,求所有可能路径,直接上代码吧
# 文件名:生成树
import pandas as pd
# 构建节点
class Node():
def __init__(self, ID: int, distance=0, parent=None):
self.ID = ID
self.distance = distance
self.parent = parent
self.son = []
def add_son(self, s):
if isinstance(s, list):
self.son.extend(s)
else:
self.son.append(s)
pass
class Tree():
"""通过位置ID列表,构建从起点到重点的路径树"""
def __init__(self, List: list, table: pd.DataFrame):
self.T = table
self.r = Node(List[0])
self.L = List[1:]
self.__tree(self.r)
self.leaList = []
self.__out(self.r)
pass
# 生成树
def __tree(self, a: Node):
if a.ID == 27: # 叶节点
return None
a.son = [Node(i, self.T.at[a.ID, i] + a.distance, a) for i in self.L if self.T.at[a.ID, i] > 0 and self.T.at[a.ID, i] + a.distance + self.T.at[i, 27] <= 30]
if not a.son:
# 叶节点
# table.at[a.ID,i] + a.distance + table.at[i,27] >= 30 就为叶节点
return None
for j in a.son:
self.__tree(j)
def __out(self, a: Node):
# 输出所有叶节点
if not a.son:
self.leaList.append(a)
for i in a.son:
self.__out(i)
@staticmethod
def output(b: Node, s: list):
"""
:param b: 叶节点
:param s: 含叶节点ID的列表
:return: 路径
"""
if s == []:
s.append(b.ID)
if b.parent == None:
return
s.insert(0, b.parent.ID)
Tree.output(b.parent, s)
@staticmethod
def routeToPara(r: list):
"""
:param r:路线列表
:return: the number of parameters and the bounds of parameters
"""
Dict = {
1: [2, [400, 600]], 12: [1, [30]], 15: [2, [400, 600]], 27: [0, []]}
a = []
[a.extend(Dict[r[i]][1]) for i in range(len(r))]
return sum(Dict[r[i]][0] for i in range(len(r))), a
if __name__ == '__main__':
table = pd.read_excel("../table/最短天数邻阶矩阵.xlsx", index_col=0, header=0)
List = list(table.index)
a = Tree(List, table)
for leaf in a.leaList:
route = []
Tree.output(leaf, route)
print(Tree.routeToPara(route))
思路:
首先构建一个玩家对象:
# @File: player.py
class Player():
# 基础数据信息
info_base = {
"负重上限": 1200,
"剩余时间": 30,
"初始资金": 10000,
"基础收益": 1000,
"水": {
"每箱质量": 3, "基准价格": 5, "晴朗": 5, "高温": 8, "沙暴": 10},
"食物": {
"每箱质量": 2, "基准价格": 10, "晴朗": 7, "高温": 6, "沙暴": 10}
}
Weather = ['高温', '高温', '晴朗', '沙暴', '晴朗', '高温', '沙暴', '晴朗', '高温', '高温',
'沙暴', '高温', '晴朗', '高温', '高温', '高温', '沙暴', '沙暴', '高温', '高温',
'晴朗', '晴朗', '高温', '晴朗', '沙暴', '高温', '晴朗', '晴朗', '高温', '高温']
def __init__(self):
self.time_re = self.info_base["剩余时间"]
self.foo_re = 0
self.wat_re = 0
self.mon_re = self.info_base["初始资金"]
self.wei_re = 0
self.day = 0
# 阶段性使用变量:路程计数
self.step = 0
def T_judge(self):
if self.day >= 30:
return False
return True
# 状态更新
# def __update(self, cons_foo=0, cons_wat=0, cons_t=0, cons_mon=0, cons_wei=0, step=0):
def __update(self, **kwargs):
f = self.foo_re + kwargs.get('cons_foo', 0)
wat = self.wat_re + kwargs.get('cons_wat', 0)
t = self.time_re + kwargs.get('cons_t', 0)
m = self.mon_re + kwargs.get('cons_mon', 0)
w = self.wei_re + kwargs.get('cons_wei', 0)
# 判断各种属性是否足够本次动作
if f < 0 or wat < 0 or t < 0 or w < 0 or w > self.info_base["负重上限"]:
return False
self.foo_re, self.wat_re, self.time_re, self.mon_re, self.wei_re = f, wat, t, m, w
self.step += kwargs.get('step', 0)
return True
# 基础消耗
def __cons_base(self, day):
return (self.info_base["水"][self.Weather[day]], self.info_base["食物"][self.Weather[day]])
# 计算分数
def score(self):
i = self.foo_re * self.info_base['食物']['基准价格'] + self.wat_re * self.info_base['水']['基准价格']
return i / 2 + self.mon_re
# 行走
def walk(self, day, ):
if not self.T_judge():
return False
if self.Weather[day] == "沙暴":
return self.rest(day)
self.day += 1
cons_foo, cons_wat = self.__cons_base(day)
cons_foo = -2 * cons_foo
cons_wat = -2 * cons_wat
cons_t = -1
cons_wei = (self.info_base['水']['每箱质量'] * cons_wat + self.info_base['食物']['每箱质量'] * cons_foo)
return self.__update(cons_foo=cons_foo, cons_wat=cons_wat, cons_t=cons_t, cons_wei=cons_wei, step=1)
# 休息
def rest(self, day):
if not self.T_judge():
return False
self.day += 1
cons_foo, cons_wat = self.__cons_base(day)
cons_foo = - cons_foo
cons_wat = - cons_wat
cons_t = -1
cons_wei = (self.info_base['水']['每箱质量'] * cons_wat + self.info_base['食物']['每箱质量'] * cons_foo)
return self.__update(cons_foo=cons_foo, cons_wat=cons_wat, cons_t=cons_t, cons_wei=cons_wei)
# 挖矿
def mining(self, day):
if not self.T_judge():
return False
self.day += 1
cons_foo, cons_wat = self.__cons_base(day)
cons_foo = -3 * cons_foo
cons_wat = -3 * cons_wat
cons_t = -1
cons_mon = +self.info_base["基础收益"]
cons_wei = (self.info_base['水']['每箱质量'] * cons_wat + self.info_base['食物']['每箱质量'] * cons_foo)
return self.__update(cons_foo=cons_foo, cons_wat=cons_wat, cons_t=cons_t, cons_mon=cons_mon, cons_wei=cons_wei)
# 购买
def buy(self, wat, foo, loc):
"""
:param foo:
:param wat:
:param loc: 表示购买东西的位置,1为起点,2为村庄
:return:
"""
cons_foo = foo
cons_wat = wat
# cons_mon = -(self.info_base["水"]["基准价格"] * wat + self.info_base["食物"]["基准价格"] * foo)
cons_mon = -(self.info_base["水"]["基准价格"] * wat + self.info_base["食物"]["基准价格"] * foo) if loc == 0 else -2 * (self.info_base["水"]["基准价格"] * wat + self.info_base["食物"]["基准价格"] * foo)
cons_wei = +(self.info_base['水']['每箱质量'] * wat + self.info_base['食物']['每箱质量'] * foo)
return self.__update(cons_foo=cons_foo, cons_wat=cons_wat, cons_mon=cons_mon, cons_wei=cons_wei)
例:起点1(x1,x2)——矿场12(x3)——村庄15(x4,x5)——终点27,参数个数为:5
最后使用差分进化算法(scipy.optimize.differential_evolution)进行求解。这两个步骤整合后,代码如下:
from player import Player
from 生成树 import Tree
from scipy.optimize import differential_evolution
import pandas as pd
def loss(x, table: pd.DataFrame, paraRoute: list, route: list):
def line(a: Player, loc1, loc2, table=table):
# 从一个地点到另一个地点
STEP = table.at[loc1, loc2]
while a.step != STEP:
if not a.walk(a.day):
return False
# print(a.day, a.step, a.Weather[a.day])
a.step = 0
return True
def mine(a: Player, T, table=table):
if T > 0: # 决定在矿山工作必须休息一天
a.rest(a.day)
T = a.day + T
while a.day <= T:
if not a.mining(a.day):
return False
return True
a = Player()
X = [int(x[i] * j) for i, j in enumerate(paraRoute)]
t = []
for i in range(len(route) - 1):
if route[i] in [1, 15]:
# t.append(a.buy(X.pop(0), X.pop(0)))
t.append(a.buy(X.pop(0), X.pop(0)),i)
elif route[i] in [12]:
t.append(mine(a, X.pop(0)))
t.append(line(a, route[i], route[i + 1]))
if not all(t): # 生存时间越长分数越高
score = 10
for i in t:
if i:
score -= 1
else:
break
return score
return -a.score()
table = pd.read_excel("../table/最短天数邻阶矩阵.xlsx", index_col=0, header=0)
List = list(table.index)
a = Tree(List, table)
for leaf in a.leaList:
route = []
Tree.output(leaf, route)
para = Tree.routeToPara(route)
# 差分进化算法
res = differential_evolution(loss, bounds=[(0, 1) for i in range(para[0])], disp=False, popsize=10, args=(table, para[1], route))
print(route, [int(res.x[i] * j) for i, j in enumerate(para[1])], -res.fun, sep='t')
对所有结果进行求解(有python可以算一下,表格需要自己做一个),最高分数为11870(启发式算法求得,不一定是最优解,可以多次计算取最高的),经过的未知为[1, 15, 12, 15, 12, 27],对应每个未知的参数为[236, 173, 75, 193, 6, 147, 118, 1]。
将最前面的最短path填进去result就完成了。本人很懒,此处就不写了。
1、第二关就是把表格换一下,最短路径换一下,一样可以求解。
2、天气未知的情况需要做一些假设,通过使用期望的方式,让玩家带足够水和食物使其在沙漠中死亡的概率低于百分之5啥的,总之是将天气未知化为某种程度的已知进行计算,再进行微调。
3、对于多人的可能会复杂点。可以让多个人一个一个进行决策,后一个人在前一个人的基础上进行决策,简化问题;将一个假设修改为任何一个位置可以选择休息,这样路径上每个位置都会多一个时间参数,参数会变多;若继续使用差分进化不能求解,则可以对所有路径的生成树进行人工剪枝,或者分析得到几个较优的候选路径。
大概就这么多吧,这些都是我自己的分析和看法,怕我的想法有错误误导了你们,所有我在比赛完了的今天发出来。
#######简陋的分界线########
谢谢@bbbroglie 的提醒,已将村庄两倍价格修改过来,修改之后的结果如下,最优为路径[1, 15, 12, 15, 27],各位置参数为[146, 379, 163, 0, 6, 41, 0],最终总分数是10442.5
上面的代码只需要把行修改一下就可以了,上面结果就不改了(我太懒了)
# 求解中36行t.append(a.buy(X.pop(0), X.pop(0)))改为:
t.append(a.buy(X.pop(0), X.pop(0)),i)
# player中111行cons_mon = -(self.info_base["水"]["基准价格"] * wat + self.info_base["食物"]["基准价格"] * foo)改为:
cons_mon = -(self.info_base["水"]["基准价格"] * wat + self.info_base["食物"]["基准价格"] * foo) if loc == 0 else -2 * (self.info_base["水"]["基准价格"] * wat + self.info_base["食物"]["基准价格"] * foo)
贴一下所有情况的计算结果:
[1, 12, 15, 12, 15, 12, 15, 12, 15, 12, 15, 27] [206, 208, 0, 103, 158, 1, 169, 160, 1, 180, 23, 0, 384, 266, 22, 195, 295] 5.0
[1, 12, 15, 12, 15, 12, 15, 12, 15, 12, 27] [256, 208, 0, 136, 188, 1, 6, 160, 0, 154, 14, 0, 326, 119, 19] 5.0
[1, 12, 15, 12, 15, 12, 15, 12, 15, 27] [253, 207, 0, 193, 156, 1, 30, 97, 0, 78, 183, 0, 274, 519] 5.0
[1, 12, 15, 12, 15, 12, 15, 12, 27] [198, 239, 0, 209, 140, 1, 56, 121, 1, 46, 140, 0] 5.0
[1, 12, 15, 12, 15, 12, 15, 27] [179, 331, 0, 168, 63, 3, 72, 77, 0, 33, 7] 6125.0
[1, 12, 15, 12, 15, 12, 27] [178, 333, 0, 191, 41, 3, 83, 104, 0] 6140.0
[1, 12, 15, 12, 15, 27] [178, 333, 0, 222, 113, 6, 40, 14] 8620.0
[1, 12, 15, 12, 27] [195, 307, 1, 225, 131, 4] 8085.0
[1, 12, 15, 27] [213, 279, 2, 58, 18] 8205.0
[1, 12, 27] [214, 234, 0] 7590.0
[1, 15, 12, 15, 12, 15, 12, 15, 12, 15, 12, 15, 27] [138, 190, 164, 105, 0, 84, 15, 0, 60, 130, 1, 54, 156, 0, 108, 297, 14, 364, 360] 7.0
[1, 15, 12, 15, 12, 15, 12, 15, 12, 15, 12, 27] [244, 114, 38, 88, 0, 33, 140, 1, 92, 71, 0, 151, 49, 0, 7, 323, 14] 7.0
[1, 15, 12, 15, 12, 15, 12, 15, 12, 15, 27] [221, 121, 80, 83, 0, 31, 184, 0, 109, 160, 1, 33, 44, 0, 187, 43] 7.0
[1, 15, 12, 15, 12, 15, 12, 15, 12, 27] [152, 181, 190, 55, 1, 59, 173, 0, 32, 83, 0, 34, 99, 0] 7.0
[1, 15, 12, 15, 12, 15, 12, 15, 27] [98, 452, 94, 14, 0, 177, 10, 3, 67, 0, 0, 16, 2] 6930.0
[1, 15, 12, 15, 12, 15, 12, 27] [98, 453, 144, 11, 0, 127, 9, 3, 83, 5, 0] 6940.0
[1, 15, 12, 15, 12, 15, 27] [148, 378, 161, 3, 6, 133, 103, 1, 19, 1] 9222.5
[1, 15, 12, 15, 12, 27] [147, 377, 162, 1, 6, 149, 106, 1] 9245.0
[1, 15, 12, 15, 27] [146, 379, 163, 0, 6, 41, 0] 10442.5
[1, 15, 12, 27] [174, 339, 135, 0, 4] 9390.0
[1, 15, 27] [144, 156, 0, 0] 7720.0
[1, 27] [40, 42] 9385.0