Skip to content

Commit cd1ee30

Browse files
authored
Add Subresultant algorithm (#264)
* Add Subresultant algorithm * Fix format * Exclude subres for gcd
1 parent edc74eb commit cd1ee30

File tree

3 files changed

+157
-27
lines changed

3 files changed

+157
-27
lines changed

src/division.jl

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,13 @@ function MA.operate!(
231231
return MA.buffered_operate!(nothing, op, f, g, algo)
232232
end
233233

234+
# TODO As suggested in [Knu14, Algorithm R, p. 426] (univariate case only), if
235+
# `deg(f) = n` and `deg(g) = m`, during the first `n - m - t` steps, the
236+
# coefficient of the `t`th power will simply be multiplied by the leading
237+
# coefficient of `g` so we could speed up by multiplying this coefficient
238+
# to the power `n - m - t` using `Base.power_by_squaring`.
239+
# However, since there might be missing terms, so we don't know in advance
240+
# the needed power but we could keep track of it.
234241
function MA.buffered_operate!(
235242
buffer,
236243
op::Union{typeof(rem),typeof(pseudo_rem)},
@@ -240,8 +247,9 @@ function MA.buffered_operate!(
240247
)
241248
ltg = leading_term(g)
242249
ltf = leading_term(f)
250+
# This only makes sense in the univariate case but it's only used for univariate gcd anyway
251+
skipped_divisions = maxdegree(f) - maxdegree(g) + 1
243252
MA.operate!(remove_leading_term, g)
244-
not_divided_terms = nothing
245253
while !iszero(f)
246254
if isapproxzero(ltf) # TODO `, kwargs...)`
247255
MA.operate!(remove_leading_term, f)
@@ -253,26 +261,24 @@ function MA.buffered_operate!(
253261
if monomial(ltg) > monomial(ltf)
254262
break
255263
end
256-
if isnothing(not_divided_terms)
257-
not_divided_terms = term_type(f)
258-
end
259-
push!(not_divided_terms, ltf)
260264
MA.operate!(remove_leading_term, f)
261265
else
262266
MA.operate!(remove_leading_term, f)
263267
t = _prepare_s_poly!(op, f, ltf, ltg)
268+
skipped_divisions -= 1
264269
MA.buffered_operate!(buffer, MA.sub_mul, f, t, g)
265270
end
266-
if op === pseudo_rem && algo.primitive_rem
271+
if op === pseudo_rem && _primitive_rem(algo)
267272
f = primitive_part(f, algo, MA.IsMutable())::typeof(f)
268273
end
269-
if algo.skip_last && maxdegree(f) == maxdegree(g)
274+
if _skip_last(algo) && maxdegree(f) == maxdegree(g)
270275
break
271276
end
272277
ltf = leading_term(f)
273278
end
274279
# Add it back as we cannot modify `g`
275280
MA.operate!(unsafe_restore_leading_term, g, ltg)
281+
_set_skipped_divisions!(algo, skipped_divisions)
276282
return f
277283
end
278284

src/gcd.jl

Lines changed: 131 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ Algorithm computing the greatest common divisor of univariate polynomials using
2020
the Euclidean algorithm generalized for polynomials with coefficients over a
2121
a unique factorization domain, see [Knu14, Algorithm E, p. 426-427].
2222
23+
If `primitive_rem` is `true`, the intermediate remainders produced in the
24+
polynomial division are made primitive. If `primitive_part` is set to `false`,
25+
only the resuting remainder is made primitive (the intermediate remainders
26+
of the generalized Euclidean algorithm still need to be made primitive).
27+
2328
[Knu14] Knuth, D.E., 2014.
2429
*Art of computer programming, volume 2: Seminumerical algorithms.*
2530
Addison-Wesley Professional. Third edition.
@@ -35,17 +40,41 @@ struct GeneralizedEuclideanAlgorithm <: AbstractUnivariateGCDAlgorithm
3540
end
3641
end
3742

43+
_primitive_rem(algo::GeneralizedEuclideanAlgorithm) = algo.primitive_rem
44+
_skip_last(algo::GeneralizedEuclideanAlgorithm) = algo.skip_last
45+
function _set_skipped_divisions!(::GeneralizedEuclideanAlgorithm, ::Int) end
46+
3847
"""
39-
struct SubresultantAlgorithm <: AbstractUnivariateGCDAlgorithm end
48+
mutable struct SubresultantAlgorithm <: AbstractUnivariateGCDAlgorithm
49+
skipped_divisions::Int
50+
end
4051
4152
Algorithm computing the greatest common divisor of univariate polynomials using
4253
the Subresultant algorithm, see [Knu14, Algorithm C, p. 428-429].
4354
55+
The division by `g*h^δ` in the algorithm only works if the iteration of
56+
[Knu14, Algorithm R, p. 426] is carried out even when the divided polynomial
57+
has a zero term. For computational savings, we don't do that so we store
58+
in `skipped_division` the number of skipped divisions so that the division
59+
by `g*h^δ` can be adapted accordingly.
60+
61+
In [Knu14, Algorithm C, p. 426], it is stated that there should be ``
62+
4463
[Knu14] Knuth, D.E., 2014.
4564
*Art of computer programming, volume 2: Seminumerical algorithms.*
4665
Addison-Wesley Professional. Third edition.
4766
"""
48-
struct SubresultantAlgorithm <: AbstractUnivariateGCDAlgorithm end
67+
mutable struct SubresultantAlgorithm <: AbstractUnivariateGCDAlgorithm
68+
skipped_divisions::Int
69+
SubresultantAlgorithm() = new(0)
70+
end
71+
72+
_primitive_rem(::SubresultantAlgorithm) = false
73+
_skip_last(::SubresultantAlgorithm) = false
74+
function _set_skipped_divisions!(algo::SubresultantAlgorithm, n::Int)
75+
algo.skipped_divisions = n
76+
return
77+
end
4978

5079
_coefficient_gcd(α, β) = gcd(α, β)
5180
_coefficient_gcd::AbstractFloat, β) = one(Base.promote_typeof(α, β))
@@ -57,23 +86,23 @@ end
5786
function Base.lcm(
5887
p::_APL,
5988
q::_APL,
60-
algo::AbstractUnivariateGCDAlgorithm = GeneralizedEuclideanAlgorithm(),
89+
algo::AbstractUnivariateGCDAlgorithm = SubresultantAlgorithm(),
6190
)
6291
return p * div(q, gcd(p, q, algo))
6392
end
6493
function Base.gcd(
6594
α,
6695
p::_APL,
67-
algo::AbstractUnivariateGCDAlgorithm = GeneralizedEuclideanAlgorithm(),
68-
::MA.MutableTrait = MA.IsNotMutable(),
96+
algo::AbstractUnivariateGCDAlgorithm = SubresultantAlgorithm(),
97+
::MA.MutableTrait = MA.IsNotMutable(),
6998
mp::MA.MutableTrait = MA.IsNotMutable(),
7099
)
71100
return _coefficient_gcd(α, content(p, algo, mp))
72101
end
73102
function Base.gcd(
74103
p::_APL,
75104
α,
76-
algo::AbstractUnivariateGCDAlgorithm = GeneralizedEuclideanAlgorithm(),
105+
algo::AbstractUnivariateGCDAlgorithm = SubresultantAlgorithm(),
77106
mp::MA.MutableTrait = MA.IsNotMutable(),
78107
::MA.MutableTrait = MA.IsNotMutable(),
79108
)
@@ -87,7 +116,7 @@ function MA.promote_operation(
87116
::typeof(gcd),
88117
P::Type{<:_APL},
89118
Q::Type{<:_APL},
90-
A::Type = GeneralizedEuclideanAlgorithm,
119+
A::Type = SubresultantAlgorithm,
91120
)
92121
return MA.promote_operation(rem_or_pseudo_rem, P, Q, A)
93122
end
@@ -140,7 +169,7 @@ Addison-Wesley Professional. Third edition.
140169
function Base.gcd(
141170
p1::_APL{T},
142171
p2::_APL{S},
143-
algo::AbstractUnivariateGCDAlgorithm = GeneralizedEuclideanAlgorithm(),
172+
algo::AbstractUnivariateGCDAlgorithm = SubresultantAlgorithm(),
144173
m1::MA.MutableTrait = MA.IsNotMutable(),
145174
m2::MA.MutableTrait = MA.IsNotMutable(),
146175
) where {T,S}
@@ -179,7 +208,7 @@ end
179208
function Base.gcd(
180209
t1::AbstractTermLike{T},
181210
t2::AbstractTermLike{S},
182-
algo::AbstractUnivariateGCDAlgorithm = GeneralizedEuclideanAlgorithm(),
211+
::AbstractUnivariateGCDAlgorithm = SubresultantAlgorithm(),
183212
m1::MA.MutableTrait = MA.IsNotMutable(),
184213
m2::MA.MutableTrait = MA.IsNotMutable(),
185214
) where {T,S}
@@ -558,6 +587,98 @@ function primitive_univariate_gcd!(
558587
end
559588
end
560589

590+
function _pow_no_copy(a, b)
591+
if isone(b)
592+
# `a^1` is `copy(a)` but that copy is not needed here
593+
return a
594+
else
595+
return a^b
596+
end
597+
end
598+
599+
function primitive_univariate_gcd!(
600+
p::_APL,
601+
q::_APL,
602+
algo::SubresultantAlgorithm,
603+
)
604+
if maxdegree(p) < maxdegree(q)
605+
return primitive_univariate_gcd!(q, p, algo)
606+
end
607+
R = MA.promote_operation(gcd, typeof(p), typeof(q))
608+
u = convert(R, p)
609+
v = convert(R, q)
610+
if isapproxzero(v)
611+
return primitive_part(u, algo, MA.IsMutable())::R
612+
elseif isconstant(v)
613+
# `p` and `q` are primitive so if one of them is constant, it cannot
614+
# divide the content of the other one.
615+
return MA.operate!(one, u)
616+
end
617+
g = h = nothing # `nothing` means `1`
618+
while true
619+
δ = maxdegree(u) - maxdegree(v)
620+
d_before = degree(leading_monomial(u))
621+
r = MA.operate!(rem_or_pseudo_rem, u, v, algo)
622+
if isapproxzero(r)
623+
return primitive_part(v, algo, MA.IsMutable())::R
624+
elseif isconstant(r)
625+
return MA.operate!(one, v)
626+
end
627+
628+
d_after = degree(leading_monomial(r))
629+
if d_after == d_before
630+
not_divided_error(u, v)
631+
end
632+
if !isnothing(g)
633+
if isnothing(h) # equivalent to `iszero(δ)`
634+
ghδ = g
635+
else
636+
ghδ = g * _pow_no_copy(h, δ)
637+
end
638+
if !iszero(algo.skipped_divisions)
639+
@assert algo.skipped_divisions > 0
640+
if isnothing(h)
641+
# It is an alias to `g`
642+
ghδ = MA.copy_if_mutable(ghδ)
643+
end
644+
# TODO not sure this works, sometimes it `ghδ` is not a multiple of `leading_coefficient(v)`
645+
# we just know it divides the content of `r`, it is not guaranteed to be equal to the content of `r`
646+
# We could maybe do better than multiply `r` here though but let's start with this approach as a baseline
647+
#ghδ = div_multiple(ghδ, _pow_no_copy(leading_coefficient(v), algo.skipped_divisions), MA.IsMutable())
648+
r = MA.operate!(
649+
right_constant_mult,
650+
r,
651+
_pow_no_copy(
652+
leading_coefficient(v),
653+
algo.skipped_divisions,
654+
),
655+
)
656+
end
657+
r = right_constant_div_multiple(r, ghδ, MA.IsMutable())::R
658+
end
659+
u, v = v, r::R
660+
g = leading_coefficient(u)
661+
# Computes `h = h^(1 - δ) * g^δ` (step C3) of [Knu14, Algorithm C p. 429]
662+
# If `δ` is zero then `h^(1 - δ) * g^δ = h` so there is nothing to do
663+
if δ == 1
664+
h = g
665+
elseif δ > 1
666+
if isnothing(h) || δ == 2
667+
# `h^1` is `copy(h)` but that copy is not needed here
668+
= h
669+
else
670+
= h^- 1)
671+
end
672+
if isnothing(h)
673+
h = g
674+
else
675+
# We assume that `g^δ` is mutable since `δ > 1`
676+
h = div_multiple(g^δ, hδ, MA.IsMutable())
677+
end
678+
end
679+
end
680+
end
681+
561682
function primitive_univariate_gcdx(
562683
u0::_APL,
563684
v0::_APL,
@@ -597,7 +718,7 @@ function primitive_univariate_gcdx(
597718
return p * b, (a - b * q), g
598719
end
599720

600-
function primitive_univariate_gcd!(p::_APL, q::_APL, ::SubresultantAlgorithm)
721+
function primitive_univariate_gcdx(p::_APL, q::_APL, ::SubresultantAlgorithm)
601722
return error("Not implemented yet")
602723
end
603724

test/division.jl

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
using LinearAlgebra, Test
22
using Combinatorics
33

4-
import MutableArithmetics
5-
const MA = MutableArithmetics
4+
import MutableArithmetics as MA
65

76
using MultivariatePolynomials
87
const MP = MultivariatePolynomials
@@ -123,13 +122,15 @@ end
123122

124123
function test_gcdx_unit(expected, p1, p2, algo)
125124
test_gcd_unit(expected, p1, p2, algo)
126-
a, b, g = @inferred gcdx(p1, p2, algo)
127-
# it does not make sense, in general, to speak of "the" greatest common
128-
# divisor of u and v; there is a set of greatest common divisors, each
129-
# one being a unit multiple of the others [Knu14, p. 424] so `expected` and
130-
# `-expected` are both accepted.
131-
@test iszero(MP.pseudo_rem(g, expected, algo))
132-
@test a * p1 + b * p2 == g
125+
if !(algo isa SubresultantAlgorithm) # FIXME not implemented yet
126+
a, b, g = @inferred gcdx(p1, p2, algo)
127+
# it does not make sense, in general, to speak of "the" greatest common
128+
# divisor of u and v; there is a set of greatest common divisors, each
129+
# one being a unit multiple of the others [Knu14, p. 424] so `expected` and
130+
# `-expected` are both accepted.
131+
@test iszero(MP.pseudo_rem(g, expected, algo))
132+
@test a * p1 + b * p2 == g
133+
end
133134
end
134135
function _test_gcdx_unit(expected, p1, p2, algo)
135136
test_gcdx_unit(expected, p1, p2, algo)
@@ -214,7 +215,7 @@ end
214215

215216
function multivariate_gcd_test(
216217
::Type{T},
217-
algo = GeneralizedEuclideanAlgorithm(),
218+
algo = SubresultantAlgorithm(),
218219
) where {T}
219220
Mod.@polyvar x y z
220221
o = one(T)
@@ -411,6 +412,7 @@ end
411412
@testset "Division by multiple polynomials examples" begin
412413
multi_div_test()
413414
end
415+
univariate_gcd_test(SubresultantAlgorithm())
414416
@testset "Univariate gcd primitive_rem=$primitive_rem" for primitive_rem in
415417
[false, true]
416418
@testset "skip_last=$skip_last" for skip_last in [false, true]
@@ -422,6 +424,7 @@ end
422424
@testset "Multivariate gcd $T" for T in
423425
[Int, BigInt, Rational{BigInt}, Float64]
424426
if T != Rational{BigInt} || VERSION >= v"1.6"
427+
multivariate_gcd_test(T, SubresultantAlgorithm())
425428
# `gcd` for `Rational{BigInt}` got defined at some point between v1.0 and v1.6
426429
@testset "primitive_rem=$primitive_rem" for primitive_rem in
427430
[false, true]

0 commit comments

Comments
 (0)