【2013-1-25】算法每日一题:求非自身所有数乘积后取模

来源:互联网 发布:日语题库软件 编辑:程序博客网 时间:2024/05/20 05:47

算法每日一题中的题目来源: 九度论坛      网址:http://t.jobdu.com/forum-120-1.html

 

题目:
给出一列整数a[n],求每个数除去自身以外其他数的乘积对给定的一个质数m取模。
如:有数组a[4]分别为3、9、2、7,m为13,那么得到的第一个数是 9*2*7 mod 13 = 9,另外三个是3、7、2
取模是因为给的数很大,会乘超过int的范围。
要求O(n)的解法。  

注:本题是腾讯公司求职面试题。

 

分析:

1.本题最重要的一个思想是:如何求其余数的连乘积?

总结:先求全部数的乘积再除以当前数。这里不允许当前数为0,

 针对数列中有零的情况,要特殊考虑:如果数列中有两个或两个以上的零,那输出一定全是零。

如果只有一个零,那么除了当前这个0的对应输出不是零外,其余一定全是零。可以考虑单独记下这个零的位置,然后计算其余数的乘积。可以在第一遍扫描时,添加判断。

2.可能连乘数的乘积很大,会溢出,因此需要取模。考虑离散数学中的公式:(a*b)%k = ((a%k)*(b%k))%k

因此原题需要第一遍,求出A[i] % K 的连乘积,记为product,第二遍 [ product / (A[i]%K) ] % K 即为结果。

 

如果题目另有要求:说不能用除法,解法如下 将每个数的左边乘积与右边乘积存起来再相乘取余。

观察题目,就是“有数组a[4]分别为3、9、2、7,m为13,那么得到的第一个数是 9*2*7 mod 13 = 9”
9为除本身以外的数相乘,然后求模。
故可求得每个数左侧所有数的乘积A,与右侧所有数的乘积B,然后两数相乘再求模,即为所得。
可在遍历过程中,一次计算出数组中每个数的左侧乘积,与右侧乘积,故时间复杂度为O(n)。然而空间复杂度也为O(n),这题如果要求空间复杂度为O(1),比较有难度。

代码如下:

int *solve(int a[], int n, int m){int *lmul = new int[n];int *rmul = new int[n];lmul[0] = a[0] % m;rmul[n-1] = a[n-1] %m;for(int i = 1; i < n; i++)lmul[i] = (lmul[i-1]*a[i])%m;for(int i = n-2; i >= 0; i--)rmul[i] = (rmul[i+1] * a[i])%m;int *result = new int[n];result[0] = rmul[1];result[n-1]  = lmul[n-2];for(int i = 1; i < n-1; i++)result[i] = (lmul[i-1]*rmul[i+1])%m;delete [] lmul;delete [] rmul;return result;}
方法二:对每个数求关于m的逆元,求出所有数的积取模后,由每个数的逆元得出每个结果。#include<stdio.h>#include<string.h>#include<stdlib.h>#define MAXN 100005typedef long long LL;int a[MAXN];int n, m;LL ExGCD(LL a, LL b, LL &x, LL &y){     if(b == 0)     {         x = 1, y = 0;         return a;     }     LL d = ExGCD(b, a % b, x, y), t = x;     x = y, y = t - a / b * x;     return d;}int main(){     LL all, x, y;     while(scanf( "%d%d", &n, &m) == 2)     {         all = 1;         for(int i = 0; i < n; i ++)         {             scanf("%d", &a[i]);             all = all * a[i] % m;         }         printf("%d\n", all);         for(int i = 0; i < n; i ++)         {             ExGCD(a[i], m, x, y);             if(i) printf(" ");             printf("%lld", (all * x % m + m) % m);         }         printf("\n");     }     return 0;}




附:离散相关 有同学分析如下:

由离散数学中的结论
(a*b)%m = ((a%m)*(b%m))%m
我们先获得一个总余数M=(a1*a2*...*an)%m(计算为了防止溢出可以连环套用上面的式子)
然后对于每个数ai,设Ri=(a1*a2*...*an)/(ai)
则((Ri%m)*(ai%m))%m = M
Ri%m=(M+mx)/(ai%m),且(M+mx)%(ai%m)=0,其中x为未知数
Ri%m即我们要求的除去ai之后的乘积对m的余数
问题转换为求这个方程
(M+mx)%(ai%m)=0根据余数的性质:
mx%(ai%m)=(ai%m)-M%(ai%m)
这是个同余方程,解法如下:
设(ai%m)-M%(ai%m)=b,ai%m=c
则方程可以写为
mx%c=b

mx+cy = b
x、y是未知整数
该不定方程的使得x>0并且x最小的解可使用扩展欧几里得算法(几乎O(1),其实是O(logm))得到
从而Ri%m=(M+mx)/(ai%m)
时间复杂度为O(Nlogm),空间复杂度O(1)