Алгоритмы и структуры данных
August 31, 2021

2. Переборы. Вложенные циклы. Сложность циклических алгоритмов.


1. Перебор. Вложенные циклы.

Вложенные циклы.

Очень часто при реализации алгоритмов используются вложенные циклы – когда один цикл запускается внутри другого (например, чтобы перебрать элементы двумерного массива):

int[,] mass = new int[10, 10];
for (int i = 0; i < mass.GetLength(0); i++)
{
    for (int j = 0; j < mass.GetLength(1); j++)
    {
        команды;
    }
}

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

2. Сложность циклических алгоритмов.

Чтобы оценить сложность алгоритмов с вложенными циклами, необходимо найти самый большой порядок вложения (остальными можно пренебречь) и записать сложность О(Nz), где Z – максимальный порядок вложения, а N – количество итераций (повторов) цикла.
for ( int i = 0; i < n; i++ )
{
      for ( int j = 0; j < n; j++)
      {
             for ( int k = 0; k < n; k++)
             {
                     команды1;
              }
       }
 }
for ( int i = 0; i < n; i++)
{
       for ( int j = 0; j < n; j++)
       {
                     команды2;
        }
 }

Блок «команды1» вложен в три цикла for от 1 до n, а блок «команды 2» - в два цикла for (тоже от 1 до n).

Получим общую сложность алгоритма O(n3) + O(n2).

Но при достаточно больших n вторым слагаемым можно пренебречь.

Тогда сложность данного алгоритма будет равна O(n3).

Время работы циклических алгоритмов можно примерно оценить при любом N, учитывая, что за 1 секунду средненький компьютер выполняет примерно 1000000 операций в секунду.

Пример:

Давайте попробуем заставить компьютер «задуматься». Не будем использовать операторы, которые позволяют вычислить время работы программы (о них поговорим позже), просто оценим время визуально.

Простая программа:

Console.Write(("Введите число:"));
int n = Convert.ToInt32(Console.ReadLine());
int k = 0;

for (int i = 0; i<n; i++)
{
    for (int j = 0; j < n; j++)
    {
        k++;
    }   
}
Console.WriteLine("Расчет закончен!");

При вводе 10000 на моем компьютере (выше среднего) отрабатывает практически без пауз, 30000 – уже заметно, 50000 – 5-6 секунд. А дальше – грустно…

Получается, что если в игре зарегистрировано более 50000 игроков, использовать вложенные циклы «в чистом виде» просто нельзя!


Задачи и примеры их решения

1. Перебор подряд идущих чисел (задача про банкоматы)

Условие: Вас попросили написать программу для «однорукого бандита» (автомата для игры на деньги). Но писать алгоритм вероятности генерации победных комбинаций вам не доверили, зная вашу порядочность и честность. Ваша программа должна «подсказывать» автомату, какими купюрами (банкнотами разного номинала) можно выдать выигрыш игроку. На вход подается число S. Необходимо вывести все возможные варианты выдачи суммы ровно S рублей, если в банкомат закладывают купюры по 100, 50, 20, 10 и 5 рублей (мелочь от рубля и ниже в данном казино даже не учитывается).

Фрагмент кода:

Console.Write(("Введите сумму:"));
int s = Convert.ToInt32(Console.ReadLine());
int k = 0;
for (int k1 = 0; k1 <= s / 100; k1++)
{
    for (int k2 = 0; k2 <= s / 50; k2++)
    {
        for (int k3 = 0; k3 <= s / 20; k3++)
        {
            for (int k4 = 0; k4 <= s / 10; k4++)
            {
                for (int k5 = 0; k5 <= s / 5; k5++)
                {
                    if (k1 * 100 + k2 * 50 + k3 * 20 + k4 * 10 + k5 * 5 == s)
                    {
                        k++;
                        Console.WriteLine("Вариант №" + k + ": " + k1 + "- по 100 руб. " + k2 +
                                          "- по 50 руб. " + k3 + "- по 20 руб. " + k4 + "- по 10 руб. " +
                                          k5 + "- по 5 руб. ");
                    }
                }
            }
        }
    }
}

Вопросы к размышлению:

Предложите идеи ускорения данного алгоритма.

2. Удаление всех знаков препинания из предложения

Условие: В игре, конечно, есть игровой чат, за которым надо постоянно следить, пресекая попытки использования игроками ненормативной лексики. Все время следить за сообщениями достаточно утомительно. Попробуем написать программу, которая будет блокировать сообщения с «плохими» словами. Но сначала полезно немного преобразовать исходную строку чата, вводимую игроком - удалить из нее все знаки препинания и все буквы сделать строчными (маленькими). После этого будет проще искать «неправильные» слова.

Фрагмент кода:

string punctuationSymbols = ".,:;!?/*";

Console.WriteLine("Сообщение в чате:");
string userMessage = Console.ReadLine();
string tempString = "";

for (int i = 0; i < userMessage.Length; i++)
{
    if (userMessage[i] != '"') //В этой строке знак кавычки в одинарных апострофах
    {
        bool isPunctuationSymbol = false;
        for (int j = 0; j < punctuationSymbols.Length; j++)
        {
            if (userMessage[i] == punctuationSymbols[j])
            {
                isPunctuationSymbol = true;
                break;
            }
        }

        if (isPunctuationSymbol)
        {
            tempString += " ";
        }
        else
        {
            tempString += userMessage[i];
        }
    }
}

tempString = tempString.ToLower();
Console.WriteLine("Полученная строка:" + tempString);

Вопросы к размышлению:

  1. Оцените сложность данного алгоритма.
  2. Как изменится алгоритм, если надо удалить из строки все слова, написанные английскими буквами?

3. Контроль за «ненормативной» лексикой

Условие: Итак, знаки препинания удалены, все буквы – прописные. Теперь нам надо блокировать некоторые сообщения. Заведем словарь «плохих» слов. И если хоть одно из этих слов найдено в сообщении, сообщение должно блокироваться.

Фрагмент кода:

string[] words = {"дурак", "гад", "болван", "идиот", "редиска"};

tempString += " ";
bool hasForbiddenWord = false;
string word = "";
for (int i = 0; i < tempString.Length; i++)
{
    if (tempString[i] == ' ')
    {
        for (int j = 0; j < words.Length; j++)
        {
            if (word == words[j])
            {
                hasForbiddenWord = true;
            }
        }

        word = "";
    }
    else
    {
        word += tempString[i];
    }

    if (hasForbiddenWord)
    {
        break;
    }
}

if (hasForbiddenWord)
{
    Console.WriteLine("Сообщение заблокировано!");
}
else
{
    Console.WriteLine("Ваше сообщение будет отправлено!");
}

Вопросы к размышлению:

Как можно набирать «нехорошие» слова так, чтобы наш алгоритм их «не заметил»?


Итоги занятия

Сегодня мы поговорили про переборные алгоритмы со вложенными циклами, научились определять их сложность и примерное время работы, разобрали примеры, оценили возможность оптимизации алгоритмов.

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