存档

‘Math’ 分类的存档

差分序列

2011年9月29日 8 条评论

不小心遇到一题4次求和,推公式推了好久。。想想实在是没有必要,每次遇到还得推一边实在是累。。所以,回来就写了一个差分的版本,单次计算的效率不比直接公式差,不容易出错,而且,再也不用蛋去推多项式公式了

至于什么是差分序列,这个我就不解释了,wiki上应该可以找到。我的写法是借鉴了牛顿插值的,使用了一个递推的写法(一般求多项式都会用这个, 比如4n^3 + n^2 + 2 * n + 3,一般写成 ((4n + 1) * n + 2) * n + 3)

初始化细节说明:order是生成的多项式的阶,请确保它大于或等于实际的多项式阶。order阶的多项式需要用order+1项进行init,请把这前order+1项存入数组cof中。最后调用init函数,就可以用calc来计算任意项了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/******************************************************************************
  * Differential Sequence
  * init: O(order^2)
  * you should init the cof[i] with the value of i-th value of sequence
  * the sequene start from startfrom, default is 0, so a[0] is the first one
  * 	of sequence a
  * calc: O(order)
  ****************************************************************************/
 
const int ORDERLIM = 100;
 
double cof[ORDERLIM + 1];
int order;
const int startfrom = 0;
 
void init()
{
	int i, j;
	for(i = 1; i <= order; i++) {
		for(j = order; j >= i; j--) {
			cof[j] -= cof[j - 1];
		}
	}
}
 
double calc(double n)
{
	int i;
	double res = 0;
	for(i = order; i > 0; i--) {
		res += cof[i];
		res *= (n - i + 1 - startfrom) / i;	
	}
	res += cof[i];
	return res;
}
 
/******************************************************************************
  * MOD version of Differential Sequence
  * cof should not overflow MOD
  * MOD should should not less than order
  * MOD should be prime
  * need inverse in mod MOD system
  ****************************************************************************/
typec MOD = 1000000007;
typec cof[ORDERLIM + 1];
typec inv[ORDERLIM + 1];
int order;
const int startfrom = 0;
 
void init()
{
	int i, j;
	for(i = 1; i <= order; i++) {
		inv[i] = inverse(i, MOD);
		for(j = order; j >= i; j--) {
			cof[j] -= cof[j - 1];
		}
	}
}
 
typec calc(typec n)
{
	int i;
	typec res = 0;
	for(i = order; i > 0; i--) {
		res += cof[i], res %= MOD;
		res *= (n - i + 1 - startfrom) % MOD * inv[i] % MOD;
		res %= MOD;
	}
	res += cof[i], res %= MOD;
	while(res < 0) res += MOD;
	return res;
}
分类: ACM, C, Math 标签: ,

Lucas定理推广(组合数取模)

2011年4月21日 7 条评论

比赛的时候,意外想到了个方法来处理组合数取模。一推,发现就是Lucas定理,并且,在模数是素数乘方的情况作了些推广。使得lucas能够处理模任意数的组合数取模。

Lucas定理

首先给出Lucas定理的递归描述

lucas定理

简要证明思路

考虑连续的p个整数(p是素数),把能整除p的那个数除去,然后取模,再乘起来模p其实是-1(*)。注意,因为和p互素的原因,这东西是可约的。

再考虑这个组合公式

上下都有k项,你能取的长度为p的连续整数有k/p段(从后往前取),每一段去掉那个能被p整除的数,然后,上下因为有相同的段数,所以就被约掉了(明白为什么在这种情况下是可约的么?)。

为什么能被p整除的数要被单独的拉出来呢?因为,和模数有公因子的数加入取模运算会导致不可逆。所以这些能被p整除的数需要被提取出来单独处理。

于是,有了后面部分的C(n/p, k/p),其实就是每个被提取出来的数再提取了一个因子p(因为上下提取出来的数一样多,所以,这些p自然就被约掉了)。然后,你幸运的发现,他是个组合数,于是问题被递归。最后,我们还有个前缀没有被处理。但是,因为模数是p,所以有下面公式

把这写合并在一起,就有了Lucas定理的递归表述(公式1)


推广的Lucas

那么要怎么处理模数是素数p的乘方的情形呢?其实是一样的,只不过,有些东西就不再退化了,比如公式6。我们可以用同样的方式,先把因子p全部提取出来单独处理,于是得到如下的公式,来处理p的乘方。

其中

式2的前缀(F除以F)就是上面的公式6不能退化的情况。为什么?因为其中可能含有因子p。因子p加入会导致乘法取模的不可逆。

需要注意的是,前缀很可能不是整数,在写代码的时候这个地方很容易犯错!所以要先进入子问题(可以用一个栈搞定)。


一般的组合数取模

有了这些,我们能干什么呢?其实,Lucas及其扩展只是把原来的组合数取模问题分割成了更小规模的问题(有lg(k)/lg(p)个)

考虑一个一般的问题,模数m任意。

策略:

我们把m先素数分解,然后用Lucas及其扩展分割。然后对每个小问题解决合并。合并可以用CRT进行(同余方程组,中国剩余定理)。于是,如果简单的组合数取模能在O(k)的时间完成,我们就得到一个O(\min (k, p^t \cdot log_p(t)))时间复杂度的算法(其中p是m的素因子,t是该素因子的个数),然后我们需要用lg级别的时间,完成每一个的合并,空间取决于素数表的大小(当然,可以没有,没有的话,就是lg级别的空间了)

可能的退化:如果p非常小,t非常大,即模数是小素数的高次。这个时候问题被退化到差不多O(k)的复杂度,k很大的情况下会很慢。但是经过测试,平均的情况非常快。

下面给出代码
CombinationNumber.cpp
包含一个比扩展lucas更稳定的版本

(很长,包括了需要用到的所有模块,同余方程组素数表什么的都在,用的时候记得先生成素数表):

PS:公式5: Wilson定理,当然这东西是什么不重要,因为都会被约掉。

分类: ACM, Math 标签: , ,

素数的平方和表示

2011年4月11日 3 条评论

问题:

找到这样的正整数A和B,使得A^2 + B^2 = p

 

当然,只有模4余1的素数(大于2)的才有平方和表示^^

假设p(>2)是两个数a和b平方的和,因为p是奇数,所以a和b有一个是奇数另一个是偶数。假设a=2m, b=2n+1, 则a^2 + b ^2 = 4m^2+ 4n^2 + 4n + 1, 显然,它模4余1


现在证明以下定理,证明本身就是一个算法(费马降阶Fermat's method of descent)

For \; p \; is \; prime \; more \; than \; 2: \; p \equiv 1 \mod 4 \Leftrightarrow \exists a, b \in N^*, p = a^2 + b^2

 

  • 首先,由二次互反律,存在整数A, B使得A^2 + B^2 = Mp, 其中M是小于p的正整数

若A=1,则B^2 \equiv -1 \mod p,所以只要我们能找到这样的B,就OK了

二次互反律说明,-1是二次剩余,所以这样的B是可以被找到的(稍后说怎么找,算法)


  • 然后就是用费马降阶对这个初始的式子进行变换了
    1. 取u, v,使得u \equiv A (\mod M), v \equiv B (\mod M), -\frac{M}{2} \leqslant u, v \leqslant \frac{M}{2}
    2. 注意到u^2 + v^2 \equiv A^2 + B^2 \equiv 0 \; (\mod M),所以我们有u^2 + v^2 = Mr\;(1 \leqslant r < M)
    3. 所以(uA+vB)^2+(vA-uB)^2=(u^2+v^2)(A^2+B^2)=M^2 rp
    4. M^2除到左边,得到(\frac{uA+vB}{M})^2+(\frac{vA-uB}{M})^2=rp
    5. 这个式子和初始的A^2 + B^2 = Mp一个格式,并且有r \leqslant \frac{M}{2}
    6. 不断重复以上步骤,M的规模每次至少减半,直到成为1,于是算法结束。此时我们也得到了p的平方和表示,并且完成了证明。
    7. 费马降阶算法的时间复杂度为O(log(M)),因为每次M的规模至少减半

PS:很漂亮的恒等式:(uA+vB)^2+(vA-uB)^2=(u^2+v^2)(A^2+B^2),值得好好研究下

  • 这样,这个定理是证明了,但是,算法却没有结束。因为初始的B还没有找到(虽然它必然存在),以下给出一个随机算法
    1. 注意到欧拉准则a^{\frac{p-1}{2}} \equiv (\frac{a}{p})( \mod p)
    2. 所以二次非剩余的(p-1)/2次同余-1模p
    3. 因为二次非剩余有一半(在1~p-1之间),所以每次随即的选取a,我们有一半的机会获得二次非剩余
    4. k次全部失败的概率是0.5^k,数学期望在两次左右就可找到二次非剩余
    5. 每次计算a^{\frac{p-1}{2}} \mod p的代价是O(log(p))的
    6. 所以算法在期望O(log(p))的时间结束

 

PS:一些猜想:如果素数模4余1,则平方剩余成对出现,即a是平方剩余,则-a也是平方剩余。如果素数模4余3,a是平方剩余,导致-a必然不是平方剩余,即他们总是落单(这其实是显然的,否则就可以用费马降阶,构造出模4余3的素数的平方和表示了,而这是不可能的。但猜想的前半句却不是那么显然的)

上面的算法主要来自《数论概论》这本书,26章是直接对于这个问题的。

分类: Math 标签: ,

基于二进制的递推

2011年3月15日 没有评论

快半年没更新了,囧。。

把去年的某类结果整理了下,总结如下:

我们都习惯了前后项的逐项递推策略,这类一般的策略,可以称之为线性方式。下面,我说一种当序列满足某些性质是能狗构造的平方递推模式(基于项的二进制表示)


序列需要满足的性质

***我们假设序列为a[1], a[2], a[3], a[4], a[5], ..., a[n],以下所说的“简单”,意思是计算时间代价低于线性

  1. a[n] -> a[2n]是简单的(n是2的幂)
  2. a[n], a[m] -> a[n + m]是简单的(n是2的幂,且m小于n)

当然,有一个更强的性质会使问题简化(它是上面条件的充分条件,但不是必要的)

a[n], a[m] -> a[n + m]是简单的(m, n为任意正整数)


算法构造

首先考虑到因为性质1,所以,在已知a[1]推a[n]是非常简单的(其中n是2的幂)。
其次,假设我们已经知道a[1]~a[n-1]的所有值,依赖性质2,我们可以轻易的把这些和a[n]合并,然后得到a[n + m]的值。于是,我们获得了a[1]~a[2n - 1]的所有值。
注意到我们有a[1]所以,我们能对整个序列产生一个覆盖。

接下去我们构造算法,来求特定的位置的a[p]

  1. 假设p = n + m,n是不大于p的最大的2的幂,我们要做的只是求a[n]和a[m]。m大小的子问题和p大小的问题只在规模上存在不同。问题很快被递归到二进制低位
  2. 现在我们倒过来,你懂的。按照二进制展开逐位往高位递推(而且,我们在递推过程中逐位生成了a[n],n是二的幂,真是一举两得)。

再给张图片,方便大家理解,推导规模为1101的情况

recurrence


时间效率分析

这个还是依赖于合并和地推的效率的,如果性质1和2的效率是O(1)的,那么,最终效率就是O(lgn),这个效率已经达到了信息量上的线性,单从界的角度已经是最好的了。


实例

逐次平方求幂,移位乘法,等比前n项和等各种都可以用这种方法构造,效率就不用多说了,时间O(lgn),空间O(1)

分类: ACM, Math 标签:

组合数取模总结

2010年10月25日 8 条评论

组合数取模,其实实际用途不大。。好吧,acm老喜欢出这样的题目,因为数据可能很大。。

以下给出第一个约分版本的组合数取模,能够处理当C(n,m) mod s当m不是很大的情况,有很多优化,先给代码吧^^

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int combination_Mod_m (int a, int b, int m)
{
    const int gate = 2000000000;  ///larger gate for more Optimization
    if (b > a) return 0;
    b = (a - b < b) ? a - b : b;
    int d[b], tmp = 1 ,l = 0, h, j, g;
    for (l = 0, j = a - b + 1; a - l >= j; l++)
    {
		d[l] = a - l;
		while(gate / d[l] >= j && a - l != j)
			d[l] *= j, j++;
    }
    for (j = 2, h = b; j <= h; h--)
    {
    	tmp *= h;
        while (gate / tmp >= j && h != j)
        	tmp *= j, j++;
        for (int i = 0; tmp != 1; i++)
        {
            g = gcd (tmp, d[i]);
            d[i] /= g, tmp /= g;
        }
    }
    int result = 1;
    for (int i = 0; i < l; i++)
        d[i] %= m, result *= d[i], result %= m;
    return result;
}

思路很直接——约分,基于这个公式(^n _m) = \frac{n \cdot (n-1) \cdots (n-m+1)}{1 \cdot 2 \cdot (m-1)\cdots m},直接吧分子全部保存在一个数组里,然后用下面的数不断gcd,约去公因子,直到去约的数成为1,即下面所有的数都成为1。注意我加了一个优化——把分子和分母尽可能合并,以减少gcd运行的次数,这是一种贪心的策略。

基于以下的问题:给定若干容器,每个里面存放了一个数,每个有一个容量的上限。规定一次合并操作——把两个容器合并为一个,但是其中保存的数为两个数的乘积。求若干合并之后,容器总数最少。

策略:先排序,然后从最大的数开始,找最小的数和它乘,看有没有溢出,没有就合并这两个数,并且“删掉”最小的,用新的最小的数重复刚才的过程(和选定的那个大的数相乘),否则取次大的,不断进行这个过程,直到到达序列末。不难证明它是最优的。基于这个优化,在m不是很大的时候(一般不超过100),这个算法是接近线性(O(m))的,joj 2606就是这么被我优化到0.00s的^^


第二个版本,使用素数分解约分,这个版本很快,估计应该是亚线性的,但它不能处理n非常大的情况,一般能够处理n在最大1000000左右的情况,并且需要用到素数表。基于以下公式:(^n _m) = \frac{n!}{m!\cdot(n-m)!},先贴代码^^

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
///need prime table,and n < N
int combination_Mod_h (int n, int m, int h)
{
	if(h == 1) return 0;
        int result = 1, cnt = 0, temp;
	for(int i = 1; i < prime[0] && prime[i] <= n; i++)
	{
		temp = n, cnt = 0;
		while(temp)
			temp /= prime[i], cnt += temp;
		temp = n - m;
		while(temp)
			temp /= prime[i], cnt -= temp;
		temp = m;
		while(temp)
			temp /= prime[i], cnt -= temp;
		temp = prime[i];
		while(cnt)
		{
			if(cnt & 1)
                                result *= temp, result %= h;
			temp *= temp, cnt >>= 1, temp %= h;
		}
		if(result == 0) return 0;
	}
	return result;
}

这个基本没啥好说的,都比较直接,有一点要提一下:m!中素因子p的数目,求法:m不断除p,除的结果不断加到一个变量保存,当m为0的时候,那个保存的结果即所求(第9,12,15行都是这个过程),如果你大脑能作一个数域的映射的话,这个是显然的,当然你也可以技巧性的通过公式变形得到,具体就不解释了^^


接下去介绍一个线性O(m)的方法,简单的说,就是把模数分解,然后同余方程组合并,这里给出模素数p的情况和模素数的乘方p^k的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
///O(m) p is prime number, p*p < INT_MAX
int combination_Mod_p (int n, int m, int p)
{
    if (m > n) return 0;
    m = (n - m < m) ? n - m : m;
    int a = 1, b = 1, x, y, pcnt = 0;
    for (int i = 1; i <= m; i++)
    {
        a *= n - i + 1, b *= i;
        while (a % p == 0) a /= p, pcnt++;
        while (b % p == 0) b /= p, pcnt--;
        b %= p, a %= p;
    }
	if (pcnt) return 0;
    extended_gcd(b, p, x, y);
    if (x < 0) x += p;
    x *= a, x %= p;
    return x;
}
///O(m)
///p is prime and p^k < INT_MAX
int combination_Mod_pk (int n, int m, int p, int k)
{
    if (m > n) return 0;
    m = (n - m < m) ? n - m : m;
    int a = 1, b = 1, x, y, pa = 1, pb = 1, pcnt = 0;
    int q = power(p, k);
    for (int i = 1; i <= m; i++)
    {
        a *= n - i + 1, b *= i;
        while(a % p == 0) pa *= p, a /= p;
        while(b % p == 0) pb *= p, b /= p;
        while (pa % q == 0) pa /= q, pcnt++;
        while (pb % q == 0) pb /= q, pcnt--;
        b %= q, a %= q;
    }
    if(pa < pb) pcnt--, pa *= q;
    pa /= pb, a *= pa, a %= q;
    if (pcnt) return 0;
    extended_gcd(b, q, x, y);
    if (x < 0) x += q;
    x *= a, x %= q;
    return x;
}
/**
  * Extended Euclid's Algorithm
  * ax+by=gcd(a,b);
  **/
int extended_gcd (int a, int b, int &x, int &y)
{
    if (!b)
        return x = 1, y = 0, a;
    int ret = extended_gcd (b, a % b, x, y), tmp = x;
    x = y, y = tmp - (a / b) * y;
    return ret;
}
int power (int x, int k)
{
    int result = 1;
    while (k)
    {
        if (k & 1)
            result *= x;
        x *= x, k >>= 1;
    }
    return result;
}

这个原理不难,就是分子分母分别对p取模,然后求逆。。当然,需要注意可能的退化情况。
假设分母为b,分子是a,则最后需要求a \equiv b \cdot x\, (mod \, p)的x值。
注意,如果b和p有大于1的公因子,会有多解,这个就是退化情况,所有在过程中需要不断抽取b中p的因子,抽取之后再不断取模,这样就可以了。
当然,因为a中相应的要约去b中被抽取的因子(这些都被一些临时变量保存),所以a也要抽取p的因子(需要注意的是,乘了再约和约了再乘是不一样的,前者会导致失真)


当然,上面是通过同余方程组,不用同余方程组的话,可以得到一个m*lgn的算法,不断抽取模数m的因子^^,也给出代码吧,因为没有同余方程组,实现稍简单一些,如果要求不是太高,也可以用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/// s*s < INT_MAX , and s can be any positive number
int combination_Mod_s (int n, int m, int s)
{
    if (m > n) return 0;
    m = (n - m < m) ? n - m : m;
    int a = 1, b = 1, x, y, g, pcnt = 0;
    int pa = 1, pb = 1; ///To be sure that pa and pb is not overflow
    for (int i = 1; i <= m; i++)
    {
        a *= n - i + 1, b *= i;
        while( (g = gcd(a, s)) > 1) pa *= g, a /= g;
        while( (g = gcd(b, s)) > 1) pb *= g, b /= g;
        g = gcd(pa, pb), pa /= g, pb /= g;
        while (pa % s == 0) pa /= s, pcnt++;
        while (pb % s == 0) pb /= s, pcnt--;
        b %= s, a %= s;
    }
    while(pcnt) pcnt--, pa *= s;
    pa /= pb, pa %= s, a *= pa, a %= s;
    if (pcnt) return 0;
    extended_gcd(b, s, x, y);
    if (x < 0) x += s;
    x *= a, x %= s;
    return x;
}

最后,是一个我还没完全参透的定理:Lucas定理,具体可以参看wiki,目前还不太清楚能不能处理p的乘方(要是能就好了^^),慢慢研究吧

1
2
3
4
5
6
7
8
9
10
11
/**
  * Lucas' theorem---combination number mod prime p
  * Be sure that p*p<INT_MAX
  **/
int lucas (int m, int n, int p)
{
    int result = 1;
    while (m && n && result)
        result *= combination_Mod_p (m % p, n % p, p), result %= p, m /= p, n /= p;
    return result;
}
分类: ACM, Math 标签: , ,