欧几里得算法--求最大公约数
欧几里德算法又称辗转相除法,用于计算两个整数a,b的最大公约数。基本算法:设a=qb+r,其中a,b,q,r都是整数,则gcd(a,b)=gcd(b,r),即gcd(a,b)=gcd(b,a%b)。 证明略去了。
基本代码实现:
int gcd(int a,int b)
{
if(b==0)
return a;
return
gcd(b,a%b);
}
最小公倍数: 最大公因数×最小公倍数=两数的乘积
long lcm(long x,long y) {
return x*y/gcd(x,y);
}
扩展欧几里得算法
扩展欧几里德算法是欧几里得算法的扩展。
已知整数a、b,扩展欧几里得算法可以在求得a、b的最大公约数的同时,能找到整数x、y(其中一个很可能是负数),使它们满足贝祖等式。有两个数a,b,对它们进行辗转相除法,可得它们的最大公约数——这是众所周知的。然后,收集辗转相除法中产生的式子,倒回去,可以得到ax+by=gcd(a,b)的整数解。
用类似辗转相除法,求二元一次不定方程47x+30y=1的整数解。
- 47=30*1+17
- 30=17*1+13
- 17=13*1+4
- 13=4*3+1
然后把它们改写成“余数等于”的形式
- 17=47*1+30*(-1) //式1
- 13=30*1+17*(-1) //式2
- 4=17*1+13*(-1) //式3
- 1=13*1+4*(-3)
然后把它们“倒回去”
- 1=13*1+4*(-3) //应用式3
- 1=13*1+[17*1+13*(-1)]*(-3)
- 1=13*4+17*(-3) //应用式2
- 1=[30*1+17*(-1)]*4+17*(-3)
- 1=30*4+17*(-7) //应用式1
- 1=30*4+[47*1+30*(-1)]*(-7)
- 1=30*11+47*(-7)
得解x=-7, y=11。
基本算法:对于不完全为 0 的非负整数 a,b,gcd(a,b)表示 a,b 的最大公约数,必然存在整数对 x,y ,使得 gcd(a,b)=ax+by。
证明:设 a>b。
推理1,显然当 b=0,gcd(a,b)=a。此时 x=1,y=0;//推理1
推理2,ab!=0 时
设 ax1+by1=gcd(a,b);
bx2+(a mod b)y2=gcd(b,a mod b);
根据朴素的欧几里德原理有 gcd(a,b)=gcd(b,a mod b);
则:ax1+by1=bx2+(a mod b)y2;
即:ax1+by1=bx2+(a-(a/b)*b)y2=ay2+bx2-(a/b)*by2;
根据恒等定理得:x1=y2; y1=x2-(a/b)*y2;//推理2
这样我们就得到了求解 x1,y1 的方法:x1,y1 的值基于 x2,y2.
上面的思想是以递归定义的,因为 gcd 不断的递归求解一定会有个时候 b=0,所以递归可以结束。
扩展欧几里德的递归代码:
#include <iostream>
using namespace std;
int exgcd(int a,int b,int & x,int & y){
if(b == 0){
//根据上面的推理1,基本情况
x = 1;
y = 0;
return a;
}
int r = exgcd(b, a%b, x, y);
//根据推理2
int t = y;
y = x - (a/b)*y;
x = t;
return r;
}
int main() {
int x,y;
exgcd(47,30,x,y);
cout << "47x+30y=1 的一个整数解为: x=" << x << ", y=" << y << endl;
return 0;
}
整数集合中找出3的最大倍数
题目描述:给一个包含非负整数的数组(长度为n),找出由这些数字组成的最大的3的倍数,没有的话则输出impossible。
例如,如果输入的数组为{8,1,9},输出应为“9 8 1”,并且如果输入的数组为{8,1,7,6,0},输出应为”8760″。
方法一 暴力
直接用蛮力的话,生成所有的组合,为 2^n个,对每个数字再进行比较判断,需要 O(n)的时间,因为n可能会比较大,需要每个位的比较。
总的时间复杂度为O(n * 2^n).
方法二 数学技巧
可以借助O(n)的额外空间有效的解决这个问题。该方法是基于对数以下的简单性质:
1)一个数是3的倍数,则该数字各位总和为3的倍数。
例如,让我们考虑8760,它是3的倍数,因为数字总和为8 + 7 + 6 + 0 = 21,这是3的倍数。
2)如果一个数是3的倍数,那么它的所有排列也为3的倍数,例如,6078是3的倍数,数字8760,7608,7068,…也为3的倍数。
3)一个数的所有位之和与该数会有相同的余数。例如,如果对于151和它的各位之和7,对于3的余数都为1。
我们可以用下面的算法:
1,对数组进行非递减排序。
2,用3个队列 queue0,queue1,queue2,分别存储除以3余数为 0、1、2的数字。
3,求得所有的为的总和sum
4,有下面三种情况:
a) sum除以3余0。出列的所有三个队列中的数字,以非递减顺序排序输出到结果数组中。
b) sum除以3余1。则尝试从queue1中移除一个元素或从queue2中移除两个元素,如果不可以的话,则说明impossible
c) sum除以3余2。则尝试从queue1中移除两个元素或从queue2中移除一个元素,如果不可以的话,则说明impossible
5,最后将3个队列中的所有元素都输出到结果数组中,非递减排序,即为最终结果。
#include <stdio.h>
#include <stdlib.h>
// 自定义队列节点
typedef struct Queue
{
int front;
int rear;
int capacity;
int* array;
} Queue;
//创建一个队列
Queue* createQueue( int capacity )
{
Queue* queue = (Queue *) malloc (sizeof(Queue));
queue->capacity = capacity;
queue->front = queue->rear = -1;
queue->array = (int *) malloc (queue->capacity * sizeof(int));
return queue;
}
// 检测队列是否为空
int isEmpty (Queue* queue)
{
return queue->front == -1;
}
//想队列添加一个元素
void Enqueue (Queue* queue, int item)
{
queue->array[ ++queue->rear ] = item;
if ( isEmpty(queue) )
++queue->front;
}
// 从队列删除一个元素
int Dequeue (Queue* queue)
{
int item = queue->array[ queue->front ];
if( queue->front == queue->rear )
queue->front = queue->rear = -1;
else
queue->front++;
return item;
}
// 打印数组
void printArr (int* arr, int size)
{
int i;
for (i = 0; i< size; ++i)
printf ("%d ", arr[i]);
}
int compareAsc( const void* a, const void* b )
{
return *(int*)a > *(int*)b;
}
int compareDesc( const void* a, const void* b )
{
return *(int*)a < *(int*)b;
}
// 将3个队列中的元素输出到辅助数组
void populateAux (int* aux, Queue* queue0, Queue* queue1,
Queue* queue2, int* top )
{
while ( !isEmpty(queue0) )
aux[ (*top)++ ] = Dequeue( queue0 );
while ( !isEmpty(queue1) )
aux[ (*top)++ ] = Dequeue( queue1 );
while ( !isEmpty(queue2) )
aux[ (*top)++ ] = Dequeue( queue2 );
}
int findMaxMultupleOf3( int* arr, int size )
{
// 第1步,排序
qsort( arr, size, sizeof( int ), compareAsc );
Queue* queue0 = createQueue( size );
Queue* queue1 = createQueue( size );
Queue* queue2 = createQueue( size );
// 第2,3步
int i, sum;
for ( i = 0, sum = 0; i < size; ++i )
{
sum += arr[i];
if ( (arr[i] % 3) == 0 )
Enqueue( queue0, arr[i] );
else if ( (arr[i] % 3) == 1 )
Enqueue( queue1, arr[i] );
else
Enqueue( queue2, arr[i] );
}
//第四部,b)
if ( (sum % 3) == 1 )
{
if ( !isEmpty( queue1 ) )
Dequeue( queue1 );
else
{
if ( !isEmpty( queue2 ) )
Dequeue( queue2 );
else
return 0;
if ( !isEmpty( queue2 ) )
Dequeue( queue2 );
else
return 0;
}
}
// 第4步,c)
else if ((sum % 3) == 2)
{
if ( !isEmpty( queue2 ) )
Dequeue( queue2 );
else
{
if ( !isEmpty( queue1 ) )
Dequeue( queue1 );
else
return 0;
if ( !isEmpty( queue1 ) )
Dequeue( queue1 );
else
return 0;
}
}
int aux[size], top = 0;
// 第5步
populateAux (aux, queue0, queue1, queue2, &top);
qsort (aux, top, sizeof( int ), compareDesc);
// 打印结果
printArr (aux, top);
return 1;
}
// 测试
int main()
{
int arr[] = {8, 1, 7, 6, 0};
int size = sizeof(arr)/sizeof(arr[0]);
if (findMaxMultupleOf3( arr, size ) == 0)
printf( "Not Possible" );
return 0;
}
阶乘末尾0的个数
这个题目是编程之美一书中给出的题目。
给定一个整数N,那么N的阶乘N!末尾有多少个0? 比如:N=10,N!=3628800,N!的末尾有2个0。
编程之美一书给出两个例如质因数的性质的解法。考虑哪些组合可以得到10即可,考虑哪些数相乘能得到10,N!= K * 10M其中K不能被10整除,则N!末尾有M个0。
对N!进行质因数分解: N!=2X*3Y*5Z…,因为10=2*5,所以M与2和5的个数即X、Z有关。每一对2和5都可以得到10,故M=min(X,Z)。因为能被2整除的数出现的频率要比能被5整除的数出现的频率高,所以M=Z。其实也很好推出,1-9 中两两相乘,末位有0的话必须要有5,其它的数则是2的倍数。
int countZero(int N)
{
int ret = 0;
int j;
for(int i=1; i<=N; i++)
{
j = i;
while(0==j%5)
{
ret++;
j /= 5;
}
}
return ret;
}
当然还有一种解法:
int countZero(int N)
{
int ret = 0;
while(N)
{
ret += N/5;
N /= 5;
}
return ret;
}
幸运数字
幸运数也是属于整数的一个集合。来看下是幸运数是怎么生成的,大家就明白什么是幸运数了。
由一组由1开始的数列为例:
1,2,3,4,5,6,7,8,9,10,11,12,14,15,16,17,18,19,……
先将数列中的第2n个数(偶数)删除,只留下奇数:
1,3,5,7,9,11,13,15,17,19,…………
将新数列的第3n个数删除:
1, 3, 7, 9, 13, 15, 19,….….
重复上一个过程,无限的循环。最后留下来的那些数字,就是幸运数。(这里说的幸运数和维基百科说的不一样)
问题:
给一个数n,判断n是否是一个幸运数字。
算法分析:
主要是递推,考虑数字n的位置变化。第一n轮删除时,n的位置即为n,结束后的位置即为 n-n/2, 依此类推下去即可。
#include <stdio.h>
#include <iostream>
using namespace std;
#define bool int
bool isLucky(int n)
{
static int counter = 2;
/* next_position 只是为了可读性,可以只用n */
int next_position = n;
if(counter > n)
return 1;
if(n%counter == 0)
return 0;
/*计算数字n的下一个位置*/
next_position -= next_position/counter;
counter++;
return isLucky(next_position);
}
/*测试程序*/
int main()
{
int x = 19;
if( isLucky(x) )
printf("%d is a lucky no.", x);
else
printf("%d is not a lucky no.", x);
}
卡特兰(Catalan)数的应用及相关面试题
由于Catalan数经常会在算法题或面试题中出现,在这里做一下小小的总结。
介绍
Catalan数是组合数学中一个常在各种计数问题中出现的数列。一般项公式为
Cn的另一个表达形式为
一般来讲,我们编程时用递推关系会更方便计算:
或 即:C(n) = C(1)*C(n-1) + C(2)*C(n-2) + … + C(n-1)C(1).
它的前几项为: 1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796。可以先通过前几项判断问题是否属于卡特兰数。
典型应用
这里有一本书介绍了66个相异的可由卡塔兰数表达的组合结构。 http://www-math.mit.edu/~rstan/ec/catadd.pdf (英文PDF)
1、括号化问题。矩阵链乘: P=A1×A2×A3×……×An,依据乘法结合律,不改变其顺序,只用括号表示成对的乘积,试问有几种括号化的方案?
一个有n个X和n个Y组成的字串,且所有的部分字串皆满足X的个数大于等于Y的个数。以下为长度为6的dyck words:
XXXYYY XYXXYY XYXYXY XXYYXY XXYXYY
将上例的X换成左括号,Y换成右括号,Cn表示所有包含n组括号的合法运算式的个数:
((())) ()(()) ()()() (())() (()())
2、将多边行划分为三角形问题。将一个凸多边形区域分成三角形区域(划分线不交叉)的方法数?
类似:在圆上选择2n个点,将这些点成对连接起来使得所得到的n条线段不相交的方法数?
3、出栈次序问题。一个栈(无穷大)的进栈序列为1、2、3、…、n,有多少个不同的出栈序列?
类似:有2n个人排成一行进入剧场。入场费5元。其中只有n个人有一张5元钞票,另外n人只有10元钞票,剧院无其它钞票,问有多少中方法使得只要有10元的人买票,售票处就有5元的钞票找零?(将持5元者到达视作将5元入栈,持10元者到达视作使栈中某5元出栈)
类似:一位大城市的律师在他住所以北n个街区和以东n个街区处工作,每天她走2n个街区去上班。如果他从不穿越(但可以碰到)从家到办公室的对角线,那么有多少条可能的道路?
分析:对于每一个数来说,必须进栈一次、出栈一次。我们把进栈设为状态‘1’,出栈设为状态‘0’。n个数的所有状态对应n个1和n个0组成的2n位二进制数。由于等待入栈的操作数按照1‥n的顺序排列、入栈的操作数b大于等于出栈的操作数a(a≤b),因此输出序列的总数目=由左而右扫描由n个1和n个0组成的2n位二进制数,1的累计数不小于0的累计数的方案种数。
4、给顶节点组成二叉树的问题。
给定N个节点,能构成多少种形状不同的二叉树?
(一定是二叉树!先取一个点作为顶点,然后左边依次可以取0至N-1个相对应的,右边是N-1到0个,两两配对相乘,就是h(0)*h(n-1) + h(2)*h(n-2) + …… + h(n-1)h(0)=h(n)) (能构成h(N)个)
在2n位二进制数中填入n个1的方案数为c(2n,n),不填1的其余n位自动填0。从中减去不符合要求(由左而右扫描,0的累计数大于1的累计数)的方案数即为所求。
不符合要求的数的特征是由左而右扫描时,必然在某一奇数位2m+1位上首先出现m+1个0的累计数和m个1的累计数,此后的2(n-m)-1位上有n-m个 1和n-m-1个0。如若把后面这2(n-m)-1位上的0和1互换,使之成为n-m个0和n-m-1个1,结果得1个由n+1个0和n-1个1组成的2n位数,即一个不合要求的数对应于一个由n+1个0和n-1个1组成的排列。
反过来,任何一个由n+1个0和n-1个1组成的2n位二进制数,由于0的个数多2个,2n为偶数,故必在某一个奇数位上出现0的累计数超过1的累计数。同样在后面部分0和1互换,使之成为由n个0和n个1组成的2n位数,即n+1个0和n-1个1组成的2n位数必对应一个不符合要求的数。
因而不合要求的2n位数与n+1个0,n-1个1组成的排列一一对应。
显然,不符合要求的方案数为c(2n,n+1)。由此得出输出序列的总数目=c(2n,n)-c(2n,n+1)=1/(n+1)*c(2n,n)。
(这个公式的下标是从h(0)=1开始的)
相关面试题
问题描述:
12个高矮不同的人,排成两排,每排必须是从矮到高排列,而且第二排比对应的第一排的人高,问排列方式有多少种?
这个笔试题,很YD,因为把某个递推关系隐藏得很深。
问题分析:
我们先把这12个人从低到高排列,然后,选择6个人排在第一排,那么剩下的6个肯定是在第二排.
用0表示对应的人在第一排,用1表示对应的人在第二排,那么含有6个0,6个1的序列,就对应一种方案.
比如000000111111就对应着
第一排:0 1 2 3 4 5
第二排:6 7 8 9 10 11
010101010101就对应着
第一排:0 2 4 6 8 10
第二排:1 3 5 7 9 11
问题转换为,这样的满足条件的01序列有多少个。
观察1的出现,我们考虑这一个出现能不能放在第二排,显然,在这个1之前出现的那些0,1对应的人
要么是在这个1左边,要么是在这个1前面。而肯定要有一个0的,在这个1前面,统计在这个1之前的0和1的个数。
也就是要求,0的个数大于1的个数。
OK,问题已经解决。
如果把0看成入栈操作,1看成出栈操作,就是说给定6个元素,合法的入栈出栈序列有多少个。
这就是catalan数,这里只是用于栈,等价地描述还有,二叉树的枚举、多边形分成三角形的个数、圆括弧插入公式中的方法数,其通项是c(2n, n)/(n+1)。
在<<计算机程序设计艺术>>,第三版,Donald E.Knuth著,苏运霖译,第一卷,508页,给出了证明:
问题大意是用S表示入栈,X表示出栈,那么合法的序列有多少个(S的个数为n)
显然有c(2n, n)个含S,X各n个的序列,剩下的是计算不允许的序列数(它包含正确个数的S和X,但是违背其它条件)。
在任何不允许的序列中,定出使得X的个数超过S的个数的第一个X的位置。然后在导致并包括这个X的部分序列中,以S代替所有的X并以X代表所有的S。结果是一个有(n+1)个S和(n-1)个X的序列。反过来,对一垢一种类型的每个序列,我们都能逆转这个过程,而且找出导致它的前一种类型的不允许序列。例如XXSXSSSXXSSS必然来自SSXSXXXXXSSS。这个对应说明,不允许的序列的个数是c(2n, n-1),因此an = c(2n, n) – c(2n, n-1)。
验证:其中F表示前排,B表示后排,在枚举出前排的人之后,对应的就是后排的人了,然后再验证是不是满足后面的比前面对应的人高的要求
质因数分解及算法实现
每个合数都可以写成几个质数相乘的形式,这几个质数就都叫做这个合数的质因数。如果一个质数是某个数的因数,那么就说这个质数是这个数的质因数。而这个因数一定是一个质数。
定义
例子
-
1没有质因子。
-
5只有1个质因子,5本身。(5是质数。)
-
6的质因子是2和3。(6 = 2 × 3)
-
2、4、8、16等只有1个质因子:2(2是质数,4 = 2,8 = 2,如此类推。)
-
10有2个质因子:2和5。(10 = 2 × 5)
计算方法
//返回质因数数组
Integer[] decPrime(int n) {
List<Integer> list = new ArrayList<Integer>();
for (int i=2;i <= n;i++){
while(n != i){
if(n%i != 0){
break;//不能整除肯定不是因数,能够整除在这里一定是质数。因为所有的2,3,5,7
//都被除完之后。剩下的因数只能是奇数,且是质数。
}
list.add(Integer.valueOf(i));
n = n/i;
}
}
list.add(Integer.valueOf(n));
return list.toArray(new Integer[list.size()]);
}
约瑟夫环的数学优化方法
约瑟夫环问题的原来描述为,设有编号为1,2,……,n的n(n>0)个人围成一个圈,从第1个人开始报数,报到m时停止报数,报m的人出圈,再从他的下一个人起重新报数,报到m时停止报数,报m的出圈,……,如此下去,直到所有人全部出圈为止。当任意给定n和m后,设计算法求n个人出圈的次序。 稍微简化一下。
问题描述:n个人(编号0~(n-1)),从0开始报数,报到(m-1)的退出,剩下的人继续从0开始报数。求胜利者的编号
下面利用数学推导,如果能得出一个通式,就可以利用递归、循环等手段解决。下面给出推导的过程:
(1)第一个被删除的数为 (m - 1) % n。
(2)假设第二轮的开始数字为k,那么这n - 1个数构成的约瑟夫环为k, k + 1, k + 2, k +3, .....,k - 3, k - 2。做一个简单的映射。
k -----> 0k+1 ------> 1
k+2 ------> 2
...
...
k-2 ------> n-2
这是一个n -1个人的问题,如果能从n - 1个人问题的解推出 n 个人问题的解,从而得到一个递推公式,那么问题就解决了。假如我们已经知道了n -1个人时,最后胜利者的编号为x,利用映射关系逆推,就可以得出n个人时,胜利者的编号为 (x + k) % n。其中k等于m % n。代入(x + k) % n <=> (x + (m % n))%n <=> (x%n + (m%n)%n)%n <=> (x%n+m%n)%n <=> (x+m)%n
(3)第二个被删除的数为(m - 1) % (n - 1)。
(4)假设第三轮的开始数字为o,那么这n - 2个数构成的约瑟夫环为o, o + 1, o + 2,......o - 3, o - 2.。继续做映射。
o -----> 0
o+1 ------> 1
o+2 ------> 2
...
...
o-2 ------> n-3
这是一个n - 2个人的问题。假设最后的胜利者为y,那么n -1个人时,胜利者为 (y + o) % (n -1 ),其中o等于m % (n -1 )。代入可得 (y+m) % (n-1)要得到n - 1个人问题的解,只需得到n - 2个人问题的解,倒推下去。只有一个人时,胜利者就是编号0。下面给出递推式:
f [1] = 0;
f [ i ] = ( f [i -1] + m) % i; (i>1)
有了递推公式,实现就非常简单了,给出循环的两种实现方式。再次表明用标准库的便捷性。
int JosephusProblem_Solution3(int n, int m)
{
if(n < 1 || m < 1)
return -1;
int *f = new int[n+1];
f[0] = f[1] = 0; //f[0]其实用不到的
for(unsigned i = 2; i <= n; i++)
f[i] = (f[i-1] + m) % i; //按递推公式进行计算
int result = f[n];
delete []f;
return result;
}