C语言学习.docx

上传人:b****5 文档编号:29470085 上传时间:2023-07-23 格式:DOCX 页数:16 大小:24.83KB
下载 相关 举报
C语言学习.docx_第1页
第1页 / 共16页
C语言学习.docx_第2页
第2页 / 共16页
C语言学习.docx_第3页
第3页 / 共16页
C语言学习.docx_第4页
第4页 / 共16页
C语言学习.docx_第5页
第5页 / 共16页
点击查看更多>>
下载资源
资源描述

C语言学习.docx

《C语言学习.docx》由会员分享,可在线阅读,更多相关《C语言学习.docx(16页珍藏版)》请在冰豆网上搜索。

C语言学习.docx

C语言学习

编写递归程序有几个重要的原则可以遵循:

   1. 要解决的问题可拆分为几个与原问题类似的子问题(子问题仍可拆分)。

   2.每个子问题必须比原来问题的规模更小(即使小一号也行,当然如果能够迅速减小规模更好)。

   3. 遇到足够小的子问题时就直接解决之,防止问题无限细分下去,也就是防止无限递归(递归终止条件很重要)。

 

先看一个最简单的递归程序,下面程序求整数n的阶乘:

intfactorial(intn)

{

   returnn<=1?

1:

n*factorial(n-1);

}

所谓递归程序,就是程序在执行中又调用其自身,如上面函数factorial在其函数体内调用了函数factorial。

函数在每次被调用时都会生成一个包含自身局部变量的副本,即算是函数调用自身时也是如此。

递归程序主体主要由两部分构成:

一是递归执行部分,包含递归执行所需的条件;一是递归终止部分,含递归终止条件。

上面求阶乘的程序中把这两部分写在一行代码里面,把其分别抽出来便是:

intfactorial(intn)

{

   if(n<=1)return1;    // 递归终止体

    returnn*factorial(n-1);//递归执行体

}

下面以几个经典算法问题的递归程序求解为例来分析编写递归程序中可以遵循的简单规则。

1.求正整数n所有可能的和式的组合,如给定正整数3,所有和加起来等于3的和式如下:

3=3+0

3=2+1

3=1+1+1

其中一个和式中的因子只能为自然数,因子允许重复出现。

这个问题是一个组合问题的变形,用递归程序实现如下(分析见注释):

#defineMAXSIZE50       

staticintbuff[MAXSIZE];//存储和式因子组合的缓冲

///求当前和式中因子之和。

intSum(intbuff[],inti)

{

   intsum=0;

   for(intm=0;m<=i;m++)

      sum+=buff[m];

   returnsum;

}

///打印当前满足条件的和式。

voidprintFactor(intbuff[],inti,constintnumber)

{

   intcount=0,m;

   for(m=0;m<=i;m++)

      printf("%4d",buff[m]);

   if(number==buff[0])

      printf("%4d",0);

   printf("\n");

}

///递归求解所有和式组合。

///i-当前可选自然数的最大值。

///top-当前组合中因子个数。

///number-问题中要求解和式的正整数。

voiddivide(inti,inttop,intnumber)

{   

   for(intj=i;j>0;j--)              //注意这里的循环条件,可以有效防止出现重复的组合

   {

      buff[top]=j;

      if(number==Sum(buff,top))     //首先要确定递归中止条件

         printFactor(buff,top,number);

      elseif(number

         continue;

      if(top>=number)               //和式中的因子不应该超过和的大小

         continue;

      divide(j,top+1,number);         //减小问题规模,继续递归

   }

   return;

}

intmain()

{

   inti,top=0;

   intnumber=8;

   printf("inputasoucenumber:

");

   scanf("%d",&number);

   i=number;

   divide(i,top,number);

   return0;

}

2.给定正整数k,以及1-k共k个正整数的一个排列,假如是1,2,3,...,k,求所有和此排列“不相交”的排列。

所谓不相交,是指新的排列和已有排列在同一位置上的数都不相同。

如假设k=3,则其排列:

123和321是相交的,因为第二个位置上都是2;而排列123和312则不相交。

问题分析:

此问题是普通排列问题的变形,只要在排列问题的递归程序中加上位置的限制即可,程序实现如下,具体分析见注释。

constintN=5;

intrange[9]={1,2,3,4,5,6,7,8,9};//预定义好位置的排列

///判断当前选择的数j是否已经被使用。

boolselected(intj,int*buffer)

{

   for(inti=0;i

      if(buffer[i]==j)

         returntrue;

   returnfalse;

}

///求不相交排列。

///i-当前排列的位置或者说当前排列元素个数。

///buffer-存储当前排列的数组。

///count-存储不相交排列的数目。

voidarrange(inti,int*buffer,int&count)

{

   if(i==0)                     //递归终止条件

   {

      for(intk=0;k

         cout<

      cout<

      

      count++;

      return;

   }

   for(intj=1;j<=N;j++)        //循环挑选当前位置可以放置的数

   {

      if(selected(j,buffer))     //判断当前选定的数是够已经用过

         continue;

      buffer[i-1]=j;

      if(buffer[i-1]==range[i-1])//回溯的限制条件,同一位置的数不能和预定义的排列中相应位置的数相同

         continue;

       arrange(i-1,buffer,count);  //继续递归

   }

   buffer[i-1]=0;                //回溯,重新初始化当前位置

}

intmain()

{

   intbuffer[N]={0};

   intcount=0;

   arrange(N,buffer,count);

   cout<

   return0;

}

递归程序求解问题虽然优美简洁,但是和相应的非递归程序比较,随着问题规模的增大,程序的效率会逐渐下降,这也是递归程序的一大缺陷

 

尽管排列组合是生活中经常遇到的问题,可在程序设计时,不深入思考或者经验不足都让人无从下手。

由于排列组合问题总是先取组合再排列,并且单纯的排列问题相对简单,所以本文仅对组合问题的实现进行详细讨论。

以在n个数中选取m(0

1.首先从n个数中选取编号最大的数,然后在剩下的n-1个数里面选取m-1个数,直到从n-(m-1)个数中选取1个数为止。

2.从n个数中选取编号次小的一个数,继续执行1步,直到当前可选编号最大的数为m。

很明显,上述方法是一个递归的过程,也就是说用递归的方法可以很干净利索地求得所有组合。

下面是递归方法的实现:

///求从数组a[1..n]中任选m个元素的所有组合。

///a[1..n]表示候选集,n为候选集大小,n>=m>0。

///b[1..M]用来存储当前组合中的元素(这里存储的是元素下标),

///常量M表示满足条件的一个组合中元素的个数,M=m,这两个参数仅用来输出结果。

voidcombine(inta[],intn,intm, intb[],constintM)

 for(inti=n;i>=m;i--)  //注意这里的循环范围

 {

  b[m-1]=i-1;

  if(m>1)

   combine(a,i-1,m-1,b,M);

  else                    //m==1,输出一个组合

  {   

   for(intj=M-1;j>=0;j--)

    cout<

   cout<

  }

 }

}

因为递归程序均可以通过引入栈,用回溯转化为相应的非递归程序,所以组合问题又可以用回溯的方法来解决。

为了便于理解,我们可以把组合问题化归为图的路径遍历问题,在n个数中选取m个数的所有组合,相当于在一个这样的图中(下面以从1,2,3,4中任选3个数为例说明)求从[1,1]位置出发到达[m,x](m<=x<=n)位置的所有路径:

1 2 3 4

   2 3 4

       3 4

上图是截取n×n右上对角矩阵的前m行构成,如果把矩矩中的每个元素看作图中的一个节点,我们要求的所有组合就相当于从第一行的第一列元素[1,1]出发,到第三行的任意一列元素作为结束的所有路径,规定只有相邻行之间的节点,并且下一行的节点必须处于上一行节点右面才有路径相连,其他情况都无路径相通。

显然,任一路径经过的数字序列就对应一个符合要求的组合。

下面是非递归的回溯方法的实现:

///求从数组a[1..n]中任选m个元素的所有组合。

///a[1..n]表示候选集,m表示一个组合的元素个数。

///返回所有组合的总数。

intcombine(inta[],intn,intm)

{  

 m=m>n?

n:

m;

 int*order=newint[m+1];   

 for(inti=0;i<=m;i++)

 order[i]=i-1;           //注意这里order[0]=-1用来作为循环判断标识

 

 intcount=0;                               

 intk=m;

 boolflag=true;          //标志找到一个有效组合

 while(order[0]==-1)

 {

 if(flag)                  //输出符合要求的组合

 {  

  for(i=1;i<=m;i++)                   

   cout<

  cout<

  count++;

  flag=false;

 }

 order[k]++;               //在当前位置选择新的数字

 if(order[k]==n)         //当前位置已无数字可选,回溯

 {

  order[k--]=0;

  continue;

 }    

 

 if(k

 {

  order[++k]=order[k-1];

  continue;

 }

 

 if(k==m)

  flag=true;

 }

 delete[]order;

 returncount;

}

下面是测试以上函数的程序:

intmain()

{

 constintN=4;

 constintM=3;

 inta[N];

 for(inti=0;i

  a[i]=i+1;

 //回溯方法

 cout<

 //递归方法

 intb[M];

 combine(a,N,M,b,M); 

 return0;

}

由上述分析可知,解决组合问题的通用算法不外乎递归和回溯两种。

在针对具体问题的时候,因为递归程序在递归层数上的限制,对于大型组合问题而言,递归不是一个好的选择,这种情况下只能采取回溯的方法来解决。

   n个数的全排列问题相对简单,可以通过交换位置按序枚举来实现。

STL提供了求某个序列下一个排列的算法next_permutation,其算法原理如下:

1.从当前序列最尾端开始往前寻找两个相邻元素,令前面一个元素为*i,后一个元素为*ii,且满足*i<*ii;

2.再次从当前序列末端开始向前扫描,找出第一个大于*i的元素,令为*j(j可能等于ii),将i,j元素对调;

3.将ii之后(含ii)的所有元素颠倒次序,这样所得的排列即为当前序列的下一个排列。

其实现代码如下:

template

boolnext_permutation(BidirectionalIteratorfirst,BidirectionalIteratorlast)

{

 if(first==last)returnfalse;   //空範圍

 BidirectionalIteratori=first;

 ++i;

 if(i==last)returnfalse;       //只有一個元素

 i=last;                         //i指向尾端

 --i;

 for(;;)

 {

  BidirectionalIteratorii=i;

  --i;

  //以上,鎖定一組(兩個)相鄰元素

  if(*i<*ii)                    //如果前一個元素小於後一個元素

  { 

   BidirectionalIteratorj=last;  //令j指向尾端

   while(!

(*i<*--j));           //由尾端往前找,直到遇上比*i大的元素

   iter_swap(i,j);                //交換i,j

   reverse(ii,last);              //將ii之後的元素全部逆向重排

   returntrue;

  }

  if(i==first)                  //進行至最前面了

  { 

   reverse(first,last);           //全部逆向重排

   returnfalse;

  }

 }

}

下面程序演示了利用next_permutation来求取某个序列全排列的方法:

intmain()

{

 intia[]={1,2,3,4};

 vectoriv(ia,ia+sizeof(ia)/sizeof(int));

 copy(iv.begin(),iv.end(),ostream_iterator(cout,""));

 cout<

 while(next_permutation(iv.begin(),iv.end()))

 {

  copy(iv.begin(),iv.end(),ostream_iterator(cout,""));

  cout<

 }

 return0;

}

注意:

上面程序中初始序列是按数值的从小到大的顺序排列的,如果初始序列无序的话,上面程序只能求出从当前序列开始的后续部分排列,也就是说next_permutation求出的排列是按排列从小到大的顺序进行的。

最优化原理

  1951年美国数学家R.Bellman等人,根据一类多阶段问题的特点,把多阶段决策问题变换为一系列互相联系的单阶段问题,然后逐个加以解决。

一些静态模型,只要人为地引进“时间”因素,分成时段,就可以转化成多阶段的动态模型,用动态规划方法去处理。

与此同时,他提出了解决这类问题的“最优化原理”(Principle of optimality):

    “一个过程的最优决策具有这样的性质:

即无论其初始状态和初始决策如何,其今后诸策略对以第一个决策所形成的状态作为初始状态的过程而言,必须构成最优策略”。

简言之,一个最优策略的子策略,对于它的初态和终态而言也必是最优的。

    这个“最优化原理”如果用数学化一点的语言来描述的话,就是:

假设为了解决某一优化问题,需要依次作出n个决策D1,D2,…,Dn,如若这个决策序列是最优的,对于任何一个整数k,1

    最优化原理是动态规划的基础。

任何一个问题,如果失去了这个最优化原理的支持,就不可能用动态规划方法计算。

能采用动态规划求解的问题都需要满足一定的条件:

 

   

(1)问题中的状态必须满足最优化原理;

    

(2)问题中的状态必须满足无后效性。

    所谓的无后效性是指:

“下一时刻的状态只与当前状态有关,而和当前状态之前的状态无关,当前的状态是对以往决策的总结”。

问题求解模式 

    动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。

这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。

如图所示。

动态规划的设计都有着一定的模式,一般要经历以下几个步骤。

  初始状态→│决策1│→│决策2│→…→│决策n│→结束状态

    图1动态规划决策过程示意图

    

(1)划分阶段:

按照问题的时间或空间特征,把问题分为若干个阶段。

在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。

    

(2)确定状态和状态变量:

将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。

当然,状态的选择要满足无后效性。

    (3)确定决策并写出状态转移方程:

因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。

所以如果确定了决策,状态转移方程也就可写出。

但事实上常常是反过来做,根据相邻两段各状态之间的关系来确定决策。

    (4)寻找边界条件:

给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

算法实现

   动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。

使用动态规划求解问题,最重要的就是确定动态规划三要素:

问题的阶段,每个阶段的状态以及从前一个阶段转化到后一个阶段之间的递推关系。

递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。

确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。

下面分别以求解最大化投资回报问题和最长公共子序列问题为例阐述用动态规划算法求解问题的一般思路。

1.最大化投资回报问题:

某人有一定的资金用来购买不同面额的债卷,不同面额债卷的年收益是不同的,求给定资金,年限以及债卷面额、收益的情况下怎样购买才能使此人获得最大投资回报。

   程序输入约定:

第一行第一列表示资金(1000的倍数)总量,第二列表示投资年限;第二行表示债卷面额总数;从第三行开始每行表示一种债卷,占用两列,前一列表示债卷面额,后一列表示其年收益,如下输入实例,

100001

2

4000400

3000250

程序实现如下,注释几乎说明了一切,所以不再另外分析。

///此数组是算法的关键存储结构,用来存储不同阶段各种债卷

///组合下对应可获取的最大利息。

intsaifa[80005];

///此函数用于计算当前债卷在不同购买额下的最优利息情况,

///注意此时的利息情况是基于上一次债卷的情况下计算得到的,

///也就是说当前利息最优是基于上一次利息最优的基础上计算出来的,

///这也正好体现了动态规划中“最优化原则”:

不管前面的策略如何,

///此后的决策必须是基于当前状态(由上一次决策产生)的最优决策。

/*

   动态规划的求解过程一般都可以用一个最优决策表来描述,

   对于本程序,以示例输入为例,对于第一年,其最优决策表如下:

   0 1 2 3  4  5  6  7  8  9  10(*1000)  --

(1)

   0 0 0 0  400400400400800800800        --

(2)

   0 0 0 250400400500650800900900       --(3)

   

(1)--表示首先选利息为400的债卷在对应资金下的最优利息。

   

(2)--表示可用来购买债卷的资金。

   (3)--表示在已有状态下再选择利息为300的债卷在对应资金下的最优利息。

   注意上面表格,在求购买利息为300的债卷获得的最优收益的时候,

   参考了以前的最优状态,以3行8列的650为例,7(*1000)可以

   在以前购买了0张4000的债卷的基础上再2张3000的,也可以在以前购

   买了1张4000的基础上再买1张3000,经比较取其收益大的,这就是典

   型的动态规划中的当前最优状态计算。

   本程序中把上面的最优决策二维表用一个一维数组表示

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 幼儿教育 > 家庭教育

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1