码迷,mamicode.com
首页 > 其他好文 > 详细

ACM总结

时间:2016-07-19 10:10:05      阅读:194      评论:0      收藏:0      [点我收藏+]

标签:

贪心算法

贪心算法并不能保证得到最优解,但很多问题确实可以得到最优解比如活动选择问题活动选择问题
活动选择问题。是一个调度竞争共享资源的多个活动问题,目标是选出最大的互相兼容的活动集合。假定有要给n个活动的集合,使这些活动使用同一个资源,而这个资源在某个时刻只能供一个活动使用。每个活动都有一个开始时间和一个结束时间。如果被选中,任务发生在半开时间区间。如果两个活动不重叠则称他们是兼容的。在活动选择问题中我们希望选择出一个最大兼容活动集。假定活动已按结束时间单调递增顺序排序。
贪心算法只需要考虑一个选择(贪心选择),在做贪心选择时,子问题必须是空的。基于这些观察,我们将找到一种递归算法来解决活动调度问题,并将递归算法转化为迭代算法,以完成贪心方法的过程。

贪心选择
对于活动选择问题,直观上,我们应该选择这样一个活动,选出他后剩下的资源应能被尽量多的其他任务所用。现在考虑可选的活动,其中有必然有一个最先结束。因此直觉告诉我们,应该选择集合中最早结束的活动,因为它剩下的资源可供它之后尽量多的活动使用。(如果集合中最早结束的活动有多个,我们可以选择其中的任意一个)。换句话说,由于活动已按结束时间单调递增排序,贪心选择就是活动a1。
当做出贪心选择后,只剩下一个子问题需要我们求解:寻找在a1结束后开始的活动。

贪心算法原理
贪心算法通过一系列选择来求出问题的最优解。在每个决策点,他做出在当时看起来最佳的选择。这种启发式策略并不保证能找到最优解,但对于有些问题确实有效,如活动选择问题。

步骤:
1. 确定问题有最优子结构
2. 设计一个递归算法证明如果我们做出一个贪心选择,则只剩下一个子问题。
3. 证明贪心选择重视安全的
4. 设计一个递归算法实现贪心策略
5. 将递归算法转换为迭代算法

贪心算法伪代码

GREEDY-ACTIVITY-SELECTOR(s,f)
n=s.length
A={a1}
K=1
For m=2 to n
    If s[m]>=f[k]
        A=AU{an}
        K=m
return A

二分法

算法:当数据量很大适宜采用该方法。采用二分法查找时,数据需是排好序的。
基本思想:假设数据是按升序排序的,对于给定值x,从序列的中间位置开始比较,如果当前位置值等于x,则查找成功;若x小于当前位置值,则在数列的前半段中查找;若x大于当前位置值则在数列的后半段中继续查找
,直到找到为止。

二分法实例:
例:在有序的有N个元素的数组中查找用户输进去的数据x。
算法如下:
1.确定查找范围front=0,end=N-1,计算中项mid(front+end)/2。
2.若a[mid]=x或front>=end,则结束查找;否则,向下继续。
3.若a[mid]

#include<iostream>
#define N 10
using namespace std;
int main()
{
int a[N],front,end,mid,x,i;
cout<<"请输入已排好序的a数组元素:"<<endl;
for(i=0;i<N;i++)
cin>>a[i];
cout<<"请输入待查找的数x:"<<endl;
cin>>x;
front=0;
end=N-1;
mid=(front+end)/2;
while(front<end&&a[mid]!=x)
{
if(a[mid]<x)front=mid+1;
if(a[mid]>x)end=mid-1;
mid=front + (end - front)/2;
}
if(a[mid]!=x)
cout<<"没找到!"<<endl;
else
cout<<"找到了!在第"<<mid+1<<"项里。"<<endl;
return 0;
}

搜索

搜索算法实际上是根据初始条件和扩展规则构造一棵“解答树”并寻找符合目 标状态的节点的过程。所有的搜索算法从最终的算法实现上来看,都可以划分成两个部分——控制结构(扩展节点的方式)和产生系统(扩展节点),而所有的算法 优化和改进主要都是通过修改其控制结构来完成的。其实,在这样的思考过程中,我们已经不知不觉地将一个具体的问题抽象成了一个图论的模型——树,即搜索算法的使用第一步在于搜索树的建立。
由图一可以知道,这样形成的一棵树叫搜索树。初始状态对应着根结点,目标状态对应着目标结点。排在前的结点叫父结点,其后的结点叫子结点,同一层中的结点是兄弟结点,由父结点产生子结点叫扩展。完成搜索的过程就是找到一条从根结点到目标结点的路径,找出一个最优的解。这种搜索算法的实现类似于图或树的遍历,通常可以有两种不同的实现方法,即深度优先搜索(DFS——Depth First search)和广度优先搜索(BFS——Breadth First Search)。

深度优先搜索
深度优先遍历图的方法是,从图中某顶点v出发:
(1)访问顶点v;
(2)依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;
(3)若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。 当然,当人们刚刚掌握深度优先搜索的时候常常用它来走迷宫.事实上我们还有别的方法,那就是广度优先搜索(BFS).

深度优先搜索用一个数组存放产生的所有状态。
(1) 把初始状态放入数组中,设为当前状态;
(2) 扩展当前的状态,产生一个新的状态放入数组中,同时把新产生的状态设为当前状态;
(3) 判断当前状态是否和前面的重复,如果重复则回到上一个状态,产生它的另一状态;
(4) 判断当前状态是否为目标状态,如果是目标,则找到一个解答,结束算法。
(5) 如果数组为空,说明无解。

C++的实现

struct Node  {    int self; //数据     node *left; //左节点     node *right; //右节点  };
const int TREE_SIZE = 9; 
    std::stack<node*> visited, unvisited; 
    node nodes[TREE_SIZE]; 
    node* current; 
    for( int i=0; i<TREE_SIZE; i++) //初始化树 
        {    
            nodes[i].self = i;   
            int child = i*2+1;    
            if( child<TREE_SIZE ) //Left child       
            nodes[i].left = &nodes[child];    
            else nodes[i].left = NULL;    
            child++;    
            if( child<TREE_SIZE ) //Right child           
            nodes[i].right = &nodes[child];    
            else       nodes[i].right = NULL;
        }             
     unvisited.push(&nodes[0]); //先把0放入UNVISITED stack  
     while(!unvisited.empty()) //只有UNVISITED不空  
     {    
         current=(unvisited.top()); //当前应该访问的    
         unvisited.pop();      
         if(current->right!=NULL)     
         unvisited.push(current->right); // 把右边压入 因为右边的访问次序是在左边之后     
         if(current->left!=NULL)     
         unvisited.push(current->left);     
         visited.push(current);     
         cout<<current->self<<endl; 
      }

广度优先搜索
宽度优先搜索算法(又称广度优先搜索)是最简便的图的搜索算法之一,这一算法也是很多重要的图的算法的原型。Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想。其别名又叫BFS,属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。

BFS,其英文全称是Breadth First Search。 BFS并不使用经验法则算法。从算法的观点,所有因为展开节点而得到的子节点都会被加进一个先进先出的队列中。一般的实验里,其邻居节点尚未被检验过的节点会被放置在一个被称为 open 的容器中(例如队列或是链表),而被检验过的节点则被放置在被称为 closed 的容器中。(open-closed表)


深度优先搜索用栈(stack)来实现,整个过程可以想象成一个倒立的树形:
1、把根节点压入栈中。
2、每次从栈中弹出一个元素,搜索所有在它下一级的元素,把这些元素压入栈中。并把这个元素记为它下一级元素的前驱。
3、找到所要找的元素时结束程序。
4、如果遍历整个树还没有找到,结束程序。
广度优先搜索使用队列(queue)来实现,整个过程也可以看做一个倒立的树形:
1、把根节点放到队列的末尾。
2、每次从队列的头部取出一个元素,查看这个元素所有的下一级元素,把它们放到队列的末尾。并把这个元素记为它下一级元素的前驱。
3、找到所要找的元素时结束程序。
4、如果遍历整个树还没有找到,结束程序。[1]  


BFS在求解最短路径或者最短步数上有很多的应用。
应用最多的是在走迷宫上。
单独写代码有点泛化,取来自九度1335闯迷宫[3]  一例说明,并给出C++/Java的具体实现。
在一个n*n的矩阵里走,从原点(0,0)开始走到终点(n-1,n-1),只能上下左右4个方向走,只能在给定的矩阵里走,求最短步数。n*n是01矩阵,0代表该格子没有障碍,为1表示有障碍物。
int mazeArr[maxn][maxn]; //表示的是01矩阵
int stepArr[4][2] = {{-1,0},{1,0},{0,-1},{0,1}}; //表示上下左右4个方向
int visit[maxn][maxn]; //表示该点是否被访问过,防止回溯,回溯很耗时。
核心代码。基本上所有的BFS问题都可以使用类似的代码来解决。

structNode
{
intx;
inty;
intstep;
Node(intx1,inty1,intstep1):x(x1),y(y1),step(step1){}
};
intBFS()
{
Nodenode(0,0,0);
queueq;
while(!q.empty())q.pop();
q.push(node);
while(!q.empty())
{
node=q.front();
q.pop();
if(node.x==n-1&&node.y==n-1)
{
returnnode.step;
}
visit[node.x][node.y]=1;
for(inti=0;i<4;i++)
{
intx=node.x+stepArr[i][0];
inty=node.y+stepArr[i][1];
if(x>=0&&y>=0&&x




动态规划


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

最有子结构:问题的最优解由相关子问题的最优解组合而成,而这些自问题可以独立求解

-----------
--钢条切割问题---

自顶向下递归实现

MEMOIZED-CUT-ROD(p,n)
let r[0..n] be a new array
for i=0 to n
    r[i]=-1
return MEMOIZED-CUT-ROD-AUX(p,n,r)

MEMOIZED-CUT-ROD-AUX(p,n)
if r[n]>=0
    return r[n]
if n==0
    q=0
else q=-1
    for i=1 to n
        q=max(q,p[i]+CUT-ROD(p,n-1)

r[n]=q
return q

自底向上
BOTTO,-UP-CUT-ROD(pan)
let r[0..n]be a new array
r[0]=0
for j=1 to n
    q=-1
    for i=1 to j
        q=max(q,p[i]+r[j-1])
    r[j]=q
return r[n]

-----------
-----------

带备忘录的自顶向下法(top-down with memoization)。此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解(通常保存在一个数组或离散列表中)。当需要一个子问题的解时,过程首先检查是否已经保存过此解。如果是,则直接返回保存的值,从而节省了计算时间;否则,按照通常方式计算这个子问题。我们称这个地柜过程时代备忘录的(memoized),因为它“记住”了之前已经计算出的结果。

自底向上法(bottom-up method)。这种方法一般需要恰当的定义子问题“规模”的概念,是的任何子问题的求解都之依赖于“更小的”子问题的求解。因而我们可以将子问题按规模排序,按由小至大额顺序进行求解。当求解某个子问题时,它所以来的那个更笑得子问题都已经求解完毕,结果已经保存。每个子问题只需要求解一次,党我们求解它(也就是第一次遇到它)时,它的所有前提子问题都已求解完成。



0-1背包例题详解
#include<iostream>
#include<string.h>
#include<stdio.h>
/*
函数判断返回较大值
也可以用
#define max((a),(b))    (a)>(b)?(a):(b)
感觉这个巧妙
*/
int max(int a,int b)
{
    if(a>b) return a;
    else    return b;
}



using namespace std;
int main()
{   //这个数组实际表示在这种背包状态下的价值,但是一开始写成这样了就只能这样了
    int package[1009][1009];

    //用两个数组表示体积和价值,看到有别人用结构体。也是个好办法
    int volume[1009];
    int value[1009];

    int cases;
    cin>>cases;
    while(cases--)
    {
        //考虑到在没东西的时候体积价值都会是零,先清零
        memset(package,0,sizeof(package));
        memset(volume,0,sizeof(volume));
        memset(value,0,sizeof(value));
        int N;//number of bones
        int V;//volume of bag

        cin>>N>>V;

        for(int i=1;i<=N;i++)
        {
            cin>>value[i];//value of each bone;
        }

        for(int i=1;i<=N;i++)
        {
            cin>>volume[i];//volume of each bone;
        }

        //前a个物品放入容积为b的包里,价值的最大值
        for(int a=1;a<=N;a++)
        for(int b=0;b<=V;b++)
        {
        //b的值必须从0开始循环,但是我的数组已经清零,
        //如果所有体积是0的价值都是0的话,
        //代码不该出问题的

            if(b-volume[a]>=0)
            //核心代码
            //只需要考虑当前的骨头要不要放
            //放进去的话,再考虑放入这个骨头之后剩下的体积有多大
            //在剩下的体积里,找出能放的最大价值是多少
            //package的第二个坐标表示的是体积
            //在这个体积下,对应的最大的价值一定是前面所有骨头都考虑过得情况
            //也就是让第一个坐标-1的原因
                package[a][b]=max(package[a-1][b-volume[a]]+value[a],                    package[a-1][b]);
            else
                package[a][b]=package[a-1][b];
        }
        cout<<package[N][V]<<endl;
    }
}


0-1背包优化

原代码
for(int a=1;a<=N;a++)
for(int b=0;b<=V;b++)
{
    package[a][b]=max(package[a-1][b-volume[a]]+value[a],package[a-1][b]);
}   

package[a]只与package[a-1]有关。
如果用package[a]覆盖package[a-1]的值,只需要一维数组

如果用原来的循环顺序
for(b=0,b<=V;b++)

那么后面的package计算值时,前面的值已经改变,所以用倒序

优化代码
for(int a=1;a<=N;a++)
for(int b=V;b>=0;b--)// 循环从V开始
{
    package[b]=max(package[b-volume[a]]+value[a],package[b]);
}   

完全背包
01背包中物品只有一个,完全背包中物品个数不限
推到过程忽略
代码为

完全背包原型
for(i=1;i<=N;i++)
for(j=0;i<=V;j++)
f[i][v]=max(f[i-1][v-k*c[i]]+k*w[i]|0<=k<=V/v[i])

优化
for(int a=1;a<=N;a++)
for(int b=0;b<=V;b–)//循环从零开始这里写代码片
{
package[b]=max(package[b-volume[a]]+value[a],package[b]);
}




多重背包

//完全背包

void complete_pack(int *a,int c,int w)
{
    for(int i=c;i<=v;i++)
        a[i]=max(a[i],a[i-c]+w);    
}

//01背包
void zeroone_pack(int *a,int c,int w)
{
    for(int i=v;i<=c;i--)
        a[i]=max(a[i],a[i-c]+w); 
}
//处理一个多重背包
{
    //该种物品足以塞满背包->转化为完全背包
    if(c*m>=v)
    {
        complete_pack(a,c,w);
        return;
    }

    int k=1;
    while(k<m)
    {
        zeroome_pack(a,k*c,k*w);
        m=m-k;
        k=2*k;
    }
    zeroone_pack(a,c*m,w*m);
}

“`
感想
ACM是一门需要智力的项目。曾经接受的科目是适合大多数学生学习,而在第一节课就深切的感受到这门课与其他科目不同,这门课的性质也决定了它必定不是一门轻松的课。曾经也抱着参加比赛的信念选了这门课(学长学姐提到过第一报这门课是为了比赛选队员)。一步一步走到现在却也意识到了自己的不足,智力水平并不能撑起自己的野心,有些能力能锻炼有些则比较难改变。试着接受智力水平的不足是这门课学到的经验,毕竟对于曾经的我来说承认自己智力上的不足是一件很难以置信的事。

ACM也是一门需要耐心的课。从步入大学还没有那门课牵扯这么多的精力,而且投入了精力不见得就有收获。有些题目耗费几个小时精力,最后发现原来的思路是错的,这种经历还没有体验过。曾经看到学霸凌晨还在刷题感觉对他们来说这门课已经很费力,对我来着会更费力。有过放弃的念头最后还是撑下来了。有些路自己选了再难也要走下去。从中获得的收获就是有些路一定不要再走第二遍。

这门课也验证了爱迪生的一句话:“天才,百分之一是灵感,百分之九十九是汗水.但那百分之一的灵感是最重要的,甚至比那百分之九十九的汗水都要重要.”

ACM需要灵感,ACM需要套路,同样需要灵感,甚至更需要灵感。有些代码理解起来很难,学起来更难。理解代码往往差的就是正好需要的灵感,懂与不懂中间只隔了一层纱。比如动态规划,灵感就是状态转移方程。写出状态转移方程整个题就迎刃而解。而我缺的就是那部分灵感。

算法是我现在接触到的最难的课程,如果有机会还是会建议周围的人接触一下算法。在算法这条路上可能没有太大出路但了解到自己的不足也算是最大的人生经验吧。况且,万一我周围就有适合做算法的天才呢。

ACM总结

标签:

原文地址:http://blog.csdn.net/nierunjie/article/details/51938760

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!