【优化求解】粒子群算法解决经济调度【Matlab 299期】

一、简介

粒子群优化(PSO)是一种基于群体智能的数值优化算法,由社会心理学家James Kennedy和电气工程师Russell Eberhart于1995年提出。自PSO诞生以来,它在许多方面都得到了改进,这一部分将介绍基本的粒子群优化算法原理和过程。
1.1 粒子群优化
粒子群优化(PSO)是一种群智能算法,其灵感来自于鸟类的群集或鱼群学习,用于解决许多科学和工程领域中出现的非线性、非凸性或组合优化问题。
在这里插入图片描述
1.1.1 算法思想
许多鸟类都是群居性的,并由各种原因形成不同的鸟群。鸟群可能大小不同,出现在不同的季节,甚至可能由群体中可以很好合作的不同物种组成。更多的眼睛和耳朵意味着有更多的及时发现食物和捕食者的机会。鸟群在许多方面对其成员的生存总是有益的:
觅食:社会生物学家E.O.Wilson说,至少在理论上,群体中的个体成员可以从其他成员在寻找食物过程中的发现和先前的经验中获益[1]。如果一群鸟的食物来源是相同的,那么某些种类的鸟就会以一种非竞争的方式聚集在一起。这样,更多的鸟类就能利用其他鸟类对食物位置的发现。
抵御捕食者:鸟群在保护自己免受捕食者侵害方面有很多优势。
更多的耳朵和眼睛意味着更多的机会发现捕食者或任何其他潜在的危险;
一群鸟可能会通过围攻或敏捷的飞行来迷惑或压制捕食者;
在群体中,互相间的警告可以减少任何一只鸟的危险。
空气动力学:当鸟类成群飞行时,它们经常把自己排成特定的形状或队形。鸟群中鸟的数量不同,每只鸟煽动翅膀时产生不同的气流,这都会导致变化的风型,这些队形会充分利用不同的分型,从而使得飞行中的鸟类能够以最节能的方式利用周围的空气。
粒子群算法的发展需要模拟鸟群的一些优点,然而,为了了解群体智能和粒子群优化的一个重要性质,值得提一下是鸟群的一些缺点。当鸟类成群结队时,也会给它们带来一些风险。更多的耳朵和眼睛意味着更多的翅膀和嘴,这导致更多的噪音和运动。在这种情况下,更多的捕食者可以定位鸟群,对鸟类造成持续的威胁。一个更大的群体也会需要更多的食物,这导致更多食物竞争,从而可能淘汰群体中一些较弱的鸟类。这里需要指出的是,PSO并没有模拟鸟类群体行为的缺点,因此,在搜索过程中不允许杀死任何个体,而在遗传算法中,一些较弱的个体会消亡。在PSO中,所有的个体都将存活,并在整个搜索过程中努力让自己变得更强大。在粒子群算法中,潜在解的改进是合作的结果,而在进化算法中则是因为竞争。这个概念使得群体智能不同于进化算法。简而言之,在进化算法中,每一次迭代都有一个新的种群进化,而在群智能算法中,每一代都有个体使自己变得更好。个体的身份不会随着迭代而改变。Mataric[2]给出了以下鸟群规则:

安全漫游:鸟类飞行时,不存在相互间或与障碍物间的碰撞;
分散:每只鸟都会与其他鸟保持一个最小的距离;
聚合:每只鸟也会与其他鸟保持一个最大的距离;
归巢:所有的鸟类都有可能找到食物来源或巢穴。
在设计粒子群算法时,并没有采用这四种规则来模拟鸟类的群体行为。在Kennedy和Eberhart开发的基本粒子群优化模型中,对agent的运动不遵循安全漫游和分散规则。换句话说,在基本粒子群优化算法的运动过程中,允许粒子群优化算法中的代理尽可能地靠近彼此。而聚合和归巢在粒子群优化模型中是有效的。在粒子群算法中,代理必须在特定的区域内飞行,以便与任何其他代理保持最大距离。这就相当于在整个过程中,搜索始终停留在搜索空间的边界内或边界处。第四个规则,归巢意味着组中的任何代理都可以达到全局最优。

在PSO模型的发展过程中,Kennedy和Eberhart提出了五个判断一组代理是否是群体的基本原则:

就近原则:代理群体应该能够进行简单的空间和时间计算;
质量原则:代理群体能够对环境中的质量因素作出反应;
多响应原则:代理群体不应在过于狭窄的通道从事活动;
稳定性原则:代理群体不能每次环境变化时就改变其行为模式;
适应性原则:计算代价不大时,代理群体可以改变其行为模式。

1.1.2 粒子群优化过程
考虑到这五个原则,Kennedy和Eberhart开发了一个用于函数优化的PSO模型。在粒子群算法中,采用随机搜索的方法,利用群体智能进行求解。换句话说,粒子群算法是一种群智能搜索算法。这个搜索是由一组随机生成的可能解来完成的。这种可能解的集合称为群,每个可能解都称为粒子。
在粒子群优化算法中,粒子的搜索受到两种学习方式的影响。每一个粒子都在向其他粒子学习,同时也在运动过程中学习自己的经验。向他人学习可以称为社会学习,而从自身经验中学习可以称为认知学习。由于社会学习的结果,粒子在它的记忆中存储了群中所有粒子访问的最佳解,我们称之为gbest。通过认知学习,粒子在它的记忆中储存了迄今为止它自己访问过的最佳解,称为pbest。

任何粒子的方向和大小的变化都是由一个叫做速度的因素决定的,速度是位置相对于时间的变化率。对于PSO,迭代的是时间。这样,对于粒子群算法,速度可以定义为位置相对于迭代的变化率。由于迭代计数器单位增加,速度v的维数与位置x相同。

对于D维搜索空间,在时间步t下群体中的第ith个粒子由D维向量x i t = ( x i 1 t , ⋯   , x i D t ) T x_i^t = {(x_{i1}^t, \cdots ,x_{iD}t)T}xit​=(xi1t​,⋯,xiDt​)T来表示,其速度由另一个D维向量v i t = ( v i 1 t , ⋯   , v i D t ) T v_i^t = {(v_{i1}^t, \cdots ,v_{iD}t)T}vit​=(vi1t​,⋯,viDt​)T表示。第ith个粒子访问过的最优解位置用p i t = ( p i 1 t , ⋯   , p i D t ) T p_i^t = {\left( {p_{i1}^t, \cdots ,p_{iD}^t} \right)^T}pit​=(pi1t​,⋯,piDt​)T表示,群体中最优粒子的索引为“g”。第ith个粒子的速度和位置分别由下式进行更新:
v i d t + 1 = v i d t + c 1 r 1 ( p i d t − x i d t ) + c 2 r 2 ( p g d t − x i d t ) (1) v_{id}^{t + 1} = v_{id}^t + {c_1}{r_1}\left( {p_{id}^t - x_{id}^t} \right) + {c_2}{r_2}\left( {p_{gd}^t - x_{id}^t} \right)\tag 1vidt+1​=vidt​+c1​r1​(pidt​−xidt​)+c2​r2​(pgdt​−xidt​)(1)

x i d t + 1 = x i d t + v i d t + 1 (2) x_{id}^{t + 1} = x_{id}^t + v_{id}^{t + 1}\tag 2xidt+1​=xidt​+vidt+1​(2)

其中d=1,2,…,D为维度,i=1,2,…,S为粒子索引,S是群体大小。c1和c2为常数,分别称为认知和社交缩放参数,或简单地称为加速系数。r1和r2是满足均匀分布[0,1]之间的随机数。上面两个式子均是对每个粒子的每个维度进行单独更新,问题空间中不同维度之间唯一的联系是通过目标函数引入的,也就是当前所找到的最好位置gbest和pbest[3]。PSO的算法流程如下:
1.1.3 解读更新等式
速度更新等式(1)的右侧包括三部分3:
前一时间的速度v,可以认为是一动量项,用于存储之前的运动方向,其目的是防止粒子剧烈地改变方向。
第二项是认知或自我部分,通过这一项,粒子的当前位置会向其自己的最好位置移动,这样在整个搜索过程中,粒子会记住自己的最佳位置,从而避免自己四处游荡。这里需要注意的是,pidt-xidt是一个方向从xidt到pidt的向量,从而将当前位置向粒子的最佳位置吸引,两者的顺序不能改变,否则当前位置会远离最佳位置。
第三项是社交部分,负责通过群体共享信息。通过该项,粒子向群体中最优的个体移动,即每个个体向群体中的其他个体学习。同样两者应该是pgbt-xidt。
可以看出,认知尺度参数c1调节的是粒子在其最佳位置方向上的最大步长,而社交尺度参数c2调节的是全局最优粒子方向上的最大步长。图2给出了粒子在二维空间中运动的典型几何图形。
在这里插入图片描述
图2 粒子群优化过程中粒子移动的几何说明

从更新方程可以看出,Kennedy和Eberhart的PSO设计遵循了PSO的五个基本原则。在粒子群优化过程中,在d维空间中对一系列时间步进行计算。在任何时间步,种群都遵循gbest和pbest的指导方向,即种群对质量因素作出反应,从而遵循质量原则。由于速度更新方程中有均布随机数r1和r2,在pbest和gbest之间的当前位置随机分配,这证明了响应原理的多样性。在粒子群优化过程中,只有当粒子群从gbest中接收到较好的信息时,才会发生随机运动,从而证明了粒子群优化过程的稳定性原则。种群在gbest变化时发生变化,因此遵循适应性原则。
1.2 粒子群优化中的参数
任何基于种群的算法的收敛速度和寻优能力都受其参数选择的影响。通常,由于这些算法的参数高度依赖于问题参数,因此不可能对这些算法的参数设置给出一般性的建议。但是,已有的理论和/或实验研究,给出了参数值的一般范围。与其他基于种群的搜索算法类似,由于在搜索过程中存在随机因素r1和r2,因此通用PSO的参数调整一直是一项具有挑战性的任务。PSO的基础版本只需要很少的参数。本章只讨论了[4]中介绍的PSO基础版本的参数。

一个基本的参数是群体规模,它通常是根据问题中决策变量的数量和问题的复杂性经验地设置的。一般建议20-50个粒子。

另一个参数是缩放因子c1和c2。如前所述,这些参数决定了下一个迭代中粒子的步长。也就是说,c1和c2决定了粒子的速度。在PSO的基础版本中,选择c1=c2=2。在这种情况下,粒子s速度的增加是不受控制的,这有利于更快的收敛速度,但不利于更好地利用搜索空间。如果我们令c1=c2>0,那么粒子会吸引到pbest和gbest的平均值。c1>c2设置有利于多模态问题,而c2>c1有利于单模态问题。在搜索过程中,c1和c2的值越小,粒子轨迹越平滑,而c1和c2的值越大,粒子运动越剧烈,加速度越大。研究人员也提出了自适应加速系数[5]。
停止准则不仅是粒子群算法的参数,也是任何基于种群的元启发式算法的参数。常用的停止准则通常基于函数评估或迭代的最大次数,该次数与算法所花费的时间成正比。一个更有效的停止准则是基于算法的搜索能力,如果一个算法在一定的迭代次数内没有显著地改进解,那么应该停止搜索。

二、源代码

% pso_Trelea_vectorized.m
% a generic particle swarm optimizer
% to find the minimum or maximum of any 
% MISO matlab function
%
% Implements Common, Trelea type 1 and 2, and Clerc's class 1". It will
% also automatically try to track to a changing environment (with varied
% success - BKB 3/18/05)
%
% This vectorized version removes the for loop associated with particle
% number. It also *requires* that the cost function have a single input
% that represents all dimensions of search (i.e., for a function that has 2
% inputs then make a wrapper that passes a matrix of ps x 2 as a single
% variable)
%
% Usage:
%  [optOUT]=PSO(functname,D)
% or:
%  [optOUT,tr,te]=...
%        PSO(functname,D,mv,VarRange,minmax,PSOparams,plotfcn,PSOseedValue)
%
% Inputs:
%    functname - string of matlab function to optimize
%    D - # of inputs to the function (dimension of problem)
%    
% Optional Inputs:
%    mv - max particle velocity, either a scalar or a vector of length D
%           (this allows each component to have it's own max velocity), 
%           default = 4, set if not input or input as NaN
%
%    VarRange - matrix of ranges for each input variable, 
%      default -100 to 100, of form:
%       [ min1 max1 
%         min2 max2
%            ...
%         minD maxD ]
%
%    minmax = 0, funct minimized (default)
%           = 1, funct maximized
%           = 2, funct is targeted to P(12) (minimizes distance to errgoal)
%    PSOparams - PSO parameters
%      P(1) - Epochs between updating display, default = 100. if 0, 
%             no display
%      P(2) - Maximum number of iterations (epochs) to train, default = 2000.
%      P(3) - population size, default = 24
%
%      P(4) - acceleration const 1 (local best influence), default = 2
%      P(5) - acceleration const 2 (global best influence), default = 2
%      P(6) - Initial inertia weight, default = 0.9
%      P(7) - Final inertia weight, default = 0.4
%      P(8) - Epoch when inertial weight at final value, default = 1500
%      P(9)- minimum global error gradient, 
%                 if abs(Gbest(i+1)-Gbest(i)) < gradient over 
%                 certain length of epochs, terminate run, default = 1e-25
%      P(10)- epochs before error gradient criterion terminates run, 
%                 default = 150, if the SSE does not change over 250 epochs
%                               then exit
%      P(11)- error goal, if NaN then unconstrained min or max, default=NaN
%      P(12)- type flag (which kind of PSO to use)
%                 0 = Common PSO w/intertia (default)
%                 1,2 = Trelea types 1,2
%                 3   = Clerc's Constricted PSO, Type 1"
%      P(13)- PSOseed, default=0
%               = 0 for initial positions all random
%               = 1 for initial particles as user input
%
%    plotfcn - optional name of plotting function, default 'goplotpso',
%              make your own and put here
%
%    PSOseedValue - initial particle position, depends on P(13), must be
%                   set if P(13) is 1 or 2, not used for P(13)=0, needs to
%                   be nXm where n<=ps, and m<=D
%                   If n<ps and/or m<D then remaining values are set random
%                   on Varrange
% Outputs:
%    optOUT - optimal inputs and associated min/max output of function, of form:
%        [ bestin1
%          bestin2
%            ...
%          bestinD
%          bestOUT ]
%
% Optional Outputs:
%    tr    - Gbest at every iteration, traces flight of swarm
%    te    - epochs to train, returned as a vector 1:endepoch
%
% Example:  out=pso_Trelea_vectorized('f6',2)

% Brian Birge
% Rev 3.3
% 2/18/06

function [OUT,varargout]=pso_Trelea_vectorized(functname,D,varargin)

rand('state',sum(100*clock));
if nargin < 2
   error('Not enough arguments.');
end

% PSO PARAMETERS
if nargin == 2      % only specified functname and D
   VRmin=ones(D,1)*-100; 
   VRmax=ones(D,1)*100;    
   VR=[VRmin,VRmax];
   minmax = 0;
   P = [];
   mv = 4;
   plotfcn='goplotpso';   
elseif nargin == 3  % specified functname, D, and mv
   VRmin=ones(D,1)*-100; 
   VRmax=ones(D,1)*100;    
   VR=[VRmin,VRmax];
   minmax = 0;
   mv=varargin{
    
    1};
   if isnan(mv)
       mv=4;
   end
   P = [];
   plotfcn='goplotpso';   
elseif nargin == 4  % specified functname, D, mv, Varrange
   mv=varargin{
    
    1};
   if isnan(mv)
       mv=4;
   end
   VR=varargin{
    
    2}; 
   minmax = 0;
   P = [];
   plotfcn='goplotpso';   
elseif nargin == 5  % Functname, D, mv, Varrange, and minmax
   mv=varargin{
    
    1};
   if isnan(mv)
       mv=4;
   end    
   VR=varargin{
    
    2};
   minmax=varargin{
    
    3};
   P = [];
   plotfcn='goplotpso';
elseif nargin == 6  % Functname, D, mv, Varrange, minmax, and psoparams
   mv=varargin{
    
    1};
   if isnan(mv)
       mv=4;
   end    
   VR=varargin{
    
    2};
   minmax=varargin{
    
    3};
   P = varargin{
    
    4}; % psoparams
   plotfcn='goplotpso';   
elseif nargin == 7  % Functname, D, mv, Varrange, minmax, and psoparams, plotfcn
   mv=varargin{
    
    1};
   if isnan(mv)
       mv=4;
   end    
   VR=varargin{
    
    2};
   minmax=varargin{
    
    3};
   P = varargin{
    
    4}; % psoparams
   plotfcn = varargin{
    
    5}; 
elseif nargin == 8  % Functname, D, mv, Varrange, minmax, and psoparams, plotfcn, PSOseedValue
   mv=varargin{
    
    1};
   if isnan(mv)
       mv=4;
   end    
   VR=varargin{
    
    2};
   minmax=varargin{
    
    3};
   P = varargin{
    
    4}; % psoparams
   plotfcn = varargin{
    
    5};  
   PSOseedValue = varargin{
    
    6};
else    
   error('Wrong # of input arguments.');
end

% sets up default pso params
Pdef = [100 2000 24 2 2 0.9 0.4 1500 1e-25 250 NaN 0 0];
Plen = length(P);
P    = [P,Pdef(Plen+1:end)];

df      = P(1);
me      = P(2);
ps      = P(3);
ac1     = P(4);
ac2     = P(5);
iw1     = P(6);
iw2     = P(7);
iwe     = P(8);
ergrd   = P(9);
ergrdep = P(10);
errgoal = P(11);
trelea  = P(12);
PSOseed = P(13);

% used with trainpso, for neural net training
if strcmp(functname,'pso_neteval')
   net = evalin('caller','net');
    Pd = evalin('caller','Pd');
    Tl = evalin('caller','Tl');
    Ai = evalin('caller','Ai');
     Q = evalin('caller','Q');
    TS = evalin('caller','TS');
end


% error checking
 if ((minmax==2) & isnan(errgoal))
     error('minmax= 2, errgoal= NaN: choose an error goal or set minmax to 0 or 1');
 end

 if ( (PSOseed==1) & ~exist('PSOseedValue') )
     error('PSOseed flag set but no PSOseedValue was input');
 end

 if exist('PSOseedValue')
     tmpsz=size(PSOseedValue);
     if D < tmpsz(2)
         error('PSOseedValue column size must be D or less');
     end
     if ps < tmpsz(1)
         error('PSOseedValue row length must be # of particles or less');
     end
 end
 
% set plotting flag
if (P(1))~=0
  plotflg=1;
else
  plotflg=0;
end

% preallocate variables for speed up
 tr = ones(1,me)*NaN;

% take care of setting max velocity and position params here
if length(mv)==1
 velmaskmin = -mv*ones(ps,D);     % min vel, psXD matrix
 velmaskmax = mv*ones(ps,D);      % max vel
elseif length(mv)==D     
 velmaskmin = repmat(forcerow(-mv),ps,1); % min vel
 velmaskmax = repmat(forcerow( mv),ps,1); % max vel
else
 error('Max vel must be either a scalar or same length as prob dimension D');
end
posmaskmin  = repmat(VR(1:D,1)',ps,1);  % min pos, psXD matrix
posmaskmax  = repmat(VR(1:D,2)',ps,1);  % max pos
posmaskmeth = 3; % 3=bounce method (see comments below inside epoch loop)

% PLOTTING
 message = sprintf('PSO: %%g/%g iterations, GBest = %%20.20g.\n',me);
 
% INITIALIZE INITIALIZE INITIALIZE INITIALIZE INITIALIZE INITIALIZE
 
% initialize population of particles and their velocities at time zero,
% format of pos= (particle#, dimension)
 % construct random population positions bounded by VR
  pos(1:ps,1:D) = normmat(rand([ps,D]),VR',1);
  
  if PSOseed == 1         % initial positions user input, see comments above
    tmpsz                      = size(PSOseedValue);
    pos(1:tmpsz(1),1:tmpsz(2)) = PSOseedValue;  
  end

 % construct initial random velocities between -mv,mv
  vel(1:ps,1:D) = normmat(rand([ps,D]),...
      [forcecol(-mv),forcecol(mv)]',1);

% initial pbest positions vals
 pbest = pos;

% VECTORIZE THIS, or at least vectorize cost funct call 
 out = feval(functname,pos);  % returns column of cost values (1 for each particle)
%---------------------------
 
 pbestval=out;   % initially, pbest is same as pos

% assign initial gbest here also (gbest and gbestval)
 if minmax==1
   % this picks gbestval when we want to maximize the function
    [gbestval,idx1] = max(pbestval);
 elseif minmax==0
   % this works for straight minimization
    [gbestval,idx1] = min(pbestval);
 elseif minmax==2
   % this works when you know target but not direction you need to go
   % good for a cost function that returns distance to target that can be either
   % negative or positive (direction info)
    [temp,idx1] = min((pbestval-ones(size(pbestval))*errgoal).^2);
    gbestval    = pbestval(idx1);
 end

 % preallocate a variable to keep track of gbest for all iters
 bestpos        = zeros(me,D+1)*NaN;
 gbest          = pbest(idx1,:);  % this is gbest position
   % used with trainpso, for neural net training
   % assign gbest to net at each iteration, these interim assignments
   % are for plotting mostly
    if strcmp(functname,'pso_neteval')
        net=setx(net,gbest);
    end
 %tr(1)          = gbestval;       % save for output
 bestpos(1,1:D) = gbest;
 
% this part used for implementing Carlisle and Dozier's APSO idea
% slightly modified, this tracks the global best as the sentry whereas
% their's chooses a different point to act as sentry
% see "Tracking Changing Extremea with Adaptive Particle Swarm Optimizer",
% part of the WAC 2002 Proceedings, June 9-13, http://wacong.com
 sentryval = gbestval;
 sentry    = gbest;
 
if (trelea == 3)
% calculate Clerc's constriction coefficient chi to use in his form
 kappa   = 1; % standard val = 1, change for more or less constriction    
 if ( (ac1+ac2) <=4 )
     chi = kappa;
 else
     psi     = ac1 + ac2;
     chi_den = abs(2-psi-sqrt(psi^2 - 4*psi));
     chi_num = 2*kappa;
     chi     = chi_num/chi_den;
 end
end

三、运行结果

在这里插入图片描述

四、备注

完整代码或者代写添加QQ912100926
往期回顾>>>>>>
【优化求解】粒子群算法之充电站最优布局【Matlab 061期】
【优化求解】遗传算法之多旅行商问题【Matlab 062期】
【优化求解】遗传和模拟退火之三维装箱问题【Matlab 063期】
【优化求解】遗传算法之求最短路径【Matlab 064期】
【优化求解】粒子群之优化灰狼算法【Matlab 065期】
【优化求解】多目标之灰狼优化算法MOGWO 【Matlab 066期】
【优化求解】遗传算法之求解优化车辆发车间隔【Matlab 067期】
【优化求解】磷虾群算法简介【Matlab 068期】
【优化求解】差分进化算法简介【Matlab 069期】
【优化求解】约束优化之惩罚函数法简介【Matlab 070期】
【优化求解】改进灰狼算法之求解重油热解模型【Matlab 072期】
【优化求解】蚁群算法之配电网故障定位【Matlab 073期】
【优化求解】遗传算法之求解岛屿物资补给优化问题【Matlab 137期】
【优化求解】基于matlab冠状病毒群体免疫优化算法(CHIO)【Matlab 138期】
【优化求解】基于matlab之金鹰优化求解算法(GEO)【Matlab 139期】
【优化求解】基于GUI界面之BP神经网络优化求解【Matlab 179期】
【优化求解】基于GUI界面之遗传算法优化求解【Matlab 180期】
【优化求解】基于GUI界面之蚁群算法优化求解【Matlab 181期】
【优化求解】 免疫算法之数值逼近优化分析【Matlab 182期】
【优化求解】 启发式算法之函数优化分析【Matlab 183期】
【优化求解】改进的遗传算法(GA+IGA)之城市交通信号优化【Matlab 184期】
【优化求解】改进的遗传算法GA之城市交通信号优化【Matlab 185期】
【优化求解】改进的遗传算法IGA之城市交通信号优化【Matlab 186期】
【优化求解】罚函数的粒子群算法之函数寻优【Matlab 187期】
【优化求解】细菌觅食算法之函数优化分析【Matlab 188期】
【优化求解】引力搜索算法之函数优化分析【Matlab 189期】
【优化求解】蚁群算法之函数优化分析【Matlab 190期】
【优化求解】多元宇宙优化算法【Matlab 191期】
【优化求解】飞蛾扑火算法(MFO)【Matlab 192期】
【优化求解】实现电动汽车有序充电【Matlab 294期】
【优化求解】粒子群的智能微电网多目标优化算法【Matlab 295期】
【优化求解】PSO货物配装问题最优化【Matlab 296期】
【优化求解】人工鱼群算法求解梯级水库优化调度【Matlab 297期】
【优化求解】改进的人工鱼群算法求解高维问题【Matlab 298期】

猜你喜欢

转载自blog.csdn.net/m0_54742769/article/details/114364385