《算法笔记》学习笔记 上

文章目录

  • 五. 数学问题
    • 1. 简单数学
    • 2. 最大公约数
    • 3. 最小公倍数(least common multiple)
    • 4. 分数
    • 5. 素数(Prime number)
    • 6. 质因子分解
    • 7. 大整数
    • 8. 拓展欧几里得算法及相关问题
    • 9. 组合数
  • 四. 算法初步
    • 1. 排序--sort
    • 2. 散列--map
    • 3. 分治
    • 4. 递归
    • 5. 贪心
    • 6. 二分
      • (1) 使用左闭右闭区间进行二分查找的通用思路
      • (2)二次幂
    • 7.two pointer
      • (1)归并排序
      • (2)快速排序
      • (3) 随机
    • 8. 打表
    • 9. 活用递推
    • 10. 随机选择算法
  • 三. 入门模拟
    • 1. 查找元素
    • 2. 日期处理
    • 3. 进制转换
  • 二. C++ 快速入门
    • 黑盒测试
      • 单点测试
      • 多点测试
        • 输入方式
        • 输出格式
    • 浮点数的比较
    • cin, cout
    • string.h
    • sscanf sprintf
    • gets,puts
    • scanf
    • getchar, putchar
    • memset
    • math 常用函数 (math.h)
    • tips
  • 特别提醒
    • 1. 编译错误
    • 2. 数据表示范围,尤其注意中间数据
    • 3. 栈区、静态区、堆大小的限制
    • 4. 时间及性能评估
      • (1)获取当前时间
      • (2)关于各种运算耗时评测
    • 5. 除模运算
  • 技巧
    • 1. 用累加替代乘
    • 2. 用纯粹累加的方式进行枚举

五. 数学问题

1. 简单数学

一些问题利用已知数学知识或公式,可使问题简化

2. 最大公约数

原理:gcd(a,b) = gcd(b,a%b)
推广 a,b 的公约数与 a%b, b 的全部相等

int gcd(int a, int b){
	return b?gcd(b, a%b):a;
}

3. 最小公倍数(least common multiple)

l c m ( a , b ) = a ∗ b / g c d ( a , b ) lcm(a,b) = a*b/gcd(a, b) lcm(a,b)=ab/gcd(a,b)

4. 分数

struct Fracture{
	int up, down;
}

假分数的三项规则

  1. 使down为非负数
  2. 若为0,则分子为0,分母为1
  3. 分子分母互质

5. 素数(Prime number)

  • 枚举法 1~sqrt(n),若用枚举法求素数表,复杂度 O ( n n ) O(n\sqrt{n}) O(nn )
  • 筛法, 求素数表时时间复杂段 O ( n l o g l o g ( n ) ) O(nloglog(n)) O(nloglog(n))
  • 素数大致分布范围:第100个:557; 第1000个7933; 第10 000个:104 759

6. 质因子分解

  • int 范围内的整数只需要 100 000的素数表就可以了,大概10 000个素数。
  • 1 不是质因子,但是因子, 注意1的特殊处理

定理一:任意一个大于1的自然数可以拆分成质因子乘积
n = 2 e 1 ∗ 3 e 2 ∗ 5 e 3 ∗ 7 e 4 ∗ 1 1 e 5 + . . . . . .              ( n > 1 ) n = 2^{e1}*3^{e2}*5^{e3}*7^{e4}*11^{e5}+......\ \ \ \ \ \ \ \ \ \ \ \ ( n>1) n=2e13e25e37e411e5+......            (n>1)
且 n 函数决定 序列 {e1, e2, e3, e4, e5 …}

推论1:n的因子个数为(n>1):
∏ i = 1 , 2 , 3 , . . . ( 1 + e i ) \prod_{i=1,2,3,...}{(1+e_i)} i=1,2,3,...(1+ei)
推论2:n 的因子之和
∏ i = 1 , 2 , 3 , . . . ( 1 + p i 1 + . . . + p i e i ) = 1 − p 1 e 1 + 1 1 − p 1 ∗ 1 − p 2 e 2 + 1 1 − p 2 ∗ . . . ∗ 1 − p k e k + 1 1 − p k \prod_{i=1,2,3,...}{(1+p_i^1+...+p_i^{e_i})}=\frac{1-p_1^{e_1+1}}{1-p_1}*\frac{1-p_2^{e_2+1}}{1-p_2}*...*\frac{1-p_k^{e_k+1}}{1-p_k} i=1,2,3,...(1+pi1+...+piei)=1p11p1e1+11p21p2e2+1...1pk1pkek+1
推论3:对于自然数 n 只存在 1 个或 0个质数 大于 n \sqrt{n} n
    反证: 若存在因子 pi, pj > n \sqrt{n} n , 则 pi,*pj也是因子,而 p   i   , ∗ p   j   > n p~i~,*p~j~>n p i ,p j >n

7. 大整数

拓展一:大浮点数

  • 麻烦的地方在于小数部分的对齐,同时低位为0时不输出

拓展二:进制转换

  • 原理和一般的进制转换一样,只不过需要使用一些大整数作为中间变量
  • 任何进制互转都可以采用逐次除余的方法。
  • 数据结构:

      class Bign{
      private:
      	int d[1000];
      	int len;
      public:
      	Bign(){
      		memset(d,0,sizeof(d));
      		len=0;
      	}
      	void change(char str[]);
      	int compare(const Bign&b) const;
      	void show();
    
      	Bign add(const Bign &b) const;
      	Bign sub(const Bign &b) const; 
      	Bign prod(int multiplier) const;
      	Bign div(int divisor, int &remainder);
    
      	void vert(Bign &c, int basef, int baseto);   // 进制转换
      };
    
  • 四则运算

Bign Bign::sub(const Bign &b1)const{
	if(compare(b1)<0){        // 如果b1 更大,则调用 b1的sub
		return b1.sub(*this);
	}
	Bign c;
	int carry=0;
	for(int i=0;i=b1.d[i]){
			c.d[i]=d[i]-b1.d[i]-carry;
			carry=0;
		}else if(d[i]-carry1&&c.d[c.len-1]==0){  // 去掉多余的0,且保证至少有一位
		c.len--;
	}
	return c;
}

Bign Bign::prod(int multiplier) const{
	int carry=0,tmp;
	Bign c;
	for(int i=0;i0){
		c.d[c.len++] = carry%10;
		carry/=10;
	}
	return c;
}
Bign Bign::div(int divisor, int &remainder){
	Bign c;
	int dividend=0, init=1;
	for(int i=len-1;i>=0;i--){
		dividend = dividend*10+d[i];
		c.d[i]=dividend / divisor;
		dividend %= divisor;
	}
	remainder = dividend;		// 可以在整个函数中不使用,dividend,用remainder替换。 
	c.len = len;
	while(c.len>1&&c.d[c.len-1]==0){
		c.len--;
	}
	return c;
}

void Bign::vert(Bign &c, int basef, int baseto){
	int r=0;
	c.len=0;
	Bign t=*this;
	while(len!=0){
		r=0;
		// 除余 
		for(int i=len-1;i>=0;i--){
			r=r*basef+d[i];
			d[i]=r/baseto;
			r%=baseto;
		} 
		while(d[len-1]==0&&len>0)
			len--;
		// 余数赋值给结果 
		c.d[c.len++]=r;
	}
	*this=t;
}

8. 拓展欧几里得算法及相关问题

  • 问题:给定两个非零整数 a 和 b ,求一组整数解 (x,y), 使 a x + b y = g c d ( a , b ) ax+by=gcd(a,b) ax+by=gcd(a,b)成立。
  • 分析:
    { x 1 a + y 1 b = g c d x 2 b + y 2 ( a % b ) = g c d \left\{\begin{array}{lr} x_1a+y_1b=gcd \\ x_2b+y_2(a\%b)=gcd \end{array} \right. \\ { x1a+y1b=gcdx2b+y2(a%b)=gcd
    ⟹ \Longrightarrow
    { x 1 = y 2 y 1 = x 2 − ( a / b ) y 2 \left\{\begin{array}{lr} x_1=y_2 \\ y_1=x_2-(a/b)y_2 \end{array} \right. \\ { x1=y2y1=x2(a/b)y2
    根据此递推式可以得到如下算法:
int exGcd(int a, int b, int &x, int&y){
	if(b==0){
		x=1;y=0;     // 递推出口为 a=1*gcd ,b=0
		return a;
	}
	int tmp, gcd;
	gcd = exGcd(b, a%b, x, y);
	tmp = x;
	x = y;
	y = tmp-a/b*y;
	return gcd;
}

在得到一组解之后,可以得到全部解(K 为任意整数):
x = x 0 + b g c d ∗ K y = y 0 + b g c d ∗ K x=x_0+\frac{b}{gcd}*K \\ y=y_0+\frac{b}{gcd}*K x=x0+gcdbKy=y0+gcdbK
还可以得到最小的正x, 正y。(主要考虑x0,y0为负数的情况):
x m i n + = ( x % t + t ) % t ,    t = b g c d x^+_{min} = (x\%t+t)\%t, \ \ t=\frac{b}{gcd} xmin+=(x%t+t)%t,  t=gcdb

推广一: a x + b y = c ax+by=c ax+by=c的求解
两边同时乘以 g c d c \frac{gcd}{c} cgcd,得
a x ′ + b y ′ = g c d x = x ′ × c g c d ,      y = y ′ × c g c d ax^{'}+by^{'}=gcd \\ x = x^{'} \times \frac{c}{gcd}, \ \ \ \ y = y^{'} \times \frac{c}{gcd} ax+by=gcdx=x×gcdc,    y=y×gcdc
有解的条件为 c % g c d = = 0 c\%gcd==0 c%gcd==0, 因为x,y为整数,而可以证明 x ′ , y ′ 不 可 被 g c d 整 除 x^{'},y^{'}不可被gcd整除 x,ygcd

推广二:同余式 a x = c ( m o d   m ) ax=c(mod\ m) ax=c(mod m),求x
变形为 a x + b y = c ax+by=c ax+by=c

推广三:逆元的求解以及 (b/a)%m的计算
逆元:若 a b = 1 ( m o d   m ) ab=1(mod\ m) ab=1(mod m), 则称a和b互为模m的逆元一般记作 a = 1 b ( m o d   m ) a=\frac{1}{b}(mod\ m) a=b1(mod m),反之亦然
逆元作用:通过找到 a 的模 m 逆元 x ,有
( a / b ) % m = ( a ∗ x ) % m (a/b)\%m=(a*x)\%m (a/b)%m=(ax)%m
所以,求 a 的模 m 逆元, 相当于求 x , a x = 1 ( m o d   m ) ax=1(mod\ m) ax=1(mod m)

9. 组合数

补充知识:求 n! 有多少个质因子 p
结论: n p + n p 2 + n p 3 + … \frac{n}{p}+\frac{n}{p^2}+\frac{n}{p^3}+\dots pn+p2n+p3n+
问题一:求 C n m \bf C_n^m Cnm

  • 方法一:
    C n m = n ! m ! ( n − m ) ! C_n^m=\frac{n!}{m!(n-m)!} Cnm=m!(nm)!n!
  • 方法二:利用递推
    C n m = C n − 1 m + C n − 1 m − 1 C n n = C n 0 = 1 C_n^m=C_{n-1}^{m}+C_{n-1}^{m-1} \\ C_n^n=C_n^0=1 Cnm=Cn1m+Cn1m1Cnn=Cn0=1
    缺点是会重复计算,可以利用打表的方法减少重复计算
  • 方法三: C n m = ( n − m + 1 ) × ( n − m + 2 ) × ⋯ × ( n − m + m ) 1 × 2 × ⋯ × m = C n − , + i i × ( n − m + i + 1 ) × ⋯ × n ( i + 1 ) × ⋯ × m C_n^m=\frac{(n-m+1)\times (n-m+2)\times \dots \times (n-m+m)}{1\times 2 \times \dots \times m} \\ =C_{n-,+i}^i\times \frac{(n-m+i+1)\times \dots \times n}{(i+1)\times \dots \times m} Cnm=1×2××m(nm+1)×(nm+2)××(nm+m)=Cn,+ii×(i+1)××m(nm+i+1)××n

问题二:如何计算   C n m % p \ \bf C_n^m\%p  Cnm%p

  • 方法一:利用前面的方法二,每次递推时取模, 支持 m ≤ n ≤ 1000 , p ∗ 2 < = I N T _ M A X \bf m\leq n\leq1000,p*2<=INT\_MAX mn1000,p2<=INT_MAX
int C(int n, int m, int p){
	if(m==0||n==m){
		return 1;
	return (C(n-1,m-1)+C(n-1,m)) % p;
}
  • 方法二:对 C n m C_n^m Cnm进行质因子分解,假设
    C n m = p 1 c 1 × p 2 c 2 × … p k c k C_n^m =p_1^{c_1}\times p_2^{c_2}\times \dots p_k^{c_k} Cnm=p1c1×p2c2×pkck
    C n m % p = p 1 c 1 % p × p 2 c 2 % p × … p k c k % p C_n^m \%p =p_1^{c_1}\%p\times p_2^{c_2}\%p\times \dots p_k^{c_k}\%p Cnm%p=p1c1%p×p2c2%p×pkck%p
    每一项在模p时,可使用快速幂
    支持范围 m ≤ n ≤ 1 0 6 \bf m \leq n\leq 10^6 mn106, 由能分解的最大素数决定

四. 算法初步

1. 排序–sort

sort(第一个元素地址,最后一个元素的下一个地址,[比较函数])

2. 散列–map

  • 内部使用红黑树
  • 相关STL: unordered_map, multimap
  • 映射类型为基本类型或者STL类型

3. 分治

  • 递归只是分治的一种实现方式
  • 分治三步骤:分解、解决、合并

典型问题
(1)排列

4. 递归

5. 贪心

贪心是用来解决一类最优化问题,并希望由局部最优解来推导得到全局最优解,贪心算法适用的问题一定满足最优子结构性质,即一个问题的最优解可以由它的子问题的最优解有效地构造出来。
严谨地使用贪心解决最优问题需要对所选策略进行证明,常用反证或归纳。

  • 典型问题
  1. 求最多个数的不重复区间

6. 二分

  • 可以采用递归方式实现,但一般采用非递归方法

(1) 使用左闭右闭区间进行二分查找的通用思路

// 核心是保持要查找元素一致在 [letf, right]区间内,一旦left==right 说明查找成果
while(left

可能需要考虑当x位于[x,y]区间之外的情况
拓展: 将数组换成一个单调函数,用来解决在区间中寻找合理解

例题: 用N个线段(长度不同)首尾相接维持圆形,求最大半径
思路:将半径视作一个变量 r, 每个线段Li ,对应一个角度
θ i = f ( r , L i ) \theta_i=f(r, L_i) θi=f(r,Li)
⟹ \Longrightarrow θ = ∑ θ i = ∑ f ( r , L i ) = 360 \theta=\sum{}\theta_i=\sum{}f(r,L_i)=360 θ=θi=f(r,Li)=360
也就是说 θ \theta θ 是关于r的单调函数,所以可以先估计出 r m i n , r m a x r_{min},r_{max} rmin,rmax, 然后在此区间寻找
F ( r 0 ) − 360 = e p s F(r_0)-360=eps F(r0)360=eps

(2)二次幂

原理:
a b = { a b / 2        b & 1 = = 0 a ( b − 1 ) / 2 b & 1 ! = 0 a^b= \left\{ \begin{array}{lr} a^{b/2} & \ \ \ \ \ \ b\&1==0\\ a^{(b-1)/2} & b\&1!=0\\ \end{array} \right. ab={ ab/2a(b1)/2      b&1==0b&1!=0

由此,以 问题 a b % m a^b\%m ab%m 为例
得到该问题的递归解法和非递归解法

  • 递归解法:
typedef long long LL;//求a^b告m,递归写法

LL binaryPow(LL a, LL b,LL m) {
	if(b == 0) return1; / /如果b为0,那么a^0=1//b为奇数,转换为b-1 
	if(b&2) 
		return a * binaryPow(a,b-1,m) % m;
	else { //b为偶数,转换为b/2
		LL mul = binaryPow(a, b /2, m);
		return mul * mul%m;
	}
}
  • 非递归写法
LL binaryPow(LL a, LL b,LL m) {
	LL ans =1;
	while(b>0){
		if(b&1){
			ans = ans*a%m;
		}
		a = a*a%m;
		b>>1;
	}
	return ans;

7.two pointer

  • 是一种思想,two pointer 与 one Pointer 相比相当于增大了“缓存“,可以利用更多信息,比如一些关于递增序列的处理
  • 典型应用: 归并排序、快速排序

(1)归并排序

void mergeSort(int a[], int n){            // 非递归写法
	int mid;
	int *tmp = new int[n];
	for(int step = 2; step / 2 < n;step *= 2){
		for(int i=0;i

(2)快速排序

void quickSort(int a[], int left, int right){
	if(lefttmp){
			right--;
		}
		a[left] = a[right];
		while(left

(3) 随机

  • stdlib.h, time.h
  • 随机种子: srand((unsigned)time(NULL));
  • 获取一定范围随机数方法:
  1. 除余法
  2. 百分比法 a + r a n d ( ) / R A N D M A X ∗ ( b − a ) a+rand()/RAND_MAX *(b-a) a+rand()/RANDMAXba)
  3. 利用函数变换

8. 打表

  • 用空间换时间,一次性存储结果

9. 活用递推

  • 通过地推过程增大计算结果的利用率

10. 随机选择算法

  • 问题:找 k_th 元素
  • 为了防止在划分时遇到极端情况,在划分时进行随机选择。
int randSelect(int a[], int left, int right, int k){
	if(left==right){
		return a[left];
	}
	int p = randPartition(a, left, right);
	if(p==k-1){
		return a[p];
	}else if(p>k-1){
		return randSelect(a,left, p-1,k);
	}else if(ptmp)
			j--;
		a[i] = a[j];
		while(i

三. 入门模拟

1. 查找元素

2. 日期处理

  • 可以使用累加的方式计算日期间隔
  • 月份数组

3. 进制转换

二. C++ 快速入门

黑盒测试

单点测试

一次只测试一组数据

多点测试

在每次循环的时候要恢复初始状态,重置变量和数组。
一次测试所有数据

输入方式

  1. while…EOF

     #include
     int main(void){
         int a,b;
         // 在黑框中手动触发EOF:{Ctrl+Z}+Enter
         while(scanf("%d%d",&a,&b)!=EOF){
         	printf("%d\n",a+b);
         }
         return 0;
     }
     
     // 第二种
     while(gets(str)!=NULL){
     	......
     }
    
  2. while…break

     #include
     int main(){
     	int a,b;
     	while(scanf("%d%d",&a,&b)!=EOF, a||b){
         	printf("%d\n",a+b);
         }
     }
    
  3. while(T–)

     #include
     int main(){
         int n,a,b;
         scanf("%d",&n);
         while(n--){
             scanf("%d%d",&a,&b);
             printf("%d\n",a+b);
         }
         return 0;
     } 
    

输出格式

  1. 连续输出多行,没有额外空行
  2. 每组数据之后额外加一空行
  3. 两组数据间有一个空行,最后一组数据后没有空行

浮点数的比较

由于浮点数运算过程会积累误差,所以应该引入极小数进行误差修正
常用:
const double eps=1e-8;

cin, cout

读入整行

char str[100];
cin.getline(str, 100);
string str;
getline(cin, str);

string.h

method
strlen
strmp
strpy
strcat(str1, str2)

sscanf sprintf

char str[10]="abc";
sscanf(str,"%d", n);

ssprintf(str,"%d", n);

gets,puts

  • gets: 以换行结尾
  • puts:输出并自动加上换行

scanf

  • 除了%c以外,scanf对其它格式的输入以空白符作为结束标志,包括%s。但%c可以读入空格和换行
  • %s 遇到空白符结束, 但不会读取空白符,但也会从缓冲区去掉空白符。自动添加 \0 结尾。
  • 返回成功读入的参数的个数

getchar, putchar

  • 输入,输出单个字符
  • getchar可以识别换行符

memset

对数组每个元素赋相同的值

memset(a, 0, sizeof(a))
memset(a, -1, sizeof(a))	

memset 按字节赋值,每个字节赋相同值。如果对数组赋其它数字,需要使用 fill(

math 常用函数 (math.h)

函数 说明
fabs(double)
floor(double) 返回double
ceil(double) 返回double
pow(double, double)
sqrt(double)
log(double)
sin,cos,tan(double)
asin, acos, atan(double)
round(double) 四舍五入,返回double

tips

  • 函数内部内部申请的局部变量来自于来自于系统栈,内存较小;全局变量来自于静态存储区,允许申请的空间较大。如果数组较大(106),则应定义为全局变量。
  • 在.cpp 中,推荐用 cstring, cstdio, cmath 等替代 string.h, stdio.h, math.h
  • 基本类型的大致表示范围
类型 大致范围
int ±2*109
long long(64b) ±9*1018
float (32b) ± 2128, 6,7位精度
double (64b) ± 21024, 15,16位精度
  • long long 初始化的时候要加后缀 LL, longlong的输入输出格式都是%lld
    例:long long bignum = 1234567890LL
  • float,double 的 printf格式都是 %f, 但 double的scanf格式是%lf
  • 布尔型在C++中可以直接使用,在C中必须添加stdbool.h
  • π \pi π = acos(-1)

特别提醒

1. 编译错误

  • 不能用while((cin>>x)!=NULL), 改为while(scanf("%d",&x))

2. 数据表示范围,尤其注意中间数据

3. 栈区、静态区、堆大小的限制

(1)栈区

比2MB略小,大致相当于:
int p[5 * 105] 或 char p[2 * 106]

(2)全局区(静态区)

比 4 GB 小, 大致相当于:
int p[4 * 108] 或 char p[1 * 109]

(3)堆区

大小受限于计算机有效虚拟内存

4. 时间及性能评估

(1)获取当前时间

# 精确到毫秒,要利用系统提供的接口
#include 
#include 
#include 
char*   log_Time(void){     
   	struct  tm   *ptm; 
    struct  timeb   stTimeb;        
    static  char    szTime[19];         
    ftime(&stTimeb);        
    ptm = localtime(&stTimeb.time);        
    sprintf(szTime, "%02d-%02d %02d:%02d:%02d.%03d", ptm->tm_mon+1, ptm->tm_mday, ptm->tm_hour,  ptm->tm_min, ptm->tm_sec, stTimeb.millitm);        
    szTime[18] = 0;        
    return szTime;
}

# 精确到秒
	time_t now = time(0);
	tm *ltm = localtime(&now);
	printf( "日期:%4d%02d%02d%\n时间:%02d%02d%02d\n星期: %d", 1900 + ltm->tm_year, 1 + ltm->tm_mon, ltm->tm_mday,
			ltm->tm_hour,ltm->tm_min,ltm->tm_sec,ltm->tm_wday);

(2)关于各种运算耗时评测

  • 加法运算: 228次(2.7*108)— 0.81s
  • 内存读写:226次(6.7*107)— 0.42s
  • 乘除测试: 与读写相近
// 加法测试
	cout<>3);
	for(int i=1;i>5);
	for(int i=1;i

5. 除模运算

  • 模正数、负数都一样
  • 正数的模一定是正数,负数的模一定是负数
    a % b = a % ( − b ) = − ( ( − a ) % b )          a , b > 0 a\%b=a\%(-b)=-((-a)\%b)\ \ \ \ \ \ \ \ a,b>0 a%b=a%(b)=((a)%b)        a,b>0
  • 无论a,b正负
    a = a / b × b + a % b        b ≠ 0 a=a/b \times b + a\%b \ \ \ \ \ \ b \neq 0 a=a/b×b+a%b      b=0

技巧

1. 用累加替代乘

例:

for(int i=1000,multi = 9000;i<9999&&multi<1e4;i++,multi+=9){		// 用 multi+=9替代 multi = i*9
	panduan(i, multi);	
}

2. 用纯粹累加的方式进行枚举

  • 关键:明确初始条件、终止条件、迭代过程能通过简单运算实现
  • 例:
题目:设a、b、c均是0到9之间的数字,abc、bcc是两个三位数,且有:abc+bcc=532。求满足条件的所有a、b、c的值。

int main(){
	int num[3] = {0,0,0};
	int total, total1, total2;
	for(int a=0;a<6;a++){
		total2 = a*100;
		for(int b=0;b<=9;b++){
			total1 = total2+b*10+b*100;
			for(int c = 0;c<=9;c++){
				total = total1+c+c+c*10;
				if(total==532)
					printf("%d %d %d\n",a,b,c);
			}
		}
	} 
	return 0;
}

你可能感兴趣的:(编程,算法,csp,算法笔记)