神奇的分形艺术

我们周围世界到处是分形图形,如弯弯曲曲的海岸线,连绵不断的山脉,云彩等等这些都是分形图形。那么什么是分形图形呢?分形是组成部分以某种方式与整体相似的形。它们是一类被经典数学和经典几何学所遗忘的被视为“怪物”的图形,它们处处连续而处处又不可微,它们否定了微积分,它们具有自相似性,可以从它们的局部看到其整体,它们的长度无法用普通的尺子来度量,它们的维数并非整数而是分数,其实这也正是我们身边真实的世界。

随着计算机技术快速的发展,其短时间进行成千上万次计算的能力极大推动了各个学科的发展。从前科学家只能通过笔算想象出来的分形,如今可以用计算机通过简单的重复迭代就可以绘制出来。这些有趣的分形图,在非线性物理领域常常被用到。

在分形这个大家庭中,除了实数迭代产生的分形树、Sierpinski三角形、科赫雪花外,有一类称之为复数迭代分形的集合它是复平面上有理映射送代理论的一部分,而 $Mandelbrot$集与 $Julia$集正是这复数分形集中的两株奇葩,它们之间有着联系又有着区别。接下来就让我来带大家了解复变函数迭代产生分形的原理,感受复数分形的奥秘,并尝试探讨一下$Mandelbrot$集与$Julia$集的联系和区别!

模型介绍

对于$Mandelbrot$集与$Julia$集,它们同出于一个数学模型

$$Z_{k+1}=Z_k^n+C (k=0,1,2,...)$$

当n=2时,如果令$C=C_0(C_0为常数)$进行多次迭代,所得的$Z$集合称之为$Julia$集。如果取$Z=Z_0(Z_0为常数)$,对不同的C进行迭代,所得的Z集合称之为$Mandelbrot$集,当$n>2$时,迭代的结果称之为广义$Julia$集和广义$Mandelbrot$集。

利用$Julia$集和$Mandelbrot$集可以画出许多极美丽的图形,它们不仅成为分形物理研究的对象,也是计算机图形学和计算机艺术的探讨对象。还出现了所谓的分形艺术,分形图形被用于制造邮卡、纺织面料的团、服装设计和一些装饰用的工艺品等。

$Julia$集

在迭代式中取$n=2,C=0$,则有$Z_{k+1}=Z_k^2$,显然$|Z_{k+1}|=|Z_k|^2$,那么有三种情况:

  1. 当$|Z_k|<1$时,经过多次迭代,$Z$将趋于零,且是稳定的;
  2. 当$|Z_k|>1$时,经过多次迭代,$Z$将趋无穷且是稳定的;
  3. 当$|Z_k|=1$时,经过多次迭代,$Z$仍在单位圆上

因此,复平面上的初始点$Z_0$可以分成两个区域,一个是由轨迹趋于零的点和趋于无穷的点所组成的稳定集,另一个是单位圆上的点组成的集合,叫$Julia$集。

当$C\neq0$时,为了划分$Julia$集,可以规定一个整的常数$M$和迭代次数$Maxit$,如果$Z$经过$Maxit$次迭代后,仍有$|Z|\leq M$,则认为该点属于$Julia$集,对于趋于内部的点也是如此处理。如果经过若干次迭代后,出现$|Z|>M$,则认为该点已经逃离,这里我们默认$M=2$,并根据达到逃离时需要的迭代次数对该起始点进行分类,用不同的颜色来表示他们,就会得到非常美丽的图案。
下面是几幅有代表性的图案:

各种各样的$Julia$集

$Mandelbrot$集

在迭代式中,固定Z的起始点而改变C的取值进行迭代计算,得到的就是$Mandelbrot$集。也要规定一个正的常数$M$和一定的迭代次数$Maxit$,如果经过$Maxit$次迭代,仍有$|Z|\leq M$,,则这个C值就属于$Mandelbrot$集,这里我们默认$M=2$。然后再根据达到逃离时所经过的迭代次数的不同对$Mandelbrot$集中的$C$加一分类并标示成不同的颜色,就会得到很漂亮的分形图性。

$Mandelbrot$集的放大过程

下标为1的图是$Mandelbrot$集的整体情况,图形很像乌龟的头与身子,细看又会发现,乌龟的头与身子周边又长出了许多更小的像乌龟的图形,在放大的图形中看得更清楚。

前面说过,分形图形是可以无限递归下去的,它的复杂度不随尺度减小而消失。$Mandelbrot$集的神奇之处就在于,你可以对这个分形图形不断放大,不同的尺度下你所看到的景象可能完全不同。放大到一定时候,你可以看到更小规模的$Mandelbrot$集,这证明$Mandelbrot$集是自相似的。上面的15幅图演示了$Mandelbrot$集的一个放大过程,你可以在这个过程中看到不同样式的分形图形。

$Mandelbrot$集与$Julia$集的联系

事实上,将$Mandelbrot$集放大数倍会找到许多与原来$Mandelbrot$集相似的图形。

生成$Mandelbrot$集的算法和生成 $Julia$集的算法几乎完全一样,唯一区别是:

  1. $Mandelbrot$集固定的是初始值$Z$,而把$C$当作变量。
  2. $Julia$集固定的是常量$C$,而把$Z$当做变量

如果在$Mandelbrot$集中任选一个点$C$,把他周围的图形放大,就会找到与这个$C$值对应的$Julia$集。也就是说,$Mandelbrot$集包括了所有的$Julia$集,他是无穷多个$Julia$集的直观的图像目录表,或者说,$Mandelbrot$集是$Julia$集的微缩字典

Julia集在Mandelbrot中的状态

更加准确的说,$Mandelbrot$集上每一点只是在C的复平面上模型“$Z_{k+1}=Z_k^2+C$”以$Z=0$为初始值迭代$Maxit$次后$Z$的状态,也就是Mandelbrot集上每一点$C=a+bi$的状态只代表了$Julia$集中当$C=a+bi$时$Z=0$时由模型迭代$Maxit$次后的状态。

  1. 如果点$C=a+bi$是$Mandelbrot$集中扩散区域中的点,说明对应于$C$点的$Julia$集$(0,0)$点是扩散点;
  2. 如果点$C=a+bi$是$Mandelbrot$集中吸收区域中的点,说明对应于$C$点的$Julia$集$(0,0)$点是吸引点;
  3. 如果点$C=a+bi$是$Mandelbrot$集中不动点集中的点,说明对应于$C$点的$Julia$集$(0,0)$点是不动点;

从而可以看出$Mandelbrot$集并非$Julia$集的字典,而是$Julia$集中$(0,0)$点的状态字典。如果我们在程序中去初始值$Z$为任意复数,则我们就可得到$Julia$集中$(Z.a,Z.b)$点的状态字典。

因此我们可以得到这样的结论:

$(C.a,C.b)$点的$Julia$集是以$(Z.a,Z.b)=(0,0)$为初值的$Mandelbrot$集在$(C.a,C.b)$点的状态

$Mandelbrot$ 集内的每一个点就对应了一个连通的 $Julia$ 集,$Mandelbrot$ 集合外的点则对应了不连通的 $Julia$ 集,并且很容易想到,越靠近 $Mandelbrot$ 集的边界,对应的 $Julia$ 集形状就越诡异。

Mandelbrot集与Julia集的高维拓展

让我们总结一下 $Julia$ 集和 $Mandelbrot$ 集的关系。在迭代过程 $Z=Z^2+C$ 中,我们有四个参数:

$Z$的初始值的实部虚部,$C$的实部虚部

$Julia$集就是给定$C$的实部、虚部后所得的结果,而$Mandelbrot$集则是限定$Z$的实部和虚部均为$0$后的结果。

大家可能想到,任意限定其中两个参数,把另外两个参数当作变量,我们还能得到很多不同的图形。事实上,如果把所有不同的$Julia$集重合起来,我们将会得到一个四维图形,它的其中两个维度是不同的初始值$Z$构成的复平面,另外两个维度则是不同的常数$C$构成的复平面。这个四维空间就包含了所有不同的初始值在所有不同的常数$C$之下迭代的发散情况。而$Mandelbrot$集,则是这个四维图形在 $C=0$处的一个切片,并且是最具有概括力的一个切片。

切片-Mandelbrot

因此,我们相当于有了$Mandelbrot$集的一个四维扩展,从这个四维图形中,我们可以切出很多二维的或者三维的切片,得到更多惊人而漂亮的图形。$Mandelbrot$集还有另外一种高维扩展,即用四元数 a + b i + c j + d k 来代替复数,从而得到另一种四维$Mandelbrot$集。可惜,这些扩展都是四维的,我们只能从它们的切片中获取三维图形。要想欣赏真正的三维版 $Mandelbrot$集,我们还得想想别的方法。数学家们创造了很多漂亮的三维版 $Mandelbrot$集,不过它们的定义有些生硬,并不自然。另外还有一个叫做 $Multibrot$集的东西,它就是把$Mandelbrot$集产生规则中的$Z^2$一般化,用$Z^n$代替,即广义$Mandelbrot$集。随着 n 的连续变化,$Multibrot$集也会连续地变化。如果把不同$n$所对应的$Multibrot$集重叠在一起,我们就会得到一个三维图形,这也勉强算得上是 Mandelbrot 的三维扩展。

3d Mandelbrot Fractal

程序介绍

通过上一节$Mandelbrot$集和$Julia$集的介绍,我们可以知道,要计算一个$Mandelbrot$集或者$Julia$集需要将复数域的每个复数点都通过多次迭代才可以。虽然现在计算机的处理器运算速度很快,但是把无穷多的复数点都进行迭代对计算机来说也是不现实的。
根据计算机量化的思想,我们需要划分出一个方形的复数域,并在这个方形的复数域内划分出许多的点,用这些点去进行迭代,并标记的迭代次数,从而展示不同的颜色。只要我们再这个方形的复数域内划分出的点足够多,也就是说分辨率足够大,这块复数域的分形图就可以清楚的展示出来。
但是显示出这一块复数域的分形图并不是我想达到的最终目的,我希望通过生成的分形图选择某一点进行放大观察,这才能该受到分形的艺术所在。为了达到这个目的,有两种解决方案

  1. 若希望放大的图也能清晰展示分形的局部细节,我们需要将这块复数域划分成非常非常非常多的点,简而言之就是提高分辨率
  2. 在已得到的分形图中重新划分一块更小的复数域,重新对这块复数域进行计算

如我们使用第一种方案,我们需要运算速度足够快的计算机来计算高像素的图像。但这也会产生一个问题,图像放大十倍一百倍是可以接受的,但是当我们需要放大几千倍、上万倍时,要求的图像分辨率会呈指数级上升,就算交给超级计算机计算也会需要很多时间。
考虑到我们放大图像的时候,其他位置的图像就不需要了,我们也没有计算他的必要。因此我们果断选择第二种方案,就算对于普通的计算机也是可以承受的。
因此我们设计了一个产生分形图的函数:fractal_geometry
这个函数所需要传入的参数有:

  1. res——分辨率
  2. Center_x——需要显示分形图像的复数域实部的中心点
  3. Center_y——需要显示分形图像的复数域虚部的中心点
  4. lim——方形复数域的边长
  5. colormap——输出图像的颜色模式
  6. set——分形模式,可选择Julia Set或者Mandelbrot Set从而绘制相应的分形图
  7. maxit——分形公式的迭代次数
  8. Z0real——绘制$Mandelbrot$集时设置的Z0初值的实部
  9. Z0imag——绘制$Mandelbrot$集时设置的Z0初值的虚部
  10. Creal——绘制$Julia$集时设置的常数C的实部
  11. Cimag——绘制$Julia$集时设置的常数C的虚部
  12. n——迭代公式:$Z_{k+1}=Z_k^n+C$中的n

好在所有的分形图都在0点附近,因此刚开始时我们可以初始设置lim为一个较大的数比如lim=3,来查看以$0$点为中心,范围为3内的分形图,然后调节Center_x,y将想要放大的位置移动到图像中心,然后缩小lim从而进行分形图的局部放大

代码展示

我们借助Python进行我们$Mandelbrot$集与$Julia$集的绘制。为了完成快速进行矩阵的运算、图形的输出、GUI参数控制。我们需要借助python的几个库

  1. Numpy——进行多维数组与矩阵运算
  2. Matplotlib——Python 2D绘图套件,进行数据图形化,多样化的输出
  3. Ipywidgets——notebook中进行界面设计,以及一些简单的Notebook交互式控件操作

这些库中有许多高效实用的函数,具体的函数实用可以参考官方文档,或者百度参考众多优秀的博客的实例演示程序

  1. Numpy官方文档
  2. Matplotlib官方文档
  3. Ipywidgets官方文档

所有的代码都已做好注释,相信大家可以很容易的理解

# 引入numpy库,命名为np,此后我们可以通过np.func()使用numpy库的函数
import numpy as np
# 引入numpy库,命名为plt,此后我们可以通过plt.func()使用库matplotlib.pyplot的函数
import matplotlib.pyplot as plt
# 引入numpy库,命名为widgets,此后我们可以通过widgets.func()使用库ipywidgets的函数
import ipywidgets as widgets
# 引入ipywidgets库中的多个GUI控件
from ipywidgets import IntSlider, Dropdown, BoundedFloatText, RadioButtons
# 引入math库,进行数学运算
import math


# 分型图像输出核心函数:fractal_geometry
# 为函数添加默认参数,若调用函数时没用参数输入,则会使用默认参数
# 因此可直接使用fractal_geometry()输出默认的Mandelbrot集图像
def fractal_geometry(res=1000,
                     maxit=20,
                     Center_x=-0.3,
                     Center_y=0,
                     lim=2.8,
                     colormap=None,
                     set='Mandebrot Set',
                     Z0real=0,
                     Z0imag=0,
                     Creal=0,
                     Cimag=1,
                     n=2):
    # 通过坐标中心点cx,cy及范围long确定整个坐标面的范围[xmin,xmax],[ymin,ymax]
    [xmin, xmax, ymin, ymax] = [
        Center_x - lim / 2, Center_x + lim / 2, Center_y - lim / 2,
        Center_y + lim / 2
    ]
    # 生成x,y在[xmin,xmax],[ymin,ymax]的网格
    # y是res维的列向量,x是res维行向量
    y, x = np.ogrid[ymin:ymax:res * 1j, xmin:xmax:res * 1j]
    # 通过Numpy广播计算得到res*res维的复数域矩阵complex_field
    complex_field = x + y * 1j
    if (set == 'Julia Set'):
        # 若是Julia集,c是输入的参数作为固定常数,z初始是复数域
        c = complex(Creal, Cimag)
        z = complex_field
        set = set + ' (C=' + str(Creal) + ' + ' + str(Cimag) + 'i)'
    else:
        # 若是Mandebrot集,c是复数域,z是输入的参数作为固定的初始值
        c = complex_field
        z = complex(Z0real, Z0imag)
        set = set + ' ($Z_0$=' + str(Z0real) + ' + ' + str(Z0imag) + 'i)'
    # 初始化像素矩阵,设置所有复数点迭代maxit次(最大迭代次数)都没有发散
    divtime = maxit + np.zeros(complex_field.shape, dtype=int)
    for i in range(maxit):
        # 进行第i次迭代,迭代后z已经变成res*res维的复数矩阵
        z = z**n + c
        # z的模大于4记为发散(True),否则为(False)
        diverge = z * np.conj(z) > 100
        # 本次迭代发散的下标=此次计算发散 且 之前从来没有发散过的下标
        div_now = diverge & (divtime == maxit)
        # 运用Numpy中的布尔索引,将本次才发散的下标元素表示的迭代次数更新为i
        divtime[div_now] = i
        # 将已经发散的元素赋值为2,防止多次迭代后溢出
        z[diverge] = 2
    # 绘制图像,并将坐标轴顺序呈从左到右(x轴)从下到上(y轴)递增
    plt.imshow(divtime, origin='lower', cmap=colormap)
    # 设置x,y轴label和title
    plt.xlabel('real part(x)')
    plt.ylabel('imaginary part(iy)')
    plt.title(set + '\n${z_{n+1}}={z_{n}^{' + str(n) + '}+c}$' + '\ncenter=(' +
              str(Center_x) + ', ' + str(Center_y) + '), lim=' + str(lim) +
              '\nMAXit=' + str(maxit) + ', res=' + str(res) + ', colormap=' +
              colormap)
    # 将原来x,y轴的刻度从像素坐标[0,w],[0,h]刻度映射成实际对应的实部虚部坐标[xmin,xmax],[ymin,ymax]
    # 显示8个刻度值,x,y轴刻度显示保留两位小数
    plt.yticks(np.linspace(0, res, 8), np.around(np.linspace(ymin, ymax, 8),
                                                 2))
    plt.xticks(np.linspace(0, res, 8), np.around(np.linspace(xmin, xmax, 8),
                                                 2))
    # 显示次刻度线
    plt.minorticks_on()
    # 展示图像
    plt.show()
    # 函数运行结束,返回二维图形矩阵
    return divtime


# 中心(Center_x,Center_y)即为图像中心像素代表的复数域坐标
# cx,cy坐标组件,用于修改图像中心点的坐标位置(之所以设置成带button的Text组件是因为Slider组件不能对浮点数值做微小的细调)
# 选中控件后,可通过键盘的“上下键”来调节其值,每次调节的幅度是一个step,上键是增加,下键是减少
# step因为回调函数的原因,因lim值的变化而变化
# 选中cx,左增右减(想把图像左移就通过上键增加cx的值,右移则通过下键减小cx的值)
# 选中cy,下增上减(想把图像下移就通过上键增加cy的值,上移则通过下键减小cy的值)
Center_x = BoundedFloatText(description='x',
                            value=-0.3,
                            min=-20,
                            max=20,
                            step=0.1,
                            readout_format='.10f')
Center_y = BoundedFloatText(description='y',
                            value=0,
                            min=-20,
                            max=20,
                            step=0.1,
                            readout_format='.10f')
# 以(cx,cy)为中心的图像所能表示的复数域范围
lim = BoundedFloatText(description='范围',
                       value=4,
                       min=0.0000000001,
                       max=100,
                       step=1,
                       readout_format='.10f')
# 图像分辨率组件,建议不要设置太大,因为会增加计算量
# 且图像分辨率达到一定程度之后,再升高分辨率,输出的图像已无明显区别
# 若想查看高清图,可将代码复制到本地python环境,并将max调高然后运行查看结果
res = IntSlider(description='分辨率', value=500, min=10, max=1000, step=10)
# 迭代次数组件,随着我们查看的坐标范围越来越来小,增加迭代次数可以看到分型中更细小的图形
# 建议当long的范围很小的时候,适当调大maxit,会有惊喜等着你
maxit = IntSlider(description='最大迭代次数', value=20, min=1, max=1000, step=1)
# 图像色谱模式组件,建议选默认模式下输出的图像比较耐看
colormap = Dropdown(
    options=[
        'viridis', 'plasma', 'inferno', 'magma', 'Greys', 'Purples', 'Blues',
        'Greens', 'Oranges', 'Reds', 'binary', 'bone_r', 'pink_r', 'hot_r',
        'PuOr', 'RdGy', 'RdBu', 'Pastel1', 'Set1', 'tab10', 'CMRmap_r',
        'cubehelix_r', 'brg', 'gist_rainbow'
    ],
    value='viridis',
    description='colormap',
)

# 集合类型组件
set = RadioButtons(options=['Mandebrot Set', 'Julia Set'],
                   value='Mandebrot Set',
                   description='集合类型')
# Mandelbrot集Z0初始值设置控件
Z0real = BoundedFloatText(description='$Z_0$实部',
                          value=0,
                          min=-100,
                          max=100,
                          step=0.01,
                          disabled=False)
Z0imag = BoundedFloatText(description='$Z_0$虚部',
                          value=0,
                          min=-100,
                          max=100,
                          step=0.01,
                          disabled=False)
# Julia集常数C设置控件
Creal = BoundedFloatText(description='$C$实部',
                         value=0,
                         min=-100,
                         max=100,
                         step=0.01,
                         disabled=True)
Cimag = BoundedFloatText(description='$C$虚部',
                         value=1,
                         min=-100,
                         max=100,
                         step=0.01,
                         disabled=True)
n = BoundedFloatText(description='n', value=2, min=0, max=50, step=0.1)
# 图像输出组件,调用 mandelbrot函数,输入参数分别为res,maxit,cx,cy,long,colormap组件的值
out = widgets.interactive_output(
    fractal_geometry, {
        'res': res,
        'maxit': maxit,
        'Center_x': Center_x,
        'Center_y': Center_y,
        'lim': lim,
        'colormap': colormap,
        'set': set,
        'Z0real': Z0real,
        'Z0imag': Z0imag,
        'Creal': Creal,
        'Cimag': Cimag,
        'n': n
    })


# lim的回调函数,通过lim的值来动态的调节步长step,方便大家更好的调节lim查看更细小的范围
# 整个程序若maxit和res过高,生成图像的速度会非常的慢
# 若没有等图像生成完毕就持续调节lim,可能会使lim的值直接变为lim.min
def update_step(*args):
    i = 0
    d = lim.value
    interger = math.floor(d)
    decimal = d - interger
    while interger == 0:
        d = decimal * 10
        interger = math.floor(d)
        decimal = d - interger
        i = i + 1
    if (interger == 1):
        i = i + 1
    lim.step = 1 / (10**i)
    Center_x.step = lim.step / 10
    Center_y.step = Center_x.step


# set的回调函数
# 当set选择为Mandelbrot集时,Z为输入的初始参数,C为整个复数域,C不可作为输入
# 当set选择为Julia集时,Z为整个复数域,C为输入的常量参数,Z不可作为输入
def change_set(*args):
    if (set.value == 'Mandebrot Set'):
        Creal.disabled = True
        Cimag.disabled = True
        Z0real.disabled = False
        Z0imag.disabled = False
    else:
        Creal.disabled = False
        Cimag.disabled = False
        Z0real.disabled = True
        Z0imag.disabled = True


# 注册lim的回调函数,通过lim的值来调节cx,Center_y,long的step
lim.observe(update_step, 'value')
set.observe(change_set, 'value')

# 将所有的组件排列起来
widgets.HBox([
    out,
    widgets.VBox([
        res, maxit, Center_x, Center_y, lim, set,
        widgets.HBox([Z0real, Z0imag]),
        widgets.HBox([Creal, Cimag]), n, colormap
    ])
])

本地版程序

由于通过ipywidgetsNotebook上的GUI交互功能十分受限,不能通过快捷键和鼠标点击进行交互
因此我又基于TKinter库写了一个可以本地运行的GUI程序,可以通过快捷键和鼠标进行图像的交互,点击fractal.exe即可在本地运行
exe程序与源代码我已打包上传到百度网盘,欢迎有兴趣的同学们下载体验!

链接:https://pan.baidu.com/s/13gg3mxpz_wfD2DLRRFnqxg
提取码:gquz

交互展示

选中上方的代码块,点击菜单栏的运行按钮,就可以看到输出图形和交互控件

点击三角运行代码

我们可以通过调节不同的参数来进行不同的操作,快去发现专属你的分型图形吧!

  1. 调节Centerx,y,lim分形图的移动,放大,缩小,寻找自相似
    Mandelbrot Set局部放大图
  2. 改变set选项来进行$Julia$集与$Mandelbrot$集的对比
  3. 观察某个C=a+bi时的$Julia$集,并在对应Center=a+bi,Z0=0时的$Mandelbrot$集中查找该$Julia$集
    Julia集在Mandelbrot中的状态
  4. 调节res——分辨率来体会不同分辨率对产生的分形图的影响
    分辨率对分形图像的影响
  5. 调节maxit——迭代次数来感受不同的迭代次数对产生的分形图的影响
    迭代次数对分形图像的影响
  6. 逐步调节n来感受广义$Julia$集与广义$Mandelbrot$集的变化
    广义Mandelbrot集与广义Julia集
  7. 逐步调节C来感受$Julia$集的变化
    Julia集的变化
  8. 逐步调节Z来感受$Mandelbrot$集的变化
    Mandelbrot集的变化

小结

Mandelbrot集合除了自我相似性、拥有无限的细节外,还有一些其它比较有趣的特性,例如边界处的触角数量分布与斐波那契数列有密切的联系,许多艺术家和程序员对它比较入迷。对于我来说,Mandelbrot最令人着迷的地方是其简单规律后蕴藏着的复杂而精美的结构,我想这也许是数学和程序引人入胜的共性吧

参考

  1. Python基础教程
  2. Numpy官方文档
  3. Matplotlib官方文档
  4. Ipywidgets官方文档
  5. Markdown的使用方法
  6. The Real 3D Mandelbrot Set
  7. 神奇的分形艺术(四):Julia集和Mandelbrot集
  8. 再谈Julia集与Mandelbrot集
  9. Mandelbrot集Wikipedia
  10. Mandelbrot集的最新变化形态一览
  11. "上帝的指纹" - 走进无限美妙的曼德博集合
Last modification:November 20th, 2020 at 07:03 pm
如果觉得我的写的还有点意思,欢迎看官赞赏