layout: post
title: 算法笔记(五)数论、进制、位运算、统计抽样、计算几何
description: 算法笔记(五)数论、进制、位运算、统计抽样、计算几何
tag: 算法
算法笔记(五)数论、进制、位运算与计算几何
求素数
试除法
最笨的方法:枚举[2,n-1]之间有没有直接能够被n整除的,如果有,那么返回false这个就不是素数,否则就是素数,代码如下:
boolean isprime(int value){
for(int i=2;i<value;i++)
{
if(value%i==0)
{return false;}
}
return true;
}
但是其实这种太浪费时间了,完全没必要这样,可以优化一下 。如果一个数不是质数,那么必定是两个数的乘积,而这两个数通常一个大一个小,并且小的小于等于根号n,大的大于等于根号n;
// 注意sqrt需要 #inlcude<cmath>
boolean isprime(int value){
for(int i=2; i<= sqrt(value);i++)
{
if(value%i==0)
{return false;}
}
return true;
}
// 或者不用sqrt
boolean isprime(int value){
for(int i=2; i * i< value + 1);i++)
{
if(value%i==0)
{return false;}
}
return true;
}
埃氏筛法
求多个素数的时候(小于n的素数),上面的方法就很繁琐了,因为有大量重复计算,因为 计算某个数的倍数 是否为素数的时候出现大量的重复计算,如果这个数比较大那么对空间浪费比较多。
下边以求解第i个素数为例,介绍素数筛选法则。
埃氏筛的核心思想就是将素数的倍数确定为合数。
假设刚开始全是素数,2为素数,那么2的倍数均不是素数;然后遍历到3,3的倍数标记一下;下个是5(因为4已经被标记过);一直到n-1为止。具体流程可以看图:
#include<iostream>
#include<cstring>
using namespace std;
const int N = 1e7; // 筛选上限
int primes[N]; // 记录素数数组
bool isCompose[N]; // 标记由素数倍数构成的合数
int k = 1; // 素数的编号
void getPrimes() {
// 埃氏筛法:筛掉素数的所有倍数
for (int i = 2; i < N; ++i) {
if (!isCompose[i]) { // 如果非合数,说明找到一个素数,把他加入到素数数组中,开始筛除它的倍数
primes[k++] = i;
for (int j = 2 * i; j < N; j += i) {
isCompose[j] = true;
}
}
}
}
int main() {
memset(st, 0, sizeof(st));
memset(isPrime, 0, size); //
int n;
while (cin >> n) {
cout << primes[n] << endl;
}
}
欧拉筛法
埃氏筛是遇到一个质数将它的倍数计算到底,这其中有会大量计算素数的公倍数,而欧拉筛法则是只用它乘以已知晓的素数的乘积进行标记,如果素数能够被整除那就停止往后标记。
欧拉筛法核心:每个合数只被最小的质因数筛除
比如6 = 2*3,那么它只会被2筛除。
具体实现:
在实现上同样也是用两个数组,一个存储真实有效的素数primes,一个用来作为标记筛除的合数isCompose。
- 在遍历到一个数时,如果这个数非合数,那么它是质数,加入到质数数组,下标+1。
- 不管这个数是不是素数,遍历已知素数,将它和已知素数的乘积值标记为合数,如果当前数能够整除某个素数,退出循环。
由于我们得到的素数都是从小到大的,如果某个数能够整除一个素数,那么说明这个素数就是它的最小质因数,如果已经遍历到最小质因数,那么退出循环,这样数字6就只会被质因数2标记,而不会被3再次标记。
#include<iostream>
using namespace std;
const int N = 1e7; // 定义筛选上限
int primes[N]; // 素数记录数组
int isCompose[N];
int k = 0;
void getPrimes() {
for (int i = 2; i < N; ++i) {
if (!isCompose[i]) primes[k++] = i;
for (int j = 0; j < k && primes[j] * i < N; ++j) {
isCompose[i * primes[j]] = true;
if (i % primes[j] == 0) break;
}
}
}
int main() {
int n;
while (cin >> n) {
cout << prime[n - 1] << endl;
}
};
最大公约数
最大公约数GCD(Greatest Common Divisor)
最常见的求两个数的最大公约数的算法是辗转相除法,也叫欧几里得算法
算法流程如下:
对于任意两个数a
和b
,保证a是较大者(假如a<b
,则两数交换),用大数除以小数取余数r = a % b
,将令a = b, b = r
,
继续求a和b的最小公约数,直至b = 0
,返回a
即为最大公约数。
注:
1、辗转相除的数学原理核心是:
g
c
d
(
a
,
b
)
=
g
c
d
(
b
,
a
m
o
d
b
)
gcd(a, b) = gcd(b, a mod b)
gcd(a,b)=gcd(b,amodb)
g
c
d
(
a
,
b
)
=
g
c
d
(
b
,
r
)
gcd(a, b) = gcd(b, r)
gcd(a,b)=gcd(b,r)
r = a mod b
证明过程如下:
假设:
c
=
a
/
b
c = a / b
c=a/b (c为整数)
则:
a
=
c
∗
b
+
r
a = c * b + r
a=c∗b+r ----->
r
=
c
∗
b
−
a
r = c*b - a
r=c∗b−a
对a和b的任意公约数k(a和b都是k的倍数,假设分别为m倍和n倍),那么:
r
=
c
∗
k
∗
n
−
k
∗
m
=
k
∗
(
c
∗
n
−
m
)
r = c * k* n - k*m = k*(c*n - m)
r=c∗k∗n−k∗m=k∗(c∗n−m)
r也是k的倍数。
即(r和b也都是k的倍数),故a和b任意公约数k也是b和a mod b 的公约数。
对于b和r的任意公约数h(b和r都是h的倍数,假设分别为p倍和q倍),则根据:
a
=
c
∗
b
+
r
=
c
∗
p
∗
h
+
q
∗
h
=
h
∗
(
c
∗
p
+
q
)
a = c * b + r = c * p * h + q * h = h*(c*p + q)
a=c∗b+r=c∗p∗h+q∗h=h∗(c∗p+q)
a也是h的倍数。
即(a和b也都是h的倍数),b和a mod b 的任意公约数也是a和b的公约数。
综上所述:
辗转相除的过程中,公约数是一致连贯的,而当余数第一次为0时,即b = 0时,说明出现了整除的情况,而能够整除的两个数,最大公约数就是被除数。上述所求公约数最终一定是最大公约数。
2、辗转相除时始终保证a
是较大的数,b
是较小的数,r = a % b
,则 0 <= r < b
,故赋值时,令 a = b, b = r
,b
作为新的大数,而r
作为新的小数。
根据框图流程可以写出求最大公约数的函数:
int gcd(int a, int b) {
// 交换
if (a < b) {
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
while (b)
{
int r = a % b;
a = b;
b = r;
}
return a;
}
根据核心原理
g
c
d
(
a
,
b
)
=
g
c
d
(
b
,
a
m
o
d
b
)
gcd(a, b) = gcd(b, a mod b)
gcd(a,b)=gcd(b,amodb)
写出递归版本的:
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
递归版本的为啥取消了a和b谁是大数小数的判断呢?
以a = 45 和 b = 10为例
如果一开始a > b, 再次调用的时
b, a % b, 保证了 b > a % b;
如果a = 10 和 b = 45
一开始a < b,
再次调用时,
b, a % b,此时b为45, 而a % b就是a = 10,因此即使一开始输入gcd(10, 45),递归后
又变为了gcd(45,10);
故递归版本无需考虑输入两个数的大小关系。
最小公倍数
最小公倍数LCM(Lowest 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)=a∗b/GCD(a,b)
// 最小公倍数
int lcm(int a, int b) {
return a * b / gcd(a, b);
}
进制
2、8、16进制相互转换
进制编码
位运算
模运算
定义
取模运算(C++中用符号%
表示)是求两个数相除的余数。其概念与取余操作类似,又不完全相同
。
在C++中,对于a%b
- 取a,b绝对值进行运算取余数
- 运算结果符号与a保持一致
运算性质
- 交换律
- 结合律
注意:这里最外部的模p不能删掉!!!
- 分配律
比较坑的就是当%p移入到括号以内后,外部的%p仍然需要保留。
应用
求解ab mod p
LeetCode372超级次方
你的任务是计算 ab 对 1337 取模,a 是一个正整数,b 是一个非常大的正整数且会以数组形式给出。
提示:
1 <= a <= 231 - 1
1 <= b.length <= 2000
0 <= b[i] <= 9
b 不含前导 0
根据快速幂算法:
ab 可分解为若干个a的幂次(根据b的二进制分解)相乘。
再由模运算的乘法分配律性质:
a×b%p = (a%p ×b%p)%p,即所有子项模p后再模p
故我们可以自定义直接模p版本的快速幂。
由于这里给的指数是以数组形式呈现的,代表十进制的某个特别大的数字。因此在遍历时候,考虑从后往前遍历,对于数组倒数第一个数,即个位,底数为a,对于倒数第二个,底数为a的10次方。
class Solution {
public:
const int MOD = 1337;
int superPow(int a, vector<int>& b) {
int ans = 1;
for (int i = b.size() - 1; i >= 0; i--)
{
ans = ans * myPow2(a, b[i]) % MOD;
a = myPow2(a, 10);
}
return ans;
}
int myPow2(int x, int n) {
int ans = 1;
while (n)
{
if (n & 1) {
ans = (long)(x * ans)% MOD;
}
x = (long) x * x % MOD;
n >>= 1;
}
return ans;
}
};
矩阵快速幂
快速幂算法
「快速幂算法」的本质是分治算法。举个例子,如果我们要
计算3的10次方,我们可以一步步将指数除2,底数平方,如果指数为奇数,则指数除2向下取整,底数平方,并再乘上底数
LeetCode50
实现 pow(x, n) ,即计算 x 的整数 n 次幂函数
提示:
-100.0 < x < 100.0
-231 <= n <= 231-1
-104 <= xn <= 104
递归版快速幂:
这里N必须非负
double quickMul(double x, long long N) {
if (N == 0) {
return 1.0;
}
double y = quickMul(x, N / 2);
return N % 2 == 0 ? y * y : y * y * x;
}
整体中,如果N为负数,则返回1.0 / quickMul(x, -N);
double myPow(double x, int n) {
long long N = n;
return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);
}
迭代版快速幂
还是以3的10次方为例,如果将10表示为2进制:1010
同底数幂相乘 = 底数不变,指数相加
指数10,可以由它的2进制表达相加得到。
10 = 0 × 1 + 1 × 2 + 0 × 4 + 1 × 8 = 2 + 8
即
3^10 = 3 ^ 2 × 3 ^ 8
那么自然就想到可以利用指数的2进制分解,通过迭代指数2进制的每一位来分解幂乘。
设要求的 xn = contribute1 ×contribute2 ……组成
初始贡献contribute为底数x,contribute每迭代一位都会平方(指数上乘2),如果该位是1,则最终结果需要乘上该contribute。
迭代版:
double quickMul(double x, long long N) {
// N = 0 ,直接返回ans = 1;
double ans = 1;
double contribute = x;
while (N > 0)
{
if (N & 1) {
ans *= contribute;
}
contribute *= contribute;
N >>= 1;
}
return ans;
}
double myPow(double x, int n) {
long long N = n;
return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);
}
注:这里使用了位运算来取每一位数字,和1求与运算,求出最低位是否为1,再求下一位时,将N右移一位后,再和1与运算求最低位即可。
矩阵快速幂
斐波那契数列 0、1、1、2、3、5……
斐波那契数列的定义如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
最终的f(n)就是M的n-1次幂的第一行与f(1),f(0)对应相乘,而f(0) = 0, f(1) = 1,故最终结果就是矩阵M的n-1次幂的第一个元素值
首先定义二阶矩阵相乘的函数:
// 定义二阶矩阵乘法
vector<vector<long>> multiply(vector<vector<long>>& a, vector<vector<long>>& b) {
vector<vector<long>> ans = { {0, 0}, {0 ,0} };
for (int i = 0; i < 2; i++)
{
for (int j = 0; j < 2; j++) {
ans[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] % MOD;
}
}
return ans;
}
注:这里关于取模运算,在矩阵乘法的时候就已经进行取模防止溢出
我们最后要求的结果是m_pow[0][0] % MOD
根据模运算的分配律性质:
(a + b)%p = (a % p + b%p)%p
a* b%p = (a%p) *(b%p)%p
模运算在运算中是一致的,在内层进行模运算可以提高速度。
随后依据快速幂的思路,撰写,快速矩阵幂:
vector<vector<long>> martrixPow(vector<vector<long>> &m, n){
// 初始化为单位阵
vector<vector<long>> ans = {{1, 0}, {0, 1}};
while(n > 0){
if ( n&1 ) ans = multiply(ans, m);
m = multiply(m, m);
n >>= 1;
}
return ans;
}
主函数:
int fib(int n) {
if (n < 2) {
return n;
}
vector<vector<long>> m = { {1, 1}, {1, 0} };
vector<vector<long>> m_pow = martrixPow(m, n - 1);
return m_pow[0][0];
}
统计抽样
摩尔投票
LeetCode169、多数元素
给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
摩尔投票 :在集合中寻找可能存在的多数元素,这一元素在输入的序列重复出现并占到了序列元素的一半以上;在第一遍遍历之后应该再进行一个遍历以统计第一次算法遍历的结果出现次数,确定其是否为众数;如果一个序列中没有占到多数的元素,那么第一次的结果就可能是无效的随机元素。
换句话说,每次将两个不同的元素进行「抵消
」,如果最后有元素剩余,则「可能」为元素个数大于总数一半的那个
int majorityElement(vector<int>& nums) {
int candidate, count = 0;
for(int num : nums){
if(count == 0) candidate = num;
if(num == candidate) count++;
else count--;
}
return candidate;
}
拓展到n/k
摩尔投票可以拓展为「统计出现次数超过 n/k的数」。
以LeetCode229为例,给定一个大小为 n 的整数数组,找出其中所有出现超过 ⌊ n/3 ⌋ 次的元素。
首先明确,数组中出现次数超过 ⌊ n/3 ⌋ 次的元素的数目最多有2个(拓展到n/k,就是 k - 1个),那么就可以用两个变量代表候选数,按照摩尔投票的标准做法,在遍历数组的同时,检查这两个候选数,假设当前遍历元素为x:
- 如果x本身是候选数,则其出现次数加一
- 如果x本身不是候选数,检测是否有候选数的出现次数为0。
- 若有,则令x称为候选数,记录次数为1
- 若无,则令所有候选数出现次数减1(三个一组抵消)
一次遍历出现完毕后,必然得到两个候选数,但不一定都是符合出现次数超过1/3 的。
需要进行二次遍历,确定候选者是否符合要求,将符合要求的数添加到答案。
vector<int> majorityElement(vector<int>& nums) {
int candidate1 = 0, candidate2 = 0, count1 = 0, count2 = 0;
for(int num : nums){
if(count1 > 0 && num == candidate1){
count1++;
}else if(count2 > 0 && num == candidate2){
count2++;
}else{
if(count1 == 0){
candidate1 = num;
count1++;
}else if(count2 == 0){
candidate2 = num;
count2++;
}else{
count1--;
count2--;
}
}
}
vector<int> ans;
count1 = 0, count2 = 0;
for(int num : nums){
if(num == candidate1) count1++;
else if(num == candidate2) count2++;
}
if(count1 > nums.size() / 3) ans.push_back(candidate1);
if(count2 > nums.size() / 3) ans.push_back(candidate2);
return ans;
}
水塘采样
leetcode 382, 随机输出链表的一个节点值。
该做法对标「进阶」问题:对于数据流中的每个样本,决策其是否成为答案样本,无须知晓总样本数量,且等概率,同时无须使用额外空间。
class Solution {
public:
Solution(ListNode* head) {
this->_head = head;
}
int getRandom() {
int ans = 0, i = 1;
for(ListNode* cur = _head; cur != nullptr; cur = cur->next, ++i){
if(rand() % i == 0){
ans = cur->val;
}
}
return ans;
}
ListNode* _head;
};