square_root_modulo_n_tonelli-shanks.pl 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. #!/usr/bin/perl
  2. # Daniel "Trizen" Șuteu
  3. # Date: 30 October 2017
  4. # https://github.com/trizen
  5. # Find all the solutions to the congruence equation:
  6. # x^2 = a (mod n)
  7. # Defined for any values of `a` and `n` for which `kronecker(a, n) = 1`.
  8. # When `kronecker(a, n) != 1`, for example:
  9. #
  10. # a = 472
  11. # n = 972
  12. #
  13. # which represents:
  14. # x^2 = 472 (mod 972)
  15. #
  16. # this algorithm may fail find all the solutions, although there exist four solutions in this case:
  17. # x = {38, 448, 524, 934}
  18. # Code inspired from:
  19. # https://github.com/Magtheridon96/Square-Root-Modulo-N
  20. use 5.020;
  21. use warnings;
  22. use experimental qw(signatures);
  23. use List::Util qw(uniq);
  24. use ntheory qw(factor_exp is_prime chinese forsetproduct);
  25. use Math::AnyNum qw(:overload kronecker powmod invmod valuation ipow);
  26. sub tonelli_shanks ($n, $p) {
  27. my $q = $p - 1;
  28. my $s = valuation($q, 2);
  29. $s == 1
  30. and return powmod($n, ($p + 1) >> 2, $p);
  31. $q >>= $s;
  32. my $z = 1;
  33. for (my $i = 2 ; $i < $p ; ++$i) {
  34. if (kronecker($i, $p) == -1) {
  35. $z = $i;
  36. last;
  37. }
  38. }
  39. my $c = powmod($z, $q, $p);
  40. my $r = powmod($n, ($q + 1) >> 1, $p);
  41. my $t = powmod($n, $q, $p);
  42. while (($t - 1) % $p != 0) {
  43. my $k = 1;
  44. my $v = $t * $t % $p;
  45. for (my $i = 1 ; $i < $s ; ++$i) {
  46. if (($v - 1) % $p == 0) {
  47. $k = powmod($c, 1 << ($s - $i - 1), $p);
  48. $s = $i;
  49. last;
  50. }
  51. $v = $v * $v % $p;
  52. }
  53. $r = $r * $k % $p;
  54. $c = $k * $k % $p;
  55. $t = $t * $c % $p;
  56. }
  57. return $r;
  58. }
  59. sub sqrt_mod_n ($a, $n) {
  60. $a %= $n;
  61. return 0 if ($a == 0);
  62. if (($n & ($n - 1)) == 0) { # n is a power of 2
  63. if ($a % 8 == 1) {
  64. my $k = valuation($n, 2);
  65. $k == 1 and return (1);
  66. $k == 2 and return (1, 3);
  67. $k == 3 and return (1, 3, 5, 7);
  68. if ($a == 1) {
  69. return (1, ($n >> 1) - 1, ($n >> 1) + 1, $n - 1);
  70. }
  71. my @roots;
  72. foreach my $s (sqrt_mod_n($a, $n >> 1)) {
  73. my $i = ((($s * $s - $a) >> ($k - 1)) % 2);
  74. my $r = ($s + ($i << ($k - 2)));
  75. push(@roots, $r, $n - $r);
  76. }
  77. return uniq(@roots);
  78. }
  79. return;
  80. }
  81. if (is_prime($n)) { # n is a prime
  82. kronecker($a, $n) == 1 or return;
  83. my $r = tonelli_shanks($a, $n);
  84. return ($r, $n - $r);
  85. }
  86. my @pe = factor_exp($n); # factorize `n` into prime powers
  87. if (@pe == 1) { # `n` is an odd prime power
  88. my $p = Math::AnyNum->new($pe[0][0]);
  89. kronecker($a, $p) == 1 or return;
  90. my $r = tonelli_shanks($a, $p);
  91. my ($r1, $r2) = ($r, $n - $r);
  92. my $pk = $p;
  93. my $pi = $p * $p;
  94. for (1 .. $pe[0][1]-1) {
  95. my $x = $r1;
  96. my $y = invmod(2, $pk) * invmod($x, $pk);
  97. $r1 = ($pi + $x - $y * ($x * $x - $a + $pi)) % $pi;
  98. $r2 = ($pi - $r1);
  99. $pk *= $p;
  100. $pi *= $p;
  101. }
  102. return ($r1, $r2);
  103. }
  104. my @chinese;
  105. foreach my $f (@pe) {
  106. my $m = ipow($f->[0], $f->[1]);
  107. my @r = sqrt_mod_n($a, $m);
  108. push @chinese, [map { [$_, $m] } @r];
  109. }
  110. my @roots;
  111. forsetproduct {
  112. push @roots, chinese(@_);
  113. } @chinese;
  114. return uniq(@roots);
  115. }
  116. say join(' ', sqrt_mod_n(993, 2048)); #=> 369 1679 655 1393
  117. say join(' ', sqrt_mod_n(441, 920)); #=> 761 481 209 849 531 251 899 619 301 21 669 389 71 711 439 159
  118. say join(' ', sqrt_mod_n(841, 905)); #=> 391 876 29 514
  119. say join(' ', sqrt_mod_n(289, 992)); #=> 417 513 975 79 913 17 479 575
  120. say join(' ', sqrt_mod_n(472, 972)); #=> 448 524
  121. # The algorithm works for arbitrary large integers
  122. say join(' ', sqrt_mod_n(13**18 * 5**7 - 1, 13**18 * 5**7)); #=> 633398078861605286438568 2308322911594648160422943 6477255756527023177780182 8152180589260066051764557