关于矩形排样问题(三)

原创 2015年07月11日 01:41:24

上一篇博客 关于矩形排样问题(二) 给出了矩形排样问题的背景,并对遗传算法做出了详细的介绍。但是没有给出具体的解决方法,这里针对矩形排样问题,给出基于遗传算法的解法。

利用率的定义

谈到矩形排样问题,必然涉及到板材的利用率问题,这里的利用率定义如下:
假设n种小矩形Ri的面积分别为Si(i=1,2,…,n),数量分别为ni(i=1,2,…,n),宽为wi,高为li,以 (wi,li)表示,则要下料的矩形毛坯总数 这里写图片描述,设原料板材宽为 W,高为L ,以 (W,L)表示,矩形件优化排样的目标是,找到一种下料这个矩形毛坯的方法,使得“一刀切”后余料面积 尽可能大,以使得余料可以用来继续加工成可用的新零件,如下图所示,这样达到板材利用率这里写图片描述

尽可能大。
这里写图片描述

布局规则

还要考虑的一个问题就是布局规则,主要有以下几种方式:定位规则、组合规则和邻接规则

  • 定位规则

    待排矩形件零件编号确定后,接下来的工作就是确定被选待排炬形件在布局
    空间中的摆放位置,总体有以下3种:
    ①占角策略,即将待排矩形件摆放在布局空间的某一角;
    ②顺放策略,即从布局空间的某一角开始,将待排矩形件沿着布局空间的
    某一边摆放;
    ③在底盘装载问题中,先将待排矩形件沿布局空间的四边放置,最后摆放
    布局空间的中心。
    考虑到钢结构排样的实际约束条件,本文采用的是第1种先占左下角的定位规则。

  • 组合规则
    为提高工业生产的加工效率,在设计布局方案时应考虑组合矩形。组合矩形是由相同长宽的矩形件以同一种排放方式顺次排在同一层上而形成的。组合矩形的4种情况,如下图所示:
    这里写图片描述

  • 邻接规则
    领接规则是指将长宽相同或相近的矩形件放在一起进行排料。
    规则1:将长或宽相同的矩形相邻排放.规则1的3种情况如下图所示。
    这里写图片描述
    规则2:将长或宽相近的矩形相邻排放.规则2的3种情况如下图所示。
    这里写图片描述
    一般在排样过程即将结束,当(剩余矩形总面积)/(板材面积)小于一定比例时采用领接规则。尤其对于最后一块板材的排料,采用领接规则能更进一步地提高排样布局的合理性,减少材料浪费。

解决方案

对于矩形排样问题,一个简单的方法就是根据各种长度限制,列出相应的公式,进行暴力搜索,但这种方法的时间复杂度较高,在计算量小的情况下还可以接受,但是在实际的工程应用中用处不大。这里利用最低轮廓线的思想,其核心想法是采用最佳匹配搜索策略来确定矩形件的最佳排放位置,即搜索最低轮廓线中轮廓线的长度与待排矩形件的宽度差值最小的一条轮廓线,直至所有矩形件排样完毕。为了满足实际工程中“一刀切”的要求,需要对其进行修改,这里结合分层的思想。分层排样的思想体现的是一种剪切排样的方式,满足“一刀切”的工艺约束,它是将矩形零件按一定的顺序排放,在排放下一零件时若当前层剩余空间不足,则以当前层所排矩形零件的最高水平线为基准开辟新层。传统的分层排样方式是用水平线将板材分成多层的,排样方式如下图所示:
这里写图片描述
下面进入解决问题的正题:结合遗传算法解决排料问题。采用遗传算法对一刀切矩形排料问题进行求解,在排料过程中引入了改进的最低轮廓线算法思想。上一篇博客对遗传算法进行了详细的介绍,这里不再赘述。总结来说,GA仿照染色体的基因在进化的过程中进行选择交叉变异生成下一代种群。 计算开始时对种群进行初始化,并计算每一个个体的适应度函数,生成新的一代。如果生成的种群不满足优化条件,则按照适应度选择个体,父代进行交叉或变异生成子代,然后子代取代父代,再生成下一个子代。这一过程循环执行,直到满足优化准则为止。下面给出各个步骤的具体实施方案:
问题编码:
个体编码是n 个小矩形Ri 的下标 1,2,3,…,n的一个排列如下图所示
这里写图片描述
初始种群:
设种群中个体的数量为P_SIZE ,随机产生 P_SIZE个个体Xi,Xi 的基因序列是 1,2,3,…,n的一个排列。
定义适应值函数:
对于个体Xi 的基因序列这里写图片描述对应 n个小矩形 这里写图片描述 的一个排列:这里写图片描述按照上述的排料方法, 依次布局在板材上,得到板材的利用率r(i) 。显然板材的利用率r(i) 越大,个体 Xi对应的布局就越优。 因此定义个体的适应值函数为:
这里写图片描述
遗传选择操作:
计算个体的生存概率这里写图片描述
在父代种群中按轮盘赌的选择方式选择生存概率大的个体进入子代。
遗传交叉操作:
以交叉概率 Pc选择 2个个体 Xi和 Xj进行交叉操作。随机选择一个交叉点,在交叉点处,交换 2个个体后半部分得到 2个新的个体Xi’ 和 Xj’, 如下图所示:
这里写图片描述
很显然,交叉操作可能产生非法个体,即个体中有重复的基因。所以必须对 2个新个体的基因进行调整。

  • 第1步:找出个体Xi’ 的重复基因1、2、4,个体 Xj’的重复基因9、7、8;
  • 第2步:将1、3、4与9、7、8对应交换,得到了2个合法的新个体Xi” 和 Xj” 如下图所示:
    这里写图片描述
    遗传变异操作:
    以变异概率Pm 选择某一个体进行变异操作。若按普通的变异操作也会产生不合法的个体,所以要设计一种新的变异算子。

  • 第1步:在要进行变异的个体中,随机选择2个基因位;

  • 第2步:将所选择 2个基因位上的基因值交换,得到 1个新个体。
    这里写图片描述
    在每代的遗传操作过程中,保存最优个体进入子代,这样至少使子代不会比父代差。同时加入一些新品种,即随机产生一些新个体替换老个体,以防止陷入僵局。整个遗传算法的流程图如下图所示:
    这里写图片描述

编码实现

下面给出具体的编码实现。对于遗传算法的实现网上有一大堆解法,编程语言也各式各样,典型的编程工具包是MATLAB,研究得很透彻,很成熟,已经封装成函数,直接调用传入相应的参数就可以得到结果。这里给出基于VC的编码方法。
整个窗体设计比较简陋,几个Button和文本编辑框以及图形控件用来显示排样的结果。
这里写图片描述
下面只给出核心部分的实现(程序写得比较粗糙,只实现了算法,细节的部分还待完善,呵呵)。
首先在主窗体中声明表示个体的结构体:

typedef struct//针对遗传算法定义的一个个体
    {
        int gene[1000]; 
        double fitness;
        double rfitness;
        double cfitness;
    }GenoType;

以及作为原材料的矩形板材:

typedef struct //矩形板
    {
        int l; 
        int w;
        int x;
        int y; //矩形的右下角
        int flag;
    }RectAA;

    typedef struct //矩形板
    {
        int l; 
        int w;
        int x;
        int y;//矩形的左上角
    }RectBB;

考虑到算法实现的可视化,中间大部分区域用于显示排样的结果,以彩色矩形块的方式显示出来,实现比较简单,不是重点。
下面是遗传算法实现的核心,这里只给出代表核心操作的几个函数:

//基因换位
void CMyDlg::swap(int *a, int *b)
{
    int temp;
    temp=*a;
    *a=*b;
    *b=temp;
}

//随机数
int CMyDlg::IntGenerate()
{

    int RANGE_MIN=0;
    int RANGE_MAX=m_Snumber;
    int randpop=(rand()%1000/1000.0)*RANGE_MAX;
    return randpop; 
}

//选择种群
void CMyDlg::SecletMember()
{
    int *a=new int[m_Snumber];
    int x1,x2,j=0,temp1;

    for(int i=0; i<m_Snumber; i++)
        {   
            a[j]=i;
            j++;
        }
        for(int j=0;j<m_NumAll;j++)    
        {

            for(int i=0;i<1000;i++)                //100次交换足以产生各种结果了
            {
                x1=IntGenerate();
                x2=IntGenerate();
                if (x1!=x2)
                {
                    temp1=a[x1];
                    a[x1]=a[x2];
                    a[x2]=temp1;
                }
            }
            for(int i=0;i<m_Snumber;i++)
                population[j].gene[i]=a[i];
        }
        if(m_flag2)
        {
            for(int i=0;i<m_Snumber;i++)
                population[m_NumAll].gene[i]=i;
            population[m_NumAll].fitness=Valuecount(population[m_NumAll]);
        }       
}

//适应性
void CMyDlg::evaluate()
{   
    int j=0;
    for(int i=0;i<(m_NumAll*5000000/m_GenS);i++)//1000控制界面速度
    {   if(0==i%(m_NumAll*50000/m_GenS))
    {   
        population[j].fitness=Valuecount(population[j]);
        j++;
    }
    }

}

//评价
double CMyDlg::Valuecount(GenoType node)
{  
    int rl=rect[0].x;
    for (int i=0;i<m_Snumber;i++)
    {  
        rectdraw[i]=recto[node.gene[i]];
    }
    OnDrawfirst();
    OnDrawbest();
    for (int i=1;i<m_Snumber;i++)
    {
        if(rect[i].x>=rl)
        rl=rect[i].x;
    }

    return 1.000*rl/m_width;
}


//选种
void CMyDlg::KeepTheBest()
{
    int mem,i;
    curbest=0;  
    for(mem=1;mem<m_NumAll;mem++)   
        if(population[mem].fitness<population[curbest].fitness)
            curbest=mem;
        if(population[curbest].fitness<population[m_NumAll].fitness)
        population[m_NumAll]=population[curbest];//获得当前世代里的最好基因序列,并保存在当前世代的最后一个染色体中
}

//传代
void CMyDlg::elitist()
{   
    int i;
    double best,worst;
    int best_mem=0,worst_mem=0;

    best=population[0].fitness;
    worst=population[0].fitness;

    for(i=1;i<m_NumAll;i++)
    {
        if(population[i].fitness<=best)
        {
            best=population[i].fitness;
            best_mem=i;
        }
        if(population[i].fitness>=worst)
        {
            worst=population[i].fitness;
            worst_mem=i;
        }
    }

    if(best<=population[m_NumAll].fitness) //后一个体不如前一个体,就不要动前一世代
    {
        for(i=0;i<m_Snumber;i++)
            population[m_NumAll].gene[i]=population[best_mem].gene[i];
        population[m_NumAll].fitness=best;    
    }
    else                            //否则
    {
        for(i=0;i<m_Snumber;i++)
            population[worst_mem].gene[i]=population[m_NumAll].gene[i];     
        population[worst_mem].fitness=population[m_NumAll].fitness;
    }
}

void CMyDlg::SecletBetter()
{
    int mem,i;
    double sum=0;
    double *x=new double[m_NumAll];
    double p;
    int p1,p2;

    for(mem=0;mem<m_NumAll;mem++)
        sum+=population[mem].fitness;

    for(mem=0;mem<m_NumAll;mem++)    
        x[mem]=sum-population[mem].fitness;
    sum=0;
    for(mem=0;mem<m_NumAll;mem++)
        sum+=x[mem];
    for(mem=0;mem<m_NumAll;mem++) //以对总体的贡献来确定其在种群中的相对适应度
        population[mem].rfitness=(double)x[mem]/sum;
    population[0].cfitness=population[0].rfitness;

    for(mem=1;mem<m_NumAll;mem++)                         
    {
        population[mem].cfitness=population[mem-1].cfitness+population[mem].rfitness;
    }  
    for(i=0;i<m_NumAll;i++)
    {
        p=rand()%1000/1000.0;
        if(population[0].cfitness>p) //适者生存
            newpopulation[i]=population[0];
        else
        {
            for(int j=0;j<m_NumAll;j++)//弱肉强食
                if(p>=population[j].cfitness && p<population[j+1].cfitness)
                    newpopulation[i]=population[j+1];
        }
    }

    for(i=0;i<m_NumAll;i++)
        population[i]=newpopulation[i];
/*  for(i=0;i<100;i++)       //随机PK
    {
        p1=(rand()%1000/1000.0)*100;
        p2=(rand()%1000/1000.0)*100;
        if(p1!=p2)
        {
            if (population[p1].rfitness>population[p2].rfitness)
            {
                population[p2]=population[p1];
            }
            else
                population[p1]=population[p2];
        }
    }
*/
    delete [] x;
}

void CMyDlg::crossover()//交叉
{
    int i,j;
    int min,max,flag;
    double x;

    for(i=0;i<m_Snumber;i++)
    {   
        x=rand()%1000/1000.0;
        if(x<m_pxCross)
        {
            min=0;max=0;
            while(min==0)
                min=IntGenerate();
            while(max==0)
                max=IntGenerate();
            if(max<min)
            {
                int temp;
                temp=max;
                max=min;
                min=temp;
            }
            flag=max;
            for(j=min;j<=(max+min)/2;j++)//从min到max倒序
            {
                swap(&population[i].gene[j],&population[i].gene[flag]);
                flag=flag-1;
            }
        }
    }
}


void CMyDlg::mutate()//变异
{
    int i;
    int x1,x2;
    double x;   
    for(i=0;i<m_Snumber;i++)
    {    
        x=rand()%1000/1000.0;
        if(x<m_pMutation)
        {
            x1=0;x2=0;
            while(x1==0)
                x1=IntGenerate();
            while(x2==0)
                x2=IntGenerate();
            swap(&population[i].gene[x1],&population[i].gene[x2]);
        }
    }
}

//优化函数
void CMyDlg::OnProcess() 
{   
    UpdateData(TRUE);
    m_flag1=TRUE;
    generation=0;
    srand(time( NULL ) ); //取系统时间为随机种子
    SecletMember();
    evaluate();
    KeepTheBest();
    SetTimer(1,10,NULL);

}

//数据初始化
void CMyDlg::InitData(CString filename)
{   
    CString str;
    DataFile.Open(filename,CFile::modeRead);
    int i=0;
    DataFile.ReadString(str);
    while(str!=_T(""))
    {  
        str.TrimLeft(' ');
        Data[i][0]=atoi(str.Left(str.Find(' ')));
        str=str.Right(str.GetLength()-str.Find(' ')-1);
        Data[i][1]=atoi(str);
        DataFile.ReadString(str);
        i++;
    }
    m_Snumber=i;
    DataFile.Close();


    for (i=0;i<m_Snumber;i++)
    {
        rect[i].l=Data[i][0];
        rect[i].w=Data[i][1];
        recto[i]=rect[i];
    }

    sum=0;
    for (i=0;i<m_Snumber;i++)
    {
        sum+=rect[i].l*rect[i].w;//根据面积计算利用率
    }
    if (sum>m_height*m_width)
    MessageBox("超出原始板料!请重新输入");

    for(i=0;i<m_Snumber;i++)//长宽统一
    {   int a,b;
    a=rect[i].l;
    b=rect[i].w;
    rect[i].l=max(a,b);
    rect[i].w=min(a,b);
    }

/*  int max0,max1,max2,max3;
    for (i=9;i>0;i--)
    {  
        for (int j=9;j>9-i;j--)
        {
            if (rect[j][0]>rect[j-1][0])//按面积预先排列
            { 
                max0=rect[j][0];
                rect[j][0]=rect[j-1][0];
                rect[j-1][0]=max0;
                max1=rect[j][1];
                rect[j][1]=rect[j-1][1];
                rect[j-1][1]=max1;
                max2=rect[j][2];
                rect[j][2]=rect[j-1][2];
                rect[j-1][2]=max2;
                max3=rect[j][3];
                rect[j][3]=rect[j-1][3];
                rect[j-1][3]=max3;
            }

        }
    }
*/
    empty[0].l=m_width;
    empty[0].w=m_height;
    empty[0].x=0;
    empty[0].y=m_height;
}

//画图
void CMyDlg::OnDrawfirst() 
{   

//  m_flag0=TRUE;
//  Invalidate(TRUE);
    UpdateData(TRUE);
    for(int i=0;i<m_Snumber;i++)
    {
        rect[i]=rectdraw[i];
    }
    empty[0].l=m_width;
    empty[0].w=m_height;
    empty[0].x=0;
    empty[0].y=m_height;
    for(int i=1;i<m_Snumber+1;i++)
    {
        empty[i].l=2000;
        empty[i].w=2000;
        empty[i].x=2000;
        empty[i].y=2000;
    }

    CClientDC dc(this);
    if(m_height!=0 &&m_width!=0)
    {
        CRect r;
        GetClientRect(r);
        r.left=r.left+21;
        r.right=r.right-130;
        r.bottom=r.bottom-18;
        r.top=r.bottom-m_height*r.Width()/m_width;
        dc.Rectangle (&r);
    }
    else
        MessageBox("长宽不能为0!");
    m_flag0=TRUE;
}

//绘制竞争胜利的个体
void CMyDlg::OnDrawbest() 
{
    int num=0;
    int flag1;//横竖
    while (num!=m_Snumber)
    {  
            for(int i=0;i<num+1;i++ )
        {
            flag1=CalcuRate(empty[i],rect[num]);
            if(3==flag1);
            if(1==flag1)
            {   
                rect[num].x=empty[i].x+rect[num].w;
                rect[num].y=empty[i].y-rect[num].l;
                empty[num+1].l=empty[i].l-rect[num].w;
                empty[num+1].w=empty[i].w;
                empty[num+1].x=empty[i].x+rect[num].w;
                empty[num+1].y=empty[i].y;
                empty[i].l=rect[num].w;
                empty[i].w=empty[i].w-rect[num].l;
                empty[i].y=empty[i].y-rect[num].l;
                OrderEmpty();
                rect[num].flag=flag1;
                break ;
            }
            if(0==flag1)
            {   
                rect[num].x=empty[i].x+rect[num].l;
                rect[num].y=empty[i].y-rect[num].w;
                empty[num+1].l=empty[i].l-rect[num].l;
                empty[num+1].w=empty[i].w;
                empty[num+1].x=empty[i].x+rect[num].l;
                empty[num+1].y=empty[i].y;
                empty[i].l=rect[num].l;
                empty[i].w=empty[i].w-rect[num].w;
                empty[i].y=empty[i].y-rect[num].w;
                OrderEmpty();
                rect[num].flag=flag1;
                break ;
            }
        }
        num++;
    }
    DrawBest();

}

//绘图
void CMyDlg::DrawBest()
{
    CClientDC dc(this);
    for (int i=0;i<m_Snumber;i++)
    {
        if(rect[i].l!=0 && rect[i].w!=0)
        {   
            COLORREF color;
            color=RGB((int)1.00*i*250/m_Snumber+20,(int)1.00*i*255/2*m_Snumber+50,(int)1.00*i*255/3*m_Snumber+50);

            CRect r;
            if (rect[i].flag==0)
            {
                r.left=Transformx(rect[i].x-rect[i].l);
                r.top=Transformy(rect[i].y+rect[i].w);
                r.right=Transformx(rect[i].x);
                r.bottom=Transformy(rect[i].y);
                dc.Rectangle(r);
                dc.FillSolidRect(r.left+1,r.top+1,r.Width()-2,r.Height()-2,color);
            }
            if (rect[i].flag==1)
            {
                r.left=Transformx(rect[i].x-rect[i].w);
                r.top=Transformy(rect[i].y+rect[i].l);
                r.right=Transformx(rect[i].x);
                r.bottom=Transformy(rect[i].y);
                dc.Rectangle(r);
                dc.FillSolidRect(r.left+1,r.top+1,r.Width()-2,r.Height()-2,color);
            }           
        }
    }

}

//计算比率
int CMyDlg::CalcuRate(RectBB empty,RectAA rect)
{

    if((empty.w>=rect.l && empty.l>=rect.w) && (empty.w>=rect.w && empty.l>=rect.l))
        return 1;
    if((empty.w>=rect.l && empty.l>=rect.w) && (empty.l<rect.l || empty.w<rect.w))
        return 1;
    if((empty.w>=rect.w && empty.l>=rect.l) && (empty.l<rect.w || empty.w<rect.l))
        return 0;
    if((empty.l<rect.l || empty.w<rect.w) && (empty.l<rect.w || empty.w<rect.l))
        return 3;
}

//边界x处理计算
int CMyDlg::Transformx(int x)
{   int xx;
    CRect r;
    GetClientRect(r);
    r.left=r.left+21;
    r.right=r.right-130;
    r.bottom=r.bottom-18;
    r.top=r.bottom-m_height*r.Width()/m_width;
    xx=int(r.left+x*r.Width()/m_width+0.5);
    return xx;
}

//边界y处理计算
int CMyDlg::Transformy(int y)
{
    int yy;
    CRect r;
    GetClientRect(r);
    r.left=r.left+21;
    r.right=r.right-130;
    r.bottom=r.bottom-18;
    r.top=r.bottom-m_height*r.Width()/m_width;
    yy=int(r.bottom-y*r.Width()/m_width+0.5);
    return yy;

}


//排序处理
void CMyDlg::OrderEmpty()
{ RectBB mid;
    for (int i=m_Snumber;i>0;i--)
    {
        for (int j=m_Snumber;j>m_Snumber-i;j--)
        {
            if (abs(empty[j].x-0)<abs(empty[j-1].x-0))
            { 
                mid=empty[j];
                empty[j]=empty[j-1];
                empty[j-1]=mid;
            }
        }
    }

}

//利用率计算
void CMyDlg::OnTimer(UINT nIDEvent) 
{

    if(generation<m_GenS)
    {  
        generation++;
        SecletBetter();
        crossover();
        mutate();
        evaluate();
        elitist();
    }
    else
    {   
        KillTimer(TRUE);
        CString str,str1;
        str.Format("板材利用率:%.2f%%",100*sum/(Valuecount(population[m_NumAll])*m_height*m_width));
        MessageBox(str);
        m_flag1=TRUE;
        m_flag2=FALSE;
    }
    CDialog::OnTimer(nIDEvent);
}

//绘图
void CMyDlg::DrawTheLast(GenoType node)
{

    int rl=rect[0].x;
    for (int i=0;i<m_Snumber;i++)
    {  
        rectdraw[i]=recto[node.gene[i]];
    }
    OnDrawfirst();
    OnDrawbest();
}

下面给出测试的例子:
这里写图片描述
测试例子的输入以文件的方式存放,输入如下测试例子:

 400 230
 400 160
 340 130
 246 100
 350 100
 170 110
 220 120
 420 280
 330 220
 290 170
 230 200
 210 170
 240 130
 260 150
 180 160
 200 180
 245 160
 210 200
 220 140
 250 170
 300 300
 350 320
 320 280

在原始板料长宽分别为2000、1000的情况下,点击优化布局后,结果如下:
这里写图片描述
注:这里的板材利用率,不是上述定义的利用率,因为上述定义的利用率很合理,但是确实不好算,需呀手工计算。为了简单起见,算的是切割矩形总面积/原材料面积。。。呵呵
工程源码
下载

版权声明:本文为博主原创文章,未经博主允许不得转载。 举报

相关文章推荐

关于矩形排样问题(二)

上一篇博客 关于矩形排样问题(一) 引出了矩形排样问题,但是未给出解决方案,下面给出具体的解决方法以及相应的编码思路。 注:本问题的背景是第八届华中地区数学建模本科组的A题,本人侥幸拿了一等奖,呵呵...

关于矩形排样问题(一)

最近看到几个比较有意思的软件:极致下料、Cutlogic 2D、新易优化板材切割等软件,都围绕一个共同的话题,即板材切割,主要是针对二维的。对于上述软件,下载安装测试了一下,极致下料还算可以吧。让我好...

我是如何成为一名python大咖的?

人生苦短,都说必须python,那么我分享下我是如何从小白成为Python资深开发者的吧。2014年我大学刚毕业..

矩形排样问题 遗传算法解决方案

最近在实习中,遇到了一个实际问题。客户要将若干大小不一的小矩形,排到大矩形上,而且还要求可以设置小矩形之间的间距,和大矩形的margin值,便于裁切。 排样问题是一个经典的NP问题,有很多解决方案。神...

matlab中的unique

向量中的那些唯一的元素 语法 b = unique(A) b = unique(A,'rows') [b,m,n] = unique(...) 描述 1、b = unique(A)  返回...

unique-----返回唯一值并排序------------matlab

unique Find unique elements of vectorSyntax b = unique(A)b = unique(A, rows)[b, m, n] = unique(...)[...
  • zzh_my
  • zzh_my
  • 2010-01-04 10:49
  • 3157

随机梯度下降法求解SVM(附matlab代码)

随机梯度下降法求解SVM的原理的简单介绍,以及Matlab代码
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)