ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

多目标优化实战:NSGA-II与Matlab求解电动汽车充电负荷调度问题

多目标优化实战:NSGA-II与Matlab求解电动汽车充电负荷调度问题 1. 峰谷分时电价下的充电负荷优化到底在优化什么先聊几句这个题目的背景。我自己最早接触这个方向是因为看到一组实测数据某小区配电变压器在冬季傍晚的负载率能从日常的 40% 左右直接干到 92%根因就是下班回家后大量电动车同时开始充电。那时候物业经理很头疼变压器扩容成本太高不改又面临过载跳闸风险。这个场景其实就是电动汽车充电负荷优化的核心价值所在把无序的充电行为在时间和空间上重新排列让电网更平稳、让用户花更少的钱。很多初学者一上来就盯着 NSGA-II 和 Matlab 代码看觉得这是个算法题。但如果你不从物理模型和经济学模型出发直接套用遗传算法框架最后跑出来的结果往往很漂亮却完全不能落地。原因很简单你优化的目标函数和约束条件没有真实反映电网和用户的诉求。所谓峰谷分时电价引导下的充电负荷优化本质上是一个双层逻辑上层是电网侧希望充电负荷曲线尽量削峰填谷即用电高峰时段少充电、低谷时段多充电降低电网峰谷差和运行压力。下层是用户侧在电价信号引导下用户或充电运营商希望充电费用尽量低同时满足出行需求。这两层目标之间存在冲突。用户可能为了省电费全挤到凌晨 0 点以后充电这又会在凌晨制造一个新峰。因此这类问题天然具备多目标优化的属性简单的单目标加权求和很难处理好因为它没法在 Pareto 前沿上给出完整的方案集合供决策者挑选。而 NSGA-II 之所以成为这个领域的主流算法是因为它在处理两个或三个相互冲突目标时有非常成熟的三板斧快速非支配排序、拥挤度距离、精英保留策略。后面我会逐个拆解。这篇博文我会按照一个完整的项目实施路径来写从问题建模开始到算法设计再到 Matlab 代码实现的关键片段最后给出仿真结果分析和几个我实际踩过的坑。目标是让你拿到这篇内容之后能够独立复现一套可运行的充电负荷优化程序并且知道怎么调参、怎么看结果。2. 充电负荷建模先搞清楚每辆车的充电画像2.1 单辆电动汽车的充电需求参数在做任何优化之前得先回答一个问题一辆电动汽车它什么时候需要充电充多久功率多大这里面涉及以下几个关键参数每个都需要先定义清楚电池容量kWh常见的家用电动车在 40~100 kWh 之间仿真里通常取 40~60 kWh。起始荷电状态State of ChargeSOC用户开始充电时的剩余电量百分比。这个值直接影响需要充入的电量。目标 SOC用户期望充到的电量一般设置为 0.8~0.9很少会追求充到 100%因为最后阶段充电功率下降时间长且对电池有一定影响。充电功率家用交流慢充常见 3.5 kW、7 kW直流快充可达 50~120 kW。在居民区充电负荷优化中通常按 7 kW 来算部分场景也会考虑 3.5 kW 的慢充。可充电时间窗用户从到家插枪到第二天出门拔枪的时间区间。这个时间窗一般取 18:00 到次日 07:00。把这些参数整合起来每一辆车就可以表示成一个结构化的充电请求。在 Matlab 中我习惯用结构体数组或表格来存储比如% 单台车辆的充电需求参数 vehicle(1).batteryCapacity 60; % kWh vehicle(1).startSOC 0.2; % 起始SOC vehicle(1).targetSOC 0.9; % 目标SOC vehicle(1).chargePower 7; % kW, 慢充功率 vehicle(1).arriveTime 18; % 到家时刻小时连续值 vehicle(1).leaveTime 7; % 第二天离开时刻需要充入的电量就是vehicle(1).requiredEnergy (vehicle(1).targetSOC - vehicle(1).startSOC) * vehicle(1).batteryCapacity;这个逻辑很简单但它决定了后续优化的可行域不管怎么优化每辆车在整个时间窗内充入的总电量必须不小于 requiredEnergy否则用户第二天出不了门。2.2 时间离散化与决策变量设计充电负荷优化的时间尺度通常是 15 分钟或者 1 小时。我之前做过 1 小时粒度的版本也做过 15 分钟粒度的版本。需要提醒的是粒度越细决策变量的维度越高NSGA-II 的搜索空间指数级膨胀计算耗时显著增加。如果你刚开始做建议先用 1 小时粒度把整个流程跑通再做精细化。假设优化时段为 18:00 至次日 07:00总共 13 个小时。如果按 1 小时间隔那么决策变量就是这 13 个时段每辆车的充电状态0 表示不充1 表示充满功率充电。对于 N 辆车决策变量的维度是 N×13。在 NSGA-II 中种群中的每个个体就是一组完整的充电决策矩阵。比如 100 辆车、13 个时段一个体就有 1300 个二进制决策变量。这个规模对 NSGA-II 来说并不算大但需要合理安排编码方式否则交叉变异会产生大量不可行解。我用的是二进制编码加分段修复策略把每个车辆的充电序列当作一个独立的染色体片段交叉时按车辆维度分段进行尽量避免破坏单辆车的时间连续性约束。2.3 约束条件的数学表达充电负荷优化的约束分两类硬约束和软约束。硬约束违反之后解就是不可行的必须修复或淘汰。典型的硬约束包括每辆车的总充电电量约束在 h 个时段内累积充电量要达到需求电量。充电时间窗约束只能在到达之后、离开之前充电。充电功率上限约束单车的充电功率不能超过充电桩的额定功率。可中断约束可选如果默认充电过程不可中断那充电时段必须连续如果允许中断很多实际充电桩支持APP远程启停则不需要这个约束。软约束则用于构造优化目标比如用户充电费用要低与分时电价直接相关。整体负荷曲线要尽量平坦电网侧诉求。峰时充电比例要低削峰。写数学表达式的时候我建议把所有约束都先整理成矩阵形式这样后面写 Matlab 向量化代码会非常快。以总充电量为例hourlyEnergy decisionMatrix .* chargePower; % N x H 矩阵 totalEnergy sum(hourlyEnergy, 2); % 每辆车总充电量 violation max(0, requiredEnergy - totalEnergy); % 欠充量这个 violation 就是约束违反量后面在 NSGA-II 里做约束处理时可以直接用它做可行性偏好判断。3. 峰谷分时电价多目标优化中的第一驱动力3.1 电价曲线的设定与实际政策特征峰谷分时电价是引导用户充电行为的核心信号。不同地区的电价政策不同但基本逻辑一致把一天 24 小时划分为峰段、平段、谷段执行不同的电价水平。我在仿真里常用的一个典型分时电价方案如下时段时间范围电价元/kWh峰时08:00 - 11:00, 18:00 - 21:001.088平时07:00 - 08:00, 11:00 - 18:000.685谷时21:00 - 次日07:000.366从这些数字能看到两个关键特征一是峰谷价差接近 3 倍这是引导用户行为的经济杠杆二是谷时段正好覆盖夜间和电动车夜间停车充电的场景天然匹配。不过在建模时有一个容易忽略的细节电动汽车的充电时间窗18:00 - 次日07:00里既有峰时段18:00 - 21:00又有谷时段21:00 - 次日07:00还有部分平时段。因此用户的充电费用问题其实是一个如何在不同电价时段分配充电量的决策问题。3.2 电价为引导而非强制的建模方式很多人做这个题目时有个误区认为峰谷电价条件下用户一定全部选择谷时充电。现实中完全不是这样。原因在于充电时间窗可能很短。比如用户 20:00 到家、次日 06:00 离开如果用 7 kW 慢充给 60 kWh 电池从 20% 充到 90%需要约 6 小时。就算从 21:00 谷时开始充也要充到凌晨 03:00 左右才能满足需求。此时没有太多选择空间。出行需求是硬约束。如果第二天早上用车必须保证电量充足不能因为想省电费而冒险。充电桩的功率逐年提升7 kW 升级到 20 kW 以上之后充电时间缩短谷时充电的经济优势不再绝对。所以在多目标优化中电价是引导而不是强制。目标函数必须能够量化这种引导作用费用越低个体越优秀但若只盯着费用又会让负荷曲线变得很差。这正是引入 NSGA-II 来做多目标权衡的核心原因。3.3 充电费用目标函数的细节设计我常用的充电费用目标函数如下% priceCurve: 每个时段的电价元/kWh % hourlyEnergy: 每辆车每个时段的充电量kWh % 总费用 所有车辆所有时段的充电电费之和 totalCost sum(sum(hourlyEnergy .* priceCurve));这个式子本身特别简单但实际使用时建议再做两点扩展第一如果考虑电池损耗可以在峰时充电额外加一个惩罚系数表示快充或高峰充电对电池寿命的影响。第二如果考虑充电负荷对变压器寿命的影响可以在负荷高峰时段加额外的电网侧惩罚。当然这些属于进阶玩法初期验证算法时先不要加太多权重否则不容易判断算法行为是否正常。4. NSGA-II 核心机制非支配排序与拥挤度究竟在做什么4.1 为什么要用 NSGA-II而不是加权求和先打个比方。你手里有两个目标省钱和稳电网。加权求和的做法相当于先主观拍板说省钱比稳电网重要 3 倍然后在优化中要求算法严格按这个比例去折中。问题是这个 3 倍的比例你凭什么定在峰谷差特别大的小区和峰谷差已经很小的小区这个比例应该一样吗NSGA-II 的思路完全不一样它不要求你事先定权重而是把解集里所有互不支配的方案都找出来构成一个 Pareto 前沿。什么叫互不支配简单说就是方案 A 在省钱上优于 B但在稳电网上劣于 BA 和 B 谁也不能完全碾压对方它们都是非劣解。这个特性在实际工程决策中特别有价值你可以先跑一次算法拿到几十个 Pareto 最优解然后根据当时当地的实际情况比如近期变压器负荷压力大不大、电价政策有没有调整空间从里面挑选一个合适的折中方案而不是被一个固定权重锁死。4.2 快速非支配排序的原理与实现NSGA-II 的第一个核心步骤是快速非支配排序。它的任务是把种群中的所有个体划分成不同的非支配层级第一层Pareto rank 1种群中不被任何其他个体支配的个体集合。第二层Pareto rank 2去掉第一层个体后剩余个体中不被支配的集合。依此类推。用代码实现的时候每个个体要记录两个指标被多少个体支配domination count以及支配了哪些个体dominated set。然后逐层剥洋葱。下面是一个简化版本的非支配层级分配逻辑供参考function ranks fastNonDominatedSort(objVals) N size(objVals, 1); ranks zeros(N, 1); dominatedSet cell(N, 1); dominationCount zeros(N, 1); for i 1:N for j 1:N if i j, continue; end if dominates(objVals(i,:), objVals(j,:)) dominatedSet{i}(end1) j; dominationCount(j) dominationCount(j) 1; elseif dominates(objVals(j,:), objVals(i,:)) dominationCount(i) dominationCount(i) 1; end end end currentRank 1; while any(ranks 0) front find(dominationCount 0 ranks 0); for k 1:length(front) idx front(k); ranks(idx) currentRank; for m 1:length(dominatedSet{idx}) dominatedIdx dominatedSet{idx}(m); dominationCount(dominatedIdx) dominationCount(dominatedIdx) - 1; end end currentRank currentRank 1; end end function isDom dominates(a, b) isDom all(a b) any(a b); end这里 for i/j 的双重循环在种群规模大时性能会下降适合小规模示例。实际工程可以改成向量化批次处理但逻辑核心不变。4.3 拥挤度距离让 Pareto 前沿的皮肤更光滑只有非支配排序还不够。想象一下如果所有第一层个体都挤在 Pareto 前沿的某一小段虽然它们都是非支配的但对你做决策帮助不大。你更希望前沿上的解均匀铺开能清楚看到省钱和稳电网之间的权衡全貌。拥挤度距离就是为此设计的。对每个目标维度先把个体按目标值排序然后计算每个个体在目标空间里和邻居的距离。边界个体的拥挤度设为无穷大保证它们一定被保留下来。计算拥挤度的核心代码function crowdDist crowdingDistance(objVals) [N, M] size(objVals); crowdDist zeros(N, 1); for m 1:M [~, sortIdx] sort(objVals(:, m)); crowdDist(sortIdx(1)) inf; crowdDist(sortIdx(end)) inf; fmin objVals(sortIdx(1), m); fmax objVals(sortIdx(end), m); if fmax - fmin 1e-12, continue; end for i 2:N-1 idx sortIdx(i); crowdDist(idx) crowdDist(idx) ... (objVals(sortIdx(i1), m) - objVals(sortIdx(i-1), m)) / (fmax - fmin); end end end注意这里每个目标都要算一次。目标之间的量纲差异极大费用可能是几十元负荷方差可能是几千所以必须做归一化。这就是除 (fmax - fmin) 的原因。4.4 精英保留策略每一代的最优解都不能丢NSGA-II 的第三个机制是精英保留把父代种群和子代种群合并构成规模 2N 的临时种群然后按非支配层级低优先 同层拥挤度大优先的原则筛选出 N 个个体进入下一代。这样做的好处是整个进化过程中一旦产生过比较好的解它们就有机会一直活到算法结束不会因为交叉变异而丢失。这一点在实际跑代码时非常重要你会发现如果不做精英保留算法收敛曲线会震荡得很厉害最终结果时好时坏极不稳定。5. Matlab 实现从降价处理到分层优化的完整流程5.1 NSGA-II 主循环的基本框架在 Matlab 里实现 NSGA-II我的习惯是把它拆成几个函数文件不要都塞在一个脚本里。基本框架如下% NSGA-II 主循环伪代码框架 popSize 100; maxGen 200; nVar N * H; % 决策变量维度 % 初始化种群 population initPopulation(popSize, nVar); for gen 1:maxGen % 评价适应度两个目标 约束违反量 [objVals, violations] evaluate(population, vehicles, priceCurve); % 快速非支配排序 ranks fastNonDominatedSort(objVals); % 计算拥挤度 crowdDist crowdingDistance(objVals); % 二进制锦标赛选择 parentIndex tournamentSelection(ranks, crowdDist); % 交叉与变异 offspring crossoverAndMutation(population(parentIndex, :), pc, pm); % 合并父代与子代做精英保留 [population, objVals] elitismSelection(...); end5.2 二进制锦标赛选择的具体逻辑锦标赛选择的做法是随机挑两组个体每组通常 2 个在每组内部按照先比非支配层级再比拥挤度的规则选出胜者作为父代。function idx tournamentSelection(ranks, crowdDist) candidates randperm(length(ranks), 2); c1 candidates(1); c2 candidates(2); if ranks(c1) ranks(c2) idx c1; elseif ranks(c2) ranks(c1) idx c2; else if crowdDist(c1) crowdDist(c2) idx c1; else idx c2; end end end这个操作的目的是让种群持续向 Pareto 前沿方向推进同时保留一定随机性避免过早收敛到局部最优。5.3 交叉变异中的自适应调整最开始的版本里我固定使用交叉概率 0.9、变异概率 0.05跑下来整体效果还可以但总有个问题进化前期收敛快后期却容易陷入局部最优。后来我改成自适应策略效果明显改善。自适应策略的基本思路是根据当前种群的非支配层级动态调整变异概率。当种群中第一层个体比例太低时说明多样性差增大变异概率当第一层个体比例很高时说明种群可能已经接近前沿减小变异概率加强局部搜索。% 自适应变异概率示例 front1Ratio sum(ranks 1) / length(ranks); if front1Ratio 0.2 pm 0.08; elseif front1Ratio 0.5 pm 0.05; else pm 0.02; end5.4 约束处理的实现细节进化算法处理约束最粗暴的做法是罚函数但罚因子怎么定是个玄学。我后来采用了更稳妥的办法约束支配原则。具体做法是如果两个个体都可行则按非支配关系比较优劣。如果一个可行一个不可行则可行个体优先。如果两个个体都不可行则约束违反量小者优先。这种做法不需要设罚因子结果更加稳定。在 matlab 代码里比较两个个体优劣时多判断一层即可function better isBetter(rank1, crowd1, viol1, rank2, crowd2, viol2) if viol1 0 viol2 0 better true; elseif viol1 0 viol2 0 better false; elseif viol1 0 viol2 0 better viol1 viol2; else % 两者均可行 if rank1 ~ rank2 better rank1 rank2; else better crowd1 crowd2; end end end这个约束支配技巧在所有需要处理约束的进化优化问题里都能用强烈建议记下来。6. 两个核心目标函数的设计经济成本与电网负荷波动6.1 目标一用户充电总费用最小之前已经给出了总费用的表达式这里要补充一个容易踩坑的地方电费的计算单位。如果电价单位是元/kWh充电量单位是kWh那么两者直接相乘得到的是元。这个逻辑听起来简单但你一旦把时间粒度从 1 小时改成 15 分钟电价曲线也要按同样粒度重新生成否则计算会差 4 倍。我的经验是把电价曲线显式地做出来而不是每次在循环里临时查表这样既清楚又不容易出错。另一个值得注意的点是目标一里要不要考虑基本电费或容量费我的建议是初期仿真先不分摊基本电费只算电量电费。因为基本电费是固定的不影响优化结果中充电时段的相对优劣只影响总费用的绝对值。6.2 目标二电网侧负荷波动最小化电网侧的目标怎么定义大多数人第一反应是负荷峰值最小。但实际用下来我更推荐用负荷标准差最小或负荷方差最小因为只压峰值可能导致谷时疯狂充电把负荷曲线做成一个大坑填上之后又挖出一个小坑的形状。% 计算电网侧负荷方差 baseLoad 200; % 基础负荷不含充电负荷单位 kW hourlyTotalLoad baseLoad sum(hourlyEnergy, 1); % 每个时段的总体负荷 avgLoad mean(hourlyTotalLoad); loadVariance mean((hourlyTotalLoad - avgLoad).^2); % 目标二即 loadVariance这个目标看起来简单但它对充电负荷曲线的形状非常敏感可以有效引导 NSGA-II 找到那种多辆车错峰充电、整体负荷平稳的方案。6.3 两个目标之间的信息传递与绘图完成 NSGA-II 后要输出最终 Pareto 前沿图。我在 Matlab 里通常用散点图来展示横轴是总费用纵轴是负荷方差每个点代表一个非支配解颜色深浅表示非支配层级。绘制时有一个细节先把所有个体按非支配层级过滤只画出第一层的解。如果你把很多层都画上去图会非常乱看不出 Pareto 前沿的形状。front1Idx find(ranks 1); scatter(objVals(front1Idx, 1), objVals(front1Idx, 2), 30, filled); xlabel(Total Charging Cost (yuan)); ylabel(Load Variance (kW^2)); title(Pareto Front of Charging Load Optimization);7. 仿真结果分析方法不从解集里挑一个等于白跑7.1 必看的三张图很多新手跑完 NSGA-II打印几个数字就觉得完事了。实际上做优化研究最忌讳的就是只看数字不看图。我建议至少做以下三张图第一张是 Pareto 前沿散点图看解的分布是否均匀、前沿是否光滑。如果前沿出现明显断带说明拥挤度处理或种群多样性还有问题。第二张是充电负荷日曲线对比图。把优化前的无序充电负荷和优化后的负荷叠加在一起画能直观看出削峰填谷的效果。通常你会看到优化后的负荷曲线的峰明显降低谷稍微抬高整体更加平缓。第三张是各时段平均充电车辆数或充电功率的柱状图。它会告诉你优化后的充电行为主要集中在了几点到几点是否贴合谷时电价区间。如果曲线完全集中在谷时段可能说明用户费用目标权重压过了电网波动目标如果曲线上还有不少峰时段充电可能说明车辆充电约束太紧没有调度空间。7.2 从 Pareto 解集中选最终方案的两种思路拿到一组 Pareto 最优解之后最终怎么落地选一个工程上常用这两种方法。一是最小距离法先算一个理想点每个目标都取所有解中的最优值现实中达不到然后在 Pareto 前沿上找一个离理想点欧氏距离最近的解。这个方法简单直观适合快速取舍。二是人工决策法根据当时的实际需求选。比如某天电网负荷压力特别大就选负荷方差更小的解哪怕电费稍高。我实际做过一个案例50 辆电动车、13 个时段、两个优化目标NSGA-II 跑 200 代。最终 Pareto 前沿上的费用跨度大约在 800 元到 1250 元之间负荷方差跨度大约在 900 到 1600 之间。用最小距离法选出的折中解比最小费用解多花了约 100 元电费但负荷方差下降了约 35%。这 100 元的额外成本对电网来说可能就是避免一台变压器过载的代价。7.3 遗传代数与种群规模怎么调关于参数设置我的实际经验如下参数推荐范围说明种群规模100~300车辆数越多、时段越细种群应该越大最大遗传代数200~500先用小值跑通再按收敛趋势加大交叉概率0.7~0.95太高容易破坏好解结构变异概率0.01~0.1自适应当前种群非支配层比例更稳锦标赛规模22 是最稳妥的规模太大会降低多样性判断是否收敛不要只看总费用有没有下降要看 Pareto 前沿在迭代后期是否还在明显移动。如果连续 50 代前沿结构几乎不变基本可认为收敛了。8. 常见踩坑我对这套模型最有价值的几点教训8.1 二进制编码下的无效解问题我最初处理决策变量时踩过一个典型的坑直接对整条染色体做单点交叉结果交叉后某个片段出现了两次充电之间夹着一段不充电的断续模式。如果约束条件限定充电过程不可中断这种个体就大量变成了不可行解导致种群质量急剧下降。解决方法我前面提到过就是按车辆维度分段交叉并在变异后增加一个修复函数对每辆车的充电序列进行连续性检查。修复函数不复杂核心逻辑是找到第一个充电时段和最后一个充电时段把中间的断点全部填充为充电状态然后根据目标 SOC 对尾部时段做削填调整。function repairedSeq repairContinuity(seq, requiredDuration) % 找到最早和最晚的充电时段 firstIdx find(seq, 1, first); lastIdx find(seq, 1, last); if isempty(firstIdx), repairedSeq seq; return; end % 中间全部置 1 seq(firstIdx:lastIdx) 1; % 若总充电时长超过需求从尾部开始削减 while sum(seq) requiredDuration seq(lastIdx) 0; lastIdx lastIdx - 1; end repairedSeq seq; end当然这个修复逻辑比较朴素更精细的做法是优先削减电价较高的时段但作为参考已经能解决大部分不可行解问题。8.2 分时电价曲线和优化时段必须对齐有一次我在跑一个案例时把电价曲线的时段设置成了 0~24 点而优化变量只覆盖 18 点到次日 7 点。结果计算电费时出现了莫名其妙的错位高峰期和低谷期的电价匹配完全错误。最后排查发现是索引偏移问题。我的建议是在代码开头强制做一次断言检查确保电价曲线和决策变量的时间轴完全一致。哪怕每天只跑一次这个检查的价值也远大于它消耗的那几毫秒。assert(length(priceCurve) H, 电价曲线长度必须等于优化时段数);8.3 车辆数量较大时的计算加速思路当车辆数量超过 200、种群规模 300、遗传代数 300 时Matlab 纯循环跑起来会很慢一次实验可能半小时以上。我的经验是可以从两个方向加速。一是向量化。评价目标函数时尽量避免 per-vehicle 的 for 循环直接用矩阵运算。比如 hourlyEnergy 用 N×H 矩阵整体计算总费用时sum(sum(hourlyEnergy .* priceCurve)) 的速度远快于双层 for 循环。二是用并行计算。Matlab 的并行池在 NSGA-II 这种每代评价相互独立的结构下很有效。不过要注意如果目标函数比较简单并行反而因为通信开销变慢所以先用向量化再考虑并行。8.4 关于结果看起来很好的警惕最后分享一个我在实际研究中的体会。当你跑完一次优化看到负荷曲线从双峰变成平滑曲线时不要急着高兴。先检查是不是约束条件太松了导致所有车都完美避开了峰时但这种完美避让是否透支了车辆的时间灵活性。比如我遇到过一次案例所有车的充电时间完全被推到了凌晨 2 点到 6 点负荷方差非常漂亮但仔细看发现有的车是晚上 20 点到达硬等了 6 个小时才开始充电。虽然从算法角度这个解完全可行但从用户体验角度要求所有用户都深更半夜启动充电器不太现实。所以我在模型里加了一个用户等待时间的软约束把它作为第三个目标或者罚项加入车辆到达后充电开始时间与到达时间之间的延迟不宜超过 4 小时。这样得到的解才真正具有工程落地价值。这也是多目标优化有意思的地方优化的本质不过是在多个相互冲突的合理诉求之间找平衡。NSGA-II 只是把这个平衡过程系统化、可视化了。你掌握它之后不只电动车充电负荷像微电网调度、储能充放电策略、园区能源管理甚至生产排程这类问题都能用同一套思路去求解。做研究的乐趣和功力往往就体现在这种从具体场景到抽象方法、再从抽象方法回到具体场景的循环往复之中。
返回列表