集训第二十六天(2017/8/25):集训总结

   

   这个暑假留在学校参加了ACM集训终于接近尾声了,收获很多,学到了很多算法和编程的知识,虽然非常累,但过得非常充实。

   每天的训练强度非常大,从早上九点到晚上十点,一直在训练,钻研知识点,刷题,比赛,但正是这高强度的训练,感觉自己学到了很多,进步了很多,这是仅仅平时上课所不能给予的,在不断地刷题过程中,提升了自己编程的能力,也渐渐形成了自己的代码风格。

  这个假期到现在为止一共打了7场比赛,从第一场手足无措一道题也没能AC,到能独立AC几道题,进步还是有的,总算没有辜负这些天的努力吧。还参加了CCPC,增长了见识,拓展了视野,至于结果,就是被虐了。下面就谈一谈具体学到的东西。

  集训分了四大块专题,分别是“动态规划和背包问题”,“搜索和图论算法”,“二分法和单调队列”和“树状数组”四大块。

  “动态规划和背包问题”专题是暑假前两周开的,自己着重学习了“背包问题”,终于能看懂了,要知道以前上课都听不大懂老师讲的,自己学习了“01背包问题”,“完全背包问题”,“多重背包问题”,还有“二进制优化”。把以前的一大困惑解决了,感觉很好~~~虽然运用的时候还不熟练,但收获还是很大的。

  “搜索和图论算法”专题包含的东西有很多,有“DFS,BFS”,“图的遍历问题”,“最短路径算法”,“图的连通性问题”等等。自己着重学习了DFS,BFS,图的遍历问题,最短路径算法,下面来一一总结一下:

首先说DFS(深度优先搜索),也叫做搜索与回溯算法,要用到递归,有一句话叫做“Man understands iteration, god understands recursion”,翻译过来就是“人理解迭代,神理解递归”,递归的难度,可见一斑。刚开始接触真的很难理解,DFS是对递归的更深入的应用,代码也非常长且复杂。 

   深度优先算法其实就是回溯+搜索。它的基本思想是:为了求得问题的解,先选择某一种可能的情况向前探索,在探索过程中,一旦发现这一步的选择是错误的,就退回一步返回上一层次,然后重新选择继续向下探索,如此反复,直至得解或证明无解。那么什么是回溯呢?回溯法是在用来求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索遍才结束。而回溯法在用来求问题的任一解时,只要搜索到问题的一个解就可以结束。这种以深度优先的方式系统地搜索问题的解的算法称为回溯法,它适用于解一些组合数较大的问题。回溯主要用递归来实现,在递归构造中,生成和检查过程可以有机结合起来,从而减少不必要的枚举。附上一道比较能深刻体现这种思想的例题:

eg.素数环

     问题简述:将从1n20整数围成一个圆环,若其中任意2个相邻的数字相加,结果均为素数,那么这个环就成为素数环。

#include<iostream>
#include<cmath>
using namespace std;
bool b[21]={0};
int total=0,a[21]={0};
int search(int);//
回溯过程
int print();//
打印结果
bool pd(int,int);//
判断素数
int main()
{
    search(1);

}
int search(int t)
{
    int i;
    for(i=1;i<=20;i++)
    if(pd(a[t-1],i)&&(!b[i]))//
满足条件
    {
        a[t]=i;//
保存结果
        b[i]=1;//
保存结果
        if(t==20)
        {if(pd(a[20],a[1]))
            print();}//
如果到达目的地输出解
        else search(t+1);/*
                         
回溯就是让计算机自动的去搜索,碰到符合的情况就结束或者保存起来,
                         
在一条路径上走到尽头也不能找出解,就回到上一级的岔路口,选择一条以前没有走过的路继续探测,直到找到解或者走完所有路径为止。
                         
就这一点,回溯和所谓的DFS(深度优先搜索)是一样的。那现在的关键是,怎么实现搜索呢?
                         
回溯既然一般使用递归来实现,那个这个递归调用该如何来写呢?
                         
我现在的理解就是,进行回溯搜索都会有一系列的步骤,每一步都会进行一些查找。
                         
而每一步的情况除了输入会不一样之外,其他的情况都是一致的。
                         
这就刚好满足了递归调用的需求。通过把递归结束的条件设置到搜索的最后一步,就可以借用递归的特性来回溯了。
                         
因为合法的递归调用总是要回到它的上一层调用的,
                         
那么在回溯搜索中,回到上一层调用就是回到了前一个步骤。
                         
当在当前步骤找不到一种符合条件情况时,那么后续情况也就不用考虑了,
                         
所以就让递归调用返回上一层(也就是回到前一个步骤)找一个以前没有尝试过的情况继续进行。
                         
当然有时候为了能够正常的进行继续搜索,需要恢复以前的调用环境。*/
        b[i]=0;
    }
}
int print()
{
    total++;
    if(total<=20)   //
限定打印前20个结果
    {cout<<"<"<<total<<">";
    for(int j=1;j<=20;j++)
      cout<<a[j]<<" ";
      cout<<endl;}
}
bool pd(int x,int y)
{
    int k=2,i=x+y;
    while(k<=sqrt(i)&&i%k!=0) k++;
    if(k>sqrt(i)) return 1;
    else return 0;

}
输出结果:

    更多有意思的题目限于篇幅没有附上,最让我印象深刻的一道题目就是HDU1013sudoku,是用DFS得出数独的答案,相当于数独游戏的一个外挂吧。暑假里一高中同学求教一数独难题,用这个程序不到1s就解决~~~

经过将近两周的训练,到现在也大致理解了递归的原理和用法,磕磕绊绊也能做出来题了,也能看懂题解了,记得刚接触ACM的时候是连题解都看不懂的

   然后是BFS(广度优先搜索),主要用队列来实现,这个思维量感觉比DFS小很多啊,但是代码依然很长很复杂。这种算法能解决哪类问题呢?如果目标节点的深度与“费用”(如路径长度)成正比,那么找到第一个解即为最优解,这时,搜索速度比dfs更快,再求最优解时一般采用bfs。其实广度优先搜索的核心思想是:从初始节点开始,应用算符生成第一层节点,检查目标节点是否在这些后继结点中,若没有,再用产生式规则将所有第一层节点逐一扩展,得到第二层节点,并逐一检查第二层节点中是否包含目标节点,若没有,再用算符逐一扩展到第二层的所有节点......如此依次拓展检查下去,直至发现目标节点位置。例题就不贴了,因为真的很繁琐很复杂,没有数据结构相关知识和没有学习ACM相关算法的非acmer是看不懂的,有兴趣的非acmer了解原理即可。

   至于“图的遍历问题”,主要包含“一笔画问题”和“哈密尔顿环”这两个块,也是用DFSBFS实现,不过这个“哈密尔顿环”用到了一个叫“图的邻接表存储法(又叫“链式存储法”)”,主要用链表实现,没搞懂这种方法的原理,所以“哈密尔顿环”还有点困惑。

   最后是“最短路径算法”,包含的东西很多,我着重学习了Floyed算法和Dijkstra算法,都可以求关于最短路径问题,最小花费问题等等,其实还有很多别的算法,因为我的进度比较慢,没有时间看了,以后抽时间补上。

 “二分法和单调队列”专题开了一个星期,二分法相比较而言还比较简单,但某些细节的处理还是很令人抓狂的,稍微漏掉一点或多了一点, wrong answer妥妥的(好像别的程序也是这样),而且最重要的是不好找出错的原因而单调队列本身代码实现就非常麻烦了,单调队列虽然原理简单,但用代码实现过程却不容易理解,因为它的队首和队尾都可以操作,灵活性很大。

     首先来看一下单调队列的原理:

     •队列中元素之间的关系具有单调性(可以是单调不增加或单调不减小,也就是说可以有重复,每个元素不必严格单调),而且,队首和队尾都可以进行出队操作,只有队尾可以进行入队操作。。

     •单调队列的常用操作如下:

     •(1)插入:若新元素从队尾插入后会破坏单调性,则删除队尾元素,直到插入后不再破坏单调性为止,再将其插入单调队列。

     •(2)获取最优(最大、最小)值:访问首尾元素。

举个例子:

    一组数(132156),进入单调不减队列的过程

1入队,得到队列(1);

3入队,得到队列(13);

2入队,这时,队尾的的元素3>2,将3从队尾弹出,新的队尾元素1<2,不用弹出,将2入队,得到队列(12);

1入队,2>1,将2从队尾弹出,得到队列(11);

5入队,得到队列(115);

6入队,得到队列(1156);

   “树状数组”这个知识点更是难懂,可以说是这次集训的最难的一块内容了,结构非常抽象,实现的非常巧妙,发明这个的人简直是个天才!还是阐述一下基本的概念:

树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询)

假设数组a[1..n],那么查询a[1]+...+a[n]的时间是log级别的,而且是一个在线的数据结构,支持随时修改某个元素的值,复杂度也为log级别。

来观察这个图:

 

令这棵树的结点编号为C1C2...Cn。令每个结点的值为这棵树的值的总和,那么容易发现:

C1 = A1

C2 = A1 + A2

C3 = A3

C4 = A1 + A2 + A3 + A4

C5 = A5

C6 = A5 + A6

C7 = A7

C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8

...

C16 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 + A9 + A10 +A11 + A12 + A13 + A14 + A15 + A16

这里有一个有趣的性质:

设节点编号为x,那么这个节点管辖的区间为2^k(其中kx二进制末尾0的个数)个元素。因为这个区间最后一个元素必然为Ax

所以很明显:Cn = A(n – 2^k + 1) + ... + An

算这个2^k有一个快捷的办法,定义一个函数如下即可:

 

int lowbit(int x){

return x&-x;

}

1

2

3

当想要查询一个SUM(n)(a[n]的和),可以依据如下算法即可:

step1: 令sum = 0,转第二步;

step2: 假如n <= 0,算法结束,返回sum值,否则sum = sum + Cn,转第三步;

step3: n = n – lowbit(n),转第二步。

可以看出,这个算法就是将这一个个区间的和全部加起来,为什么是效率是log(n)的呢?以下给出证明:n = n – lowbit(n)这一步实际上等价于将n的二进制的最后一个1减去。而n的二进制里最多有log(n)1,所以查询效率是log(n)的。

那么修改呢,修改一个节点,必须修改其所有祖先,最坏情况下为修改第一个元素,最多有log(n)的祖先。

所以修改算法如下(给某个结点i加上x):

step1: i > n时,算法结束,否则转第二步;

step2: Ci = Ci + x i = i + lowbit(i)转第一步。

i = i +lowbit(i)这个过程实际上也只是一个把末尾1补为0的过程。

对于数组求和来说树状数组简直太快了!

    到此为止,大致就是集训的内容,限于篇幅,以上只是总体的阐述了一下各个专题的基本思想与概念,并没有深入探讨,深度与难度也远不及真正的比赛,因为每个专题都能出一本书啊~~~

   暑假终于结束,紧接着就是迎接新学期了。继续努力吧。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值