算法学习--数学相关

欧几里得算法--求最大公约数

欧几里德算法又称辗转相除法,用于计算两个整数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(其中一个很可能是负数),使它们满足贝祖等式ax + by = \gcd(a, b).。有两个数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即可,考虑哪些数相乘能得到10N!= K * 10M其中K不能被10整除,则N!末尾有M0。

N!进行质因数分解: N!=2X*3Y*5Z…,因为10=2*5,所以M25的个数即XZ有关。每一对25都可以得到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;
   }

当然还有一种解法:

    Z =[N/5] + [N/52] + [N/53] + …

    [N/5] 表示不大于N的的数中5的倍数贡献一个5, [N/52] 表示不大于N的数中52的倍数在贡献一个5……

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数是组合数学中一个常在各种计数问题中出现的数列。一般项公式为 C_n = \frac{1}{n+1}{2n \choose n} = \frac{(2n)!}{(n+1)!n!}

Cn的另一个表达形式为C_n = {2n\choose n} - {2n\choose n+1} \quad\mbox{ for }n\ge 1

一般来讲,我们编程时用递推关系会更方便计算:C_0 = 1 \quad \mbox{and} \quad C_{n+1}=\frac{2(2n+1)}{n+2}C_n,

或 C_0 = 1 \quad \mbox{and} \quad C_{n+1}=\sum_{i=0}^{n}C_i\,C_{n-i}\quad\mbox{for }n\ge 0.  即: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条线段不相交的方法数?

Distant Galaxy

3、出栈次序问题。一个栈(无穷大)的进栈序列为1、2、3、…、n,有多少个不同的出栈序列?
类似:有2n个人排成一行进入剧场。入场费5元。其中只有n个人有一张5元钞票,另外n人只有10元钞票,剧院无其它钞票,问有多少中方法使得只要有10元的人买票,售票处就有5元的钞票找零?(将持5元者到达视作将5元入栈,持10元者到达视作使栈中某5元出栈)

类似:一位大城市的律师在他住所以北n个街区和以东n个街区处工作,每天她走2n个街区去上班。如果他从不穿越(但可以碰到)从家到办公室的对角线,那么有多少条可能的道路?

Distant Galaxy

分析:对于每一个数来说,必须进栈一次、出栈一次。我们把进栈设为状态‘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)个)
Distant Galaxy

在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没有质因子,1与任何正整数(包括1本身)都是互质正整数的因数分解可将正整数表示为一连串的质因子相乘,质因子如重复可以指数表示。根据算术基本定理 任何正整数皆有独一无二的质因子分解式。只有一个质因子的正整数为质数。

例子

  • 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)
就是一个数的 约数,并且是 质数,比如8=2×2×2,2就是8的质因数。12=2×2×3,2和3就是12的质因数。把一个式子以12=2×2×3的形式表示,叫做 分解质因数。16=2×2×2×2,2就是16的质因数, 把一个合数写成几个质数相乘的形式表示,这也是分解质因数[1]
分解质因数的方法是先用一个合数的最小质因数去除这个合数,得出的数若是一个质数,就写成这个合数相乘形式;若是一个合数就继续按原来的方法,直至最后是一个质数 。
分解质因数的有两种表示方法,除了大家最常用知道的“短除分解法”之外,还有一种方法就是“塔形分解法”。
分解质因数对解决一些 自然数乘积的问题有很大的帮助,同时又为求 最大公约数最小公倍数做了重要的铺垫。

计算方法

短除法
求一个数分解质因数,要从最小的质数除起,一直除到结果为质数为止。分解质因数的算式的叫短除法,和除法的性质差不多,还可以用来求多个个数的公因式
最大公因数的一种方法,也可用来求 最小公倍数
求几个数最大公因数的方法,开始时用观察比较的方法,即:先把每个数的因数找出来,然后再找出公因数,最后在公因数中找出最大公因数。
例如:求12与18的最大公因数。
12的因数有:1、2、3、4、6、12。
18的因数有:1、2、3、6、9、18。
12与18的 公因数有:1、2、3、6。
12与18的最大公因数是6。
这种方法对求两个以上数的最大公因数,特别是数目较大的数,显然是不方便的。于是又采用了给每个数分别分解质因数的方法。
12=2×2×3
18=2×3×3
12与18都可以分成几种形式不同的乘积,但分成质因数连乘积就只有以上一种,而且不能再分解了。所分出的质因数无疑都能 整除原数,因此这些质因数也都是原数的约数。从分解的结果看,12与18都有 公约数2和3,而它们的乘积2×3=6,就是 12与18的最大公约数。
采用分解质因数的方法,也是采用短除的形式,只不过是分别短除,然后再找 公约数和最大公约数。如果把这两个数合在一起短除,则更容易找出 公约数和最大公约数。
从短除中不难看出,12与18都有 公约数2和3,它们的乘积2×3=6就是12与18的最大公约数。与前边分别分解质因数相比较,可以发现:不仅结果相同,而且 短除法 竖式左边就是这两个数的公共质因数,而两个数的最大公约数,就是这两个数的公共质因数的连乘积。
实际应用中,是把需要计算的两个或多个数放置在一起,进行短除。
在计算多个数的最小公倍数时,对其中任意两个数存在的约数都要算出,其它无此约数的数则原样落下。最后把所有约数和最终剩下无法 约分的数连乘即得到最小公倍数。
只含有1个质因数的数一定是 亏数
(短除法详解:
短除符号就是除号倒过来。短除就是在除法中写 除数的地方写两个数共有的 质因数,然后落下两个数被公有质因数整除的商,之后再除,以此类推,直到结果 互质为止(两个数互质)。
而在用短除计算多个数时,对其中任意两个数存在的因数都要算出,其它没有这个因数的数则原样落下。直到剩下每两个都是 互质关系。
求最大公因数 乘一边,求最小公倍数 乘一圈。
公约数:亦称“公 因数”。是几个整数同时均能整除的整数。如果一个整数同时是几个整数的约数,称这个整数为它们的“公约数”;公约数中最大的称为最大公约数。)
在用短除计算多个数时,对其中任意两个数存在的因数都要算出,其它没有这个因数的数则原样落下。直到剩下每两个都是互质关系。 求最大公约数遍乘左边所有数公共的因数,求最小公倍数遍乘一圈。这种方法对求两个以上数的最大公因数,特别是数目较大的数,显然是不方便的。于是又采用了给每个数分别 分解质因数的方法
//返回质因数数组  
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         ----->  0 
             k+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;  
} 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值