巧解数学问题
在这里放一个暴论:数学方法基本上就是编程类问题的最优解。所以做题的时候如果能够运用到数学的知识,会大大减少解题的复杂度。
公因数和公倍数
使用辗转相除法可以求解两个数的最大公因数,从而根据两个数的最大公因数可以求出两个数的最小公约数。
这里介绍一下辗转相除法的原理以及简单的证明过程:
欧几里得算法又称辗转相除法,是指用于计算两个非负整数a,b
的最大公约数。计算公式gcd(a,b) = gcd(b,a mod b)
证明过程:
a可以表示成a = kb + r
(a,b,k
,r皆为正整数,且r<b
),则r = a mod b
假设d
是a,b
的一个公约数,记作d|a
,d|b
,即a
和b
都可以被d
整除。
而r = a - kb
,两边同时除以d
,r/d=a/d-kb/d=m
,由等式右边可知m为整数,因此d|r
因此d
也是b
,a mod b
的公约数。因(a,b)
和(b,a mod b)
的公约数相等,则其最大公约数也相等,得证。
int gcd(int a,int b)
{
return b==0?a:gcd(b,a%b);
}
而最小公约数则好求了,直接是两数相乘之后除以最大公因数即可。
int lcm(int a,int b)
{
return a*b/gcd(a,b);
}
这里进一步引入扩展的欧几里得算法:
扩展欧几里得算法是欧几里得算法(又叫辗转相除法)的扩展。除了计算a、b
两个整数的最大公约数,此算法还能找到整数x、y
(其中一个很可能是负数)。通常谈到最大公因子时, 我们都会提到一个非常基本的事实: 给予二整数 a
与b
, 必存在有整数 x 与 y 使得ax + by = gcd(a,b)
。有两个数a,b
,对它们进行辗转相除法,可得它们的最大公约数——这是众所周知的。然后,收集辗转相除法中产生的式子,倒回去,可以得到ax+by=gcd(a,b)
的整数解。
求解的思路:假设当前我们要处理的是求出 a
和 b
的最大公约数,并求出 x
和y
使得a*x + b*y= gcd
,而我们已经求出了下一个状态:b
和a%b
的最大公约数,并且求出了一组x1
和y1
使得: b*x1 + (a%b)*y1 = gcd
, 那么这两个相邻的状态之间是否存在一种关系呢?我们知道: a%b = a - (a/b)*b
(这里的 “/” 指的是整除,例如 5/2=2 , 1/3=0),那么,我们可以进一步得到:
gcd = b*x1 + (a-(a/b)*b)*y1
= b*x1 + a*y1 – (a/b)*b*y1
= a*y1 + b*(x1 – a/b*y1)
对比之前我们的状态:求一组x
和 y
使得:a*x + b*y = gcd
,可以知道:
x = y1
y = x1 – a/b*y1
代码:
int cGCD(int a,int b,int &x,int &y)
{
if(b==0)//递归的基线情况
{
x = 1,y = 0;
return a;
}
int x1, y1,gcd = xGCD(b,a%b,x1,y1);
x = y1, y = x1 - (a/b)*y1;
return gcd;
}
质数
这里是介绍一种方法,他能够在判断一个整数n是否是质数的同时也能够判断小于n的质数的个数。其原理就是:从1到n进行遍历的时候。假设当前遍历到m,则把所有的小于n的,且是m的倍数的整数标为和数,遍历完成之后,没有被标为和数的数字即为质数。(埃氏筛法)
这里给出优化了的代码:
int countprimes(int n)
{
if(n<=2) return 0;
vector<bool> prime(n,true);
int i = 3, sqrtn = sqrt(n), count = n/2;//偶数一定不是质数
while(i<=sqrtn)
{
for(int j = i*i; j < n;j+= 2*i)
{
if(prime[j])
{
--count;
prime[j] = false;
}
}
do
{
i+=2;
}while(i<=sqrtn&&!prime[i]);
}
return count;
}
进制转换
进制类转换的题目一般分为常规手法和特殊手法。常规手法指的就是使用除法和取模的运算;而特殊手法则是使用之前我们提到的位运算的手法。同时当原始数据是字符串而不是普通的数字类型的时候,需要注意整数 int 是否会出现越界的问题,也就是是否会溢出,这个时候考虑使用 long 或者做溢出情况的特殊处理。对了,也要记得考虑负数和零的情况哦。
例如给定一个整数,输出一个字符串,求相关的七进制的表示。
class Solution {
public:
string convertToBase7(int num) {
//这里需要考虑负数和零的特殊情况
if(num==0) return "0";
string ans;
bool is_negative = num<0;
num = abs(num);
while(num)
{
int b = num%7;
ans = to_string(b) + ans;
num /= 7;
}
return is_negative?"-"+ans:ans;
}
};
某数的次方
这里主要也是可以使用两种思路,正常的除数和取余的操作或者位运算的操作。
这里使用求一个整数是否是3(质数)的次方为案例来讲解一下此类问题的做法,当然其他数如法炮制(注意对于2的次方数直接使用位运算就可以判断)
第一种解法,使用换底公式来求解
return fmod(log10(n)/log10(3),1) == 0;
第二种解法,使用的是这个数的次数在整数范围内的上限值
return n>0&&1162261467%n==0;
同时这里也引入对于求一个整数是否是4的次方数的二进制位运算方法
return n>0&&!n&(n-1)&&(n&1431655765);
随机与采样
这里分为两种情况,一个一般是针对于数组,我们知道这个数组的长度,这个时候可以采取Fisher-Yates洗牌办法;一个一般是针对于链表,一般这个链表的长度我们是不知道的,这个时候使用著名的书库采样方法。
首先针对于Fisher-Yates洗牌办法,其原理主要是通过在一个范围之内随机交换位置一次,然后再缩小范围的过程,其实现有正向洗牌和反向洗牌两种。
这里主要通过 leetcode 384的代码,对这个方法做一个展示:
class Solution {
public:
vector<int>orgin;
//随机与取样问题,其中很重要的一个就是如何使用函数来产生随机值
//难点就在于说函数的调用次数比较多,怎么样更好的节约其复杂度呢
Solution(vector<int>& nums):orgin(move(nums)) {
}
/** Resets the array to its original configuration and return it. */
vector<int> reset() {
return orgin;
}
/** Returns a random shuffling of the array. */
vector<int> shuffle() {
if(orgin.empty()) return {};
vector<int> ans(orgin);
//反向洗牌,也就是随机取出来的数字放到数组的末尾,在这里也就是
//采用swap函数
for(int i = ans.size()-1;i>=0;--i)
{
//srand((unsigned)time(NULL));
swap(ans[i],ans[rand()%(i+1)]);
}
//正向洗牌
//for(int i = 0;i<ans.size();++i)
//{
// int pos = rand%(n-i);
// swap(ans[i],ans[i+pos]);
//}
return ans;
}
};
现在针对于我们事先不清楚链表的长度的时候,可以采用水库采样法(又称为蓄水池采样法),也就是遍历一遍链表,在遍历到m个节点的时候,有1/m的概率覆盖掉之前的节点选择。可以简单的证明,这样选取到的每一个节点在理论上被选取到的概率为1/n(这里我们不妨假设最终的这个链表的长度为n)
这里我们采用 leetcode 382的代码来作为一个展示:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode*first;
/** @param head The linked list's head.
Note that the head is guaranteed to be not null, so it contains at least one node. */
Solution(ListNode* head):first(head) {
//水库采样 要把我笑死 看看这个采样方法如何吧
}
/** Returns a random node's value. */
int getRandom() {
int i = 2;
ListNode*node = first,*tmp = node->next;
int ans = node->val;
while(tmp)
{
if(rand()%i==0)
{
ans = tmp->val;
}
++i;
tmp = tmp->next;
}
return ans;
}
};
/**
* Your Solution object will be instantiated and called as such:
* Solution* obj = new Solution(head);
* int param_1 = obj->getRandom();
*/