五大基础算法(枚举、递归、分治、贪心、模拟)

编程基础文章目录:

五大基础算法 基础数据结构(栈和队列) 散列表
常见C++知识 基础数据结构(数组、串、广义表) 四大比较排序算法
基础数据结构(线性表) 基础数据结构(树和堆)  

微博:LinJM-机器视觉 Blogger:LinJM

关于基础算法,就是我们设计算法时首先应该想到的几类算法。在本文我主要会介绍的基础算法有:枚举法、递归法、分治法、贪心法、模拟法。

一、枚举法

枚举法,本质上就是搜索算法。

基本思想:

  • 枚举也称作穷举,指的是从问题所有可能的解的集合中一一枚举各元素。
  • 用题目中给定的检验条件判定哪些是无用的,哪些是有用的。能使命题成立。即为其解。

优缺点:

  • 优点:算法简单,在局部地方使用枚举法,效果十分的好
  • 缺点:运算量过大,当问题的规模变大的时候,循环的阶数越大,执行速度越慢

例题解析

例题1、百钱买百鸡问题:有一个人有一百块钱,打算买一百只鸡。到市场一看,公鸡一只3元,母鸡一只5元,小鸡3只1元,试求用100元买100只鸡,各为多少才合适?

根据题意我们可以得到方程组:

  3X + 5Y + Z/3 = 100;
  X + Y + Z = 100;
  (100 > X,Y,Z > 0, Z%3 == 0),根据这两个公式,我们可以写出最为简单的代码,一一列举所有解进行枚举

代码:

int x,y,z;
for( x = 0; x < 100; x++ )
   for( y = 0; y < 100 ; y++ )
       for( z = 0; z < 100; )
         {
           if( x + y + z == 100 && 3 * x + 5 * y + z / 3 == 100 )
                {
                  cout << x << " " << y << " " << z << endl;
                }
                z += 3;
            }

然而我们可以根据已知条件来进行优化代码,减少枚举的次数:

三种鸡的和是固定的,我们只要枚举二种鸡(x,y),第三种鸡就可以根据约束条件求得(z = 100 - x - y),这样就缩小了枚举范围。
另外我们根据方程特点,可以消去一个未知数,得到下面
4X + 7Y = 100;
X + Y + Z = 100;
(X,Y,Z > 0, Z%3 == 0),=>>    0 <= x < = 25因此代码可以优化为下面这样子:

for( x = 0; x <= 25; x++ )
{
            y = 100 - 4 * x;
            if( y % 7 == 0 && y >= 0 )
            {
                y /= 7;
                z = 100 - x - y;
                if( z % 3 == 0 && 3 * x + 5 * y + z / 3 == 100  )
                   cout << x << " " << y << " " << z << endl;
            }
}

采用枚举的方法进行问题求解,需要注意3个问题:

  • 简单数学模型,数学模型中变量数量尽量少,它们之间相互独立。这样问题解的搜索空间的维度就小,反应到程序代码中,循环嵌套的层次就会少。我们上面从3维优化到一维。
  • 减少搜索的空间。利用已有知识,缩小数学模型中各个变量的取值范围,避免不必要的计算。反应到程序代码中,循环体被执行的次数少
  • 采用合适的搜索顺序。对搜索空间的遍历顺序要与数学模型中的条件表达式一致。
例题2、生理周期问题

人生来就有三个生理周期,分别为体力、感情和智力周期,它们的周期长度为23天、28天和33天。每一个周期中有一天是高峰。在高峰这天,人会在相应的方面表现出色。例如,智力周期的高峰,人会思维敏捷,精力容易高度集中。因为三个周期的周长不同,所以通常三个周期的高峰不会落在同一天。对于每个人,我们想知道何时三个高峰落在同一天。对于每个周期,我们会给出从当前年份的第一天开始,到出现高峰的天数(不一定是第一次高峰出现的时间)。你的任务是给定一个从当年第一天开始数的天数,输出从给定时间开始(不包括给定时间)下一次三个高峰落在同一天的时间(距给定时间的天数)。例如:给定时间为10,下次出现三个高峰同天的时间是12,则输出2(注意这里不是3)。  
Input  
输入四个整数:p, e, i和d。 p, e, i分别表示体力、情感和智力高峰出现的时间(时间从当年的第一天开始计算)。d 是给定的时间,可能小于p, e, 或 i。 所有给定时间是非负的并且小于365, 所求的时间小于21252。    
当p = e = i = d = -1时,输入数据结束。  
Output  
从给定时间起,下一次三个高峰同天的时间(距离给定时间的天数)。  

#include <iostream>
using namespace std;
int main()
{ int p, e ,i, d; 
int x; 
int num = 1; 
while(cin>>p>>e>>i>>d) { 
   if(p!=-1 && e!=-1 && i!=-1 && d!=-1) 
   { for( x=d+1;x<=21252;x++)
       { if((x-p)%23==0&&(x-e)%28==0&&(x-i)%33==0) 
		{ cout <<"Case " << num << ": the next triple peak occurs in " << x - d <<" days." << endl; 
		  break; 
         } 
        } 
    num++;} 
	else break; 
    }
 return 0;
}

但是这样做是没有必要的,我们可以进行代码优化,在问题的数学模型中,有多个条件可以满足时(我们要满足3个条件),可以采用逐步减小搜索空间的方法提高计算的效率,依次按照条件一,条件二,。。进行搜索。在最初的搜索空间上,首先按照条件一就行判定,然后将符合条件一的搜索空间,作为下面条件的索索空间。代码可以优化成:

#include<iostream>
using namespace std;
int main()
{
	int p,e,i,d;
	int j;
	int count = 0;
	while (cin>>p>>e>>i>>d)
	{
		count++;
		if (p==-1&&e==-1&&i==-1&&d==-1)
			break;
		for (j=d+1;j<=21252;j++)
			if ((j-p)%23==0)
				break;
		for(;j<=21252;j+=23)
			if((j-e)%28==0)
				break;
		for(;j<=21252;j+=644)
			if((j-i)%33==0)
				break;
		cout<<"Case "<<count<<": the next triple peak occurs in "<<j-d<<" days."<<endl;
	}
	return 0;
}

例题3、Safecracker
"The item is locked in a Klein safe behind a painting in the second-floor library. Klein safes are extremely rare; most of them, along with Klein and his factory, were destroyed in World War II. Fortunately old Brumbaugh from research knew Klein's secrets and wrote them down before he died. A Klein safe has two distinguishing features: a combination lock that uses letters instead of numbers, and an engraved quotation on the door. A Klein quotation always contains between five and twelve distinct uppercase letters, usually at the beginning of sentences, and mentions one or more numbers. Five of the uppercase letters form the combination that opens the safe. By combining the digits from all the numbers in the appropriate way you get a numeric target. (The details of constructing the target number are classified.) To find the combination you must select five letters v, w, x, y, and z that satisfy the following equation, where each letter is replaced by its ordinal position in the alphabet (A=1, B=2, ..., Z=26). The combination is then vwxyz. If there is more than one solution then the combination is the one that is lexicographically greatest, i.e., the one that would appear last in a dictionary." 
v - w^2 + x^3 - y^4 + z^5 = target 
"For example, given target 1 and letter set ABCDEFGHIJKL, one possible solution is FIECB, since 6 - 9^2 + 5^3 - 3^4 + 2^5 = 1. There are actually several solutions in this case, and the combination turns out to be LKEBA. Klein thought it was safe to encode the combination within the engraving, because it could take months of effort to try all the possibilities even if you knew the secret. But of course computers didn't exist then." 

题目其实意思很简单,就是给定一个目标值target,再给你一个备选字符串(5~12个字符),要你在这个字符串里选5个出来,满足题中给定的等式,并且你选择的这5个字符组成的字符串必须是所有可能情况中按字典序最大的情况。简单分析下就可以看出,就是一个组合问题,问题解的最大规模就是12取5,就是12*11*10*9*8*7,而最小规模是5取5,所以用枚举法就可以搞定。

#include<stdio.h>  
#include<string.h>  
#include<math.h>  
int vist[1000],flog,len,target,s[10],top,loction[10];
int m[1000];
int cmp()
{
    int v,w,x,y,z;//一定要注意转换成int型的精度问题
    v=s[1];
    w=(int)(pow(s[2]*1.0,2.0)+0.5);
    x=(int)(pow(s[3]*1.0,3.0)+0.5);
    y=(int)(pow(s[4]*1.0,4.0)+0.5);
    z=(int)(pow(s[5]*1.0,5.0)+0.5);
    if(v-w+x-y+z==target)
     return 1;//相等反回1
     return 0;
}
void DFS(int n)
{
    int i;
    s[++top]=m[n];//放数的栈
    loction[top]=n;//记绿m[n]的位置
    vist[n]=1;//表示第n个位置被访问了
 
    if(top==5)//相等就可以比较
        flog=cmp();
    if(flog)//为真,则找到了不用往下
        return ;
    if(top<5)//只有栈中小于5个数才继续可以放数
    {
        for(i=1;i<=len;i++)
        if(vist[i]==0)
        {
            DFS(i);
            if(flog)//用来结束上层的DFS
            return ;
        }
    }
    vist[n]=0;top--;//退回
}
int main()
{
   char str[1000],tem;
   int i,j,e;
   while(scanf("%d %s",&target,str)>0)
   {
        if(target==0&&strcmp(str,"END")==0)
        break;
       flog=0;
       len=strlen(str);
       for(i=0;i<len;i++)//按字典序从大到小排
       {
           e=i;
           for(j=i+1;j<len;j++)
           if(str[e]<str[j])
           e=j;
           tem=str[i];str[i]=str[e];str[e]=tem;
       }
       for(i=0;i<len;i++)//变成数字
       m[i+1]=str[i]-'A'+1;
 
       for(i=1;i<=len;i++)//以第i个数为开头
       {
           memset(vist,0,sizeof(vist));
           top=0;
           DFS(i);
           if(flog)
           break;
       }
       if(flog)
       {
           for(i=1;i<=top;i++)
           printf("%c",str[loction[i]-1]);
       }
       else
       printf("no solution");
       printf("\n");
   }
}

二、递归法
递归法是设计和描述算法的一种有力的工具,它在复杂算法的描述中被经常采用。
设一个未知函数f,用其自身构成的已知函数g来定义, 上述这种用自身的简单情况来定义自己的方式成为递归定义。
3个要素:
递归边界:算法要有一个边界出口,能结束程序

参数收敛:每次调用参数都是收敛于递归边界

自身调用
递归函数常使用堆栈,算法效率低,费时和费内存
递归按其调用方式分为直接递归和间接递归。所谓直接递归是指递归过程P直接自己调用自己;间接递归是指P包含另一个过程D,D调用P。

递归算法适用的一般场合

按递归定义的数据定义形式

例如,Fibonacci数列:1,1,2,3,5,8,13,21,34,55
按递归定义的数据之间的关系(数据结构)
树的遍历、图的搜索
例题1:Hanoi题问题
Hanoi塔是根据一个传说形成的一个问题:
有三根杆子A,B,C。A杆上有N个(N>1)穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至B杆:
每次只能移动一个圆盘;
大盘不能叠在小盘上面。
问:如何移?最少要移动多少次?
例题2:整数划分问题
整数划分问题是算法中的一个经典问题之一,有关这个问题的讲述在讲解到递归时基本都会有涉及。

将整数n表示成一系列正整数之和:n=n1+n2+n3+...+nk,其中n1>=n2>=n3>=...>=nk,k>1。求n的不同划分个数。

例题3:全排列问题
已知集合R={r1,r2,r3,...,rn},请设计一个算法生成R中n个元素的全排列。
全排列:从n个不同元素中任取m(m<=n)个元素,按照一定的顺序排列起来,叫做从n个元素中取出m个元素的一个排列。当m=n时,所有的排列情况叫全排列。
令Rj=R-{rj}
记集合X中元素的全排列记为perm(X)。那么(rj)perm(X)表示在全排列perm(X)的每一种排列前加上前缀rj所得到的排列,所以,R的全排列可归纳定义如下:
当n=1时,perm(R)=(r)

当n>1时,perm(R)由(r1)perm(R1),(r2)perm(R2) ... (rn)perm(Rn)构成

三、分治法
有许多算法在结构上是递归的:为了解决一个给定问题,算法要一次或多次地调用其自身来解决相关的子问题。这些算法通常采用分治策略:将原问题分成n个规模较小而结构与原问题相似的子问题。递归地对这些子问题求解,然后合并其结果就得到原问题的解。n=2时的分治法又称为二分法。
步骤
分解——将原问题分解成一系列子问题
解决——递归地解各子问题
合并——将子问题的结果合并成原问题的解
例题解析
例题1:快速排序
输入数组为a[p:r],通过排序算法使其有序。
思路:
划分:以a[q]为基准元素将a[p:r]划分成3个部分:a[p:q-1],a[q],a[q+1:r]。使a[p:q-1]中的任一元素都小于a[q],a[q+1:r]中的任一元素都大于等于a[q],q是在划分过程中确定的。
通过递归调用,对a[p:q-1]和a[q+1:r]进行同样的操作
例题2:归并排序
理牌时一种可行的方案是将一副牌一分为二,一人洗一半,然后合并起来,请将该方法用程序表示出来。
思路:将待排序的元素分成大致相同的2个子集合,分别对2个子集合进行排序,最终将排序的子集合合并成排好序的集合。
四、贪心法
贪心法:即每次选择最优值,从而达到全局最优的方法,它是从问题的某一个初始解出发,向给定的目标递推。推进的每一步做一个当时看似最佳的贪心选择,不断地将问题实例归纳为更小的相似的子问题,并期望通过所作的局部最优选择产生一个全局最优解。
例题1 删数问题
键盘输入一个高精度的正整数n(<=240位),去掉任意s个数字后剩下的数字按原左右次序将组成一个新的正整数。编程对给定的n和s,寻找一种方案,使得剩下的数最小。
思路 首先必须注意,试题中N的有效位为240位,而计算机中整数有效位充其量也不过11位,无论如何都不能达到试题的数值要求。因此,必须采用可含256个字符的字串来代替整数。
以字串形式输入N,使用尽可能逼近目标的贪心法来逐一删除其中的s个数符,每一步总是选择一个使剩下的数最小的数符删除。
考虑只删一个数的情况,最优解是删除出现的第一个左边>右边的数,因为删除之后高位减小,很容易想...那全局最优解也就是这个了,因为删除S个数字就相当于执行了S次删除一个数,因为留下的数总是当前最优解。
五、模拟法
模拟法是最直观的算法,通常是对某一类事件进行描述,通过事件发生的先后顺序进行输入输出。模拟法只要读懂问题的要求,将问题中的各种事件进行编码,即可完成。
模拟法(Simulation)主要是在考验程式设计人员编写程式码的功力,而非考验程式设计者的急智和创意。Simulation可说是程式设计的基本功──问题说得清清楚楚,不用设计复杂的演算法,只要照着规定做就好。这类题目很适合程式设计的初学者。
Simulation的问题有时相当难缠。若是问题规定的错综复杂,那么写起程式码就会相当累人。若是一不小心犯了错误,只能望着那长篇大论、杂乱无章的程式码,从中找出错误所在,相当痛苦。

另外,有一些目前尚未解决的经典题目,像是著名的Josephus Problem,由于目前没有好的演算法,所以大家遇到了这种问题,就只能乖乖的照着题目的要求来做。这一类的题目也就变成了Simulation的题目。


参考文献:
[1] ACM枚举法讲解和练习[EB/OL]. [2013-12-23]. http://www.cnblogs.com/m6830098/p/3405647.html
[2] 归并排序与快速排序的比较[EB/OL]. [2013-12-23]. http://blog.csdn.net/intrepyd/article/details/4341769
[3] 台湾师范大学ACM算法入门教程[EB/OL]. [20132-12-23].http://acm.nudt.edu.cn/~twcourse/
[4] 余立功.ACM/ICPC算法训练教程[M].北京:清华大学出版社,2013.
[5] 维基百科相关词条


你可能感兴趣的:(贪心算法,递归法,分治算法,枚举法)