1 前言

网上能找到的python调用cplex示例实在太少,全英文的官方文档又十分难啃,还是从例子学起比较好,转了一圈找到github,发现IBM写了几个例子,但是又没有中文博客解释。借助网页和help,代码看半天终于懂了个大概,搬运一下,同时记录菜鸟笔记,抛砖引玉,欢迎大家批评指正。

2 IBM官方说明

IBM在下述网页中提供了DOcplex说明手册、示例、云端求解等内容,可以大致看一下按需获取。

IBMDecisionOptimization/docplex-examples
https://github.com/IBMDecisionOptimization/docplex-examples

在examples文件夹中,IBM给出了cp和mp的求解例子,我们主要关注mp问题。mp/modeling/中有6个例子,代码点进一看都很长,就先从第一个diet问题开始学习吧。

3 diet问题

题目来源:https://github.com/IBMDecisionOptimization/docplex-examples/blob/master/examples/mp/modeling/diet.py

3.1 背景及建模

数据来源:https://neos-guide.org/content/diet-problem-solver

该问题以最小化费用为目标,选择满足日常营养需求的一组食物。

目标函数:min总费用

每种食品费用 = 数量 × 单价,n种食品累加可得总费用

约束:满足各营养物质的取值范围要求

每种营养物质取值 = sum(数量 × 某食品中每单位该营养物质含量),n种食品累加

如:Calories,取值范围[2000, 2250]; Cholesterol,取值范围[0, 300]

3.2 代码详解

(1)导入库

from collections import namedtuple
from docplex.mp.model import Model
from docplex.util.environment import get_environment

分别用于命名元组、导入docplex和配置环境

(2)导入数据并初始化

FOODS = [("Roasted Chicken", 0.84, 0, 10),("Spaghetti W/ Sauce", 0.78, 0, 10),("Tomato,Red,Ripe,Raw", 0.27, 0, 10),("Apple,Raw,W/Skin", .24, 0, 10),("Grapes", 0.32, 0, 10),("Chocolate Chip Cookies", 0.03, 0, 10),("Lowfat Milk", 0.23, 0, 10),("Raisin Brn", 0.34, 0, 10),("Hotdog", 0.31, 0, 10)
]NUTRIENTS = [("Calories", 2000, 2500),("Calcium", 800, 1600),("Iron", 10, 30),("Vit_A", 5000, 50000),("Dietary_Fiber", 25, 100),("Carbohydrates", 0, 300),("Protein", 50, 100)
]FOOD_NUTRIENTS = [("Roasted Chicken", 277.4, 21.9, 1.8, 77.4, 0, 0, 42.2),("Spaghetti W/ Sauce", 358.2, 80.2, 2.3, 3055.2, 11.6, 58.3, 8.2),("Tomato,Red,Ripe,Raw", 25.8, 6.2, 0.6, 766.3, 1.4, 5.7, 1),("Apple,Raw,W/Skin", 81.4, 9.7, 0.2, 73.1, 3.7, 21, 0.3),("Grapes", 15.1, 3.4, 0.1, 24, 0.2, 4.1, 0.2),("Chocolate Chip Cookies", 78.1, 6.2, 0.4, 101.8, 0, 9.3, 0.9),("Lowfat Milk", 121.2, 296.7, 0.1, 500.2, 0, 11.7, 8.1),("Raisin Brn", 115.1, 12.9, 16.8, 1250.2, 4, 27.9, 4),("Hotdog", 242.1, 23.5, 2.3, 0, 0, 18, 10.4)
]

FOODS用于存储食品名称、单价、数量范围信息;NUTRIENTS用于存储各营养物质名称和取值范围;FOOD_NUTRIENTS用于存储食品内各营养物质含量。

Food = namedtuple("Food", ["name", "unit_cost", "qmin", "qmax"])
Nutrient = namedtuple("Nutrient", ["name", "qmin", "qmax"])

使用namedtuple命名元组,使元组中的数据不仅能通过索引,也能通过自命名访问,增强可读性。

(3)模型建立函数

def build_diet_model(name='diet', **kwargs):ints = kwargs.pop('ints', False)

首先定义build_diet_model函数用于建立模型。**kwargs用于不知道要往函数中传入多少个关键词参数,或者想传入字典的值作为关键词参数时。ints用于判断决策变量类型(选择食品时的单位是整型还是连续型)。

①数据存储形式处理

    foods = [Food(*f) for f in FOODS]nutrients = [Nutrient(*row) for row in NUTRIENTS]

*可传入任意个数的参数。这两条语句可使命名后的元组与原来的数据对应,同时可以print查看foods和nutrients的具体存储信息。

foods = [Food(name=‘Roasted Chicken’, unit_cost=0.84, qmin=0, qmax=10), Food(name=‘Spaghetti W/ Sauce’, unit_cost=0.78, qmin=0, qmax=10), Food(name=‘Tomato,Red,Ripe,Raw’, unit_cost=0.27, qmin=0, qmax=10), Food(name=‘Apple,Raw,W/Skin’, unit_cost=0.24, qmin=0, qmax=10), Food(name=‘Grapes’, unit_cost=0.32, qmin=0, qmax=10), Food(name=‘Chocolate Chip Cookies’, unit_cost=0.03, qmin=0, qmax=10), Food(name=‘Lowfat Milk’, unit_cost=0.23, qmin=0, qmax=10), Food(name=‘Raisin Brn’, unit_cost=0.34, qmin=0, qmax=10), Food(name=‘Hotdog’, unit_cost=0.31, qmin=0, qmax=10)]

nutrients = [Nutrient(name=‘Calories’, qmin=2000, qmax=2500), Nutrient(name=‘Calcium’, qmin=800, qmax=1600), Nutrient(name=‘Iron’, qmin=10, qmax=30), Nutrient(name=‘Vit_A’, qmin=5000, qmax=50000), Nutrient(name=‘Dietary_Fiber’, qmin=25, qmax=100), Nutrient(name=‘Carbohydrates’, qmin=0, qmax=300), Nutrient(name=‘Protein’, qmin=50, qmax=100)]

    food_nutrients = {(fn[0], nutrients[n].name):fn[1 + n] for fn in FOOD_NUTRIENTS for n in range(len(NUTRIENTS))}

fn[0]输出FOOD_NUTRIENTS第1列,即食品名称;nutrients[n].name输出营养物质名称;fn[1 + n],n in range(0,7)输出FOOD_NUTRIENTS的第2~8列,即营养物质含量。

为了将原来的FOOD_NUTRIENTS字典变成food_nutrients中的多键值形式,即{(食品i,营养物质j):值}的形式,需要对每一种食品循环7次(7种营养物质)。这样我们就可以根据食品名称+营养物质名称,快速查找到各食物对应的不同营养含量。

最后得到:food_nutrients = {(‘Roasted Chicken’, ‘Calories’): 277.4, (‘Roasted Chicken’, ‘Calcium’): 21.9, (‘Roasted Chicken’, ‘Iron’): 1.8, (‘Roasted Chicken’, ‘Vit_A’): 77.4, (‘Roasted Chicken’, ‘Dietary_Fiber’): 0, (‘Roasted Chicken’, ‘Carbohydrates’): 0, (‘Roasted Chicken’, ‘Protein’): 42.2, (‘Spaghetti W/ Sauce’, ‘Calories’): 358.2, (‘Spaghetti W/ Sauce’, ‘Calcium’): 80.2, (‘Spaghetti W/ Sauce’, ‘Iron’): 2.3, (‘Spaghetti W/ Sauce’, ‘Vit_A’): 3055.2, (‘Spaghetti W/ Sauce’, ‘Dietary_Fiber’): 11.6, (‘Spaghetti W/ Sauce’, ‘Carbohydrates’): 58.3, (‘Spaghetti W/ Sauce’, ‘Protein’): 8.2, (‘Tomato,Red,Ripe,Raw’, ‘Calories’): 25.8, (‘Tomato,Red,Ripe,Raw’, ‘Calcium’): 6.2, (‘Tomato,Red,Ripe,Raw’, ‘Iron’): 0.6, (‘Tomato,Red,Ripe,Raw’, ‘Vit_A’): 766.3, (‘Tomato,Red,Ripe,Raw’, ‘Dietary_Fiber’): 1.4, (‘Tomato,Red,Ripe,Raw’, ‘Carbohydrates’): 5.7, (‘Tomato,Red,Ripe,Raw’, ‘Protein’): 1, (‘Apple,Raw,W/Skin’, ‘Calories’): 81.4, (‘Apple,Raw,W/Skin’, ‘Calcium’): 9.7, (‘Apple,Raw,W/Skin’, ‘Iron’): 0.2, (‘Apple,Raw,W/Skin’, ‘Vit_A’): 73.1, (‘Apple,Raw,W/Skin’, ‘Dietary_Fiber’): 3.7, (‘Apple,Raw,W/Skin’, ‘Carbohydrates’): 21, (‘Apple,Raw,W/Skin’, ‘Protein’): 0.3, (‘Grapes’, ‘Calories’): 15.1, (‘Grapes’, ‘Calcium’): 3.4, (‘Grapes’, ‘Iron’): 0.1, (‘Grapes’, ‘Vit_A’): 24, (‘Grapes’, ‘Dietary_Fiber’): 0.2, (‘Grapes’, ‘Carbohydrates’): 4.1, (‘Grapes’, ‘Protein’): 0.2, (‘Chocolate Chip Cookies’, ‘Calories’): 78.1, (‘Chocolate Chip Cookies’, ‘Calcium’): 6.2, (‘Chocolate Chip Cookies’, ‘Iron’): 0.4, (‘Chocolate Chip Cookies’, ‘Vit_A’): 101.8, (‘Chocolate Chip Cookies’, ‘Dietary_Fiber’): 0, (‘Chocolate Chip Cookies’, ‘Carbohydrates’): 9.3, (‘Chocolate Chip Cookies’, ‘Protein’): 0.9, (‘Lowfat Milk’, ‘Calories’): 121.2, (‘Lowfat Milk’, ‘Calcium’): 296.7, (‘Lowfat Milk’, ‘Iron’): 0.1, (‘Lowfat Milk’, ‘Vit_A’): 500.2, (‘Lowfat Milk’, ‘Dietary_Fiber’): 0, (‘Lowfat Milk’, ‘Carbohydrates’): 11.7, (‘Lowfat Milk’, ‘Protein’): 8.1, (‘Raisin Brn’, ‘Calories’): 115.1, (‘Raisin Brn’, ‘Calcium’): 12.9, (‘Raisin Brn’, ‘Iron’): 16.8, (‘Raisin Brn’, ‘Vit_A’): 1250.2, (‘Raisin Brn’, ‘Dietary_Fiber’): 4, (‘Raisin Brn’, ‘Carbohydrates’): 27.9, (‘Raisin Brn’, ‘Protein’): 4, (‘Hotdog’, ‘Calories’): 242.1, (‘Hotdog’, ‘Calcium’): 23.5, (‘Hotdog’, ‘Iron’): 2.3, (‘Hotdog’, ‘Vit_A’): 0, (‘Hotdog’, ‘Dietary_Fiber’): 0, (‘Hotdog’, ‘Carbohydrates’): 18, (‘Hotdog’, ‘Protein’): 10.4}

②创建模型

    mdl = Model(name=name, **kwargs)

③设置决策变量(食物数量)

    ftype = mdl.integer_vartype if ints else mdl.continuous_vartypeqty = mdl.var_dict(foods, ftype, lb=lambda f: f.qmin, ub=lambda f: f.qmax, name=lambda f: "q_%s" % f.name)

ftype表示决策变量类型,当调用函数是注明ints=True是整型,False则为连续型。qty表示每种食品的选择数量,下限lb取foods中的f.qmin(至少选多少);上限ub取f.qmax(最多选多少)。变量命名为“q_食物名”,如q_Roasted Chicken。

④添加约束条件(营养物质取值范围)

    for n in nutrients:amount = mdl.sum(qty[f] * food_nutrients[f.name, n.name] for f in foods)mdl.add_range(n.qmin, amount, n.qmax)mdl.add_kpi(amount, publish_name="Total %s" % n.name)

对于每种营养物质,amount为各营养物质总值,qty[f]为食物数量,food_nutrients[f.name, n.name]调用字典得到各食物的不同营养取值,相乘后再累加。再通过mdl.add_range将营养物质总值限制在之前所给范围,用mdl.add_kpi在结果中将各营养物质总含量作为一项关键指标输出"Total 营养物质名”,如Total Calories。

⑤表示目标函数

    total_cost = mdl.sum(qty[f] * f.unit_cost for f in foods)mdl.add_kpi(total_cost, 'Total cost')

各食品数量×单价 = 选择每种食品的花费,累加后得到总花费,同时将总花费Total cost也作为一项kpi输出。

⑥继续添加kpi——所选食物种类数

    def nb_products(mdl_, s_):qvs = mdl_.find_matching_vars(pattern="q_")return sum(1 for qv in qvs if s_[qv] >= 1e-5)mdl.add_kpi(nb_products, 'Nb foods')

建立了另一个函数nb_products用于计算选择的食物种类数。使用find_matching_vars查找包含字符串“q_”的变量,当“q_”有>0的数值时食物种类数sum += 1。

⑦目标函数最小化并返回mdl

    mdl.minimize(total_cost)return mdl

(4)求解模型并输出结果

if __name__ == '__main__':mdl = build_diet_model(ints=True, log_output=True, float_precision=6)mdl.print_information()s = mdl.solve()if s:qty_vars = mdl.find_matching_vars(pattern="q_")for fv in qty_vars:food_name = fv.name[2:]print("Buy {0:<25} = {1:9.6g}".format(food_name, fv.solution_value))mdl.report_kpis()# Save the CPLEX solution as "solution.json" program outputwith get_environment().get_output_stream("solution.json") as fp:mdl.solution.export(fp, "json")else:print("* model has no solution")

调用build_diet_model函数,决策变量为整型,输出日志文件,浮点数显示精度6位数;print_information()打印模型相关信息;mdl.solve() 用于求解模型;mdl.report_kpis()显示关键指标数据;最后将解决方案另存为“ solution.json”程序输出。

if语句内进行格式化输出处理。fv = q_Roasted Chicken,…,截取后面字段得到食物名,输出模型结果“Buy 食物名 = 数量”,如“Buy Spaghetti W/ Sauce = 2”。

其中,qty_vars = [docplex.mp.Var(type=I,name=‘q_Roasted Chicken’,ub=10), docplex.mp.Var(type=I,name=‘q_Spaghetti W/ Sauce’,ub=10), docplex.mp.Var(type=I,name=‘q_Tomato,Red,Ripe,Raw’,ub=10), docplex.mp.Var(type=I,name=‘q_Apple,Raw,W/Skin’,ub=10), docplex.mp.Var(type=I,name=‘q_Grapes’,ub=10), docplex.mp.Var(type=I,name=‘q_Chocolate Chip Cookies’,ub=10), docplex.mp.Var(type=I,name=‘q_Lowfat Milk’,ub=10), docplex.mp.Var(type=I,name=‘q_Raisin Brn’,ub=10), docplex.mp.Var(type=I,name=‘q_Hotdog’,ub=10)]

3.3 结果分析

最后程序输出如下:

先显示变量个数、约束个数等模型信息

再显示求解具体过程



分割线下的就是最后的模型求解结果了

买2单位Spaghetti W/ Sauce,1单位Apple,Raw,W/Skin,10单位Chocolate Chip Cookies,2单位Lowfat Milk和1单位Hotdog可以在满足营养物质要求的基础上实现费用最小化(Total cost = 2.87)。

此外还可以看到其他kpi信息,即各营养物质的总含量和食物种类数。

3.4 完整代码

# --------------------------------------------------------------------------
# Source file provided under Apache License, Version 2.0, January 2004,
# http://www.apache.org/licenses/
# (c) Copyright IBM Corp. 2015, 2018
# --------------------------------------------------------------------------# The goal of the diet problem is to select a set of foods that satisfies
# a set of daily nutritional requirements at minimal cost.
# Source of data: http://www.neos-guide.org/content/diet-problem-solverfrom collections import namedtuplefrom docplex.mp.model import Model
from docplex.util.environment import get_environment# ----------------------------------------------------------------------------
# Initialize the problem data
# ----------------------------------------------------------------------------FOODS = [("Roasted Chicken", 0.84, 0, 10),("Spaghetti W/ Sauce", 0.78, 0, 10),("Tomato,Red,Ripe,Raw", 0.27, 0, 10),("Apple,Raw,W/Skin", .24, 0, 10),("Grapes", 0.32, 0, 10),("Chocolate Chip Cookies", 0.03, 0, 10),("Lowfat Milk", 0.23, 0, 10),("Raisin Brn", 0.34, 0, 10),("Hotdog", 0.31, 0, 10)
]NUTRIENTS = [("Calories", 2000, 2500),("Calcium", 800, 1600),("Iron", 10, 30),("Vit_A", 5000, 50000),("Dietary_Fiber", 25, 100),("Carbohydrates", 0, 300),("Protein", 50, 100)
]FOOD_NUTRIENTS = [("Roasted Chicken", 277.4, 21.9, 1.8, 77.4, 0, 0, 42.2),("Spaghetti W/ Sauce", 358.2, 80.2, 2.3, 3055.2, 11.6, 58.3, 8.2),("Tomato,Red,Ripe,Raw", 25.8, 6.2, 0.6, 766.3, 1.4, 5.7, 1),("Apple,Raw,W/Skin", 81.4, 9.7, 0.2, 73.1, 3.7, 21, 0.3),("Grapes", 15.1, 3.4, 0.1, 24, 0.2, 4.1, 0.2),("Chocolate Chip Cookies", 78.1, 6.2, 0.4, 101.8, 0, 9.3, 0.9),("Lowfat Milk", 121.2, 296.7, 0.1, 500.2, 0, 11.7, 8.1),("Raisin Brn", 115.1, 12.9, 16.8, 1250.2, 4, 27.9, 4),("Hotdog", 242.1, 23.5, 2.3, 0, 0, 18, 10.4)
]Food = namedtuple("Food", ["name", "unit_cost", "qmin", "qmax"])
Nutrient = namedtuple("Nutrient", ["name", "qmin", "qmax"])# ----------------------------------------------------------------------------
# Build the model
# ----------------------------------------------------------------------------def build_diet_model(name='diet', **kwargs):ints = kwargs.pop('ints', False)# Create tuples with named fields for foods and nutrientsfoods = [Food(*f) for f in FOODS]nutrients = [Nutrient(*row) for row in NUTRIENTS]food_nutrients = {(fn[0], nutrients[n].name):fn[1 + n] for fn in FOOD_NUTRIENTS for n in range(len(NUTRIENTS))}# Modelmdl = Model(name=name, **kwargs)# Decision variables, limited to be >= Food.qmin and <= Food.qmaxftype = mdl.integer_vartype if ints else mdl.continuous_vartypeqty = mdl.var_dict(foods, ftype, lb=lambda f: f.qmin, ub=lambda f: f.qmax, name=lambda f: "q_%s" % f.name)# Limit range of nutrients, and mark them as KPIsfor n in nutrients:amount = mdl.sum(qty[f] * food_nutrients[f.name, n.name] for f in foods)mdl.add_range(n.qmin, amount, n.qmax)mdl.add_kpi(amount, publish_name="Total %s" % n.name)# Minimize costtotal_cost = mdl.sum(qty[f] * f.unit_cost for f in foods)mdl.add_kpi(total_cost, 'Total cost')# add a functional KPI , taking a model and a solution as argument# this KPI counts the number of foods used.def nb_products(mdl_, s_):qvs = mdl_.find_matching_vars(pattern="q_")return sum(1 for qv in qvs if s_[qv] >= 1e-5)mdl.add_kpi(nb_products, 'Nb foods')mdl.minimize(total_cost)return mdl# ----------------------------------------------------------------------------
# Solve the model and display the result
# ----------------------------------------------------------------------------if __name__ == '__main__':mdl = build_diet_model(ints=True, log_output=True, float_precision=6)mdl.print_information()s = mdl.solve()if s:qty_vars = mdl.find_matching_vars(pattern="q_")for fv in qty_vars:food_name = fv.name[2:]print("Buy {0:<25} = {1:9.6g}".format(food_name, fv.solution_value))mdl.report_kpis()# Save the CPLEX solution as "solution.json" program outputwith get_environment().get_output_stream("solution.json") as fp:mdl.solution.export(fp, "json")else:print("* model has no solution")

4 总结

从以上内容中可以看出,数据存储形式处理、模型表示是docplex求解的重难点。对于简单的规划问题,docplex求解并不困难,但当问题或数据稍显复杂时,程序中设计的环节变多,需要我们更加仔细、耐心地分析模型和每一个因素。

(由于初学者水平有限,文章不可避免地会出现一些错误,还望大佬们多多指点交流。)

Python+Cplex学习笔记(三)—— docplex官方示例之营养膳食选择相关推荐

  1. Python基础学习笔记三

    Python基础学习笔记三 print和import print可以用,分割变量来输出 import copy import copy as co from copy import deepcopy ...

  2. Programming Computer Vision with Python (学习笔记三)

    概要 原书对于PCA的讲解只有一小节,一笔带过的感觉,但我发现PCA是一个很重要的基础知识点,在机器机视觉.人脸识别以及一些高级图像处理技术时都被经常用到,所以本人自行对PCA进行了更深入的学习. P ...

  3. python爬虫学习笔记(三)——淘宝商品比价实战(爬取成功)

    2020年最新淘宝商品比价定向爬取 功能描述 目标:获取淘宝搜索页面的信息,提取其中的商品名称和价格. 理解:淘宝的搜索接口 翻页的处理 技术路线:requests­          re 程序的结 ...

  4. Python基础学习笔记三(变量和字面量)

    版权声明:本文为 小异常 原创文章,非商用自由转载-保持署名-注明出处,谢谢! 本文网址:https://blog.csdn.net/sun8112133/article/details/957661 ...

  5. Python+cplex运筹优化学习笔记(三)-营养膳食选择

    Python+cplex运筹优化学习笔记(三)-营养膳食选择 前言 首先呢,说明一下,本文只是自己在学习过程中运用到的例子,然后规整总结一下,随便写写自己所做的一些笔记.小白学习,有不对的地方还望大家 ...

  6. Python学习笔记三之编程练习:循环、迭代器与函数

    Python学习笔记三之编程练习 1. 编程第一步 # 求解斐波纳契数列 #/user/bin/python3#Fibonacci series:斐波那契数列 #两个元素的总和确定了下一个数 a,b= ...

  7. python自动化测试学习笔记合集三

    上次我们学到了redis的一些操作,下面来实际运用以下. 这里我们先来学习一下什么是cookie和session. 什么是Cookie 其实简单的说就是当用户通过http协议访问一个服务器的时候,这个 ...

  8. python做直方图-python OpenCV学习笔记实现二维直方图

    本文介绍了python OpenCV学习笔记实现二维直方图,分享给大家,具体如下: 官方文档 – https://docs.opencv.org/3.4.0/dd/d0d/tutorial_py_2d ...

  9. iView学习笔记(三):表格搜索,过滤及隐藏列操作

    iView学习笔记(三):表格搜索,过滤及隐藏某列操作 1.后端准备工作 环境说明 python版本:3.6.6 Django版本:1.11.8 数据库:MariaDB 5.5.60 新建Django ...

最新文章

  1. C语言在一个有序数组里插入一个元素,使其成为一个新的有序数组
  2. vue方法传值到data_vue组件传值的几种方式
  3. 分布式服务框架 Zookeeper — 管理分布式环境中的数据
  4. SFB 项目经验-14-为某客户用Exchange 2016 UM作为总机的问题
  5. atm系统的用例模型_战斗系统执行式测试经验汇总
  6. 如何判断程序员是在装逼还是有真本事?
  7. 精选论文集|Transformer在视觉领域中的应用
  8. 超全面的的常用RAID详解
  9. cfree mysql_如何配置CFree才能开发MySql数据库应用 | 学步园
  10. ppt上显示无法显示图片计算机可能,把手机里做好的PPT导入电脑,为何有些图片会显示不出来?该如何解决?...
  11. 死循环之----恐怖游轮
  12. 六、路由(routing)
  13. 鹏鹏:python 机器学习初学者 三剑客介绍。
  14. MPI多进程问题记录
  15. ICEM-圆柱与长方体相切
  16. Windows系统日志文件分析
  17. 输入名字显示其电话号码
  18. 接口请求之qs的简单应用
  19. Android手机64位APP兼容
  20. 关于2020年全国大学生电子设计竞赛 ——信息科技前沿专题邀请赛(瑞萨杯)竞赛时间调整的通知

热门文章

  1. 考研计算机专业课408,【21计算机考研】专业课统考408院校汇总
  2. adjacency list(邻接表)神物
  3. qq音乐会员联合会员都有哪些
  4. 超详细版-计算网络地址、子网、广播地址、主机数
  5. 接触C#的第一天和回锅Python的第一天
  6. 《娱乐至死》毁掉我们的,恰恰是我们所热爱的东西!
  7. vue即时通讯,一个很好用的插件
  8. android国家码
  9. 幽默感七个技巧_如何让自己变得幽默-16个聊天幽默技巧
  10. 单页双曲面 matlab,如何画双叶双曲面