游戏编程中的人工智能技术(3-1)

来源:互联网 发布:单片机串口通信步骤 编辑:程序博客网 时间:2024/05/23 14:52
 
<script type="text/javascript"><!--google_ad_client = "pub-3269163127493396";google_ad_width = 120;google_ad_height = 90;google_ad_format = "120x90_0ads_al";//2007-05-08: ITgoogle_ad_channel = "0064192373";google_color_border = "6699CC";google_color_bg = "003366";google_color_link = "FFFFFF";google_color_text = "AECCEB";google_color_url = "AECCEB";//--></script><script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>
第3章        遗传算法入门
有一天,一群著名科学家聚焦在一起,他们确信人类已经得到了巨大进步,现在已经不再需要上帝了。因此他们选出了一个科学家作为代表去见上帝,决定要“休”了他。
这位科学家跑到上帝那里,对他说:“上帝,我们人类已经能够自己克隆自己,能做各种各样神奇的事情,现在已经不再需要你,你为什么还不退休呀?”
上帝耐心听了这位使者的话,然后说:“很好,但是,首先让我们来进行一场制作人的比赛吧,怎么样?”
这位科学家回答说:“行啊,好极了!”
但上帝又补充说:“现在就让我们来做,就像我从前与亚当一起制作人那样。”
科学家说:“没问题,没问题,”并弯下腰去自己抓了一把泥土。
上帝看见后,说:“不,不,不,你得去拿你自己的泥土呀。”
3.1       鸟和蜜蜂
生物只有经过许多世代的进化,才能使生存与繁衍的任务获得更大成功。遗传算法也遵循同样的方式,需要经过长时间的成长,演化,最后才能收敛得到针对某类特定问题的一个或多个解。因此,了解一些有关有生命机体如何演化的知识,对理解遗传算法如何工作是有帮助的。本章的开始几页,将扼要阐述自然演化的机制(通常称为“湿”演化算法)以及与之相关的术语。即使读者以前在中学里对生物并不擅长,也无须担心。本章不会涉及过深的细节,但对于理解自然演化的基本机制已经足够。抛开以上不论,当你读完配音或下一章后,我想,你也会和我一样,深深叹服自然母亲的令人着迷!
从本质上说,任何生物机体不过就是一大堆细胞的集合。每个细胞都包含着称作染色体的相同集合DNA链。染色体中包含的DNA分成为两股,它们以螺旋形状缠绕在一起,这就是人们所熟悉的DNA双螺旋结构。
单个的染色体是由称作基因(gene)的更小的结构模块组成,而基因则又由称作核苷酸(nucleotide)的物质组成。核苷酸一共只有4种类型,即腺嘌呤(thymine),鸟嘌呤(adenine),胞嘧啶(cytocine),胸腺嘧啶(guanine)。它们常简写为T,A,C,G。这些核苷酸相互连接起来,形成若干很长的基因链,而每个基因编码了生物机体的某种特征,如头发的颜色,耳朵的样子等。一个基因可能具有不同设置(如头发可以是棕色,黑色或金黄色),称为等位基因(allele),它们沿染色体纵向所处的物理部位称为基因座位(locus)。
重要注释:等位基因不一定就限于物理形状特性的设置,某些等位基因将用来产生不同的行为模式,例如鸟类或大巴哈鱼的本能性的回归行为,母亲具有抚育其新生一代的天性。
一个细胞中的染色体组(collection)包含了复制该机体所需的全部信息。这就是克隆怎样实行的秘密。你可以用被克隆者身上的一个细胞——哪怕是一个血细胞——中获得复制出整个生物机体的信息。例如一只羊。新的羊将会在每一个方面和被克隆者完全相同。染色体的这一集合就称为生物机体的基因组(genome)。在一特殊基因组中等位基因的一种状态称为该机体的遗传类型(genotype)。这些就是用来生成实际的生物机体(表现型,phentype本身的硬编码指令)。你我都是表现型。人类DNA携带了人类自身的遗传类型。如将这些用到其他领域中,则设计汽车用的成套蓝图就是一个遗传类型,在生产线上隆隆作响的成品汽车就是一个表现型。只有设计被定型以前的,那些完全陈旧的设计,才勉强称得上是一个基因组。
现在开始讨论,怎样把所有这些应用到进化中去。一个生物的成功的量度就是它的适应性环境。生物机体愈能适应环境,它的子孙后代也就愈多。
当两个生物机体配对和复制时,它们的染色体相互混合,产生一个由双方基因组成的全新的染色体组。这一过程就叫重组(recombination)或杂交(crossover)。这样就意味着后代继承的大部分可能是上一代的优良基因,也可能继承了它们的不良基因。如果是前一种情况,后代就可能变得比它的父母更能成功(例如,它对掠食者有更强的自卫机制);如为后一种情况,后代甚至就有可能不能再复制自己。这里要着重注意的是,愈能适应环境的子孙后代就愈有可能继续复制并将其基因传给下一个子孙后代。由此就会显示一种趋向,每一代总是比其父母一代生存和匹配得更完美。
但是,有些读者可能已经意识到,如果这是系列期间唯一进行的事情,那么即使经历成千上万代后,适应能力最强的成员的眼睛尺寸也只能像初始群体中的最大眼睛一样。而根据对自然的观察中可以看到,人类的眼睛尺寸实际存在一代比一代大的趋势。之所以会发生这种情况,是因为当基因传递给子孙后代的过程中,会有很小的概率发生差错,从而使基因得到微小的改变。这称为变异(mutation),同样的变异在生物的繁衍过程中会在它们的基因中出现。发生变异的概率通常都很小,但在经历大量的世代之后变异就会很明显。一些变异或突变对特征将是不利的(这有最大的可能),另一些则对生物的适应性可能没有任何影响,但也有一些则可能会给生物带来一些明显的利益,使它能超过与其同类的生物。
进化机制除了能改进已有的特征外,也能产生完全新的特征。而只有成功的基因才会得到继承。观察自然界中存在的任何特征就能发现,它们的进化都是利用无数微小的变异发展而来的,且它们都是对拥有者有利。
3.2       二进制数速成
 
3.3       计算机内的进化
遗传算法的工作过程实质上就是模拟生物的进化过程。首先,应确定一种编码方法,使得你的问题的任何一个潜在可行解都表示成为一个“数字”染色体。然后,创建一个由随机的染色体组成的初始群体(每个染色体代表一个不同的候选解),并在一段时期中,以培育适应性最强的个体的办法,让它们进化。在此期间,染色体的某些位置上要加入少量的变异。经过许多代后,运气好一点,遗传算法将会收敛到一个解。遗传算法不确保一定能得到解,如果有解也不确保找到的是最优解,但只要采用的方法正确,通常都能为遗传算法编出一个能够很好运行的程序。遗传算法的最大优点就是,你不需要知道怎么去解决一个问题:仅需知道用怎么的方式对可行解进行编码,使得它能被遗传算法机制所利用。
通常,代表可行解的染色体采用一系列的二进制位作为编码。在运行开始时,首先创建一个染色体的群体,每个染色体都是一组随机的二进制位。二进制位(即染色体)的长度在整个群体中都是一样的。
每个染色体都用这样的方式编码成为由0和1组成的字符串,而它们通过译码就能表示当前问题的一个解。这可能是一个很差的解,也可能是一个十分完美的解,但每一个单个的染色体都代表了一个可行解。初始群体通常都是很糟的,当一个初始的群体已经被创建好了后(不妨假设共有100个成员)即可开始做下面一系列工作了。
不断循环,直到寻找出一个解:
不断循环,直到寻找出一个解:
1.检查每个染色体,看它解决问题的性能怎样?并相应地为它分配一个适应性分数。
2.从当前群体选出2个成员。选出的概率正比于染色体的适应性,适应分愈高,被选中的概率也愈大。常用的方法就是赌轮选择(roulette wheel selection)法。
3.按照预先设定的杂交率(crossover rate),从每个选中染色体的一个随机确定的点上进行杂交(crossover)。
4.按照预定的变异率(mutation rate),通过对被选染色体的位的循环,把相应的位实行翻转(flip)。
5.重复步骤2,3,4,直到100个成员的新群体被创建出来。
结束循环
算法由步骤1到步骤5的一次循环称为一个世代(或代,generation)。这里把整个的循环称为一个时代(epoch),在正文和代码中将都用这样方式来称呼。
3.3.1           什么是赌轮选择法
赌轮选择是从染色体群体中选择一些成员的方法,被选中的几率和它们的适应性分数成比例,适应性分数愈高的染色体,被选中的概率也愈大。这不保证适应性分数最高的成员一定能选入下一代,仅仅说明它有最大的概率被选中。其工作过程如下:
设想群体全体成员的适应性分数由一张饼图来代表,这一饼图就和用于赌博的转轮形状一样。需要为群体中每一染色体指定饼图中一个小块。块的大小与染色体的适应性分数成正比,适应性分数愈高,它在饼图中对应的步块所占面积也愈大。为了选取一个染色体,先旋转这个轮子,并把一个小球抛入其中,让它翻来翻去地跳动,直到轮盘停止时,看小球停止在哪一块上,就选中与它对应的那个染色体。后面的章节中会讲述编写这种程序的准确算法。
3.3.2           杂交率
杂交率就是用来确定两个染色体进行局部位的互换以产生两个新的子代的概率。实验表明这一数值通常取为0.7左右是理想的,尽管某些问题领域可能需要更高或较低的值。
每一次从群体中选择两个染色体,同时生成0和1之间的一个随机数,然后根据此数据的值来确定两个染色体是否需要进行杂交。如果数据低于杂交率(0.7),就进行杂交,然后沿着染色体的长度随机地选择一个位置,并把在此位置之后的所有的bit进行互换。
例如,设给定的两个染色体为:
10001001110010010
01010001001000011
沿着它们的长度随机选择一个位置,比如说第10位,然后互换第10位之后所有位。这样两个染色体就变成了(这里在开始互换的位置加了一个空格):
100010011 01000011
010100010 10010010
3.3.3           变异率
变异率(突变率)就是在一个染色体中将位实行翻转(flip,即0变1,1变0)的几率。这对于二进制编码的基因来说通常都是很低的值,比如0.001。
因此,无论在群体中怎样选择染色体,首先应检查是否要杂交,然后再从头到尾检查子代染色体的各个位,并按所规定的几率对其中的某些位实行突变(翻转)。
3.3.4           建议的学习方法
从现在开始直到本章结束,所有阅读材料大多数都被设计用来重读两遍。这里有很多需要理解的新概念,且它们都是相互混杂在一起的。相信对于读者这是最好的学习方法。
3.4       帮助Bob回家
寻找路径问题是游戏人工智能的一块“神圣基石”,下面就来创建一个遗传算法用在一个非常简单的场景中解决寻找路径问题。首先创建一个迷宫,它的右边有一入口,左边有一个出口,并有一些障碍物散布在其中。在出发点放置一个虚拟的角色(Bob),然后要为他解决如何寻找路径的问题,使他能找到出口,并避免与所有障碍物相碰撞。下面讲述如何产生Bob的染色体的编码,但首先需要解释怎样来表示迷宫。
迷宫是一个二维整数数组;用0来表示开放的空间,1代表墙壁或障碍物,5为起始点,8为出口。因此,整数数组如下:
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
 1,0,1,0,0,0,0,0,1,1,1,0,0,0,1,
 8,0,0,0,0,0,0,0,1,1,1,0,0,0,1,
 1,0,0,0,1,1,1,0,0,1,0,0,0,0,1,
 1,0,0,0,1,1,1,0,0,0,0,0,1,0,1,
 1,1,0,0,1,1,1,0,0,0,0,0,1,0,1,
 1,0,0,0,0,1,0,0,0,0,1,1,1,0,1,
 1,0,1,1,0,0,0,1,0,0,0,0,0,0,5,
 1,0,1,1,0,0,0,1,0,0,0,0,0,0,1,
 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,}
这种地图设计方法被封装在一个称为CbobsMap的类中,并定义为:
class CbobsMap{
         
private:
         
//保存地图的存储器(一个2维整数数组)
         static const int map[MAP_HEIGHT][MAP_WIDTH];
         
static const int m_iMapWidth;//地图的宽度
         static const int m_iMpaHeight;//地图的高度
         
//起始点在数组中的下标
         static const int m_iStartX;
         
static const int m_iStartY;
         
//终点的数组下标
         static const int m_iEndX;
         
static const int m_iEndY;
         
public:
         
//如果需要,可以利用这一数组作为Bob走过的路程的存储器
         int memory[MAP_HEIGHT][MAP_WIDTH];
         
         CbobsMap()
{
                   ResetMemory();
         }

 
         
//利用一个字符串来记录Bob行进的方向,其中每一个字符代表Bob所走的一步
         
//检查Bob离开出口还有多远,返回一个与到达出口距离成正比的适应性分数
         double TestRoute(const Vector<int> &vecPath,CbobsMap &memory);
         
//Render函数利用Windows GDI在一个给定的作图表面上呈现地图
         void Render(const int cxClient,const int cyClient,HDC surface);
         
//画出能够存放于存储器中的任意路径
         void MemoryRender(const int cxClient,const int cyClient,HDC surface);
         
void ResetMemory();
}
;
由上可知,只需要以常量的形式来保存地图数组以及起点和终点就行了。这些数据是在文件CbobsMap.cpp中定义的,除了存储迷宫,这个Map类也用来记录Bob在迷宫中行进的路程:memory[][]。这对遗传算法本身而言不是本质的,仅为了使读者能看到Bob怎样在迷宫中漫游,设置一个记录是必须的。这里重要的成员函数是TestRoute(),它需要利用一系列的行进方向来检测Bob走了多远。这里不列出TestRoute函数的清单,只给予说明。
给出一个方向向量,它的每个分量能代表四个方向(东,南,西,北)之一,让Bob按照它在地图中行走,TestRoute计算Bob能到达的最远点的位置,然后返回一个适应性分数,它正比于Bob最终位置离出口的距离。他所到达的位置离出口越近,奖励给他的适应性分数也越高。如果他实际已到达了出口,他将得到满分1,这时循环就会自动结束。这时已经找到了一个解。
3.4.1           为染色体编码
每个染色体必须把Bob的每一个行动编入代码中。Bob的行动仅限为4个方向:东,南,西,北,故编码后的染色体应该就是代表这4个方向信息的一个字符串。传统的编码方法就是把方向变换成二进制的代码。4个方向只要两位就够了。例如下表所示的那样:
二进制代码
十进制译码
代表的方向
00
0
向北
01
1
向南
10
2
向东
11
3
向西
这样,如果得到了一个随机的二进制字符串,就能根据它译出Bob行动时所遵循的一系列方向。例如染色体:
111110011011101110010101
代表的基因就是:
11,11,10,01,10,11,10,11,10,01,01,01
当把二进制代码转化为十进制时,就成为
3,3,2,1,2,3,2,3,2,1,1,1
再把这些放进一个表格中。
二进制代码
十进制译码
代表的方向
11
3
西
11
3
西
10
2
01
1
10
2
11
3
西
10
2
11
3
西
10
2
01
1
01
1
01
1
到此,所要做的全部就是将Bob置于迷宫的起点,然后告诉他根据这张表中所列的方向一步步地走。如果一个方向使Bob碰到了墙壁或障碍物,则只需忽略该指令并继续按下一条指令去走就行了。这样不断下去,直到用完所有方向或Bob到达出口为止。假如有几百个这样的随机的染色体,它们中的某些就可能为Bob译码出到达出口的一套方向(问题的一个解),但它们中的大多数将是失败的。遗传算法以随机的二进制串(染色体)作为初始群体,测试它们每一个能让Bob走到离出口有多么近,然后让其中最好的那些来孵化后代,期望它们的“子孙”中能有比Bob走得离出口更近一点。这样继续下去,直到找出一个解,或直到Bob绝望地在一个角落里被困住不动为止。
因此,必须来定义一种结构,其中包含一个二进制位串(染色体),以及一个与该染色体相联系的适应性分数。这个结构称为SGenome结构,它的定义如下:
 
如果在创建SGenome对象时把一个整型数作为参数传递给构造函数,则它就会自动创建一个以此整数为长度的随机二进制位串,并将其适应性分数初始化为零,完成对基因组的设置。
class CgaBob
{
private:
 
    
//基因组群休
    vector<SGenome> m_vecGenomes;
    
    
//群体大小
    int             m_iPopSize;
 
    
double          m_dCrossoverRate;
    
    
double          m_dMutationRate;
    
    
//决定每个染色体含有多少个位
    int             m_iChromoLength;
 
    
//决定每个基因有多少个位
    int             m_iGeneLength;
    
    
int             m_iFittestGenome;
    
    
double          m_dBestFitnessScore;
    
    
double          m_dTotalFitnessScore;
    
    
int             m_iGeneration;
 
    
//为map类创建一个实例
    CBobsMap        m_BobsMap;
 
    
//我们使用另一个CbobsMap对象来保存每一代中的最佳路线
    
//它是一些Bob经过的小方格的数组
    
//仅用于显示
    CBobsMap        m_BobsBrain;
 
    
//检测运行是否仍在进行
    bool            m_bBusy;
    
 
    
    
void        Mutate(vector<int> &vecBits);
    
    
void        Crossover(const vector<int> &mum,
                        
const vector<int> &dad,
                        vector
<int>       &baby1,
                        vector
<int>       &baby2);
    
    SGenome
&        RouletteWheelSelection();
    
    
//用新的适应性分数更新基因组原有的适应性分数
 
//并计算群体的最高适应性分数和适应性分数最高的那个成员
 void           UpdateFitnessScores();
 
    
//把每个位译成为方向(用十进制整数代表)
 vector<int>   Decode(const vector<int> &bits);
    
    
//把位转换为十进制整数
 int                BinToInt(const vector<int> &v);
 
    
//创建一个随机的二进制位串的初始群体
 void           CreateStartPopulation();
 
public:
    
    CgaBob(
double cross_rat,
         
double mut_rat,
         
int    pop_size,
         
int    num_bits,
         
int    gene_len):m_dCrossoverRate(cross_rat),
                          m_dMutationRate(mut_rat),
                          m_iPopSize(pop_size),
                          m_iChromoLength(num_bits),
                          m_dTotalFitnessScore(
0.0),
                          m_iGeneration(
0),
                          m_iGeneLength(gene_len),
                          m_bBusy(
false)
        
    
{
        CreateStartPopulation();
    }

    
    
void            Run(HWND hwnd);
 
    
void            Render(int cxClient, int cyClient, HDC surface);
 
 
void          Epoch();
    
    
//可访问方法
    int             Generation(){return m_iGeneration;}
    
int             GetFittest(){return m_iFittestGenome;}
 
bool      Started(){return m_bBusy;}
 
void          Stop(){m_bBusy = false;}
}
;
由上述内容可知,当这个类的一个实例被创建时,构造函数初始化所有的变量,并调用CreateStartPopulation()。这一短小函数创建了所需数量的基因组群体。每个基因一开始包含的是一个由随机二进制位串组成的染色体,其适应性分数则被设置为零。
3.4.2           Epoch(时代)方法
遗传算法类中最为实际的内容就是Epoch()方法。这就是本章早些时候讲过的遗传算法的那个循环。它是这个类中执行工作的部门。这一方法与所有工作或多或少都联系在一起。
下面就对它进行详细讲述。
void CgaBob:epoch(){
    UpdateFitnessScores();
在每一个epoch循环内所要做的第一件事情,就是测试染色体群中每一个成员的适应性分数。UpdateFitnessScores()是用来对每个基因组的二进制染色体编码进行译码的函数,而由它再把译码所得到的一系列结果,也就是由代表东,南,西,北4个方向的整数,发送给CBobsMap::TestRoute。这个方法检查Bob在地图中走了多远,并根据Bob离开出口的最终距离,返回一个相应的适应性分数。计算Bob的适应性分数程序如下:
    int DiffX=abs(posX-m_iEndX);
    
int DiffY=abs(posY-m_iEndY);
这里,DiffX和DiffY就是Bob所在格子的位置相对于迷宫出口的水平和垂直偏离值。
    return 1/(double)(DiffX+DiffY+1);
上面这一行程序就是计算Bob的适应性分数。它把DiffX与DiffY这两个数字加起来然后求倒数。DiffX与DiffY的和还加了一个1,这是为了确保除法不会出现分母为零的错误(如果Bob到达了出口,DiffX+DiffY=0)。
UpdateFitnessScores也保持对每一代中适应性分数最高的基因组以及与所有基因组相关的适应性分数的跟踪。这些数值在执行赌轮选择时要使用。
下面接着说Epoch函数。由于在每一个Epoch中将会创建一个新的基因组群,因此,当它们在创建出来时(每次2个基因组),需要寻找一些地方来保存它们。
   
//现在创建一个新的群体
    int NewBabies=0;
    
//为婴儿基因组创建存储器
    vector<SGenome> vecBabyGenomes;
后面就是遗传算法循环。我们来详细讨论一下。
   
while(NewBabies<m_iPopSize){
        
//用赌轮法选择两个上辈(parents)
        SGenome mum=RouletteWheelSelection();
        SGenome dad
=RouletteWheelSelection();
在每次迭代过程中,需要选择两个基因组来作为两个新生婴儿的染色体的父辈,分别称为dad和mum(母亲)。一个基因组的适应性愈强,则由赌轮方法选择作为父母的几率也愈大。
       
 //杂交操作
        SGenome baby1,baby2;
        Crossover(mum.vecBitsdad.vecBits,baby1.vecBits,baby2.vecBits);
以上2行程序的工作是:创建两个空白基因组,这就是两个婴儿,还有所选的父辈一起传递给杂交函数Crossover()。这一函数执行了杂交(需依靠m_dCrossoverRate变量来进行),并把新的染色体的二进制位存放到baby1和baby2中。
       
//变异操作
        Mutate(baby1.vecBits);
        Mutate(baby2.vecBits);
以上两个步对婴儿实行突变。一个婴儿的位的突变概率依赖于m_dMutationRate变量。
       
//把两个新生婴儿加入新群体
        vecBabyGenomes.push_back(baby1);
        vecBabyGenomes.push_back(baby2);
 
        NewBabies
+=2;
    }
这两个新生后代最终要加入到新的群体中,这样就完成了一次Loop的迭代过程。这一过程需要不断重复,直到创建出来的后代总量和初始群体的大小相同。
 
   //把所有婴儿复制到初始群体
    m_vecGenomes=vecBabyGenomes;
    
//代的数量加1
    ++m_iGeneration;
}
这里,原有的那个群体由新生一代所组成的群体来代替,并把代的数量加1,以跟踪当前的代。
这一Epoch函数将无止境地重复,直到染色体收敛到了一个解,或用户要求停止时为止。上述各种操作的代码会在后面章节给出,在此首先讲述如何确定使用参数的值。
3.4.3           参数值选
程序所用的所有参数存放在文件defines.h中。这些参数中大多数是一目了然的,但有几个需要说明一下,即
#define CROSSOVER_RATE 0.7
#define MUTATION_RATE 0.001
#define POP_SIZE 140
#define CHROMO_LENGTH 70
如何确定这些变量的初值?这是价值百万美元问题,但至今还没有快速而有效的规则,有的只是一些原则性的指导。而且,选择这些值最终还应归结为每个程序员对遗传算法的体验,程序员只能通过自己的编程实践,用各种不同的参数值进行调试,看结果会发生什么,并从中选择适合的值。不同的问题需要不同的值,但是通常来说,如果在使用二进制编码的染色体,则应把杂交率定在0.7,变异率定在0.001,这将是很好的初始默认值。而确定群体大小的一条有用规则是将基因组的数目取为染色体长度的2倍。
Bob可以走35步,而每一步需要2个二进制位,所以,染色体长度被定为了70。它比Bob为穿越地图到达出口所需的步数还要大一些。学习了以后几章的知识后可以使遗传算法变得更为有效,到时将这个长度减小即可。
历史的注释:遗传算法是John Holland创造的,在20世纪60年代初期,他已提出了这种想法。但不可思议的是,他没有感到需要在计算机上实际试验出结果,而是用笔和纸来修修改改。直到后来他的一名学生编写出程序并在一台个人计算机上运行后,才使人们终于看到在软件中利用他的思想能够得到什么。

 

您好:
    当您在阅读和使用我所提供的各种内容的时候,我非常感谢,您的阅读已是对我最大的支持。
    我更希望您能给予我更多的支持。
    1.希望您帮助我宣传我的博客,让更多的人知道它,从中获益(别忘记了提醒他们帮我点点广告,嘿嘿)。
    2.希望您能多提出宝贵意见,包括我所提供的内容中的错误,建设性的意见,更希望获得哪些方面的帮助,您的经验之谈等等。
    3.更希望能得到您经济上的支持。
   
    我博客上面的内容均属于个人的经验,所有的内容均为开源内容,允许您用于任何非商业用途,并不以付费为前提,如果您觉得在阅读和使用我所提供的各种内容的过程中,您得到了帮助,并能在经济上给予我支持,我将感激不尽。

    您可以通过点击我网站上的广告表示对我的支持。

    您可以通过银行转帐付款给我:
    招商银行一卡通:
    卡号:6225888712586894
    姓名:牟勇
   
    您也可以通过汇款的方式:
    通讯地址:云南省昆明市女子(28)中学人民中路如意巷1号
    收信人:陈谦转牟勇收
    邮编:650021
   
    无论您给予我怎么样的支持,我都衷心的再次感谢。
    欢迎光临我的博客,欢迎宣传我的博客
    http://blog.csdn.net/mouyong
    http://blog.sina.com.cn/mouyong
    EMail:mouyong@yeah.net
    QQ:11167603
    MSN:mouyong1973@hotmail.com

 

<script type="text/javascript"><!--google_ad_client = "pub-3269163127493396";google_ad_width = 120;google_ad_height = 90;google_ad_format = "120x90_0ads_al";//2007-05-08: ITgoogle_ad_channel = "0064192373";google_color_border = "6699CC";google_color_bg = "003366";google_color_link = "FFFFFF";google_color_text = "AECCEB";google_color_url = "AECCEB";//--></script><script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>
原创粉丝点击