modular_square_root_all_solutions.pl 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. #!/usr/bin/perl
  2. # Find all 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. use 5.020;
  7. use strict;
  8. use warnings;
  9. use Test::More tests => 11;
  10. use experimental qw(signatures);
  11. use Math::AnyNum qw(:overload ipow);
  12. use ntheory qw(factor_exp sqrtmod forsetproduct chinese);
  13. sub sqrtmod_all ($A, $N) {
  14. $A = Math::AnyNum->new("$A");
  15. $N = Math::AnyNum->new("$N");
  16. $N = -$N if ($N < 0);
  17. $N == 0 and return ();
  18. $N == 1 and return (0);
  19. $A = ($A % $N);
  20. my $sqrtmod_pk = sub ($A, $p, $k) {
  21. my $pk = ipow($p, $k);
  22. if ($A % $p == 0) {
  23. if ($A % $pk == 0) {
  24. my $low = ipow($p, $k >> 1);
  25. my $high = ($k & 1) ? ($low * $p) : $low;
  26. return map { $high * $_ } 0 .. $low - 1;
  27. }
  28. my $A2 = $A / $p;
  29. return () if ($A2 % $p != 0);
  30. my $pj = $pk / $p;
  31. return map {
  32. my $q = $_;
  33. map { $q * $p + $_ * $pj } 0 .. $p - 1
  34. } __SUB__->($A2 / $p, $p, $k - 2);
  35. }
  36. my $q = sqrtmod($A, $pk) // eval {
  37. require Math::Sidef;
  38. Math::Sidef::sqrtmod($A, $pk);
  39. } || return;
  40. return ($q, $pk - $q) if ($p != 2);
  41. return ($q) if ($k == 1);
  42. return ($q, $pk - $q) if ($k == 2);
  43. my $pj = ipow($p, $k - 1);
  44. my $q2 = ($q * ($pj - 1)) % $pk;
  45. return ($q, $pk - $q, $q2, $pk - $q2);
  46. };
  47. my @congruences;
  48. foreach my $pe (factor_exp($N)) {
  49. my ($p, $k) = @$pe;
  50. my $pk = ipow($p, $k);
  51. push @congruences, [map { [$_, $pk] } $sqrtmod_pk->($A, $p, $k)];
  52. }
  53. my @roots;
  54. forsetproduct {
  55. push @roots, chinese(@_);
  56. } @congruences;
  57. @roots = map { Math::AnyNum->new($_) } @roots;
  58. @roots = grep { ($_ * $_) % $N == $A } @roots;
  59. @roots = sort { $a <=> $b } @roots;
  60. return @roots;
  61. }
  62. #<<<
  63. is_deeply([sqrtmod_all(43, 97)], [25, 72]);
  64. is_deeply([sqrtmod_all(472, 972)], [38, 448, 524, 934]);
  65. is_deeply([sqrtmod_all(43, 41 * 97)], [557, 1042, 2935, 3420]);
  66. is_deeply([sqrtmod_all(1104, 6630)], [642, 1152, 1968, 2478, 4152, 4662, 5478, 5988]);
  67. is_deeply([sqrtmod_all(993, 2048)], [369, 655, 1393, 1679]);
  68. is_deeply([sqrtmod_all(441, 920)], [21, 71, 159, 209, 251, 301, 389, 439, 481, 531, 619, 669, 711, 761, 849, 899]);
  69. is_deeply([sqrtmod_all(841, 905)], [29, 391, 514, 876]);
  70. is_deeply([sqrtmod_all(289, 992)], [17, 79, 417, 479, 513, 575, 913, 975]);
  71. is_deeply([sqrtmod_all(306, 810)], [66, 96, 174, 204, 336, 366, 444, 474, 606, 636, 714, 744]);
  72. is_deeply([sqrtmod_all(2754, 6561)], [126, 603, 855, 1332, 1584, 2061, 2313, 2790, 3042, 3519, 3771, 4248, 4500, 4977, 5229, 5706, 5958, 6435]);
  73. is_deeply([sqrtmod_all(17640, 48465)], [2865, 7905, 8250, 13290, 19020, 24060, 24405, 29445, 35175, 40215, 40560, 45600]);
  74. #>>>
  75. say join', ', sqrtmod_all(-1, 13**18 * 5**7); # 633398078861605286438568, 2308322911594648160422943, 6477255756527023177780182, 8152180589260066051764557