A bit of theory. The discrete Fourier transform is a linear transform defined by a Vandermonde matrix. The matrix consists of degrees of a primitive root of a unit of degree n , where n is the length of the vectors and the dimension of the square matrix. It's easier to show how it is built, than to describe in words.

enter image description here

The inverse transformation matrix is ​​generated by the powers of the element inverse to the selected primitive root of one.

Fourier transform over a finite number field is somehow unpopular with Roisse coders, but it is and is used in cryptography. The difference is that the root of the unit is not extracted in the complex field, but in the field of residue classes. The rest is all the same.

Convolution can be viewed as the multiplication of polynomials with coefficients from a field or a finite ring of classes of residues. If we apply the normalization (transfer the transfer when the digits overflow), then we get the multiplication of long numbers. As I understand it, convolutions and Fourier transforms are smart enough to expand the final field when multiplying numbers if there are not enough digits in it. Here it is necessary to make an important explanation of the example.

We want to find the Fourier transform of the vector (4, 3, 2, 1) over the Galois number field GF(5) characteristic 5. The Vandermonde matrix is ​​generated by the primitive root of this field, i.e. two.

Note: not every element that is mutually simple with the module on which the field is built is a primitive root. Russian pedivikiya is lying. For example, powers of two will not cover all elements of the GF(7) field.

As a result of the action of this matrix on the vector (4, 3, 2, 1) , a vector is obtained (0, 1, 2, 3) . But what if we want to find the Fourier transform of the vector (7, 1, 7, 1) ? In the field GF(5) there is no element 7, but there is an equivalence class [2] containing a seven. How does the conversion work in this case?

A similar situation will arise if we multiply the numbers with the help of these transformations. For example, multiply the number 7777 (vector (7, 7, 7, 7) ) by itself. Here again, the GF(5) field is used, in which again there is no seven.

The convolution works like this: we supplement the two vector data with zeros ( their length doubles ), apply the Fourier transform to them, multiply the two vectors obtained by term, and apply the inverse Fourier transform to the result. This operation works as a multiplication of two polynomials with given coefficients from the field.

Example. Find the convolution (2, 1) and (1, 1) . Let's add vectors with zeros, we get a' = (2, 1, 0, 0) and b' = (1, 1, 0, 0) . Apply the direct transformation to them: F(a') = (3, 4, 1, 0) and F(b') = (2, 3, 0, 4) . Multiply the last vectors by term: (1, 2, 0, 0) . Apply to the resulting vector the inverse transform: (2, 3, 1, 0) .

My implementation of Fourier transforms, convolutions and multiplications of long numbers:

 #include <iostream> #include <vector> // Π’ΠΎΠ·Π²Π΅Π΄Π΅Π½ΠΈΠ΅ a Π² ΡΡ‚Π΅ΠΏΠ΅Π½ΡŒ b ΠΏΠΎ ΠΌΠΎΠ΄ΡƒΠ»ΡŽ p int powmod (int a, int b, int p) { int res = 1; while (b) if (b & 1) res = int ((long long) res * a % p), --b; else a = int ((long long) a * a % p), b >>= 1; return res; } // ВычислСниС ΠΎΠ±Ρ€Π°Ρ‚Π½ΠΎΠ³ΠΎ ΠΊ a элСмСнта ΠΏΠΎ ΠΌΠΎΠ΄ΡƒΠ»ΡŽ n int inverse(int a, int n) { int b0 = n, t, q; int x0 = 0, x1 = 1; if (n == 1) return 1; while (a > 1) { q = a / n; t = n, n = a % n, a = t; t = x0, x0 = x1 - q * x0, x1 = t; } if (x1 < 0) x1 += b0; return x1; } // ВычислСниС ΠΏΡ€ΠΈΠΌΠΈΡ‚ΠΈΠ²Π½ΠΎΠ³ΠΎ корня числового поля // (Π‘Ρ‚Π°Π½Π΄Π°Ρ€Ρ‚Π½Ρ‹ΠΉ DFT ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ комплСксныС числа, поэтому ΠΈΠ·ΠΌΠ΅Π½ΠΈΠΌ Π°Π»Π³ΠΎΡ€ΠΈΡ‚ΠΌ для Ρ€Π°Π±ΠΎΡ‚Ρ‹ Π² Zp) // ΠžΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΈΠ΅: p - простоС int generator (int p) { std::vector<int> fact; int phi = p - 1, n = phi; for (int i = 2; i * i <= n; ++i) if (n % i == 0) { fact.push_back (i); while (n % i == 0) n /= i; } if (n > 1) fact.push_back (n); for (int res = 2; res <= p; ++res) { bool ok = true; for (size_t i = 0; i < fact.size() && ok; ++i) ok &= powmod (res, phi / fact[i], p) != 1; if (ok) return res; } return -1; } // РСализация прямого DFT Π½Π°Π΄ Zp // ΠŸΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ Π²Π΅ΠΊΡ‚ΠΎΡ€ Π΄Π»ΠΈΠ½Ρ‹ n, эта Π΄Π»ΠΈΠ½Π° ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ ΠΏΡ€ΠΈ вычислСнии // ΠΏΡ€ΠΈΠΌΠΈΡ‚ΠΈΠ²Π½ΠΎΠ³ΠΎ корня поля // ВычислСния приводятся ΠΏΠΎ mod (n + 1), Ρ‚.ΠΊ. Π΄Π»ΠΈΠ½Π° Π²Π΅ΠΊΡ‚ΠΎΡ€Π° Π½Π° 1 мСньшС p, // Π³Π΄Π΅ p: Zp std::vector<int> forward_dft (std::vector<int>& a) { std::vector<int> result; int prim_root = generator (a.size()); // ΠšΠΎΡ€Π΅Π½ΡŒ стСпСни n ΠΈΠ· Π΅Π΄ΠΈΠ½ΠΈΡ†Ρ‹ for (int i = 0; i < a.size(); i++) { int sum = 0; int power = 0; for (int j = 0; j < a.size(); j++) { int power_of_root = powmod (prim_root, power, a.size() + 1); sum += power_of_root * a[j]; // НакапливаСм сумму ΠΏΡ€ΠΎΠΈΠ·Π²Π΅Π΄Π΅Π½ΠΈΠΉ элСмСнтов строки ΠΌΠ°Ρ‚Ρ€ΠΈΡ†Ρ‹ Π½Π° Π²Π΅ΠΊΡ‚ΠΎΡ€ power += i; // Π£Π²Π΅Π»ΠΈΡ‡ΠΈΠ²Π°Π΅ΠΌ ΡΡ‚Π΅ΠΏΠ΅Π½ΡŒ, Π² ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ возводится ΠΏΡ€ΠΈΠΌΠΈΡ‚ΠΈΠ²Π½Ρ‹ΠΉ ΠΊΠΎΡ€Π΅Π½ΡŒ ΠΈΠ· Π΅Π΄ΠΈΠ½ΠΈΡ†Ρ‹ } result.push_back(sum % (a.size() + 1)); // Набрали сумму, ΠΏΡ€ΠΈΠ²ΠΎΠ΄ΠΈΠΌ ΠΏΠΎ mod p, p = Π΄Π»ΠΈΠ½Π° Π²Π΅ΠΊΡ‚ΠΎΡ€Π° + 1 } return result; } std::vector<int> inversed_dft (std::vector<int>& a) { std::vector<int> result; int prim_root = generator (a.size()); // ΠšΠΎΡ€Π΅Π½ΡŒ стСпСни n ΠΈΠ· Π΅Π΄ΠΈΠ½ΠΈΡ†Ρ‹ int inv_prim_root = inverse (prim_root, a.size() + 1); // ΠžΠ±Ρ€Π°Ρ‚Π½Ρ‹ΠΉ ΠΊ Π½Π°ΠΉΠ΄Π΅Π½Π½ΠΎΠΌΡƒ ΠΏΡ€ΠΈΠΌΠΈΡ‚ΠΈΠ²Π½ΠΎΠΌΡƒ ΠΊΠΎΡ€Π½ΡŽ int inv_n = inverse (a.size(), a.size() + 1); // ΠžΠ±Ρ€Π°Ρ‚Π½Ρ‹ΠΉ ΠΊ n ΠΏΠΎ mod (n + 1) for (int i = 0; i < a.size(); i++) { int sum = 0; int power = 0; for (int j = 0; j < a.size(); j++) { int power_of_inv_root = powmod (inv_prim_root, power, a.size() + 1); sum += inv_n * power_of_inv_root * a[j]; // НакапливаСм сумму ΠΏΡ€ΠΎΠΈΠ·Π²Π΅Π΄Π΅Π½ΠΈΠΉ элСмСнтов строки ΠΌΠ°Ρ‚Ρ€ΠΈΡ†Ρ‹ Π½Π° Π²Π΅ΠΊΡ‚ΠΎΡ€ power += i; // Π£Π²Π΅Π»ΠΈΡ‡ΠΈΠ²Π°Π΅ΠΌ ΡΡ‚Π΅ΠΏΠ΅Π½ΡŒ, Π² ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ возводится ΠΏΡ€ΠΈΠΌΠΈΡ‚ΠΈΠ²Π½Ρ‹ΠΉ ΠΊΠΎΡ€Π΅Π½ΡŒ ΠΈΠ· Π΅Π΄ΠΈΠ½ΠΈΡ†Ρ‹ } result.push_back(sum % (a.size() + 1)); // Набрали сумму, ΠΏΡ€ΠΈΠ²ΠΎΠ΄ΠΈΠΌ ΠΏΠΎ mod p, p = Π΄Π»ΠΈΠ½Π° Π²Π΅ΠΊΡ‚ΠΎΡ€Π° + 1 } return result; } // Π‘Π²Π΅Ρ€Ρ‚ΠΊΠ° Π΄Π²ΡƒΡ… Π²Π΅ΠΊΡ‚ΠΎΡ€ΠΎΠ² ΠΏΡ€ΠΈ ΠΏΠΎΠΌΠΎΡ‰ΠΈ дискрСтного прСобразования Π€ΡƒΡ€ΡŒΠ΅ std::vector<int> convolution (std::vector<int> a, std::vector<int> b) { a.insert(a.end(), a.size(), 0); b.insert(b.end(), b.size(), 0); a = forward_dft(a); b = forward_dft(b); std::vector<int> result(a.size(), 0); for(int i = 0; i < result.size(); i++) result[i] = a[i] * b[i]; result = inversed_dft(result); return result; } // Π£ΠΌΠ½ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π΄Π²ΡƒΡ… Π±ΠΎΠ»ΡŒΡˆΠΈΡ… чисСл std::vector<int> multiply (std::vector<int> a, std::vector<int> b) { std::vector<int> result = convolution(a, b); // Нормализация, Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ пСрСносов int carry = 0; for (int i = 0; i < result.size(); i++) { result[i] += carry; carry = result[i] / 10; result[i] %= 10; } return result; } int main() { int a[] = {1, 2, 1, 2, 1, 2, 1, 2}; int b[] = {7, 5, 7, 5, 7, 5, 7, 5}; std::vector<int> u(std::begin(a), std::end(a)); std::vector<int> v(std::begin(b), std::end(b)); std::vector<int> result = multiply(u, v); for(int i = 0; i < result.size(); i++) std::cout << result[i] << " "; /*std::vector<int> u(std::begin(a), std::end(a)); std::vector<int> v(std::begin(b), std::end(b)); std::vector<int> result = convolution(u, v); for(int i = 0; i < result.size(); i++) std::cout << result[i] << " ";*/ /*int a[] = {4, 3, 2, 1}; std::vector<int> v(std::begin(a), std::end(a)); std::vector<int> result = forward_dft(v); std::cout << "[DEBUG]: Forward DFT" << std::endl; for(int i = 0; i < result.size(); i++) std::cout << result[i] << " "; std::cout << std::endl; result = inversed_dft(result); std::cout << "[DEBUG]: Inversed DFT" << std::endl; for(int i = 0; i < result.size(); i++) std::cout << result[i] << " ";*/ } 

Multiplying long numbers in the main function does not work correctly. At first I thought that this was due to the fact that the algorithm was working on a finite field, in which there were not some β€œdigits” of a large number. But it can be seen that when multiplying the numbers 12121212 by 75757575, a field of sufficient size is used to contain the correspondence for the digit 7. There, when the number 12 is squared, the algorithm works almost correctly, i.e. a vector is obtained (1, 4, 4, 0) , but an extra zero occurs due to the dimension of the vectors and the padding with zeros. Where am I wrong?

    1 answer 1

    You do not have a search for a prime number in the program, in the field of which the discrete Fourier transform goes on - this is rather strange. I assumed that you consider that the size of the array plus the unit is a prime number (which in your example really is - 2*8+1=17 is simple).

    Next, you use the generator function inconsistently - it is declared with the parameter-prime number p , and is called from a.size() (which, in my assumption, is p-1 ). At the same time, which is funny, the primitive root is also located - it exists according to including modules, which are powers of two.

    The last and fatal problem is that module 17 is not enough for multiplying numbers of length 8 along base 10. When multiplying numbers, we move to multiplying polynomials (for example, 4321 goes into 4x^3+3*x^2+2*x+1 ), then multiply the polynomials in the field, and then turn the polynomial back into a number. If in the process of multiplication at least one coefficient of the polynomial "overflowed" (ie, it became greater than or equal to the module), then we all get the correct result of multiplying the polynomials, but it will no longer correspond to the correct number.

    For example, multiplying 4321 by 321 (i.e. (4x^3+3*x^2+2*x+1)*(3*x^2+2*x+1) ) we get the polynomial 12*x^5+17*x^4+16*x^3+10*x^2+4*x+1 , which, with the substitution x=10 gives just the number 1387041. However, if the coefficients of the polynomial are known only modulo 17, then the coefficient when x^4 vanishes and it becomes impossible to restore what it was actually equal to. You should choose a field large enough so that when multiplying two polynomials, the result coefficients do not exceed the module.