导引问题:整数求和
-
任务:给定一个正整数 n,请计算 1+2+3+…+n 的结果,其中 n <= 50000
- Sample input:10
- Sample output:55
-
常见实现方式:
#include<stdio.h> int main() { int n, i, sum; while(scanf("%d", &n) == 1) { sum = 0; for(i = 1; i <= n; i++) sum = sum + i; printf("%d\n\n", sum); } return 0; }
-
其它方法:等差数列
- SUM(n) = 1 + 2 + 3 + …… + n = n * (n + 1) / 2
- 会有风险!假设用
int
存储变量n,当n = 50000时,50000 * (50000 + 1) ≈ 25亿,已经超出了int
的存储范围; - 解决方案:
- 用
long long
存储,输入时使用的占位符是%lld
; - 可以先 / 2,但是会有奇偶问题(如果 n 是奇数,除以 2 就会影响精度)。所以可以先用
if…else…
做一个判断- 如果 n 是偶数,就先 / 2,再 * (n + 1);
- 如果 n 是奇数,就让 (n + 1) / 2,再 * n。
- 用
例1:辗转相除法(欧几里得算法)
-
任务:给定两个正整数,计算这两个数的最小公倍数;
-
样例输入:
10 14 4 6
-
样例输出:
70 12
-
-
解法:
- 暴力枚举。从 14 开始,15、16……一直枚举下去,判断各个数除以二者能否得到一个整数;
- 优雅一点的暴力枚举。以 14 的倍数开始枚举;
- 辗转相除法。LCM(A, B) = A * B / GDC(A, B),即 A 和 B 的最小公倍数 = A * B / A 和 B 的最大公约数;
- 注意:A 和 B 相乘可能会爆
int
,解决方案见导引问题:整数求和
; - 将问题从求解 A 和 B 的最小公倍数,转变成了求解 A 和 B 的最大公约数;
- 注意:A 和 B 相乘可能会爆
-
GCD求解过程:辗转相除法(欧几里得算法)
- 以求解 10 和 14 的最大公约数为例,假设这个最大公约数为 x ,那么 10 和 14 都是这个 x 的倍数;
- 可以推出:若将 14 拆分为 a1 和 a2 这两个数相加后的数,那么其中 a1 就是要求解最大公约数的另一个数的整数倍(此处是 10),a2 这个数也是 x 的倍数(此处就是 14 - 10 = 4)。所以求解 10 和 14 的最大公约数,就等价于求解 10 和 4 的最大公约数;
- 由于 10 和 4 做除法发现得到的余数依然不为 0 ,那么继续重复第二步:将 10 拆分成 4 的整数倍(此处是 8)和另一个数(此处是 2)相加,即求解 10 和 4 的最大公约数,就等价于求解 2 和 4 的最大公约数;
- 此时可以发现,用 4 / 2 = 2 余 0,所以 2 和 4 的最大公约数就是 2,所以 10 和 4 的最大公约数也是2,所以 10 和 14 的最大公约数也是2;
-
为什么叫辗转相除法?
- 可以发现,原本是除数的 10 (还有 4),变成了被除数;
- 原本是余数的 4(还有 2) ,变成了除数;
-
伪代码:
int gcd(int da, int xiao) { int temp; while (xiao != 0) { temp = da % xiao; da = xiao; xiao = temp; } return(da); }
- 思考?如果将 10 传给了 da,将14 传给了 xiao,会不会有什么影响?
- 答:不会,第一次循环就交换回来了,xiao 为 10,da 为 14。
- 思考?如果将 10 传给了 da,将14 传给了 xiao,会不会有什么影响?
例2:找规律(循环节)
- 任务:给定一个正整数N,请计算N个N相乘(NN)的结果的个位数是多少(1 <= N <= 1,000)
- Sample Input:3
- Sample Output:7
- 显然不可能直接求解 NN,因为在 N 很大的情况下,最后得出来的数肯定是一个天文数字,计算机中的数据类型存储不下;
- 解法:
- 暴力枚举:因为 N 个 N 相乘,只需要做 N-1 次乘法,每一次相乘都只取个位数(而不用整个数都算出来);
- 缺点:在 N 很大的情况下,可能会超时;
- 找规律:
- 分析 N 从 1 到 10 时 NN 的个位数情况:
- 当 N = 1 时,1 ^ 1 = 1 ,个位数是 1 ;
- 当 N = 2 时,2 ^ 2 = 4 ,个位数是 4 ;2 ^ 3 = 8 ,个位数是 8 ;2 ^ 4 = 16 ,个位数是 6 ;2 ^ 5 = 32 ,个位数是 2 ,开始循环,周期为 4 ;
- 当 N = 3 时,3 ^ 1 = 3 ,3 ^ 2 = 9 , 3 ^ 3 = 27 ,个位数是 7 ; 3 ^ 4 = 81 ,个位数是 1 ;3^5 = 243 ,个位数是 3 ,开始循环,周期为 4 ;
- 当 N = 4 时,4 ^ 1 = 4 ,4 ^ 2 = 16 ,个位数是 6 ; 4 ^ 3 = 64 ,个位数是 4 ,周期为 2 ;
- 当 N = 5 时,5 的任何正整数次幂个位数都是 5 ;
- 当 N = 6 时,6 的任何正整数次幂个位数都是 6 ;
- 当 N = 7 时,7 ^ 1 = 7 ,7 ^ 2 = 49 , 7 ^ 3 = 343 ,个位数是 3 ; 7 ^ 4 = 2401 ,个位数是 1 ; 7 ^ 5 = 16807 ,个位数是 7 ,周期为 4 ;
- 当 N = 8 时,8 ^ 1 = 8 ,8 ^ 2 = 64 ,个位数是 4 ; 8 ^ 3 = 512 ,个位数是 2 ; 8 ^ 4 = 4096 ,个位数是 6 ; 8 ^ 5 = 32768 ,个位数是 8 ,周期为 4 ;
- 当 N = 9 时,9 ^ 1 = 9 ,9 ^ 2 = 81 ,个位数是 1 ; 9 ^ 3 = 729 ,个位数是 9 ,周期为 2 ;
- 当 N = 10 时,10 的任何正整数次幂个位数都是 0 ;
- 对于任意的 N ,先看 N 的个位数,根据上述规律来确定 NN 的个位数;
- 比如 N = 13 ,只看个位数 3 ,按照 3 的幂次个位数规律来计算;
- N = 26 ,只看个位数 6 ,6 的任何正整数次幂个位数都是 6 。
- 分析 N 从 1 到 10 时 NN 的个位数情况:
- 暴力枚举:因为 N 个 N 相乘,只需要做 N-1 次乘法,每一次相乘都只取个位数(而不用整个数都算出来);
例3:找规律(循环节)
-
题目:
- 有一种 fibonacci 数列,定义如下:
- F(0) = 7,
- F(1) = 11,
- F(n) = F(n - 1) + F(n - 2) (n>=2)
- 给定一个 n (n < 1,000,000),请判断 F(n) 能否被3整除,分别输出 yes 和 no ;
- 有一种 fibonacci 数列,定义如下:
-
解法:
-
递归:但是能不用递归就不用,有超时和报栈的风险;
-
找规律:
-
F(n) % 3 = (F(n - 1) + F(n - 2)) % 3 = (F(n - 1) % 3 + F(n - 2) % 3) % 3,即每一项对 3 取余,都是前两项对 3 取余的和对 3 取余;
-
当 n = 0 时,F(0) % 3 = 7 % 3 = 1;
-
当 n = 1 时,F(1) % 3 = 11 % 3 = 2;
-
当 n = 2 时,F(2) % 3 = (F(2 - 1) + F(2 - 2)) % 3 = (F(2 - 1) % 3 + F(2 - 2) % 3) % 3 = (11 % 3 + 7 % 3) % 3 = 0;
-
当 n = 3 时,F(3) % 3 = (F(3 - 1) % 3 + F(3 - 2) % 3) % 3 = (0 + 2) % 3 = 2;
-
当 n = 4 时,F(4) % 3 = (F(4 - 1) % 3 + F(4 - 2) % 3) % 3 = (2 + 0) % 3 = 2;
-
当 n = 5 时,F(5) % 3 = (F(5 - 1) % 3 + F(5 - 2) % 3) % 3 = (2 + 2) % 3 = 1;
-
当 n = 6 时,F(6) % 3 = (F(6 - 1) % 3 + F(6 - 2) % 3) % 3 = (1 + 2) % 3 = 0;
-
当 n = 7 时,F(7) % 3 = (F(7 - 1) % 3 + F(7 - 2) % 3) % 3 = (0 + 1) % 3 = 1;
-
当 n = 8 时,F(8) % 3 = (F(8 - 1) % 3 + F(8 - 2) % 3) % 3 = (1 + 0) % 3 = 1;
-
当 n = 9 时,F(9) % 3 = (F(9 - 1) % 3 + F(9 - 2) % 3) % 3 = (1 + 1) % 3 = 2;
-
当 n = 10 时,F(10) % 3 = (F(10 - 1) % 3 + F(10 - 2) % 3) % 3 = (2 + 1) % 3 = 0;
-
……
-
-
周期为8;
-
且可以发现:
- 结果都是 0 1 2 三个数字中的一个,三个数字组成一个两位数字有 9 种可能(个位有 3 种,十位有 3 种,3 * 3 = 9);
- 在上面的找规律过程中得出的十个结果,每相邻两个结果组成一个两位数字,即12、20、02、22、21、10、01、11、12、20;
- 可以发现,从第九个两位数开始就开启新的一轮循环,所以通过这种方式也可以发现周期为8。
-
-
例4:快速幂运算
-
任务:求 A ^ B 的最后三位数表示的整数(1 <= A,B <= 10000)
-
Sample Input
2 3 12 6
-
Sample 0utput
984
-
-
解法:
-
如果 A 和 B 的值的范围很大,用暴力就无法求解;
-
使用快速幂(二分加速);
-
递归实现(递归先写出口,即不能递归的情况):
int power(int a, int n) // a表示底数,n表示指数,求解a的n次方 { int ans; if(n == 0) ans = 1; //结束条件 else { ans = power(a * a, n / 2); //递归调用。a^n = (a^2)^(n/2) if(n % 2 == 1) ans *= a; //n为奇数,就再乘一次底数 } return ans; }
- 例:求解 123234 ,要算 233 次乘法;
- 但是由于 123234 = (1232)117,就只需要算 117 次乘法
- 但是由于 (1232)117 = ((1232)2)58 * 1232,就只需要算 58 + 1 次乘法
- ……
-
非递归(循环)实现:
int power(int a, int n) // a表示底数,n表示指数,求解a的n次方 { int ans = 1; while(n) // 只要 n 不为0,就一直循环下去 { if(n % 2) ans *= a; //奇数情况(if成立的条件是n % 2 = 1,即 n 为奇数) a = a * a; //底数平方 n = n / 2; //指数减半 } return ans; }
-
对于以上两种方式,在一些数很大的情况下,只要是有乘法运算的地方,都要取模。
-
-
例5:二分查找
-
给出若干个(可以很多)有序的整数,请查找某个元素是否存在,比如:
2 3 4 5 6 8 12 20 32 45 65 74 86 95 100
- 请查找以上数列中是否存在某个整数(比如25);
-
解决:二分查找
- 2 的下标是 0,被
head
指针指向; - 100 的下标是 14,被
tail
指针指向; - 20是中间值,下标是7,被
mid
指针指向; - 将要查找的数(25),与中间值进行比较;
- 比中间值大,那么将
head
指针指向mid
所指的数的下一个(32),而mid
去指向此时head
和tail
之间的数的中间值(74); - 然后继续重复上面两步(注意也有可能移动的是
tail
指针),直到head
指针指向的数大于tail
指针指向的数;
- 2 的下标是 0,被
-
非递归实现:
int BiSearch(int a[], int n, int x) //a数组中有n个有序元素,查找x是否在a数组中 { int left = 0, right = n - 1; while(left <= right) // 注意=不能少 { int middle = (left + right)/2; //整数除法 if(a[middle] == x) //找到的情况 return middle; if(x > a[middle]) //如果比中值大 left = middle + 1; else //如果比中值小 right = middle - 1; } return -1; //循环结束都没找到,说明找不到 }
-
递归实现:
int BiSearch(int a[], int x, int left, int right) { if(left > right) //注意不能有=号 return -1; else { int mid = (left + right) / 2; if(a[mid] == x) return mid; else if(x > a[mid]) // 如果在右边 return BiSearch(a, x, mid + 1, right); else if(x < a[mid]) // 如果在左边 return BiSearch(a, x, left, mid - 1); } }
-
思考:在一百万个元素里查找某个元素大约需要比较多少次?
- 时间复杂度:O(logN)
例6:二分查找
-
给出方程:8 * x4 + 7 * x3 + 2 * x2 + 3 * x + 6 = Y,其中实数 Y 满足:fabs(Y) <= 1e10。请输出 x 在区间[0, 100]的解,精确到小数点后4位;
- 意思就是,x 的取值范围是[0, 100],将 x 代入方程,得到的 Y 的结果要满足:fabs(Y) <= 1e10;
- fabs(Y)表示求解 Y 的绝对值;
- 1e10 即 1010
-
可以发现,该方程满足单调递增,那么就可以使用二分法;
- 可以先在区间[0, 100]内取一个中间值,即 x = 50,代入方程中运算,得到的 Y 经过fabs(Y)后,若超出 1e10 的范围,说明 x 的取值肯定[0, 50)以内;
- 继续二分
-
代码:
#include <bits/stdc++.h> using namespace std; double Y; double left, right, mid; double f(double x) { return 8*pow(x, 4.0) + 7*pow(x, 3.0) + 2*pow(x, 2.0) + 3*x + 6; } int main() { int t; scanf("%d", &t); while( t-- ) { scanf("%lf", &Y ); if( f(0) <= Y && Y <= f(100) ) { left = 0; right = 100; while( right-left > 1e-6 ) { mid = (left + right) / 2; double ans = f(mid); // 将x的取值范围的中间值代入方程,得到结果 if( ans > Y ) // 如果得到的结果超出Y规定的范围,说明Y在左边 right = mid - 1e-7; // 移动右指针 else // 如果得到的结果超出Y规定的范围,说明Y在右边 left = mid + 1e-7; // 移动左指针 } printf("%.4f\n", (left+right) / 2 ); } else printf("No solution!\n"); } return 0; }
- 在编程和数学的科学计数法表示中,
1e-6
代表 1 × 1 0 − 6 1×10^{-6} 1×10−6 ,也就是 0.000001。其中e
是指数(exponent)的意思 ,e
前面的数字是基数,e
后面的数字表示 10 的指数幂,这里-6
表示 10 的 -6 次方。在代码中常用来表示一个极小的数值,比如作为精度控制的值,像判断两个浮点数是否相等或者迭代终止条件等场景。
- 在编程和数学的科学计数法表示中,
例7:三分查找
-
给出函数:F(x) = 6 * x7 + 8 * X6 + 7 * X3 + 5 * X2 - y * X,其中实数 y 满足(0 <y < 1e10)。请输出 x 在区间[0, 100]时函数 F(x) 的最小值,结果精确到小数点后4位;
-
解法:
-
该函数的原函数不满足单调性,当 x < 1 时,单调递减。当 x > 1 时,单调递增。但是再仔细观察,该函数的导数是单调递增的,所以将该函数求导后,再按照
例6
去做二分查找; -
三分查找:如果一个函数的曲线,是单调的,但是满足凸性,那么就可以使用三分法;
-
如果是一个上凸的图形:
-
将函数的取值范围三等分化(也可以不三等分);
-
若左三分之一点(LeftThird)比右三分之一(RightThird)要大,说明极值点不会在最右边那一段,那么可以直接将区间缩小至[Left, RightThird],即将 RightThird 赋值给 Right;
-
然后重复步骤1和步骤2;
-
若左三分之一点(LeftThird)比右三分之一(RightThird)要小,说明极值点不会在最左边那一段,那么可以直接将区间缩小至[LeftThird, Right],即将 LeftThird 赋值给 Left;
-
然后重复步骤1和步骤4。
-
-
-
-
注意:
- 二分的前提:单调性;
- 三分的前提:凸性。同时,凸性并不要求可导。