modular_square_root_all_solutions_cipolla.pl 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. #!/usr/bin/perl
  2. # Find all the solutions to the quadratic congruence:
  3. # x^2 = a (mod n)
  4. # Based on algorithm by Hugo van der Sanden:
  5. # https://github.com/danaj/Math-Prime-Util/pull/55
  6. # See also:
  7. # https://rosettacode.org/wiki/Cipolla's_algorithm
  8. use 5.020;
  9. use strict;
  10. use warnings;
  11. use Test::More tests => 12;
  12. use experimental qw(signatures);
  13. use ntheory qw(factor_exp chinese forsetproduct kronecker);
  14. use Math::AnyNum qw(:overload powmod ipow);
  15. sub cipolla ($n, $p) {
  16. $n %= $p;
  17. return undef if kronecker($n, $p) != 1;
  18. if ($p == 2) {
  19. return ($n & 1);
  20. }
  21. my $w2;
  22. my $a = 0;
  23. $a++ until kronecker(($w2 = ($a * $a - $n) % $p), $p) < 0;
  24. my %r = (x => 1, y => 0);
  25. my %s = (x => $a, y => 1);
  26. my $i = $p + 1;
  27. while (1 <= ($i >>= 1)) {
  28. %r = (
  29. x => (($r{x} * $s{x} + $r{y} * $s{y} * $w2) % $p),
  30. y => (($r{x} * $s{y} + $s{x} * $r{y}) % $p)
  31. )
  32. if ($i & 1);
  33. %s = (
  34. x => (($s{x} * $s{x} + $s{y} * $s{y} * $w2) % $p),
  35. y => (($s{x} * $s{y} + $s{x} * $s{y}) % $p)
  36. );
  37. }
  38. $r{y} ? undef : $r{x};
  39. }
  40. sub sqrtmod_prime_power ($n, $p, $e) { # sqrt(n) modulo a prime power p^e
  41. if ($e == 1) {
  42. return cipolla($n, $p);
  43. }
  44. # t = p^(k-1)
  45. my $t = ipow($p, $e - 1);
  46. # pp = p^k
  47. my $pp = $t * $p;
  48. # n %= p^k
  49. $n %= $pp;
  50. if ($n == 0) {
  51. return 0;
  52. }
  53. if ($p == 2) {
  54. if ($e == 1) {
  55. return (($n & 1) ? 1 : 0);
  56. }
  57. if ($e == 2) {
  58. return (($n % 4 == 1) ? 1 : 0);
  59. }
  60. ($n % 8 == 1) || return;
  61. my $r = __SUB__->($n, $p, $e - 1) // return;
  62. # (((r^2 - n) / 2^(e-1))%2) * 2^(e-2) + r
  63. return ((((($r * $r - $n) >> ($e - 1)) % 2) << ($e - 2)) + $r);
  64. }
  65. my $s = cipolla($n, $p) // return;
  66. # u = (p^k - 2*(p^(k-1)) + 1) / 2
  67. my $u = ($pp - 2 * $t + 1) >> 1;
  68. # sqrtmod(a, p^k) = (powmod(sqrtmod(a, p), p^(k-1), p^k) * powmod(a, u, p^k)) % p^k
  69. (powmod($s, $t, $pp) * powmod($n, $u, $pp)) % $pp;
  70. }
  71. sub sqrtmod_all ($A, $N) {
  72. $A = Math::AnyNum->new("$A");
  73. $N = Math::AnyNum->new("$N");
  74. $N = -$N if ($N < 0);
  75. $N == 0 and return ();
  76. $N == 1 and return (0);
  77. $A = ($A % $N);
  78. my $sqrtmod_pk = sub ($A, $p, $k) {
  79. my $pk = ipow($p, $k);
  80. if ($A % $p == 0) {
  81. if ($A % $pk == 0) {
  82. my $low = ipow($p, $k >> 1);
  83. my $high = ($k & 1) ? ($low * $p) : $low;
  84. return map { $high * $_ } 0 .. $low - 1;
  85. }
  86. my $A2 = $A / $p;
  87. return () if ($A2 % $p != 0);
  88. my $pj = $pk / $p;
  89. return map {
  90. my $q = $_;
  91. map { $q * $p + $_ * $pj } 0 .. $p - 1
  92. } __SUB__->($A2 / $p, $p, $k - 2);
  93. }
  94. my $q = sqrtmod_prime_power($A, $p, $k) // return;
  95. return ($q, $pk - $q) if ($p != 2);
  96. return ($q) if ($k == 1);
  97. return ($q, $pk - $q) if ($k == 2);
  98. my $pj = ipow($p, $k - 1);
  99. my $q2 = ($q * ($pj - 1)) % $pk;
  100. return ($q, $pk - $q, $q2, $pk - $q2);
  101. };
  102. my @congruences;
  103. foreach my $pe (factor_exp($N)) {
  104. my ($p, $k) = @$pe;
  105. my $pk = ipow($p, $k);
  106. push @congruences, [map { [$_, $pk] } $sqrtmod_pk->($A, $p, $k)];
  107. }
  108. my @roots;
  109. forsetproduct {
  110. push @roots, chinese(@_);
  111. } @congruences;
  112. @roots = map { Math::AnyNum->new($_) } @roots;
  113. @roots = grep { ($_ * $_) % $N == $A } @roots;
  114. @roots = sort { $a <=> $b } @roots;
  115. return @roots;
  116. }
  117. my @tests = ([1104, 6630], [2641, 4465], [993, 2048], [472, 972], [441, 920], [841, 905], [289, 992]);
  118. sub bf_sqrtmod ($z, $n) {
  119. grep { ($_ * $_) % $n == $z } 0 .. $n - 1;
  120. #ntheory::allsqrtmod($z, $n);
  121. }
  122. foreach my $t (@tests) {
  123. my @r = sqrtmod_all($t->[0], $t->[1]);
  124. say "x^2 = $t->[0] (mod $t->[1]) = {", join(', ', @r), "}";
  125. die "error1 for (@$t) -- @r" if (@r != grep { ($_ * $_) % $t->[1] == $t->[0] } @r);
  126. die "error2 for (@$t) -- @r" if (join(' ', @r) ne join(' ', bf_sqrtmod($t->[0], $t->[1])));
  127. }
  128. say '';
  129. # The algorithm also works for arbitrary large integers
  130. say join(' ', sqrtmod_all(13**18 * 5**7 - 1, 13**18 * 5**7));
  131. foreach my $n (1 .. 100) {
  132. my $m = int(rand(10000));
  133. my $z = int(rand($m));
  134. my @a1 = sqrtmod_all($z, $m);
  135. my @a2 = bf_sqrtmod($z, $m);
  136. if ("@a1" ne "@a2") {
  137. warn "\nerror for ($z, $m):\n\t(@a1) != (@a2)\n";
  138. }
  139. }
  140. say '';
  141. # Too few solutions for some inputs
  142. say 'x^2 = 1701 (mod 6300) = {' . join(' ', sqrtmod_all(1701, 6300)) . '}';
  143. say 'x^2 = 1701 (mod 6300) = {' . join(', ', bf_sqrtmod(1701, 6300)) . '}';
  144. # No solutions for some inputs (although solutions do exist)
  145. say join(' ', sqrtmod_all(306, 810));
  146. say join(' ', sqrtmod_all(2754, 6561));
  147. say join(' ', sqrtmod_all(17640, 48465));
  148. #<<<
  149. is_deeply([sqrtmod_all(43, 97)], [25, 72]);
  150. is_deeply([sqrtmod_all(472, 972)], [38, 448, 524, 934]);
  151. is_deeply([sqrtmod_all(43, 41 * 97)], [557, 1042, 2935, 3420]);
  152. is_deeply([sqrtmod_all(1104, 6630)], [642, 1152, 1968, 2478, 4152, 4662, 5478, 5988]);
  153. is_deeply([sqrtmod_all(993, 2048)], [369, 655, 1393, 1679]);
  154. is_deeply([sqrtmod_all(441, 920)], [21, 71, 159, 209, 251, 301, 389, 439, 481, 531, 619, 669, 711, 761, 849, 899]);
  155. is_deeply([sqrtmod_all(841, 905)], [29, 391, 514, 876]);
  156. is_deeply([sqrtmod_all(289, 992)], [17, 79, 417, 479, 513, 575, 913, 975]);
  157. is_deeply([sqrtmod_all(306, 810)], [66, 96, 174, 204, 336, 366, 444, 474, 606, 636, 714, 744]);
  158. is_deeply([sqrtmod_all(2754, 6561)], [126, 603, 855, 1332, 1584, 2061, 2313, 2790, 3042, 3519, 3771, 4248, 4500, 4977, 5229, 5706, 5958, 6435]);
  159. is_deeply([sqrtmod_all(17640, 48465)], [2865, 7905, 8250, 13290, 19020, 24060, 24405, 29445, 35175, 40215, 40560, 45600]);
  160. #>>>
  161. is_deeply([sqrtmod_all(-1, 13**18 * 5**7)],
  162. [633398078861605286438568, 2308322911594648160422943, 6477255756527023177780182, 8152180589260066051764557]);