如果要打表存储某个素数的原根的所有的相关的离散对数,而这个素数又比较大,那么可以通过哈希表和逆元结合来存储,g^z =g^km+r,g为原根,r=z%m; g^m * x =1%p; 那么如果y=g^z,
yx=g^e,
yx=g^(z-m) (g^m) x%p = ( g^e)%p; 由于(g^m) x=1%p;
那么 z=m+e;
那么根据这个性质,就可以建立这样一个哈希表,假设素数大小约为10000,那么,可以建一个100长度的哈希表,所挂链表的长度也为100,那么上面的m的值就是100,建立起1到100这一百个离散对数与g的次方的哈希表后,在查找y对应值的过程中,如果找不到,就乘以g^m的逆元后继续找,若找到后,相应的离散对数值加上m,这样的话就可以吧10000长度的表长缩短为几百,并且查询过程最坏也就是查询100次,由于是对模素数的值进行哈希,所得到的的哈希表的冲突较小,这样用时间换空间,就可以取得不错的效果,当素数的大小达到1e7或1e8时,就能大大降低空间复杂度。
参考的网上的代码。
struct HASH_TABLE {
ll top, gm, inv, m, pri;
ll hash_table[hash_size], mp[hash_size];
ll head[hash_size], nxt[hash_size];
void init() {
top = 1;
memset(hash_table, 0, sizeof hash_table);
memset(nxt, 0, sizeof nxt);
memset(head, -1, sizeof head);
}
void init_table(ll g, ll p) {
m = maxn; ll e = 1;
for(ll i = 0; i < m; i++) {
ll id = e % hash_size;
if(~head[id]) nxt[top] = head[id];
head[id] = top;
hash_table[top] = e; mp[top] = i;
e = e * g % p; top++;
}
gm = e; pri = p;
inv = exgcd(gm, p, 1);
}
ll query(ll x) {
for(ll i = 0; i <= pri / m; i++) {
ll id = x % hash_size;
ll hd = head[id];
if(~hd) for( ; hd; hd = nxt[hd]) {
if(hash_table[hd] == x) return i * m + mp[hd];
}
x = x * inv % pri;
}
return -1;
}
} hs;
卢卡斯定理由于复杂度是o(p),卡点经常是素数太大,询问次数太多,或者是一个数可以分解成多个大素数相乘,从而使直接卢卡斯的时间复杂度较高,这时候就要预处理以及运用扩展卢卡斯定理的一些知识,但不是对扩展卢卡斯定理的模板的直接套用。
由于原根有一条较强的性质g的1到fi(p)次方对应1到p-1,所以在涉及到有关模p情况下的(x)^y的这种形式,x就可以用g的相应次方来替换掉,从而得到进一步的结果,也方便运用欧拉函数,gcd进行一些推导。
对于数学渐渐的有一些感觉了,虽然在做一些题目的过程中经常被一些小的细节卡很长时间,细节多的话一两天都不能完全搞定。很难从一种形式过度到另一种形式,但理解起来没有那么费劲了,细节上的过度点很重要,比如说 x|m*p,那么p就是x/gcd(m,x)的倍数,这样就从整除引入了gcd,其实就是把x和m的共同因子去掉,又由于m/gcd(m,x)与x/gcd(m,x)互素,那么x中剩下的因子就只能由p来包含。本质上还是因数与因数之间的关系的另一种表现形式, 其实,整个整数集合的各种关系都是以素数,因数为本质的各种表现形式。
接触了一些有关莫比乌斯的一些东西,其实也就是把之前的知识点全部融入到了一个新的体系中,衔接更紧密了,如果把莫比乌斯涉及的推导以及之前的各种知识点慢慢的都关联到一起,那么一些细节过度点的寻找衔接就会有所突破。