Важные организационные моменты

  1. Не забываем отмечаться на https://icpc.appmat.ru
  2. В воскресенье 6 октября пройдёт 1/8 финала ICPC в МЭИ.
    1. Если у вас есть команда, то регаемся по инструкции тут: https://vk.com/wall-182708012_128
    2. Если у вас нет команды, но вы хотите участвовать, то в выходные мы организуем контест, который поможет вам распределиться на команды.
  3. Пройдите опрос о занятиях в субботу, нам это важно, чтобы понимать сколько аудиторий резервировать у института: https://forms.gle/qGMHjMZJqXS5p2Vm6

Теория чисел. Продолжение

Сегодня мы рассмотрим дальнейшие задачи, связанные с теорией чисел.

Простые числа

Целое число $n > 1$ называется простым, если его единственными положительными множителями являются $1$ и $n$. Например, числа $7$, $19$ и $41$ простые, а $249$ - не простое (составное), потому что $13 * 19 = 247$. Для каждого целого числа $n > 1$ существует единственное разложение на простые множители: $$ n=p^{\alpha_1}_1 p^{\alpha_2}_2 ... p^{\alpha_k}_k$$ где $p_i$ - различные простые числа, $\alpha_i$ - натуральные числа. Например, $$120=2^3*3^1*5^1$$

Задача факторизации

Если целое число $п$ не простое, то его можно представить в виде произведения $а*b$, где $a<\sqrt{n}$ или $b<\sqrt{n}$, поэтому среди его множителей обязательно имеется число от $2$ до $\sqrt{n}$. Следовательно, проверить простоту числа и найти его разложение на простые множители можно за время $O(\sqrt{n})$. Показанная ниже функция prime проверяет, является ли целое число $n$ простым. Она пытается поделить п на все целые числа от 2 до $\sqrt{n}$; если ниодно из них не является делителем, то $n$ простое:

In [1]:
#include <iostream>
#include <vector>

using namespace std;
In [2]:
bool prime(long long n) {
    if (n < 2) return false;
    for (int x = 2; x*x <= n; x++){
        if (n%x == 0) return false;
    }
    return true;
}
In [3]:
{
    cout << "997 is " << (prime(997) ? "prime" : "not a prime") << endl;
    cout << "231 is " << (prime(231) ? "prime" : "not a prime") << endl;
    cout << "1e+9 + 7 is " << (prime(1e9 + 7) ? "prime" : "not a prime") << endl;
    cout << "413 is " << (prime(413) ? "prime" : "not a prime") << endl;
    cout << "23 is " << (prime(23) ? "prime" : "not a prime") << endl;
}
997 is prime
231 is not a prime
1e+9 + 7 is prime
413 is not a prime
23 is prime

Задача факторизации является вычислительно сложной задачей, на данный момент неизвестно, существует ли эффективный алгоритм её решения. По этой причине, простые числа представляют особый интерес для криптографии.

Следующая функция строит вектор, содержащий разложение $n$ на простые множители. Функция делит число $n$ на его простые множители и добавляет их в вектор. Процесс заканчивается, когда у $n$ не останется множителей между $2$ и $\sqrt{n}$. Если при этом $n > 1$, то оно простое и добавляется в качестве последнего множителя.

Ключевое слово typedef позволяет определять пользовательские типы данных. К примеру:

In [4]:
typedef unsigned long long ull;
typedef long long ll;
typedef vector<ull> vull;

Иногда это сокращает время на запись программы и помогает предостеречь от ошибок. А иногда позволяет делать код, который комментирует сам себя.

In [5]:
vull factors(int n) {
    
    vull f;
    
    for (int x = 2; x*x <= n; x++) {
        while (n % x == 0) {
            f.push_back(x);
            n /= x;
        }
    }
        
    if (n > 1) f.push_back(n);
        return f;
}

Отметим, что каждый простой множитель встречается в этом векторе столько раз, какова его степень в разложении $n$. Например, $12 = 2 * 2 * 3$, поэтому функция вернет результат $\{2, 2, 3\}$. Поэтому мы можем написать альетрнативную версию данной функции, которая считает сколько раз каждое число встречается.

In [6]:
vull factors2(int n) {
    
    vull f(n + 1, 0);
    
    for (int x = 2; x*x <= n; x++) {
        while (n % x == 0) {
            f[x]++;
            n /= x;
        }
    }
    
    if (n > 1) f[n]++;
    
    return f;
}

Существует множество более быстрых алгоритмов факторизации: https://en.wikipedia.org/wiki/Integer_factorization#Factoring_algorithms

In [7]:
void printVector(vull& vec) {
    for (auto a: vec)
        cout << a << " ";
    cout << endl;
}
In [8]:
{
    vull res = factors(10);
    cout << "10: ";
    printVector(res);
    
    res = factors(20);
    cout << "20: ";
    printVector(res);
    
    res = factors2(20);
    cout << "20: ";
    printVector(res);
    
    res = factors2(120);
    cout << "120: ";
    printVector(res);
}
10: 2 5 
20: 2 2 5 
20: 0 0 2 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
120: 0 0 3 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 

Количество множителей и их сумма

Количество множителей числа $n$ определяется по формуле $$\tau(n)=\prod_{i=1}^k{(\alpha_i+1)}$$ потому что для каждого простого множителя $p_i$ существует $a_{i} + 1$способов выбрать, сколько раз $p_i$ входит в множитель.

Сумма множителей числа n определяется формулой $$\sigma(n)=\prod_{i=1}^{k}{(1+p_i+...+p^{\alpha_i}_i)}=\prod_{i=1}^{k}{\frac{p^{\alpha_i+1}_i-1}{p_i-1}}$$ эта формула следует из формулы суммы геометрической прогрессии.

Например, $$\tau(120)=(3+1)*(1+1)*(1+1)=16$$ $$\sigma(120)=\frac{2^4-1}{2-1}*\frac{3^2-1}{3-1}*\frac{5^2-1}{5-1}=\frac{15*8*24}{2*4}=360$$

In [9]:
ull divisiors_count(int n) {
    vull divs = factors2(n);
    ull res = 1;
    for (auto i : divs) {
        res *= (i + 1);
    }
    return res;
}
In [10]:
ull binpow (ull a, ull n) {
	if (n == 0)
		return 1;
	if (n % 2 == 1)
		return binpow (a, n-1) * a;
	else {
		int b = binpow (a, n/2);
		return b * b;
	}
}
In [11]:
ull divisiors_summ(int n) {
    vull divs = factors2(n);
    ull res_m = 1;
    ull res_n = 1;
    
    for (size_t i = 2; i < n; i++) {
                
        if (divs[i] == 0)
            continue;
        
        res_m *= binpow(i, divs[i]+1) - 1;
        
        res_n *= i - 1;
        
        if (res_m % res_n == 0) {
            res_m /= res_n;
            res_n = 1;
        }
    }
    
    res_m /= res_n;
    
    return res_m;
}
In [12]:
{
    cout << "for 120 count is " << divisiors_count(120) << " sum is " << divisiors_summ(120) << endl;
}
for 120 count is 16 sum is 360

Свойства простых чисел

  1. Их бесконечно много
  2. Количество простых чисел $\pi(n)$, не превоходящих n, оценивается формулой $$\frac{n}{ln(n)}<\pi(n)<1.25506\frac{n}{ln(n)}$$ для $n\geqslant17$. Однако, существует множество алгоритмов и приближенных формул для вычисления функции $\pi(n)$.

Задача поиска простых чисел

Во-первых есть список простых чисел: https://oeis.org/A000040

Во-вторых есть специальные алгоритмы, для поиска простых чисел.

Решето Эратосфена

Решето Эратосфена - это алгоритм предварительной обработки, который строит массив sieve, позволяющий для каждого целого числа от $2$ до $n$ эффективно (За $O(1)$) определить, является ли оно простым. Если $х$ простое, то sieve[x] = 0, иначе sieve[x] = 1.

Для построения массива алгоритм перебирает числа 2...n. Обнаружив новое простое число х, алгоритм помечает, что числа $2*х$, $3*х$, $4*х$ ит.д. непростые. Ниже показана возможная реализация алгоритма в предположении, что в начале все элементы sieve равны нулю:

In [13]:
typedef vector<bool> vb;
In [14]:
vb sieve(int n) {
    vb res(n, true);
    
    res[0] = false;
    res[1] = false;
    
    for (ull x = 2; x <= n; x++) {
        if (!res[x]) continue;
        
        for (ull u = 2*x; u <= n; u += x) {
            res[u] = false;
        }
    }
    
    return res;
}
In [15]:
{
    vb s = sieve(1000);
    
    cout << "997 is " << (s[997] ? "prime" : "not a prime") << endl;
    cout << "231 is " << (s[231] ? "prime" : "not a prime") << endl;
    cout << "413 is " << (s[413] ? "prime" : "not a prime") << endl;
    cout << "23 is " << (s[23] ? "prime" : "not a prime") << endl;
}
997 is prime
231 is not a prime
413 is not a prime
23 is prime

Трудоёмкость алгоритма решета Эратосфена времени по памяти составляет O(n). Трудоёмкость по времени можно оценить как $O(n log n)$: $$\sum_{x=2}^n{\lfloor n/x\rfloor}={\lfloor n/2\rfloor}+{\lfloor n/3\rfloor}+{\lfloor n/4\rfloor}+...=O(nlogn)$$ но с учётом того, что внутренний цикл выполняется только для простых чисел, можно получить более оптимистичную оценку $O(n log log n)$

Существует множество возможных модификаций этого алгоритма:

  1. Вместо факта простоты $x$, можно сохранить его наименьший множитель, отличный от $0$ (это число всегда простое), что обеспечит эффективную факторизацию для всех чисел $<=n$;
  2. Цикл только по нечётным числам - так как все простые числа $>2$ нечётные, можно сократить количество итераций вдвое.
  3. Использование битового массива - так как одним из сильнейших недостатков решета Эратосфена является расход памяяти, а результат вычисления - по сути, занимает один бит памяти, можно использовать "упакованный" битовый массив, 1 байт=8 бит. Недостаток - сложности при работе с такой структурой данных (хотя у стандартного вектора вроде как есть такая оптимизация для булева типа данных)
  4. Постепенное построение множества простых чисел ru.wikipedia.org/wiki/Решето_Эратосфена#Неограниченный,_постепенный_вариант
  5. Сегментированное решето Эратосфена (на каждом шаге просеиваются отрезки целых чисел меньше корня из n)

Разложение числа в сумму простых чисел

Требуется представить любое целое число n в виде суммы простых чисел.

Проблема Гольдбаха

Проблема Гольдбаха - одна из открытых математических проблем. Это теорема, гласящая, что всякое чётное число, начиная с 4, представимо в виде суммы 2 простых чисел. Следствием является тернарная проблема Гольдбаха, доказанная в 2013 году: всякое нечётное число, начиная с 7, представимо в виде суммы 3 простых чисел.

На данный момент доказано, что всякое достаточно большое целое число представимо в виде не более 3-4 простых слагаемых (4-для чётных, 3 для нечётных)

In [16]:
void twoSum(const vull& nums, ull target, ull &a, ull &b) {
    unordered_map<ull, ull> hash;

    for (size_t i = 0; i < nums.size(); i++)
        if (hash.count(target - nums[i]))
        {
            a = nums[i];
            b = target - nums[i];
            return;
        }
        else
            hash[nums[i]] = i;
    
    a = target;
    b = 0;
    return;
}
In [17]:
{
    int n = 1000;
    vb s = sieve(n);
    
    vull primes;
    for (size_t i = 2; i < n; i++) {
        if (s[i])
            primes.push_back(i);
    }
    
    ull a, b;
    ull val = 128;
    twoSum(primes, val, a, b);
    cout << val << " = " << a << " + " << b << endl;
    
    val = 322;
    twoSum(primes, val, a, b);
    cout << val << " = " << a << " + " << b << endl;
    
    val = 998;
    twoSum(primes, val, a, b);
    cout << val << " = " << a << " + " << b << endl;
}
128 = 67 + 61
322 = 173 + 149
998 = 541 + 457

Модульная арифметика

Мы ведь это помним, да?

Возведение в степень по модулю

Часто бывает нужно эффективно вычислить значение $x^n\pmod n$. Это можно сделать за время $O(log\;n)$, воспользовавшись следующим рекуррентным соотношением: $$x^n=\left\{ \begin{array}{ll} 1 &\text{n=0} \\ x^\frac{n}{2}\cdot x^\frac{n}{2} &\text{n чётно} \\ x^{n-1}\cdot x &\text{n нечётно} \end{array} \right. $$ Например, для вычисления $x^{100}$ мы сначала вычисляем $x^{50}$, а затем пользуемся формулой $x^{100}=x^{50} \cdot x^{50}$. Далее для вычисления $x^{50}$ мы сначала вычисляем $x^{25}$ и т.д. Поскольку при четном n показатель степень уменьшается вдвое, это вычисление занимает время $O(log\,n)$. Алгоритм реализуется следующей функцией:

In [18]:
ull modpow(ull x, ull n, ull m){
    
    if(n==0)
        return 1 % m;
    
    ull u = modpow(x, n / 2 ,m);
    u = (u * u) % m;
    if (n % 2 == 1)
        u = (u * x) % m;
    return u;
}

Теорема Эйлера

Два целых числа а и b называются взаимно простыми, если gcd(a,b)=1. Функция Эйлера $\phi(n)$ определяет количество целых чисел от 1 до n, взаимно простых с n. Например, $\phi(10)=4$, потому что числа 1, 3, 7 и 9 взаимно просты с 10. Для любого n значение $\phi(n)$ можно вычислить, зная разложение n на простые множители, по формуле $$\phi(n)=\prod^{k}_{i=1}{p^{\alpha_i-1}_i(p_i-1)}$$ Теорема Эйлера утверждает,что $$x^{\phi(m)}\,mod\,m=1$$ для всех положительных взаимно простых чисел x и m. Так, согласно теореме Эйлера, $7^4 mod\,10 = 1$, поскольку 7 и 10 - взаимно простые числа и $\phi(10)=4$. Если m простое, то $\phi(m)=m-1$, и эта формула принимает вид $$x^{m-1}\,mod\,m=1$$ В таком виде она известна как малая теорема Ферма. Из нее следует, что $$x^nmod\,m=x^{n\,mod(m-1)}mod\,m$$ и этот факт можно использовать для вычисления $x^n$ при очень больших n.

In [19]:
ll phi (ll n) {
	int result = n;
	for (ull i=2; i*i<=n; ++i)
		if (n % i == 0) {
			while (n % i == 0)
				n /= i;
			result -= result / i;
		}
	if (n > 1)
		result -= result / n;
	return result;
}

Рассмотрим код поиска обратного элемента при помощи функции Эйлера и бинарного возведения в степень.

$$ x^{-1} \pmod {m} = x^{\phi(m) - 1} \pmod{m} $$
In [20]:
ll inverse(ll x, ll m) {
    ll phi_val = phi(m);
    
    return modpow(x, phi_val - 1, m);
}
In [21]:
{
    int a = 5, n = 7;
    cout << "a=" << a << " n=" << n << " inv=" << inverse(a, n) << " check: " << ((a * inverse(a, n)) % n) << endl;
    
    a = 2, n = 7;
    cout << "a=" << a << " n=" << n << " inv=" << inverse(a, n) << " check: " << ((a * inverse(a, n)) % n) << endl;
    
    a = 8, n = 11;
    cout << "a=" << a << " n=" << n << " inv=" << inverse(a, n) << " check: " << ((a * inverse(a, n)) % n) << endl;
    
    a = 9, n = 1e9+7;
    cout << "a=" << a << " n=" << n << " inv=" << inverse(a, n) << " check: " << ((a * inverse(a, n)) % n) << endl;
    
    a = 3, n = 13;
    cout << "a=" << a << " n=" << n << " inv=" << inverse(a, n) << " check: " << ((a * inverse(a, n)) % n) << endl;
}
a=5 n=7 inv=3 check: 1
a=2 n=7 inv=4 check: 1
a=8 n=11 inv=7 check: 1
a=9 n=1000000007 inv=111111112 check: 1
a=3 n=13 inv=9 check: 1

Диофантовы уравнения

Это уравнения в целых числах вида:

$$ ax + by = c $$

Решений у этого уравнения может быть достаточно много, однако, как мы помним из прошлой лекции, у нас есть Расширенный алгоритм Евклида, который позволяет найти $x$ и $y$ для уравнения: $$ ax + by = gcd(x,y) $$

Диафантово уравнение разрешимо тогда и только тогда, когда

$$ c \equiv 0 \pmod {gcd(a,b)} $$

При помощи Расширенного алгоритма Евклида можно найти некоторые $x$, $y$, тогда они будут задавать серию решений:

$$\Biggl(x + \frac{kb}{gcd(a,b)}, y - \frac{ka}{gcd(a,b)}\Biggl), k \in \mathbb{Z} $$

В итоге, для поиска одного решения можно использовать следующий код.

In [22]:
void gcdext (int a, int b, int &d, int &x, int &y)
{
  int s;

  if (b == 0) {
    d = a; x = 1; y = 0;
    return;
  }

  gcdext(b,a % b,d,x,y);

  s = y;
  y = x - (a / b) * y;
  x = s;
}
In [23]:
int gcd(int a, int b)
{
  if (b == 0) return a;
  return gcd(b, a % b);
}
In [24]:
bool solve_diafant(int a, int b, int c, int &x, int &y) {
    int m = gcd(a,b);
    
    if (c % m != 0)
        return false;
    
    gcdext(a, b, m, x, y);
    
    x *= c / m;
    y *= c / m;
    
    return true;
}
In [25]:
{
    int a = 5, b = 2, c = 11;
    int x, y;
    cout << "a = " << a << " b = " << b << " c = " << c <<  ": ";
    if (!solve_diafant(a, b, c, x, y))
        cout << "no solution" << endl;
    else
        cout << "x = " << x << " y = " << y << endl;
    
    a = 39, b = 15, c = 12;
    cout << "a = " << a << " b = " << b << " c = " << c << ": ";
    if (!solve_diafant(a, b, c, x, y))
        cout << "no solution" << endl;
    else
        cout << "x = " << x << " y = " << y << endl;
}
a = 5 b = 2 c = 11: x = 11 y = -22
a = 39 b = 15 c = 12: x = 8 y = -20

Китайская теорема об остатках

Если натуральные числа $a_{1},a_{2},\dots ,a_{n}$ попарно взаимно просты, то для любых целых $ r_{1},r_{2},\dots ,r_{n}$ таких, что $ 0\leqslant r_{i}<a_{i}$ при всех $i\in \{1,2,\dots ,n\}$, найдётся число $N$, которое при делении на $a_{i}$ даёт остаток $ r_{i}$ при всех $i\in \{1,2,\dots ,n\}$.

Более того, если найдутся два таких числа $N_{1}$ и $N_{2}$, то $N_{1}\equiv N_{2}{\pmod {a_{1}\cdot a_{2}\cdot \ldots \cdot a_{n}}}$.

То есть для системы вида:

$$ \begin{array}{lcl} x & = & a_1 \bmod m_1 \\ x & = & a_2 \bmod m_2 \\ \cdots \\ x & = & a_n \bmod m_n \\ \end{array} $$

Где $m_1,m_2,\ldots,m_n$ попарно взаимопростые.

Пусть $X^{-1}_m$ обратное $x$ по модулю $m$, и $$ X_k = \frac{m_1 m_2 \cdots m_n}{m_k}.$$

Тогда решением уравнения является следующее соотношение: $$ x = a_1 X_1 {X_1}^{-1}_{m_1} + a_2 X_2 {X_2}^{-1}_{m_2} + \cdots + a_n X_n {X_n}^{-1}_{m_n}.$$

В этом решении, для каждого $k=1,2,\ldots,n$, $$a_k X_k {X_k}^{-1}_{m_k} \bmod m_k = a_k,$$ т.к. $$ X_k {X_k}^{-1}_{m_k} \bmod m_k = 1. $$

Так как все остальные числа суммы делятся на $m_k$, то они не влияют на остаток и $x \bmod m_k = a_k$.

Для примера, решние для $$ \begin{array}{lcl} x & = & 3 \bmod 5 \\ x & = & 4 \bmod 7 \\ x & = & 2 \bmod 3 \\ \end{array} $$ такое: $$ 3 \cdot 21 \cdot 1 + 4 \cdot 15 \cdot 1 + 2 \cdot 35 \cdot 2 = 263. $$

После нахождения $x$, мы можем найти ещё бесконечно много решений вида: $$x+m_1 m_2 \cdots m_n $$

Более подробно применение Китайской Теоремы об остатках можно посмотреть здесь: http://e-maxx.ru/algo/export_chinese_theorem

Разбор задач контеста

Перейдём к разбору задач последнего контеста.

1. Последняя цифра $A^B$

Требуется написать программу, которая находит цифру, на которую оканчивается число $A^B$.

Данную задачу можно решить через бинарное возведение в степень по модулю.

Вопросом является лишь то, какой выбрать модуль. Поскольку нам нужна только последняя цифра, то нам нужен модуль $10$.

#include <iostream>

using namespace std;

typedef unsigned long long ull;
typedef long long ll;

ull modpow(ull x, ull n, ull m){

    if(n==0)
        return 1 % m;

    ull u = modpow(x, n / 2, m);
    u = (u * u) % m;
    if (n % 2 == 1)
        u = (u * x) % m;
    return u;
}

int main() {
    ull a, b;
    cin >> a >> b;
    cout << modpow(a, b, 10);
}

Однако есть более интересные зависимости: https://www.geeksforgeeks.org/find-last-digit-of-ab-for-large-numbers/

2. Факториалы!!

Вычислить

$ n!!\dots!=n(n-k)(n-2k) \dots (n \pmod k)$, если n не делится на k,

$ n!!\dots!=n(n-k)(n-2k) \dots k$, если n делится на k (знаков ! в обоих случаях k штук).

Для этого требуется просто аккуратно посчитать факториал с заданным шагом.

#include <iostream>
#include <string>

typedef unsigned long long ull;
typedef long long ll;

using namespace std;

int main() {
    ll n, fact = 1;
    string s;

    cin >> n >> s;

    ll k = s.length();

    while (n > 0) { 
        fact *=n;
        n -= k;
    }
    cout << fact;
}

3. Последняя цифра N!

Нам требуется найти первую ненулевую цифру факториала $N! = 1*2*3*…*N$.

Пусть D(n) это последняя ненулевая цифра в $n!$.

Далее если в числе $n$ цифра, отвечающая за разряд "десятки" нечётная, то

D(n) = 4 * D(floor(n/5)) * D(последнний разряд n)

Иначе:

D(n) = 6 * D(floor(n/5)) * D(последний разряд n)

#include<bits/stdc++.h> 
using namespace std; 

// Запишем последние ненулевые числа для первых 10ти факториалов
ll dig[] = {1, 1, 2, 6, 4, 2, 2, 4, 2, 8}; 

ll lastNon0Digit(ll n) 
{ 
     if (n < 10) 
        return dig[n]; 

    if (((n/10)%10)%2 == 0) 
        return (6*lastNon0Digit(n/5)*dig[n%10]) % 10; 
    else
        return (4*lastNon0Digit(n/5)*dig[n%10]) % 10; 
} 

int main() 
{ 
    ll n;
    cin >> n;
    cout << lastNon0Digit(n); 
    return 0; 
}

Более полное доказательство: https://www.geeksforgeeks.org/last-non-zero-digit-factorial/

4. SpeedReaceE

В этой задаче нужно ответить на $1 ⩽ t ⩽ 10^5$ запросов. Каждый запрос состоит из двух целых чисел $2 ⩽ p ⩽ 10^9$ и $0 < a < p$, число $p$ является простым. На каждый запрос нужно вывести в отдельной строке целое число $0 < b < p$, такое что $a*a^{-1} \equiv 1 \pmod p$.

Здесь требуется в цикле выполнить Расширенный Алгоритм Евклида.

#include <iostream>
#include <vector>

using namespace std;

void gcdext (int a, int b, int &d, int &x, int &y)
{
    int s;
    if (b == 0)
    {
        d = a; x = 1; y = 0;
        return;
    }
    gcdext(b,a % b,d,x,y);
    s = y;
    y = x - (a / b) * y;
    x = s;
}

int inverse(int a, int n)
{
    int d, x, y;
    gcdext(a, n, d, x, y);
    if (d == 1) return x;
    return 0;
}

int main()
{
    // There's your code, if you dare...
    int t, i, p, a, res;
    cin >> t;
    vector <int> ans(t);
    for (i=0; i < t; i++)
    {
        cin >> p >> a;
        res = inverse(a, p);
        if (res < 0 || res >= p)
        {
            res+=p;
            res%=p;
        }
        ans[i] = res;
    }
    for (int j=0; j<t; j++)
    {
        cout << ans[j] << endl;
    }
    //Think about it later...
    return 0;
}

5. Степень

Для того чтобы проверить, как её ученики умеют считать, Мария Ивановна каждый год задаёт им на дом одну и ту же задачу – для заданного натурального $A$ найти минимальное натуральное $N$ такое, что $N$ в степени $N$ ($N$, умноженное на себя $N$ раз) делится на $A$. От года к году меняется только число $A$.

Вы решили помочь будущим поколениям. Для этого вам необходимо написать программу, решающую эту задачу.

Мы можем найти произведение всех простых множителей числа $A$. Тогда мы можем переписать искомое соотношение как:

$$(p_1^{\beta_1}*\dots*p_n^{\beta_n})^{p_1^{\beta_1}*\dots*p_n^{\beta_n}} = 0 \pmod {p_1^{\alpha_1} * \dots \ p_n^{\alpha_n}}$$

При этом нам не требуется перебирать все ${\beta_i}$, достаточно лишь те, которые меньше $30$, т.к. $2^{30} > 10^{9}$ (в этом случае ответом будет само число $A$).

Пусть $p_1 * \dots *p_n = n$, в итоге требуется найти такое $m$, что:

$$ n^{m*n^m} \equiv 0 \pmod A $$

а это равносильно:

$$ (m*n)^{n*m} \equiv 0 \pmod A $$

При этом $$ m \in [1;30] $$

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>

using namespace std;

typedef unsigned long long ull;
typedef long long ll;

ull modpow(ull x, ull n, ull m){

    if(n==0)
        return 1 % m;

    ull u = modpow(x, n / 2, m);
    u = (u * u) % m;
    if (n % 2 == 1)
        u = (u * x) % m;
    return u;
}

int main()
{
    ll a = 0;
    cin >> a;

    ll a1 = a;

    int n = 1;
    for (int i = 2; i*i <= a; ++i)
    {
        bool p = false;
        while (a % i == 0)
            a /= i, p = true;
        if (p)
            n *= i;
    }
    n *= a;

    if (n > 30)
        cout << n << endl;
    else
    {
        for (ll c = 1; c < 30; ++c)
        {
            if (modpow(c * n, c * n, a1) == 0)
            {
                cout << c * n << endl;
                break;
            }
        }
    }


    return 0;
}

6. Идемпотенты

Напишите программу, которая найдёт все идемпотенты по модулю $n$, где $n$ является произведением двух различных простых чисел $p$ и $q$.

Идемпотенты - это такие числа $x$, что:

$$ (x * x) \equiv x \pmod{n} $$

Поскольку число $n$ расскладывается в произведение $p$ и $q$ для начала нам требуется определить эти числа. Пусть при этом число $p \leq q$. Числа $0$ и $1$ будут всегда. В итоге требуется определить делятся ли на $p$ числа с интервалом $q$.

#include <iostream>
#include <cmath>

using namespace std;

int main() {
    int t;
    cin >> t;
    for (int i = 0; i < t; ++i) {
        int p, q, n;
        cin >> n;
        for (int j = 2; j < (int) sqrt(n) + 3; ++j) {
            if (n % j == 0) {
                p = j;
                q = n / p;
                break;
            }
        }
        cout << "0 1 ";
        for (int j = 1; j < p; ++j) {
            if ((q * j - 1) % p == 0) {
                cout << q * j << ' ';
            }
            if ((q * j + 1) % p == 0) {
                cout << q * j + 1 << ' ';
            }
        }
        cout << "\n";
    }
}

Важные организационные моменты

  1. Не забываем отмечаться на https://icpc.appmat.ru
  2. В воскресенье 6 октября пройдёт 1/8 финала ICPC в МЭИ.
    1. Если у вас есть команда, то регаемся по инструкции тут: https://vk.com/wall-182708012_128
    2. Если у вас нет команды, но вы хотите участвовать, то в выходные мы организуем контест, который поможет вам распределиться на команды.
  3. Пройдите опрос о занятиях в субботу, нам это важно, чтобы понимать сколько аудиторий резервировать у института: https://forms.gle/qGMHjMZJqXS5p2Vm6