深圳杯数学建模2020c题_2020数学建模B题

b8c6cbe4fbde2bc0d6d400aa7655971c.png

2020数学建模B题

本人咸鱼一条,参加过19年数学建模,当时在B题和C题之间选择,最后还是选择了C题(其实是B题不会写)。看着去年九月参加比赛的教室,今年也坐着三人一组的建模小队,查资料、分析数据、编程,触景生情(我又老了一岁),心血来潮也看了看今年的建模题目,类型还是和去年一样,三道题目B题的游戏模拟走出沙漠让我(游戏迷)眼前一亮,闲着无聊就拿这一题做一做。

真是越来越罗嗦了,废话少说,我直接上我B题的思路吧!

#######简陋的分界线########

下面均是我自己的看法和观点,是对是错我也不知道(无辜)。我只对第一问的第一关进行求解(第二关也可以求,本人太懒,不想根据图构建关联矩阵)。

问题假设

  1. 游戏终止的标志:玩家死亡 or 规定时间未走出沙漠 or 走到终点
  2. 正常情况下,玩家不会在任何位置休息,除非是天气影响。
  3. 玩家每次从一个节点走到另一个节点会走最短路径(玩家很聪明)

根据假设2和假设3,玩家的路径可以由村庄、矿场、起点和终点,以及它们之间的最短路径确定。

例如:玩家的路线可以是:起点1——line(1,12)——矿山12——line(12,27)——终点27

解题思路

1、构建节点[1,12,15,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

最短天数矩阵

acb15ecdc33df5abf7cb9ecdc2bd07f3.png
最短天数矩阵(不考虑天气)

2、计算所有可能的路径

此处使用递归生成树结构,求所有可能路径,直接上代码吧

 # 文件名:生成树
 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))

3、计算在已知路径下,规定时间内到达终点的最高分数

思路:

  • 玩家只有三个行为,买东西(水和食物)、挖矿(天数)和赶路
  • 在时间已知的情况下,从一个位置到另一个位置的赶路时间是可计算的
  • 在路径确定的情况下,影响赶路的只有挖矿天数(这里假设买东西不需要时间,也可假设买东西需要时间)与是否有足够的水和食物;
  • 路径确定的情况下,已知在起点和经过的村庄买过的水和食物,还有经过矿场时挖矿天数,就可以判断玩家是否按时到达终点,以及最终分数。

首先构建一个玩家对象:

 # @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)

4、计算每一条路径的未知参数:

例:起点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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值