算法学习心得(1)

算法学习心得(1)


前言

我应该是从大一的上学期才了解到算法竞赛,那时刚刚报名参加蓝桥杯,当时的我只会c语言,因为学校教的内容太少,大一的寒假我又重新自学了一遍c语言,在练习系统刷了一些题发现,实在没法入门,难题连别人的题解都看不懂,有许多成系统的算法分类,算法模板都不了解,之后参加蓝桥杯,凭借刷了不少题的优势,勉强拿了一个省一,但到国赛发现,自己实在没法写国赛那种成体系的算法题,国赛之后,我才开始摸索学习算法竞赛的入门方式,也把编程语言改为了c++。因为我是网安专业的,所以算法的学习也是浅尝辄止,如果有哪些地方出现错误,还请各位指出来。


编程基础

编程语言入门

因为我所用的语言是c++,所以只向大家介绍c语言如何向c++过度,在这部分的最后,还会跟大家分享一下我编写一个程序的思考方式,编写方法,以及一些调试程序的方法。

  • 首先,如果你现在使用的是c语言,那么为了参加算法竞赛,你至少要掌握以下的知识:

输入输出、循环判断、函数、结构体、字符串、指针(指针在算法竞赛中一般不会使用,因为编写太麻烦,并且出错率太高,所以一般需要用指针的地方会使用数组模拟,但在校招中,指针的考察还是很多的)

  • c语言入门推荐大家学习慕课上浙江大学翁恺老师的c程序设计课程,可以在B站上搜到。

  • 如果这些知识学完了,那么你就可以无缝转换c++,简单的转换方式就是把原来的头文件改为

#include<bits/stdc++.h>
using namespace std;

简单的入门c++只需要学习其STL库就足以应付一般的算法比赛,但如果想要深入学习,还是需要重新学习一下c++。

所以,基本的编程框架为

#include<bits/stdc++.h>

using namespace std;

int main()
{

 	return 0;//一定要记住返回值为0
}
  • cin是c++中的标准输入流对象,因为有缓存区的缘故,它的速度会很慢,所以推荐使用scanf来读入数据,当然,一些简单的题,cin并不影响结果,而且编写代码的速度会很快。

数据结构的学习

  • 在学习完语言之后,推荐大家学习一下数据结构的知识,我们的大部分算法题,都是对各种数据结构的运用,推荐课程是在慕课上的浙江大学陈越老师的数据结构课程,在算法竞赛中,我们学习的数据结构一般的实现方式是数组模拟,或者使用c++自带的各种容器。

STL常用函数

  • 算法竞赛中,c++中我认为最常使用的几个函数跟大家介绍一下,除了这些函数,还有非常多好用的函数,有兴趣的同学可以自己去搜索一下,但推荐大家的是,这些函数的使用方法不需要背,了解其功能,在刷算法题的时候能够运用上,多用几遍就可以熟悉了。因为编译器的缘故,有些函数的编译需要编译器配置支持c++11相关设置,如果自己的编译器无法编译其中一些函数,可以百度搜索,解决问题。

devc++中可选择其中工具->编译选项,添加下面的命令,可以实现编译c++11

在这里插入图片描述

c++中常用的函数

swap(变量1,变量2)//交换两个变量的值
max(变量1,变量2)//返回值为两个变量的最大值
min(变量1,变量2)// 返回值为两个变量的最小值
  • 之后的几个函数,涉及到迭代器,如果简单理解,可以把迭代器理解为指针。

  • 不过,在本质上,迭代器仅仅是实现了指针的一部分功能,跟指针完全是两种东西

  • arr.begin(), arr.end(),分别是容器的迭代器(关于容器,之后会讲到),可以把它理解为返回容器最开始位置和最后一个数据单元+1的指针。

  • 如果是数组,可以采用数组名arr代替arr.begin(),用arr+k,代表前k个元素,比如大小为10的数组,如果要使用sort,可以使用 sort(arr,arr+10); 来进行从小到大的排序。

sort(arr.begin(),arr.end())
  • 从小到大排序,如果要从大到小排序可以把数都加个负号,或者写成
sort(arr.begin(),arr.end(),greater<数据类型>)
  • 如果要以结构体中的某个数据为关键字排序,那么需要自己编写后面的函数,或者重定义一下大于号,又或者是用反向迭代器,这些方法就不在这里细说了。
reverse(arr.begin(),arr.end())//翻转数组
alls.erase(unique(arr.begin(),arr.end()), arr.end());
  • 去重加删除重复元素,一般先排序后再写这条语句
nth_element(arr.begin(),arr+k,arr.end());
  • 默认是求区间第k小的数,把数换成负数,转为求第k大的数,或者 k=arr.size()-k;
*max_element(arr.begin(),arr.end())//求一定区间内的最大值
*min_element()//求一定区间内的最小值
count(arr.begin(),arr.end(), value);// 返回容器中所求元素的数量
memset(arr,value,sizeof(arr));
  • 把整个数组或者容器内的值初始化为value,如果设置一个比较大的数,一般设置为0x3f3f3f。我们会写成
    memset(arr,0x3f,sizeof(arr));,具体为什么会写成这样,大家可以自己百度了解一下

以上这些函数是我认为在学习算法初期比较常用的几个,我列举的方法只是最基础的,更多的内容需要大家自己搜索学习,并且还有许多的函数非常好用,在算法竞赛中能为我们节省许多时间,这些大家可以一边练习算法,一边记录下来你认为非常好用的函数,在练习中学习的效果是最好的。

容器

  • c++中给我们提供了非常多的容器,首先,我们必须理解一下什么是容器,在c++中容器被定义为:在数据存储上,有一种对象类型,它可以持有其它对象或指向其它对像的指针,这种对象类型就叫做容器。
  • 很简单,容器就是保存其它对象的对象,当然这是一个朴素的理解,这种“对象”还包含了一系列处理“其它对象”的方法,因为这些方法在程序的设计上会经常被用到,所以容器也体现了一个好处,就是“容器类是一种对特定代码重用问题的良好的解决方案”。
  • 容器还有另一个特点是容器可以自行扩展。在解决问题时我们常常不知道我们需要存储多少个对象,也就是说我们不知道应该创建多大的内存空间来保存我们的对象。显然,数组在这一方面也力不从心。容器的优势就在这里,它不需要你预先告诉它你要存储多少对象,只要你创建一个容器对象,并合理的调用它所提供的方法,所有的处理细节将由容器来自身完成。它可以为你申请内存或释放内存,并且用最优的算法来执行您的命令。

容器是随着面向对象语言的诞生而提出的,所以我尽量用不是面向对象编程的各种术语重新描述一下容器,以帮助大家理解。

容器就相当与一个无限容量的电饭煲,你所存放的数据就相当于大米,你不需要考虑买多大的电饭煲,你只要把米往里面装就行,你可以很简单的看到电饭煲的温度,因为它就显示在面板上,就如同你可以用O(1)的时间查看容器是否为空,有多少个数据,你也可以按键,电饭煲就把米饭蒸熟了,就像在容器中,集成了许多方法,帮你完成一些操作,你不需要关注米饭是怎么蒸熟的,你只要学会如何操作电饭煲让它开始蒸饭,并且根据加米的多少来改变操作。

  • 容器名.方法名() 可以帮助我们去使用容器中自带的各种方法
  • 接下来,我会列举一些容器,以及一些简单的方法,容器的使用方式,定义方式,另外的各种方法的还是需要大家自己去学习。
**vector 变长数组**
    size()  返回元素个数
    empty()  返回是否为空
    clear()  清空
    front()/back()
    push_back()/pop_back() 添加一个数到数组尾部/在数组尾部弹出一个数
	begin()/end() 
	for(std::vector<int>::iterator i = arr.begin(); i != arr.end(); m++ )//支持迭代器遍历
    支持比较运算,按字典序
**pair<int, int>**
    first, 第一个元素
    second, 第二个元素
    支持比较运算,以first为第一关键字,以second为第二关键字(字典序)
**string,字符串**
    size()/length()  返回字符串长度
    empty()
    clear()
    substr(起始下标,(子串长度))  返回子串
    c_str()  返回字符串所在字符数组的起始地址
**queue, 队列**
    size()
    empty()
    push()  向队尾插入一个元素
    front()  返回队头元素
    back()  返回队尾元素
    pop()  弹出队头元素
**priority_queue, 优先队列,默认是大根堆**
    size()
    empty()
    push()  插入一个元素
    top()  返回堆顶元素
    pop()  弹出堆顶元素
定义成小根堆的方式:
priority_queue<int, vector<int>, greater<int>> q;
priority_queue<-X>;//直接传入-X,按照大根堆排序
**stack,**
    size()
    empty()
    push()  向栈顶插入一个元素
    top()  返回栈顶元素
    pop()  弹出栈顶元素
**deque, 双端队列**
    size()
    empty()
    clear()
    front()/back()
    push_back()/pop_back()
    push_front()/pop_front()
    begin()/end()
  • set, map, multiset, multimap,unordered_set, unordered_map, unordered_multiset, unordered_multimap,这些容器也非常好用,在这就不一一列举了。

编程方法

接下来,是我认为这个心得最重要的部分,就是我编写一个程序的思考方式,算法竞赛的目的实际上就是培养编程能力,我将会通过一道简单的数据结构课后习题和一道比较经典的算法题,来向大家介绍一下我的思考方式,这些方法,就算大家不参加算法竞赛也能用的到。

一般来说,我们当看到一个要写的程序时,首先要分析我们要实现什么样的功能,比如,我们输入什么,输出什么,如果考虑到面向对象编程,我们更是要考虑代码的维护,以及功能的添加,以及各种细节问题,在这里,我以简单的课后练习的要求来讲解一下自顶向下编程的思考方式

  • 例如一道数据结构的课后题,判断回文字符串,我们要实现的功能很显然是输入一个字符串,然后判断这个字符串是否是回文字符串,之后,我们要构思一下主函数的编写方式,我们整个程序一定要有一个输入,我们要让程序从键盘获得一个字符串,以及一个输出,输出是否是字符串。根据实现的功能,我们要判断字符串是否是回文字符串,从而选择输出的语句是什么,也就是说我们要有一个判断字符串的函数,自顶向下的编程不要求我们一开始就实现细节,我们只要知道有这么几个函数就可以了,于是我们的主函数就写好了。
int main()
{
	string s;
	cin>>s;
	if(check(s)) cout<<”是回文字符串”<<endl;
	else cout<<”不是回文字符串”<<endl;
	return 0;
}

  • 之后我们的工作就变成了实现 check() 函数了,这个函数的实现非常容易,在这里就不细说了。

总结来说,编写一个程序,先搭好一个架子,保证其逻辑的严密,碰到棘手的问题,我们假装有一个函数去处理了,框架搭好后再进行细节的补充,实现各种函数,我们程序的调试,也从原来的调试某一段代码,变成调试一个函数,这样我们的效率也大大提高。我们在未来编程时要尽可能的把同属的功能集成到一个函数中完成,并且尽量不要在函数当中输出某些数据,你要做的就是把结果以返回值的形式返回到调用者哪里,再进行处理,基于此类要求,把主函数做的越精简越好

这句话的意思是把有实现一个功能的代码写成一个函数,而不是把所有功能都写为一个函数,这个函数跟主函数就没啥区别了,并且一个功能的实现也是许多小功能组合形成的,它们也可以写成一个函数,形成函数套函数的形式

在面向对象编程时,我们要充分发挥。实际上,编程方式同样有自底向上编程,但在学习阶段,自顶向下更适合我们,这会让你从大框架上去思考问题,更有利于我们编程的学习。

接下来我来写一道算法题。
算法题跟上面讲的有些区别,算法题要求的是快速,准确的把题目做出来,不需要考虑把主函数写的精简,但是我们还是一般把算法写成函数,把一些输入输出和简单的操作放到主函数中。

合根植物

问题描述
  w星球的一个种植园,被分成 m * n 个小格子(东西方向m行,南北方向n列)。每个格子里种了一株合根植物。
  这种植物有个特点,它的根可能会沿着南北或东西方向伸展,从而与另一个格子的植物合成为一体。
  如果我们告诉你哪些小格子间出现了连根现象,你能说出这个园中一共有多少株合根植物吗?
  
输入格式
  第一行,两个整数m,n,用空格分开,表示格子的行数、列数(1<m,n<1000)。
  接下来一行,一个整数k,表示下面还有k行数据(0<k<100000)
  接下来k行,第行两个整数a,b,表示编号为a的小格子和编号为b的小格子合根了。
  格子的编号一行一行,从上到下,从左到右编号。
  
  比如:5 * 4 的小格子,编号:
  
  1 2 3 4
  5 6 7 8
  9 10 11 12
  13 14 15 16
  17 18 19 20

  • 看到算法题的第一步就是先把题目的意思搞懂,把用到的知识点列出来,如果看到一道题,你一点思路都没有,那么你就尝试大概1个小时的时间,如果还是毫无思路,那就看一看题解,你需要把这道题用到的解题思路好好琢磨一下,但如果你有思路,那就可以一直死磕这道题,做算法题最忌讳的就是毫无思路的死磕,浪费时间不说,可能你好不容易死磕出来了,你的解法只适用于这一道题,换道题可能就不奏效了,这样的练习是没有效果的,当然了,在比赛过程中就不要想那么多了,别的题做完后,死磕就完了,因为没有时间让你去在学习一下了。

  • 在这道题中,我们很轻易地就能读出,他想让我们维护集合,合根的意思就是把两个集合合并就行了,我们的并查集就是专门处理这种题型。

  • 然后我们处理输入输出,首先我们需要输入 m,n,k,并且要做到把所有的合根的植物编号要么保存起来,要么在输入时就处理了。在这里,我们完全可以做到边读入边处理,之后所做的工作就是套用并查集的模板了,这里没什么好讲的,关于算法的细节方面,第二部分会详细讲解。

#include<bits/stdc++.h>
using namespace std;

const int N = 1e6+10;
int n,m,k;
int p[N];
int res = 0;

int find(int x){
	if(p[x] != x) p[x]=find(p[x]);
	return p[x];
}

int main() {
  
	scanf("%d%d",&m,&n);
	 for(int i = 1;i <= n * m;i++) p[i] = i;
	sacnf(%d”,&k);
	
	
	while(k --){
		int a,b;
		scanf("%d%d",&a,&b);
		p[find(a)]=find(b);				
	}
	
	for(int i = 1;i <= n * m;i ++) if(find(i) == i) res++;
	 
	cout<< res <<endl;
    return 0;
}


  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

洛玄lx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值