菜鸟白嫖的牛客算法课程第一讲的复习

         本人是大一非科班的学生,高中未接触算法竞赛, 在大学里对算法和编程抱有很大兴趣,最近几个月刚刚自学完c语言,准备磨练一下自己的算法。最近,看别人的学习分享说,看视频+实际编码+写博客是对自我能力提升的最好的一条道路,于是我便开始尝试写自己的第一篇博客,记录并总结自己在直播课中学习到的知识。由于本人知识水平有限,如有错误,还望各位大佬指正。

                                 牛客算法第一讲(枚举(尺取法、前缀和、差分等)、贪心)           

      一:算法:

      题目1: 明明的随机数

题目描述

明明想在学校中请一些同学一起做一项问卷调查,为了实验的客观性,他先用计算机生成了NN个11到10001000之间的随机整数(N≤100)(N≤100),对于其中重复的数字,只保留一个,把其余相同的数去掉,不同的数对应着不同的学生的学号。然后再把这些数从小到大排序,按照排好的顺序去找同学做调查。请你协助明明完成“去重”与“排序”的工作。

输入格式

输入有两行,第11行为11个正整数,表示所生成的随机数的个数NN

第22行有NN个用空格隔开的正整数,为所产生的随机数。

输出格式

输出也是两行,第11行为11个正整数MM,表示不相同的随机数的个数。

第22行为MM个用空格隔开的正整数,为从小到大排好序的不相同的随机数。

输入输出样例

输入 #1复制

10
20 40 32 67 40 20 89 300 400 15

输出 #1复制

8
15 20 32 40 67 89 300 400

说明/提示

NOIP 2006 普及组 第一题

题解1:暴力求解法:1.首先外层循环遍历数组,内层循环再次遍历,若发现两值相同,则将之剔除;2.冒泡法进行排序

#include<stdio.h>
int main()
{
	int N,i,j,count_0=0;
	scanf("%d",&N);
	int array[1001];
	for(i=1;i<=N;i++)
	    scanf("%d",&array[i]);
	for(i=1;i<N;i++)
	{
		if(array[i]==0)
		   continue;
		for(j=i+1;j<=N;j++)
		    if(array[i]==array[j])
		    {	  
			     array[j]=0;
			     count_0++;
		    }
	}
	for(i=N;i>1;i--)
	   for(j=1;j<=i-1;j++)
	       if(array[j]>array[j+1])
	       {
	       	     int temp=array[j];
	       	     array[j]=array[j+1];
	       	     array[j+1]=temp;
		   }
	printf("%d\n",N-count_0);
	for(i=1;i<=N;i++)
	    if(array[i])
	       printf("%d ",array[i]);
	return 0;
 } 

  这种算法的时间复杂度是O(n^2),但是这个算法在洛谷上面可以运行出来。

 暴力求解法2:可以先进行排序,再进行去重操作,但是洛谷题目要求输出不重复的个数,我不太会写如何在冒泡排序中记录重复元素的个数;(冒泡排序+去重)这种代码的运行时间大概是第一种的1/2;

统计思想算法(桶排序)3:利用一个数组b[n]={0},其中b[i]代表i这个数是否出现过,若i元素出现,则b[i]++。这种算法的时间复杂度是O(n)。最后输出的时候对b数组输出非零值。

#include<stdio.h>
int main()
{
	int N,i,temp,count=0;
	int b[1001]={0};
	scanf("%d",&N);
	for(i=1;i<=N;i++)
    {
    	scanf("%d",&temp);
    	b[temp]++;
	}
	for(i=1;i<=1000;i++)
	    if(b[i])
	        count++;
	printf("%d\n",count);
	for(i=1;i<=1000;i++)
	    if(b[i])
	        printf("%d ",i);
	return 0;
}

             

两种代码的时间复杂度:      

算法的特征:

1.有穷性:有限步后停止。  2.确定性     3.输入:有0个或者1个输入    4.输出:有一个或者多个输出 

5.可行性

算法的评价:

1.正确性     

2.可读性:要多写注释,多利用规范的变量名

3.健壮性(容错性)——对于不规范的数据输入要有处理的能力。

4.时间复杂度; 大O表示法的定义在我的理解中 相当于高数的等价无穷大(忽视系数)

其中:速度上

O(1)> O(logn)(底数一般为二,不过可应用换底公式,然后忽略系数)> O(sqrt(n))> O(n)(线性查找,明明的算法的第三种算法,图像为一条直线) > O(n^2> O (n^3)   ....   >  O(C^n)  > O(n!)。

 关于O(C^n)的证明 需应用高数上学期学习的极限知识。(好像最大时间复杂度一旦>=10^8,oj上就可能limit)

5.空间复杂度

int 32位二进制最大值为2147483648  10位。  注意:两个int的相加,相减,相乘都会溢出,在本学期oj系统习题上 ,老师出过相关溢出的编程题。

关于double 和  float :double : 64位二进制  double的科学计数法存储是 eg:2.33*10^2333;

double 占 8 个字节,64 位 :第一位为符号位(0表示正数,1表示负数);第2-12位表示指数部分

13-64  表示小数部分;   由于整数转换成二进制,在小数部分可能存在除不尽的情况,比如十进制0.3在转换成

二进制在小数部分会出现无限循环小数,因此double 存在精度差问题;

所以判断两double数相等时 是采用fabs(a-b)<eps;  其中eps=1e-6 或者1e-9;

在使用中不建议用float,float的精度过低。

8位二进制 = 1字节

1024字节 = 1 KB

1024KB = 1 MB

1024MB = 1 GB

注意一般对于编程的空间限制是512M,此时若按上值计算在最大可开10^8的int数组;

一个长度为10^6的double类型数组大约占8M;

注意:一般函数的递归使用对于空间的占据很大,所以一般将递归使用改为循环可减少空间复杂度。

   二. 枚举

*一 一 列举      *不重复,不遗漏         但此时时间复杂度都比较大,会limit.

所以枚举算法的重点在于 

1.选择合适的枚举对象。

2.选择合适的枚举方向-排除不符合条件的情况

3.选择合适的数据维护方法 - 转化问题。

题目1:最大正方形

题目描述

在一个n*m的只包含0和1的矩阵里找出一个不包含0的最大正方形,输出边长。

输入格式

输入文件第一行为两个整数n,m(1<=n,m<=100),接下来n行,每行m个数字,用空格隔开,0或1.

输出格式

一个整数,最大正方形的边长

输入输出样例

输入 #1复制

4 4
0 1 1 1
1 1 1 0
0 1 1 0
1 1 0 1

输出 #1复制

2

思路:使用枚举,判断所枚举的正方形是否符合条件。重点:如何简化枚举数量。

应用中学数学知识,在一个平面上两个对角线点可确定一个正方形。假设对角线上两点为:A:(X1,Y1),C;(X4,Y4)  则可求出

B:(    (X1+X4)/2-(Y4-Y1)/2  ,(Y1+Y4)/2-(X4-X1)/2   )     D:( (X1+X4)/2+(Y4-Y1)/2   ,  (Y1+Y4)/2+(X4-X1)/2);

这样正方形的四个点就确定了 ,再按照题意判断是否符合条件。

代码:小姐姐讲的题目是 判断正方形四点是#的最大边长,洛谷里的题目是不包含0的最大边长,不太会写呀。准备看习题课小姐姐的代码了

题目2: 数列求和问题

给你一个数列{an}(1<=n<=100000),有q(1<=q<=100000)次询问,每次询问数列的第li个元素到第ri个元素的的和。

1.暴力求解法

   直接双重循环计算。 此时最大的时间复杂度则是 10^5*10^5=10^10,肯定会出现limit。 一般情况下最大为10^8才可以过oj

2.简化思路,用sum[i]存储数组an中前i个数的和,那么sum[i]=sum[i-1] + a[i] , 当查询第li个元素到ri个元素的和时,可以用

sum[ri] - sum[li-1];

其中sum在算法中叫做前缀和,还有前缀最大和等,要注意,一般两个前缀和相减可得出子区间相关的值此时采用前缀和。

题目3:给你一个数列{an}(1<=n<=100000),有q(1<=q<=100000)次修改,每次把数列中的第li到ri的每个元素都加上一个值ki

,求所有的修改之后每个数的值。

题目2和题目3的复杂度瓶颈 在于对区间的修改需要对整个区间进行遍历。

所以要考虑转换: 将对区间的修改 改为 对区间端点的 修改,题目2 转化成对sum的修改

而本题则同理,此时区间每个元素都加上了ki,所以区间内每个元素减去前一个元素的差值不变,只有端点处的两处差值改变。

此时问题求解如2,设置一个delta数组记录数组an这个数与前一个数的差值。  若要还原数组,则只要delta的前缀和就是an;

我感觉这两个问题都可以理解为高中时的数列,sum是数列的前n项和,delta是各项数列之差。合理应用这两种前缀和和差分的

思想可以简化一定的时间复杂度。

题目4:校门外的树

题目描述

某校大门外长度为 ll 的马路上有一排树,每两棵相邻的树之间的间隔都是 11 米。我们可以把马路看成一个数轴,马路的一端在数轴 00 的位置,另一端在 ll 的位置;数轴上的每个整数点,即 0,1,2,\dots,l0,1,2,…,l,都种有一棵树。

由于马路上有一些区域要用来建地铁。这些区域用它们在数轴上的起始点和终止点表示。已知任一区域的起始点和终止点的坐标都是整数,区域之间可能有重合的部分。现在要把这些区域中的树(包括区域端点处的两棵树)移走。你的任务是计算将这些树都移走后,马路上还有多少棵树。

输入格式

第一行有两个整数,分别表示马路的长度 l 和区域的数目 m。

接下来 m行,每行两个整数 u, v,表示一个区域的起始点和终止点的坐标。

输出格式

输出一行一个整数,表示将这些树都移走后,马路上剩余的树木数量。

输入输出样例

输入 #1复制

500 3
150 300
100 200
470 471

输出 #1复制

298

说明/提示

数据规模与约定

  • 对于 20\%20% 的数据,保证区域之间没有重合的部分。
  • 对于 100\%100% 的数据,保证 1 \leq l \leq 10^41≤l≤104,1 \leq m \leq 1001≤m≤100,0 \leq u \leq v \leq l0≤u≤v≤l。

题解1:这道题我刚学c语言数组的时候接触过,第一遍做的时候就是暴力求解,初始数组为1,代表这个点有一颗树,然后进行m次遍历,每次将区间内的值改为0,最后对数组求和,计算出还剩多少棵树。

代码如下:

#include<stdio.h>
int main()
{
	int L , M,i,j;
	scanf("%d",&L);
	scanf("%d",&M);
	int b[2*M+1],a[L+1];
	for(i=1;i<2*M+1;i=i+2)
	   scanf("%d%d",&b[i],&b[i+1]);
	
	for(i=0;i<=L;i++)
	  a[i]=1;
	for(i=1;i<2*M+1;i=i+2)
	    for(j=b[i];j<=b[i+1];j++)
	        a[j]=0;
	int count=0;
	for(i=0;i<=L;i++)
	  count+=a[i];
	printf("%d",count);
	return 0;
}

可以很明显地看出这是最笨的一种做法(奈何本人目前太菜,只能想出这样的作法),时间复杂度是O(N*M),若给的测试数据过大,最终肯定会limit;

题解2:那么如何用前缀和差分的思想求解呢?

很明显去掉树,就是对区间内的值进行减一,此时就是差分的算法。

题解3:若增大数据变为:1<=L<=10^9和1<=M<=100000,此时对于L长度为10^9,无法开数组,一般开int数组最大为10^8.

此时要用到离散化的思想,将对需要操作的M组数据进行存储并编排,因为M是<=100000,可以用数组存储。 对这M组对应数据排序,然后求树的数量只需对这M组数据进行操作。

题目5:给定长度为n的整数数列以及整数S,求出总和不小于S的连续字串的长度的最小值,如果解不存在,输出0

(n<=10^7).

题解1:三次循环,第一重循环遍历i,第二次循环遍历j,然后求i到j的和并比较。此时时间复杂度是O(n^3),肯定会limit

题解2:第一次循环 i,然后 j 从 i+1 开始遍历直到从 i 到 j 的和大于等于s,然后第二次i++,j从上次j+1开始遍历在直到>=s,然后比较长度。 这个很容易理解把S的和想象成一把尺子,这个尺子的长度不变,若尺子的前端往后移,则后端也一定往后移。

题目6:有一个N*M的灯泡(N<=10,M<=100),每次按某一个点可以使得其本身以及其上下左右共五个灯的额开关反向,给定初始状态(每个灯泡的亮或者灭),问:能否把所有灯都灭掉?

题解:如何减少枚举的次数? 当第1列 确定怎么按了的时候,此时只有第二列决定第一列灯泡的亮灭,而若要第一列的灯全部灭掉,此时第二列的开关按法也确定了。同理,第三列取决于第二列,第四列取决于第三列,第五列取决于第四列。这样第二三四五列都取决于第一列。这样问题变成对第一列进行枚举。此时时间复杂度是2^N*N*M=10^6,可以过。这种思想叫做(状态压缩)

此时对第一列的枚举,可以用位运算(可怜我一个大学生还不知道什么是位运算,我学了假的c语言?)

但位运算咋用咋也不知道,咋也听不懂!

关于这个熄灯问题,我有幸在b站上看过北大的一位老师的讲解:https://www.bilibili.com/video/BV1Hx411U7bh?p=4

我感觉这位老师讲的很详细,也有代码操作的实现与技巧,可以看一下。

位运算介绍(哎,作为一个大学生从来没接触过位运算,太丢人了!)

*  << 左移              (1011)<<1 = (10110)   二进制1011左移一位为10110,相当于1011*2.

*  >>右移                (1011)>>1= 101        二进制1011右移一位为101,相当于1011/2.

*    |或                        我感觉可以理解为 c语言 if语句中的||,不过运算符从两个竖杠变成了一个竖杠。

*    & 与                      我感觉可以理解为c语言中if语句中的&&,不过运算符从两个变成了一个。

*    ~取反                    相当于数学中的非,比如非0就是1,非1就是0. (我的理解)。

*   ^异或                     是不是不一样(相同为0,不同为1),在二进制中理解为不带进制的加法。  好处一个二进制

                                   异或0不变,一个二进制异或1不变。  

                                  (我也是才学位运算才知道^这个符号怎么打的,原来必须要换成英文键盘后shift+6才会出来^,要不然会出来……       -   - !)。

其中小姐姐说要注意注意位运算的优先级问题,她自己被坑了十多次,我还没用过位运算,咋也不知道,咋也不敢写。

三.贪心算法(早闻其名,今天终于知道啥叫贪心,贪心为什么正确了)

例1:   排队接水

题目描述

有 n 个人在一个水龙头前排队接水,假如每个人接水的时间为Ti​,请编程找出这 n 个人排队的一种顺序,使得 n 个人的平均等待时间最小。

输入格式

第一行为一个整数 nn。

第二行 n 个整数,第 i 个整数Ti​ 表示第 i 个人的等待时间 Ti​。

输出格式

输出文件有两行,第一行为一种平均时间最短的排队顺序;第二行为这种排列方案下的平均等待时间(输出结果精确到小数点后两位)。

输入输出样例

输入 #1复制

10 
56 12 1 99 1000 234 33 55 99 812

输出 #1复制

3 2 7 8 1 4 9 6 10 5
291.90

说明/提示

 n≤1000,ti​≤106,不保证 tI不重复。

当ti​ 重复时,按照输入顺序即可(sort 是可以的)

思路:应该将打水快的人放在前面。因为把打水快的人t1放在前面,此时后面有n-1个人在等待,等待时间是(n-1)*t1,如果t1是时间慢的此时(n-1)*t1就会很大,以此类推,打水快的人先打水,所有人的等待时间最短。(现实生活中,在相同时间内,打水快的人先打完,所以不用等待了,完成的人多,还需等待的人少,所以总时间就少)

代码实现:
 

#include<stdio.h>
int main()
{
      int n , i , j;
      int  water[1002],index[1002];
      scanf("%d",&n);
      for(i=1;i<=n;i++)
          scanf("%d",&water[i]);
      for(i=1;i<=n;i++)
          index[i]=i;
       for(i=n;i>1;i--)
          for(j=1;j<=n-1;j++)
          {
          	  if(water[j]>water[j+1])
          	  {
          	      int temp=water[j];
				  water[j]=water[j+1];
				  water[j+1]=temp;
				  temp=index[j];
				  index[j]=index[j+1];
				  index[j+1]=temp;	
			  }
		  }
		double ans=0;
		for(i=1;i<=n;i++)
		 {
			  printf("%d ",index[i]);
			  ans+=water[i]*(n-i);
	     }
		printf("\n");
		printf("%.2lf",ans/n);
		return 0;
 } 

这是我写的代码 时间复杂度是O(n^2),没办法,能力过弱。

例2: 连数问题

设有n个正整数,将它们连接成一排,组成一个最大的多位整数

例如: n = 3时,3个整数13,312,343,连成的最大整数为34331213.

又如:n=4,4个整数7,13,4,246,连成的最大整数为7424613.

(n<=1000,每个数都在int范围内)

思路:按字典序的思路。也就是c语言中字符串比较函数cmp的应用;

例:a<aa<ab<ad<adcb<af   然后按照这个顺序进行连数。

但此时会出先算法遗漏问题 比如 3<31<39  但组成的最大整数是39331 而不是39313.

此时要对这个字典序派法进行优化:比较A和B的大小,即比较AB和BA的大小,然后判断谁排在前面。

代码实现 容我思索几天到时候补上来。

例3:区间覆盖(工作安排)

  在0到L的数轴上有n个区间[li,ri],现在需要你选出其中尽量多个区间,使得其两两不相交。(n<=100000)

思路:这尽量多的区间应该选择ri尽量小的区间。   粗略的证明:可以以第一个为例,不行呀,感觉我自己写不出来那种感觉,就是感觉上 觉得小姐姐讲的是对的,但我自己去思索 又感觉不对,让我再思索几天。

例四:活动安排 例五:国王游戏  感觉自己听的似懂非懂,让我消化几天回来再补上。

本文只是自己上网课的学习总结,以后遇到好的网课都会写总结。由于本人目前阶段尚是弱鸡,承蒙各位大佬指正,

你们的指正是我查漏补缺的动力。  

本文只是学习交流贴,适合像我这样刚学习完一门语言,初接触算法的小白同学。

原创文章 1 获赞 8 访问量 224

猜你喜欢

转载自blog.csdn.net/m0_47269393/article/details/106140107