前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >你必须知道的基础算法

你必须知道的基础算法

作者头像
洋仔聊编程
发布2019-01-15 17:01:54
7110
发布2019-01-15 17:01:54
举报

基础算法简介

(1).贪心算法

对于贪心算法,我们要先将问题简化,然后依据贪心算法的理念,例如可以一起进行的事情,让他们一起进行。可以用一个条件完成的,就用一个条件完成。贪心算法就像人的贪心理念一样,先将可以贪的贪干净,然后在考虑特殊的情况,这样可以很好地进行代码的编写。

贪心算法也叫做贪婪算法是一种对某些求最优解问题的更简单、更迅速的设计技术。用贪婪法设计算法的特点是一步一步地进行,常以当前情况为基础根据某个优化测度作最优选择,而不考虑各种可能的整体情况,它省去了为找最优解要穷尽所有可能而必须耗费的大量时间,它采用自顶向下,以迭代的方法做出相继的贪心选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题,通过每一步贪心选择,可得到问题的一个最优解,虽然每一步上都要保证能获得局部最优解,但由此产生的全局解有时不一定是最优的,所以贪婪法不要回溯。

下面给大家分析一个经典例题:田忌赛马问题。

简单题意为: 田忌和主公赛马,输的一方要付200钱给赢方,已知各个马匹的速度,并且两人所拥有的马匹相同,求田忌所能赢得的钱数。

思路形成过程: 先将马匹的速度从大到小排列起来,根据贪心思想,我们需要先对最小值进行比较,如果田忌的马匹的速度大,则胜场加一。反之负场加一,若速度相同,在对最大值进行比较如果田忌的马匹的速度大,则胜场加一。反之负场加一。最后将胜场减去负场乘200输出即可。

代码如下:

代码语言:javascript
复制
#include<iostream> 
#include<algorithm> 

using namespace std; 
bool cmp(int a,int b) 
{ 
   return a>b;  
} 
int main()    
{ 
   int n,i,j,win,lost; 
   int a[10000]; 
   int b[10000]; 
   while(cin>>n&&n) 
   { 
        for(i=0;i<n;i++) 
           cin>>a[i]; 
        for(i=0;i<n;i++)   
           cin>>b[i];   
        sort(a,a+n,cmp);//将马匹的速度从大到小排列 
        sort(b,b+n,cmp);//同上 
        win=0; 
        lost=0; 
        inttmax=0,tmin=n-1,kmax=0,kmin=n-1; 
        while(tmax<=tmin) 
        { 
            if(a[tmin]>b[kmin])//先从最小的速度进行比较 
            { 
               win++; 
                tmin--; 
                kmin--; 
            } 
            else if(a[tmin]<b[kmin]) 
            { 
                lost++; 
                tmin--; 
                kmax++; 
            } 
            else 
            { 
                if(a[tmax]>b[kmax])//如果最小速度相同则进行最大值进行比较 
                { 
                    win++; 
                    tmax++; 
                    kmax++; 
                } 
                else 
                { 
                    if(a[tmin]<b[kmax]) 
                        lost++; 
                    tmin--; 
                    kmax++; 
                } 
           } 
        } 
       cout<<(win-lost)*200<<endl; 
   } 
}

(2)二分算法与三分算法

二分算法相对于三分算法来说简单一点,下面我们先来看看这个二分算法。

对于二分算法也叫做二分查找,二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。

二分算法的数学模型就是单调函数求零点。下面为大家介绍一个多人分披萨例题:

简单题意:有多个面积不相等的披萨,分给多个人,要求每个人拥有的披萨面积相同,并且不能重组。先输入x和y为披萨和人的个数,之后输入x个披萨的半径,求每个人所能拥有的最大面积。

思路形成:采用二分法,先记录所有面积的总和作为最大值max,最小值为零min=0,求中间值mid,然后将所有的披萨计算这个面积所能满足的人数和真实人数作比较,之后再用二分法的基本方法计算即可。(其中最重要的是当计算满足的人数时,应该将人数类型设为整形)。

代码如下:

代码语言:javascript
复制
#include<iostream> 
#include <cstdio>
#include<algorithm> 
using namespace std; 
double pai=acos(-1.0); 
int main() 
{ 
   int n,m,f,i,p,pp; 
   double k,size[10000],low,hight,sum,mid; 
   cin>>n; 
   while(n--) 
   { 
        sum=0; 
        cin>>m>>f; 
        for(i=1;i<=m;i++) 
        { 
            cin>>k; 
            size[i]=k*k*pai; 
            sum+=size[i]; 
        } 
        i=0; low=0; hight=sum/f;   p=0; 
        while(1) 
        { 
            p=0; 
            mid=(low+hight)/2;  //二分法中间值
            for(i=1;i<=m;i++) 
            { 
                pp=size[i]/mid;//将pp设为整形 
                p+=pp; 
            } 
            if(p>f) 
                low=mid;  //二分法的 基本代码
            else 
                hight=mid; 
            if((hight-low)<0.00001) 
            { 
                mid=(hight+low)/2; 
               cout<<fixed<<setprecision(4)<<mid<<endl; 
                break; 
            } 
        } 
   }     

}

二分法的基本代码:

代码语言:javascript
复制
mid=(low+hight)/2; if(p>f) 
                  low=mid;
                  hight=mid; 
                  if((hight-low)<0.00001) 
                  { 
                     mid=(hight+low)/2; 
                     cout<<    <<endl; 
                     break; 
                   } 

对于三分算法就是解决凸形或者凹形函数的极值的方法,mid = (Left + Right) / 2

midmid = (mid + Right) / 2如果mid靠近极值点,则Right = midmid;否则(即midmid靠近极值点),则Left = mid。

三分法的模板如下:

代码语言:javascript
复制
doublecal(Type a)
{
    /* 根据题目的意思计算 */
}
voidsolve()
{
    double Left, Right;
    double mid, midmid;
    double mid_value, midmid_value;
    Left = MIN; Right = MAX;
    while (Left + EPS <= Right{
        mid = (Left + Right) / 2;
        midmid = (mid + Right) / 2;
        if (cal(mid)>=cal(midmid))

            Right = midmid;
        else Left = mid; }
}

三分法应用的不多,就不再举例题了。

(3)搜索算法:DFS和BFS

DFS:深度优先搜索算法,正如算法名称那样,深度优先搜索所遵循的搜索策略是尽可能“深”地搜索图。在深度优先搜索中,对于最新发现的顶点,如果它还有以此为起点而未探测到的边,就沿此边继续汉下去。当结点v的所有边都己被探寻过,搜索将回溯到发现结点v有那条边的始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被发现为止。

BFS:广度优先搜索算法,是最简便的图的搜索算法之一,这一算法也是很多重要的图的算法的原型。Dijkstra单源最短路径算法和Prim最小生成树算法(后续有介绍)都采用了和宽度优先搜索类似的思想。他属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。它之所以称之为宽度优先算法,是因为算法自始至终一直通过已找到和未找到顶点之间的边界向外扩展,就是说,算法首先搜索和s距离为k的所有顶点,然后再去搜索和S距离为k+l的其他顶点。

下面介绍连连看典型BFS例题:

简单题意:规则是在一个棋盘中,放了很多的棋子。如果某两个相同的棋子,可以通过一条线连起来(这条线不能经过其它棋子),而且线的转折次数不超过两次,那么这两个棋子就可以在棋盘上消去。连线不能从外面绕过去的,玩家鼠标先后点击两块棋子,试图将他们消去,然后游戏的后台判断这两个方格能不能消去。

思路分析:首先明确这是一个搜索的问题,因为只要找到解决的方法即可,就考虑用广度搜索,建立一个队列,用方向数组将方向记录下来。在运用广度搜索的办法将每一级的所有可能情况压入队列,在判断是否到达最终条件即可。

代码如下: 

代码语言:javascript
复制
#include <iostream> 
#include<queue> 
#include<cstring> 
usingnamespace std; 
structnode  { 
    int x, y; 
    int t, d; }; 
queue<node>q; 
intn, m, map[1002][1002], prove; 
intvisit[1002][1002][4];  
intqry, sx, sy, ex, ey; 
intdx[4] = {0, -1, 0, 1}; 
intdy[4] = {1, 0, -1, 0}; 
intcheck(int x, int y)  { 
    if(x < 1 || x > n || y < 1 || y> m)//方向数组记录方向 
        return 0; 
    else 
        return 1; } 
voidbfs()  { 
    while(!q.empty()) 
        q.pop(); 
    memset(visit, 0, sizeof(visit)); 
    node s, e; 
    s.x = sx; s.y = sy; 
    s.t = 0;  s.d = -1; 
    q.push(s); 
    while(!q.empty())  { 
        s = q.front(); 
        q.pop(); 
        if(s.t > 2) 
            continue; 
        if(s.x == ex && s.y == ey)//最终成立的条件    { 
            prove = 1; 
            cout << "YES"<< endl; 
            break;    } 
        for(int i = 0; i < 4; i++)  { 
            e.x = s.x + dx[i];  e.y = s.y + dy[i]; 
            if(!check(e.x, e.y) ||visit[s.x][s.y][i])   
                continue; 
            if( map[e.x][e.y] == 0 || (e.x ==ex && e.y == ey) )   { 
                if(s.d == -1 || i == s.d)   { 
                    e.d = i; 
                    e.t = s.t; 
                    q.push(e); 
                    visit[s.x][s.y][i] = 1;     } 
                else   { 
                    e.d = i; 
                    e.t = s.t + 1; 
                    q.push(e); 
                   visit[s.x][s.y][i] = 1; 
                }    }  }   }  } 
intmain()  { 
    while(cin >> n >> m)  { 
        if(!n && !m) 
            break; 
            int i=1; 
            int j=1; 
        for(i = 1; i <= n; i++) 
        for( j = 1; j <= m; j++) 
            cin >> map[i][j]; 
        cin >> qry; 
        for(i=1; i<= qry;i++)   { 
            cin >> sx >> sy>> ex >> ey; 
            prove = 0; 
            if(map[sx][sy] == map[ex][ey]&& map[sx][sy] != 0) 
               bfs(); 

            if(!prove) 
                 cout << "NO" <<endl;   }   }  }

(4).动态规划以及动态规划中的背包问题

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。

动态规划中的背包问题之01背包问题:

讲述便从一个最经典的例题开始吧: 某商店有n个物品,第i个物品价值为vi,重量(或称权值)为wi,其中vi和wi为非负数, 背包的容量W,W为一非负数。目标是如何选择装入背包的物品,使装入背包的物品总价值最大,所选商品的一个可行解即所选商品的序列如何?背包问题与0-1背包问题的不同点在于,在选择物品装入背包时,可以只选择物品的一部分,而不一定要选择物品的全部。

简单题意:给定n和物品和一人背包,物品i的重量是wi,其价值为vi,问如何选择装入背包的物品,使得装入背包的物品的总价值最大?

思路形成:在代码中体现

代码如下: 

代码语言:javascript
复制
#include<stdio.h>  
int c[10][100];/*对应每种情况的最大价值*/  
int knapsack(int m,int n)  {  
int i,j,w[10],p[10],x[10];  
for(i=1;i<n+1;i++)  
        scanf("\n%d,%d",&w[i],&p[i]);  
for(i=0;i<10;i++)  
      for(j=0;j<100;j++)  
           c[i][j]=0;/*初始化数组*/  
for(i=1;i<n+1;i++)  
      for(j=1;j<m+1;j++)  
           {  
            if(w[i]<=j) /*如果当前物品的容量小于背包容量*/  
                     {  
                      if(p[i]+c[i-1][j-w[i]]>c[i-1][j])   
                          /*如果本物品的价值加上背包剩下的空间能放的物品的价值大于上一次选择的最佳方案则更新c[i][j]*/  
                            c[i][j]=p[i]+c[i-1][j-w[i]];  
                            else  
                            c[i][j]=c[i-1][j];  
                     }  
              else c[i][j]=c[i-1][j];      }  
printf("背包所能容纳商品的最大价值为:%d\n",c[n][m]);  
printf("所选择的商品的一个序列为:\n");  
for(i=n;i>=2;i--)       /*输出所选商品序列*/  
    {  
     if (c[i][m]==c[i-1][m]) x[i]=0;  
     else x[i]=1;  
     m=m-w[i];  
     }  
x[1]=c[1][m]?1:0;   
for(i=1;i<=n;i++)  
printf("%3d",x[i]);  }    
int main()  {  
    int n,W;int i,j,s;  
    clrscr();  
    printf("输入商品数量 n 和背包容量W:\n");  
    scanf("%d,%d",&n,&W);  
    printf("输入每件商品的重量,价值:\n");  
    knapsack(W,n);   
    printf("\n");   }

动态规划之完全背包问题列题:有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的体积是v[i],重量是w[i]。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且重量总和最大

思路形成:01背包中每种物品有且仅有一件,而完全背包问题则不同,每种物品均有无限件可用。显然,状态转移方程可以通过简单地修改01背包问题得s[i][j]=max{s[i-1][j-k*v[i]]+k*w[i]}(0<=k<=V/v[i]),递推边界和01背包相同,当i=0时,s[i][j]=0。从状态转移方程我们不难发现,此算法同样有O(NV)个状态需要求解,不同的是此算法求解每个状态的时间并不是O(1),所以总的时间复杂度是超过了O(NV)的。

优化:01背包问题中,需要决策的是是否选取第i种物品,同样,完全背包问题也可以这样决策。若不选取第i种物品,当前最优结果s[i][j]显然等于s[i-1][j];若选取第i种物品,则可以由之前选取了若干个第i种物品得到的最优解的基础上再加一个第i种物品得到,于是s[i][j]=s[i][j-v[i]]+w[i](s[i][j-v[i]]可以看作s[i-1][j-(k-1)*v[i]])。实际当前最优解是从以上两种情况中选取更优的结果,于是得到状态转移方程s[i][j]=max{s[i-1][j], s[i][j-v[i]]+w[i]}。递推边界和之前相同,当i=0时,s[i][j]=0。

部分代码如下:

代码语言:javascript
复制
for(int i=0; i<=V; i++) s[0][i]=0;  // 边界
for(int i=1; i<=N; i++)
{
      for (int j=0; j<=V; j++)
      {
            int t=0;
            if (j-v[i]>=0)t=s[i][j-v[i]]+w[i];
            s[i][j]=max(s[i-1][j], t);
      }

上面是个时空复杂度均为O(NV)的代码,显然通过将状态记录数组改成一维可以将空间复杂度降为O(V),具体实现见下面的代码:

代码语言:javascript
复制
for(int i=0; i<=V; i++) s[i]=0;  // 边界
for(int i=1; i<=N; i++)
{
      for (int j=v[i]; j<=V; j++)s[j]=max(s[j], s[j-v[i]]+w[i]);
}

其余的背包问题用的不多,便不再赘述。

(5)图算法

先说明两个图的存储方式邻接矩阵和邻接表,邻接矩阵的使用场合为:数据规模不大n <= 1000,m越大越好、稠密图最好用邻接矩阵、图中不能有多重边出现。

邻接表的基础代码为 

代码语言:javascript
复制
struct edge
{
    int x, y, nxt; typec c;
}bf[E];


voidaddedge(int x, int y, typec c)
{
    bf[ne].x = x; bf[ne].y = y; bf[ne].c = c;
    bf[ne].nxt = head[x]; head[x] = ne++;
}

并查集:将编号分别为1…N的N个对象划分为不相交集合,在每个集合中,选择其中某个元素代表所在集合。常见两种操作:合并两个集合,查找某元素属于哪个集合。

最小生成树问题之Prim算法:

基本思想:任取一个顶点加入生成树;在那些一个端点在生成树里,另一个端点不在生成树里的边中,取权最小的边,将它和另一个端点加进生成树。重复上一步骤,直到所有的顶点都进入了生成树为止。

基本内容:设G=(V,E)是连通带权图,V={1,2,…,n}。构造G的最小生成树的Prim算法的基本思想是:首先置S={1},然后,只要S是V的真子集,就作如下的贪心选择:选取满足条件iS,jV-S,且c[i][j]最小的边,将顶点j添加到S中。这个过程一直进行到S=V时为止。在这个过程中选取到的所有边恰好构成G的一棵最小生成树。

基础代码: 

代码语言:javascript
复制
int prim(int n,int mat[][MAXN],int* pre){
    int min[MAXN],ret=0;
    int v[MAXN],i,j,k;
    for (i=0;i<n;i++)
           min[i]=inf,v[i]=0,pre[i]=-1;
    for (min[j=0]=0;j<n;j++){
           for (k=-1,i=0;i<n;i++)
                  if(!v[i]&&(k==-1||min[i]<min[k]))
                         k=i;
           for(v[k]=1,ret+=min[k],i=0;i<n;i++)
                  if(!v[i]&&mat[k][i]<min[i])
                         min[i]=mat[pre[i]=k][i];
    }
    return ret;
}

最小生成树问题之Kruskal算法:

基本思想:将边按权值从小到大排序后逐个判断,如果当前的边加入以后不会产生环,那么就把当前边作为生成树的一条边。最终得到的结果就是最小生成树。并查集。

基本内容:把原始图的N个节点看成N个独立子图;每次选取当前最短的边,看两端是否属于不同的子图;若是,加入;否则,放弃;循环操作该步骤二,直到有N-1条边;一维数组,将所有边按从小到大的顺序存在数组里面先把每一个对象看作是一个单元素集合,然后按一定顺序将相关联的元素所在的集合合并。能够完成这种功能的集合就是并查集。对于并查集来说,每个集合用一棵树表示。它支持以下操作:Union (Root1, Root2) //合并两个集合;Findset(x) //搜索操作(搜索编号为x所在树的根)。树的每一个结点有一个指向其父结点的指针。

基础代码:

代码语言:javascript
复制
void MakeSet()
{
    long i;
    for (i=0;i<=m;++i)
    {
        father[i]=i;
    }
}

longFind(long i)
{
    long r=i;
    while (father[r]!=r)
    {
        r=father[r];
    }
    while (father[i]!=r)
    {
        long j=father[i];
        father[i]=r;
        i=j;
    }
    return r;
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016年07月07日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基础算法简介
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档