Lo scopo di questo notebook è implementare l'algoritmo impiegato nella dimostrazione del Teorema fondamentale dei polinomi simmetrici, che, dato un campo $F$, asserisce il seguente isomorfismo:
$$\operatorname{Sym}[X_n] \cong F[e_1, \ldots, e_n],$$dove $X_n$ è l'insieme $\{x_1, \ldots, x_n\}$.
Innanzitutto, si sceglie un $n \in \mathbb{N}^+$, che rappresenta il numero di variabili di cui è composto il polinomio simmetrico, e si crea l'anello di polinomi $\mathbb{Q}[x_1, \ldots, x_n]$, nel quale vale il degree lexicographic order (deglex). Vengono infine create delle variabili simboliche a rappresentare i polinomi simmetrici elementari.
%display latex
n = 5
P = PolynomialRing(QQ, ",".join(f"x_{i}" for i in range(1, n+1)), order='deglex')
P.inject_variables()
var(",".join(f"e_{i}" for i in range(n+1)))
Si definisce il valore che deve assumere la funzione $e(d)$, che rappresenta il polinomio simmetrico $e_d$, nel seguente modo:
$$e(d) = \begin{cases}1 & \text{se }d=0, \\ \displaystyle \sum_{1\leq i_1 < i_2 < \cdots < i_d \leq n} \underbrace{x_{i_1} \cdots x_{i_d}}_{d\text{ volte}} & \text{altrimenti.} \end{cases}$$from functools import reduce
from itertools import combinations
from operator import mul
def e(d: Integer):
if d == 0:
return 1
return sum(reduce(mul, [P(f"x_{j}") for j in i], 1) for i in combinations(range(1, n+1), d))
Si definisce una funzione $\beta(\operatorname{exp\_lt})$ che prende in ingresso una tupla ordinata $\operatorname{exp\_lt} \in \mathbb{N}^n$ e restituisce una tupla dello stesso tipo definita nel seguente modo:
$$\beta(\operatorname{exp\_lt})_i = \begin{cases} \operatorname{exp\_lt}_i - \operatorname{exp\_lt}_{i+1} & \text{se }1 \leq i < n, \\ \operatorname{exp\_lt}_i & \text{altrimenti.} \end{cases}$$def beta(exp_lt):
return [e - exp_lt[i+1] if i != n-1 else e for i, e in enumerate(exp_lt)]
Si definiscono due funzioni analoghe, dette $\operatorname{e\_prod}$ e $\operatorname{e\_prod\_value}$. La seconda restituisce lo stesso polinomio di $\operatorname{e\_prod}$ sostituendo ai simboli $e_i$ i corrispettivi valori $e(i)$. Pertanto si definisce solo il valore della prima funzione.
Tale funzione $\operatorname{e\_prod}$ prende in ingresso una tupla $b \in \mathbb{N}^n$ e restituisce $e^b = {e_1}^{b_1} {e_2}^{b_2} \cdots {e_n}^{b_n}$.
def e_prod(b):
return reduce(mul, [eval(f"e_{i+1}")^k for i, k in enumerate(b)], 1)
def e_prod_value(b):
return reduce(mul, [e(i+1)^k for i, k in enumerate(b)], 1)
Si definisce infine la funzione $\operatorname{combination}(\operatorname{poly}(x))$, che prende in ingresso un polinomio simmetrico $\operatorname{poly}(x)$ e ne restituisce la rappresentazione in $F[e_1, \ldots, e_n]$, secondo il seguente algoritmo.
def combination(poly):
if isinstance(poly, Integer) or isinstance(poly, Rational) or poly == 0 or poly.degree() == 0:
return poly
lt = sorted(list(poly), reverse=True)[0]
try:
b = beta(lt[1].exponents()[0])
return lt[0] * e_prod(b) + combination(poly - lt[0] * e_prod_value(b))
except (AttributeError, TypeError):
raise TypeError("The given polynomial is not of symmetric kind")
Si sceglie inoltre un polinomio $\operatorname{poly}(x)$ che deve essere simmetrico -- qualora non fosse tale, l'algoritmo non terminerà con successo (se terminasse con successo, il polinomio sarebbe combinazione di polinomi simmetrici, e sarebbe dunque anch'esso simmetrico, ↯).
# Il polinomio deve essere simmetrico...
poly = x_1 + x_2 + x_3 + x_4 + x_5 + (x_1^2 + x_2^2 + x_3^2 + x_4^2 + x_5^2 - x_1*x_2*x_3*x_4*x_5)^2
poly
Si calcola adesso la rappresentazione di $\operatorname{poly}(x)$ in funzione dei polinomi simmetrici elementari secondo l'implementazione di $\operatorname{combination}$:
combination(poly)
Data una somma di potenze simmetrica, è possibile implementare un algoritmo di rappresentazione negli $e_i$ secondo le identità di Newton-Girard. Si definisce allora la funzione $\operatorname{newton\_girard}(k)$, che prende in ingresso un $k \in \mathbb{N}$ e restituisce la rappresentazione di $\sum_{i=1}^n {x_i}^k$ nel seguente modo:
$$\operatorname{newton\_girard}(k) = \begin{cases} n & \text{se } k = 0, \\ (-1)^{k+1} \cdot (k e_{k} + \sum_{i=1}^{k-1} (-1)^i \operatorname{newton\_girard}(i) \, e_{k-1}) & \text{altrimenti}, \end{cases}$$dove si pone $e_i = 0$ per ogni $i > n$.
La funzione è implementata con l'ausilio di un sistema di caching, affinché non vengano ricalcolati i valori già calcolati.
from functools import lru_cache
@lru_cache(maxsize=None)
def newton_girard(k):
if k == 0:
return n
return ((-1)**(k+1) * ((k * eval(f"e_{k}") if k <= n else 0) +
sum((-1)**i * newton_girard(i) * eval(f"e_{k - i}") for i in range(max(1, k-n), k)))).expand()
Si definisce infine la funzione $\operatorname{sum\_of\_powers}(k)$, che, dato in ingresso un $k \in \mathbb{N}$, restituisce la rappresentazione di $\sum_{i=1}^n (x_i)^k$ secondo la funzione $\operatorname{combination}$ già definita.
def sum_of_powers(k):
return combination(sum(eval(f"x_{i}")^k for i in range(1, n+1)))
Si sceglie infine un $k \in \mathbb{N}$ al quale elevare tutti le variabili della somma.
k = 8
Si computa la rappresentazione di $\sum_{i=1}^n (x_i)^k$ prima secondo $\operatorname{newton\_girard}$, e poi secondo $\operatorname{sum\_of\_powers}$, e si verifica che siano uguali.
a = newton_girard(k)
a
b = sum_of_powers(k)
b
a - b
Sia ora:
$$f(x) = x^n + a_{n-1} x^{n-1} \ldots + a_0,$$con radici $x_1$, $x_2$, ..., $x_n$. Allora si definisce discriminante di $f$ la seguente produttoria:
$$\Delta(f) = \displaystyle \prod_{1 \leq i < j \leq n} (x_i - x_j)^2.$$In particolare, vale la seguente proprietà:
$$\exists\, i \neq j \mid x_i = x_j \iff \Delta(f) = 0.$$Si verifica facilmente che $\Delta(f)$ è un polinomio simmetrico. Poiché ogni permutazione è una composizione di trasposizioni, è sufficiente verificare che invertendo due radici $x_i$ e $x_j$ il discriminante rimanga invariato:
Poiché $\Delta(f)$ è un polinomio simmetrico nelle variabili $x_1$, ..., $x_n$, per il Teorema fondamentale dei polinomi simmetrici, si scrive in modo unico in funzione dei polinomi simmetrici elementari $e_i(x_1, \ldots, x_n)$. Tuttavia tali polinomi simmetrici elementari altro non sono che i coefficienti di $f(x)$, a meno del segno. In particolare vale che:
$$e_i(x_1, \ldots, x_n) = (-1)^{i} a_{n-i}, \quad \text{per } 0 \leq i \leq n.$$Si definiscono quindi i simboli $a_0$, ..., $a_n$ e si costruisce una funzione $\Delta(n)$ che restituisce il discriminante di un generico polinomio di $n$-esimo grado dato in ingresso in funzione degli $a_i$ mediante $\operatorname{combination}$.
var(",".join(f"a_{i}" for i in range(n+1)))
def delta(n):
f = reduce(mul, (eval(f"(x_{i}-x_{j})")^2 for i, j in combinations(range(1, n+1), 2)), 1)
c = combination(f)
for i in range(1, n+1):
if i % 2:
c = eval(f"c.substitute(e_{i}=-a_{n-i})")
else:
c = eval(f"c.substitute(e_{i}=a_{n-i})")
return c
Dunque, per l'$n$ scelto in partenza, il discriminante del generico polinomio di $n$-esimo grado è il seguente:
delta(n)
Infine si costruisce la funzione $\operatorname{evaluate\_delta}$ che, dato in ingresso un polinomio $f(x)$ di $n$-esimo grado, restituisce il valore di $\Delta(f)$, sostituendo agli $a_i$ generici di $\operatorname{delta}(n)$ i valori dei coefficienti di $f$.
def evaluate_delta(f):
d = delta(n)
for i, a in enumerate(f.list()):
d = eval(f"d.substitute(a_{i}=a)")
return d
Si verifica adesso che per un polinomio con radici multiple il valore di $\operatorname{evaluate\_delta}$ è precisamente zero:
f = (x-2)^2*(x-3)*(x-4)*(x-5)
f
f.expand()
evaluate_delta(f)
Infine, si fa lo stesso con un polinomio con radici distinte, per verificare che $\operatorname{evaluate\_delta}$ è strettamente diverso da zero.
g = (x+2)*(x+I)*(x-I)*(x-1)*(x-2)
g
g.expand()
evaluate_delta(g)
(c) 2022, ~videtta