Записи теоретических кусков

Сайт: Информатикс
Курс: Гимназия №1534
Книга: Записи теоретических кусков
Напечатано:: Гость
Дата: Суббота, 23 Август 2025, 19:39

Арифметика

Проверка на простоту

Рассмотрим множество натуральных чисел (с нулём) $\{0,\,1,\,2,\,3,\,\ldots\}$. В дальйнейшем под числами будем понимать именно натуральные числа (с нулём). Говорят, что число $n$ делится на число $d$, если найдётся такое число $k$, что $n = kd$. Число $n \ge 2$ называется простым, если оно делится только на само себя и на $1$. В противном случае число $n \ge 2$ называется составным.

Рассмотрим следующую задачу. На вход программе подаётся число $n \ge 2$. Требуется вывести prime, если оно является простым, и composite, если оно является составным.

Эту задачу можно решать, действую «по определению». Это означает, что мы явно проверим, на сколько чисел от $1$ до $n$ делится число $n$. Если делителей окажется ровно $2$, то перед нами простое число, а иначе — составное.

Приведём соответствующий код.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var
  n, num_divisors, i: integer;

begin
  read(n);
  num_divisors := 0; // количество делителей
  
  for i := 1 to n do 
    if n mod i = 0 then
      num_divisors := num_divisors + 1;
  
  if num_divisors = 2 then
    writeln('prime')
  else
    writeln('composite');
end.

Нетрудно понять, что наш алгоритм совершает порядка $n$ действий. Как говорят программисты, наш алгоритм работает за $O(n)$.

Оказывается, что можно существенно уменьшить количество действий, необходимых для проверки числа на простоту.

Теорема. Если число $n$ является составным, то у него имеется делитель $d$ такой, что $2 \le d \le \sqrt{n}$.

Доказательство. Предположим, что утверждение теоремы неверно. Это значит, что есть такое составное число $n$, все делители которого не лежат среди чисел $2,\,3,\,\ldots,\,\sqrt{n}$. Поскольку число $n$ составное, то найдутся такие $a$ и $b$, что $n = ab$. По предположению, $a \gt \sqrt{n}$ и $b \gt \sqrt{n}$, откуда $ab \gt \sqrt{n} \cdot \sqrt{n}$, т. е. $ab \gt n$. Мы пришли к противоречию.

Доказанная нами теорема позволяет вести цикл в строке 8 нашего алгоритма от 2 до $\sqrt{n}$. Таким образом, время работы нашего оптимизированного алгоритма составит $O(\sqrt{n})$, что позволит быстро проверять на простоту числа порядка $10^{16}$. Напомним, что для хранения таких чисел в Паскале следует использовать тип int64, а в C++ — long long.

Алгоритм Евклида

Определение. Число $d$ называется наибольшим общим делителем двух чисел $a$ и $b$, если $d$ делит $a$ и $b$, причём является наибольшим из подобных чисел. Обычно пишут $d = $ НОД$(a, b)$.

Пример:

Квадратичные сортировки

Постановка задачи

Одной из самых важных задач в информатике является задача сортировки произвольного набора элементов с заданных на ним порядком.

Например, пусть у нас имеется массив из $n$ чисел. Напомним, что объявление и считывание массива на паскале выглядит как-то так:

1
2
3
4
5
6
7
8
9
var
  a: array [1..1000] of integer; 
  // массив из не более чем 1000 целых чисел
  n, i: integer;

begin
  read(n); // считываем количество чисел
  for i := 1 to n do 
    read(a[i]); // считываем элементы массива

Итак, пусть нам ввели какие-нибудь числа. Скажем, 4 -5 9 2 7 1 6. При этом a[1] = 4, a[2] = -5 и т. д.

Наша задача: переупорядочить числа таким образом, чтобы они шли по возрастанию, если читать их слева направо. Иными словами, нам нужно написать код, после которого числа массива a будут лежать в такой последовательности: -5 1 2 4 6 7 9.

Сортировка выбором

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

Проиллюстрируем сказанное выше кодом.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var
  a: array [1..1000] of integer; 
  n, i, min_element: integer;

begin
  // предположим, что массив a уже считан
  min_element := a[1];
  for i := 2 to n do 
    if min_element > a[i] then 
      min_element := a[i];
  writeln('Minimum in array: ', min_element);

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

  • находим минимальное значение в массиве;
  • производим обмен этого значения со значением на первой неотсортированной позиции;
  • теперь сортируем таким же образом весь массив, исключив из рассмотрения уже отсортированные элементы.
Приведём код, который считывает массив, сортирует его и выводит на экран. Обратите внимание, что в данном случае нам при поиске минимума нужно хранить не сам минимальный элемент, а его индекс в массиве.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var
  a: array [1..1000] of integer;
  i, j, index_of_min_element, n: integer;
  temp: integer; // временная переменная для обмена значениями

begin
  read(n);
  for i := 1 to n do
    read(a[i]);
  
  for i := 1 to n - 1 do begin 
    // необходимо n - 1 раз выбрать минимум среди элементов 
    // a[i], a[i + 1], ..., a[n] и обменять его с элементом a[i] местами
    index_of_min_element := i;
    for j := i + 1 to n do
      if a[index_of_min_element] > a[j] then
        index_of_min_element := j;
    temp := a[index_of_min_element];
    a[index_of_min_element] := a[i];
    a[i] := temp;
  end;

  for i := 1 to n do
    write(a[i], ' ');
  writeln;
end.

Сортировка пузырьком

Сортировка выбором хороша и идейно проста, но на свете есть сортировка, которая ещё проще. Это сортировка пузырьком. Основная идея такова: $n - 1$ раз пробегаемся по массиву слева направо и просматриваем все пары соседних элементов. Если есть пара, в которой левый элемент больше правого, то мы меняем их местами. Заметим, что после первого прохода по массиву наибольший элемент как бы «всплывёт» на последнее место. После второго прохода по массиву то же самое сделает второй по величине элемент. Приведём код этой сортировки.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var
  a: array [1..1000] of integer;
  i, j, n: integer;
  temp: integer; // временная переменная для обмена значениями

begin
  read(n);
  for i := 1 to n do
    read(a[i]);
  
  for i := 1 to n - 1 do begin 
    for j := 1 to n - 1 do 
      if a[j] > a[j + 1] then begin
        temp := a[j];
        a[j] := a[j + 1];
        a[j + 1] := temp;
      end;
  end;

  for i := 1 to n do
    write(a[i], ' ');
  writeln;
end.

Приведём венгерский народный танец «сортировка пузырьком», который иллюстрирует рассматриваемый нами алгоритм.

Время работы

Основные операции в алгоритме сортировки пузырьком происходят в строках 13-17. Эти строки выполняются в двойном цикле от $1$ до $n$. Отсюда следует, что время работы сортировки пузырьком есть $O(n^2)$. Это значит, что сортировка пузырьком применима на массивах размера $10^4$, но неприменима на массивах порядка $10^5$.

Сортировка подсчётом

Сортировка подсчётом

Как мы выяснили, сортировка пузырьком умеет сортировать массив за $O(n^2)$. Сейчас мы научимся сортировать массивы специально вида гораздо быстрее.

Итак, предположим, что у нас имеется массив целых чисел из некоторого диапазона $[MINVALUE,\,MAXVALUE]$. Пусть также диапазон не слишком велик. Например, пусть $MAXVALUE - MINVALUE \le 10^7$. В этом случае мы можем подсчитать, сколько раз каждое значение из диапазона встречается в массиве. Затем мы можем по этой информации выписать исходный массив в отсортированном порядке.

Приведём код, который сортирует массив. Отметим, что такие параметры алгоритма, как MINVALUE и MAXVALUE, лучше выносить в секцию констант. В программировании принято имена констант записывать большими буквами.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const 
  MINVALUE = 0;
  MAXVALUE = 1000 * 1000;
  MAXN     = 1000 * 1000;

var
  n, i, j, k: longint;
  a: array [1..MAXN] of longint;
  count: array [MINVALUE..MAXVALUE] of longint;

begin
  read(n);

  for i := 1 to n do 
    read(a[i]);

  // обнуляем массив count
  for j := MINVALUE to MAXVALUE do
    count[j] := 0;

  for i := 1 to n do 
    count[a[i]] := count[a[i]] + 1;

  i := 1; // позиция ячейки в массиве чисел, 
          // которая будет заполняться следующей
  for j := MINVALUE to MAXVALUE do
    for k := 1 to count[j] do begin
      a[i] := j;
      i := i + 1;
    end;

  for i := 1 to n do
    write(a[i], ' ');
end.

Оценим время работы этой программы. Обозначим размер диапазона за $m$, т. е. $m := MAXVALUE - MINVALUE + 1$. Цикл в строчках 18-19 выполняется за $O(m)$. Цикл в строчках 21-22 выполняется за $O(n)$. В строках 26-30 суммарно совершается $n + m$ операций. Итого время работы сортировки подсчётом составляет $O(n + m)$, где $n$ — размер массива, а $m$ — размер диапазона чисел. Серьёзное улучшение по сравнению с алгоритмом сортировки пузырьком!