雅各比符号

最近,威廉·哈特提出了以下问题:

你能在30行中做什么
是C代码还是C++代码?

威廉的意图是一个数学编码挑战。他解释道:其思想是按照一组严格的规则实现数学运算:

威廉已经建立了一个github如果您开发了这样的东西,您可以为其提供帮助的存储库。

我真的很喜欢威廉的想法。对我个人来说,不幸的是,事实给这一点蒙上了阴影我既不精通C语言,也不太欣赏C语言。如果威廉在这将是我学习D的借口。

然而,我想学习威廉的框架所以我决定贡献一个数学hello-world函数:雅各比符号。为了让它不那么无聊,我加入了一个小转折:确实如此不使用通常的余数函数,而是使用“最小绝对余数”功能(拉雷姆(在下面的代码中)。

至少在一个方面,我不符合威廉预期的习惯用法:我使用单行函数代替宏我使用布尔变量:这表示我对C处理这些事情的方式的保留。

 

2://版权所有(c)2013,Peter Luschny
三://保留所有权利。
4://许可证:CC BY-SA 3.0
5://Attribution-ShareAlike 3.0未导出
6:
7: 
8:#包括<iostream>
9:#包括<cassert>
10:#包括<stdbool.h>
11:使用 命名空间标准;
12: 
13:#包括“ZZp.hpp”
14: 
15:整数雷姆(常数ZZ_t&a,无符号b);
16:ZZ_t警报(常数ZZ_t&a,常数ZZ_t/b);
17:整数雅可比符号(常数ZZ_t&a,常数ZZ_t&b);
18:布尔jacobi_要求(常数ZZ_t&a,常数ZZ_t&b);
19:空隙test_jacobi(randt状态);
20: 
21:布尔为零(常数ZZ_t(&a){返回a.大小==0;}
22:布尔正(_P)(常数ZZ_t(&a){返回a.size>0;}
23:布尔是否定的(_N)(常数ZZ_t(&a){返回a.尺寸<0;}
24:布尔连接(_N)(常数ZZ_t(&a){返回a.尺寸>=0;}
25: 
26:布尔添加(_O)(常数ZZ_t(&a){返回零(a)?0:(a.n[0]&1)==1;}
27:布尔已存在(_E)(常数ZZ_t(&a){返回零(a)?1:!添加(a);}
28: 
29:空隙减半(ZZ_t&a){ZZ_div_2exp(&a,&a,1);}
30:整数雷姆(常数ZZ_t&a,无符号b){返回a.n[0]%b;}
31: 
32:/*最小绝对余数*/
33:ZZ_t警报(常数ZZ_t&a,常数ZZ_t(&b)
34:{
35:ZZ_t q,r,h;
36: 
37:zz_div_2exp(&h,&a,1);
38:zz_divrm(&q,-r,/b,/a);
39: 
40:    如果(zz_cmp(&h,&r)<0)
41:{
42:zz_sub(&r,&r,&a);
43:}
44:    返回r;
45:}
46: 
47:布尔jacobi_要求(常数ZZ_t&a,常数ZZ_t(&b){
48:    返回is_odd(b)&&is_nonnegative(a)&&is_ppositive(b);
49:}
50: 
51:整数雅可比符号(常数ZZ_t&A、,常数ZZ_t&B)
52:{
53:    //断言(jacobi_requires(A,B));
54: 
55:ZZ_t a=a,b=b,q,t,one=1;
56:    整数j=1,r;
57:    布尔remb4、remb8;
58: 
59:    如果(为零(a))
60:        返回(1==b)?1:0;
61: 
62:    虽然(!为零(a)){
63: 
64:remb4=rem(b,4)==3;
65: 
66:        如果(是否定的(a)){
67:a=-a;
68:            如果(remb4)j=-j;
69:}
70: 
71:r=雷姆(b,8);
72:remb8=(r==3)| |(r==5);
第73页: 
74:        虽然(七(a)){
75:减半(a);
76:            如果(remb8)j=-j;
77:}
78: 
79:        如果(雷姆(a,4)==3){
80:            如果(remb4)j=-j;
81:}
82: 
83:t=a;
84:a=larem(a,b);
85:b=t;
第86页:}
87: 
88:    返回zz_cmp(&one,&b)<0?0:j;
89:}
90: 
91:/*(a、b、j)示例跟踪:
92:(109981, 737777, 1)
93:(-32090, 109981, 1)
94:(-2334, 16045, -1)
95:(-293, 1167, 1)
96条:(-5, 293, -1)
97:(-2, 5, -1)
98:1
99:*/
100: 
101:空隙test_jacobi(randt状态)
102:{
103:ZZ_ta,b;
104:    整数r1,r2,i,j=0;
105: 
106:cout公司<<“jacobi_symbol…”;
107: 
108:    静止的 整数JS[]={-1,-1,1,1,-1,1.1,-1,1,-1,-1,1,1,-1,1,-1,1};
第109页: 
110:    对于(i=0;i<10;i++)
111:{
112:        {
113:randbits(a,state,n_randint(state,500));
114:randbits(b,state,n_randint(state,500));
115: 
116:}虽然(!jacobi_要求(a,b));
117: 
118:r1=雅可比符号(a,b);
119:r2=JS[j++];
120:断言(r1==r2);
第121页: 
122:        如果(添加(a))
123:{
124页:r1=雅可比符号(b,a);
125:r2=JS[j++];
126:断言(r1==r2);
127:}
128:}
129:cout公司<<“通过”<<endl;
130:}
131: 
132:整数main()
133:{
134:rand_t状态;
135:randinit(状态);
136:test_jacobi(状态);
137: 
138:cout<<结束<<“完成!”<<endl;
139:立方英寸。得到();
140: 
141:    返回0;
142:}

C与C++

函数应该在什么时候用C实现,什么时候用C++实现?

其中一条规则规定:所有通用算法都应在C++中实现,其中底层C模块存在时,应在可行的情况下将功能下推,尤其是如果它们对模块中的其他C算法有用。

William Hart进一步解释道:“当然拉雷姆需要达到C级。例如,它可以用于gcd公司,它已经处于C级别。当然雅各比可能是在C++级别,因为它在许多低级算法中没有功能。"

好消息是William现在在C级实现了这两个函数我们可以比较这两种不同的表示。

1:/*
2:版权所有(c)2013,William Hart
三:保留所有权利。
4:
5:以源代码和二进制形式重新分发和使用,可以修改也可以不修改,
6:如果满足以下条件,则允许:
7:
8:1.重新分发源代码必须保留上述版权声明
9:条件列表和以下免责声明。
10:
11:2.二进制形式的重新分发必须复制上述版权声明,
12:此条件列表和文档中的以下免责声明和/或
13:分发时提供的其他材料。
14:
15:本软件由版权持有人和贡献者“按原样”提供
16:任何明示或暗示的保证,包括但不限于暗示的
17:不保证适销性和特定用途的适用性。
18日:在任何情况下,版权持有人或贡献者均不对任何董事、,
19:间接、偶然、特殊、惩罚性或后果性损害(包括,
20:包括但不限于采购替代货物或服务;失去使用,
21:数据或利润;或业务中断)无论是什么原因,根据任何理论
22:责任,无论是合同责任、严格责任还是侵权责任(包括疏忽
23:(或其他方式)以任何方式因使用本软件而产生的,即使已通知
24:此类损坏的可能性。
25:*/
26: 
27:/*w.b.哈特*/
28:空隙zz_larem(zz_ptr r、zz_srcptr a、zz_srkptr b)
29:{
30:    长的asize=ABS(a->尺寸);
31日:    长的bsize=ABS(b->尺寸);
32:    长的rsize=bsize;
33:    长的qsize=asize-bsize+1;
34:zz_t q,h,t;
35: 
36:zz_init(t);
37:zz_copy(t,a);
38: 
39:zz_init(q);
40:zz_init(h);
41:zz_fit(h,asize);
42: 
43:zz_div_2exp(h,a,1);
第44页:    如果(a->大小<0)
45:zz_add_1(h,h,1);
46: 
47:    如果(asize>=b大小){
48:zz_fit(q,qsize);
49: 
50:nn_divrem(q->n,t->n,asize,b->n,bsize);
51: 
52:rsize=nn_normalise(t->n,rsize);
53: 
54:t->尺寸=a->尺寸>=0?rsize:-rsize;
55: 
56:        如果((a->大小^b->大小)<0&&t->大小!=0&&zz_cmpabs(t,h)>=0)
57:zz添加(t,t,b);
58:        其他的 如果(zz_cmpabs(t,h)>=0)
59:zz_sub(t,t,b);
60:}
61: 
62:zz_swap(t,r);
63:zz_清除(t);
64位:zz_清除(h);
65:zz_清除(q);
66:}
67: 
第68页:/*w.b.哈特*/
69:整数zz _ jacobi(zz _ srcptr A,zz _ rscptr B)
70:{
71:    整数j=1,res,r8,remb4,remb8;
72:zz_ta、b;
73:    
74: 
75:    如果(zz_is_zero(A))
76:        返回zz_equal_1(B,1);
77: 
78:zz_init(a);
79页:zz_init(b);
80: 
81:zz_copy(a,a);
82:zz_copy(b,b);
83: 
84:    虽然(!zz_is_zero(a)){
85:remb4=(b->n[0]%4)==3;
86: 
87:        如果(a->大小<0){
88:zz_neg(a,a);
第89页:j=remb4-j:j;
90:}
91条: 
92:remb8=((r8=(b->n[0]%8))==3||r8==5);
93: 
94:        虽然((a->n[0]%2)==0){
95:zz_div_2exp(a,a,1);
96:            如果(remb8)j=-j;
97:}
98: 
99:j=(a->n[0]%4)==3&remb4-j:j;
100: 
101:zz_larem(b,b,a);
102:zz_swap(a,b);
103:}
104: 
105:res=zz_cmp_1(b,1)>0?0:j;
106: 
107:zz_清除(a);
108:zz_清除(b);
109: 
110:    返回物件;
111:}

我们能看到性能差异吗?

当我们将C与C++版本进行比较时,复杂性的增加是显而易见的。它的回报是效率?让我们来看一个小基准。

     MS-VC GNU-GCC(最小GW) GNU-GCC(Ubuntu)
雅各比C++ 拉雷姆C++ 0.510 0.754 0.276
雅各比C++ 拉雷姆C 0.316 0.420 0.186
雅各比C 拉雷姆C 0.312 0.417 0.184

公平警告:理解cflags的人可能会得到不同的结果。无论如何,很明显威廉在实施larem方面做得很好。另一方面,它也表明Jacobi C++和Jacobi C之间没有太大的区别。

使用的基准是:

1:#包括<iostream>
2:#包括<chrono>
三:#包括“ZZ.h”
4: 
5:使用 命名空间标准;
6: 
7:整数雅可比符号(常数ZZ_t&a,常数ZZ_t&b);
8:整数雅可比符号混合(常数ZZ_t&A、,常数ZZ_t&B);
9:布尔jacobi_要求(常数ZZ_ t和a,常数ZZ_t&b);
10: 
11:整数main()
12日:{
13:ZZ_ta,b;
14:randt状态;
15:    长的i、 代表;
16:chrono::time_point<chrono::system_clock>开始,结束;
17: 
18:开始=计时::system_clock::now();
19: 
20:    对于(代表=0;代表<5;代表++)
21:{
22:randinit(状态);
23:        对于(i=0;i<10000;i++)
24小时:{
25:            {
26:randbits(a,state,n_randint(state,500));
27:randbits(b,state,n_randint(state,500));
28:}虽然(!jacobi_要求(a,b));
29: 
30:            //雅可比符号(a,b);
31:            //雅可比符号混合(a,b);
32:雅各比(a,b);
33:}
34:}
35: 
36:end=计时::system_clock::now();
37:时间::持续时间<双重的>elapsed_seconds=结束-开始;
38: 
39:高级定制<<“已用时间:”<<elapsed_seconds.count()/rep
40:<<“秒。”<<endl;
41: 
42:cout<<结束<<“完成!”<<endl;
43:立方英寸。得到();
44: 
45:    返回0;
46:}

当然,这只是基准的第一部分:C与C++。也许更重要的是查看性能差异雷姆对雷姆Jacobi通常使用正余数;实际上,我还没有看到使用最小绝对余数的实现。切换到拉雷姆? 我和Sage写了第二个测试程序并计算了数字对余数函数的调用。

counter_larem=0计数器_ rem=0对于[ZZ.random_element(72)中的r,对于范围(200)中的_:对于[ZZ.random_element(72)中的s,对于范围(200)中的_:a=ZZ.random_element(10^r)b=ZZ.random_element(10^s)如果is_even(b):b+=1J=雅可比_雷姆(a,b)L=雅可比_面积(a,b)j=雅可比符号(a,b)如果J<>J或L<>J:打印“ERROR”,a,b,J,L,J打印“larem”,counter_larem打印“rem”,counter_rem打印(counter_larem/counter_rem).n()

结果很有趣:

larem-call=1013304rem-call=1256957larem/rem=0.806

它表明,如果我们使用最小绝对余数,我们确实可以节省相当多的除法步骤而不是正余数。

总之,我们可以得出结论,很难用两个30行C函数实现更高效的雅可比符号。

下一步:is_prime(n)。

“区分质数和合成数的问题是众所周知的在算术中最重要和最有用的。此外,科学本身的尊严似乎要求一切可能的手段探索解决一个如此优雅和著名的问题。"(《算术研究中的高斯》(1801))

令人兴奋的是,我们现在只剩下30行代码就可以解决这个问题了!

1:定义质数(n):
2: 
三:    如果n≤1:返回 
4:    如果n≤3:返回 真的
5:    如果事件(_E):返回 
6: 
7:k=(n-1)//2
8:l=地板(2*log(n)^2)
9:    对于在里面(2分钟(n-1,l)):
10:j=雅可比符号(a,n)
11:        如果j==0:返回 
12:m=功率模态(a,k,n)
13:        如果j==1:
14:            如果m-1≤0:返回 
15:        其他的:
16:            如果m+1<>n:返回     
17:     
18:    返回 真的
19:    
20:    
21:定义功率模态(a,e,n):
22: 
23:   如果e==0:返回1
24:res=1
25: 
26:   对于b条在里面反转(e.bits()):
27:res=(res*res)%n
28日:       如果b==1:res=(res*a)%n
29:   
30:   返回物件

诚然,这些是30行Python/Sage代码,但当然也可以用2x30行C代码编写符合威廉的挑战。

请注意,这是概率素数检验;假设ERH为_prime(n)返回真的当且仅当n个是质数。因此,即使是NSA也无法在此算法中建立后门。科学的尊严现在需要证明扩展的黎曼假设。

它也非常有效,因为它可以计算O((logn)^5)中的答案。事实上,它比一些流行的计算机代数系统中的类似实现(例如交响乐团)哪一个先检查n个相对来说是最好的。但这是可有可无的,因为雅各比符号返回0如果不是的话。

回想一下,我们将雅可比符号的计算基于最小绝对余数(larem),而不是最少的正余数,因为这样做的频率更高。同样,它也会更加一致地实施power_larem而不是power_mod。实际上,唯一的区别是添加了一行“ifn//2<res:return res-n”在powermod最终回归之前。然后在is_prime中,将行“if j==1:if m-1<>0:return false”和“else:if m+1<>n:return false”塌陷到更直观的“ifj<>m:return false”。显然,雅各比设计了他的标志记住最少的绝对余数。

最后注意,l=地板(0.96090602783640285*n.nbits()^2)可以代替l=floor(2*log(n)^2),以避免计算log(n)。

参考:E.巴赫和J.沙利特:算法数论。”拉雷姆工具公式(4.9)第79页,‘jacobi_symbol’在第113页,‘is_prime’是确定性的Solovay-Strassen测试,第284页。