1. Технология за състезателно програмиране и оценка на алгоритми

План:
Студентски състезания по програмиране
Стандарти на С++, компилатори, вход и изход
Вход и изход за много тестови примери
Пренасочване на входа и изхода от операционната система
Проверка на програмата в Хакерранк и от прeподавателя
Оценка на алгоритми (преговор)

Студентски състезания по програмиране


Стандарти на С++, компилатори

 Dev-C++, Code::Blocks за Windows, gcc за Linux, BSD и др. UNIX-ови ОС
Конзолни приложения, вход и изход

Стандартен вход и изход в С++

cin >> <име на променлива>;

int k;
cin >> k;

cout << <име на променлива>;

int ik = -10;
unsigned int uk = 10;
long lk = -1000000;
unsigned long ulk = 1000000; // 16 битов цял тип данни
double dk = 2.52;
cout << ik << " " <<uk << " " << lk << " "
     << ulk<< " " << dk << "\n";


Вход и изход за много тестови примери

- Четене на зададен брой числа или думи (низове без интервали)
int n;
cin >> n;
for (int i = 0; i < n; i++) { cin >> ... }


- Четене до края на файла за числа или думи
while (cin >> x) { ...}


- Четене до края на файла на редове (редица от числа; низ, съдържащ интервали)
string st;
while(getline(cin, st)) { ... }

- Четене всички числа от един ред, прочетен като низ
string st;
getline(cin, st);
int a[100], n = 0;
istringstream is(s);
while(is >> a[n]) n++;


Примери за различни входове.

Пренасочване на входа и изхода от операционната система

Изпълним файл (executable) с програма на C++ със стандартни вход (cin) и изход (cout):
- prog.exe (Windows)
- a.out (MacOS)

Текстов файл с входните данни за програмата: test1.inp

Изпълнение (стартиране) на програмата:
>prog < test1.inp > test1.sol (Windows)
$./a.out < test1.inp > test1.sol (MacOS)

Текстов файл, произведен от програмата при това изпълнение: test1.sol


Проверка на програма в Хакерранк и от прeподавателя

Решенията се проверяват с много примери, неизвестни за студента, включително гранични случаи.

В Хакерранк:
https://www.hackerrank.com/cscb325-2022 (Задачи)

Съобщения от изпълнение на програмата:
https://www.hackerrank.com/faq/after-submit

We run your code against hidden test cases. Depending on the output your code produced, you can get the following verdicts:

Accepted. Congratulations, your code passed all the test cases! It's time to solve a new challenge!

Wrong Answer. The output your code produced didn't match the output expected by the test case. Rethink your approach and think about whether you misunderstood the problem or missed a corner case.

Terminated due to timeout. Your code doesn't solve the problem efficiently enough! If you write a O(2n) solution when n = 100, it will surely time out and you're going to need to optimize your algorithm. The time limits are different for each language (some languages are slower than others), and you can see the limits for all the languages we support at our Environment page.

Runtime error/Segmentation Fault. Your code terminated unexpectedly. Did you overrun your array? Is your code trying to divide by zero?

Abort Called. Are you using too many resources? Maybe an array you created is too large and exceeds the memory limit, or an assert statement in your code is failing.

After you submit your code, hover your mouse cursor over the icon for each test case to view the verdicts and runtime for each test case your code was tested against.

Съобщения за грешки в други тестващи системи:
Проверка от преподавателя:
Проверка на програма f12345_1.cpp: с входни данни (много тестове примери) във файла 1.inp.

Пример за MacOS:
mini:CSCB325 nkirov$ ./ad.sh 1 12345

Скриптове  за проверка (файл ad.sh):

g++ f$2_$1.cpp
./a.out < $1.inp
rm a.out

clang++ -std=c++11 -stdlib=libc++ f$2_$1.cpp
time ./a.out < $1.inp
./a.out < $1.inp > $1s.sol
rm a.out
diff $1s.sol $1.sol


Тестване на програмата

- стандартни тестови примери;
- гранични и специалн тестови примери;
- тестови примери с минимални и максимални размерности на входа;
- НЕ се разглеждат примери, които са извън зададените в условието на задачата ограничения;
- НЕ се прави проверка за коректност на входа.

Практически съвети:
- оценка на границите на данните (четене на тестовите примери)
- необходими ли са масиви?
- размерности на масивите;
- избор на тип данни (int, unsigned, long, long long, double, long double);
- загуба на точност (артметични функции - sqrt);
- избор на алгоритъм - оценка с O-голямо. Дали е най-бързият?
- предварителни пресмятания (преди или след прочитане на входа, на част от входа);
- избор на примери за тестване на програмата;
- прецизиране на кода.

*** Оценка и сложност на алгоритми [1.4]  (преговор)

Три главни свойства на компютърен алгоритъм:
  • простота и елегантност;

  • Нека разгледаме следния програмен фрагмент:

    cin >> n;
    sum = 0;
    for (i=0; i<n; i++)
     for (j=0; j<n; j++) sum++;
     

    * Колко бързо ще работи горната програма, т.е. какви са критериите по които се определя бързината й?
    * Eкспериментално да проверим за колко време ще се изпълни програмата.
    * За да изследваме по-общо нейното поведение е ще я изпълним с различни стойности на
    n.

    * Резултатите са обобщени в следната таблица (могат да бъдат такива):
    Размер на входа
    n
    Време за изпълнение
    сек.
    10 10-6
    100 10-4
    1000 0.01
    104 1.071
    106 106.5
    108 10663.6
    * От таблицата се вижда, че когато увеличаваме n (размерността на входа) 10 пъти, времето за изпълнение се увеличава 100 пъти.
    * Времето за изпълнение е пропорционално на  g(n) = c1n2 + c2n + c3, където c1, c2, c3 са константи, които се определят от дадената част от програмата (виж по-долу).
     
    * Сравняване на две функции:
     g1(n)= 2n2 и g2(n)= 200n,
    които показват времето за изпълнение на два дадени алгоритъма А1 и A2, в зависимост от n

     
     

    * Асимптотично алгоритъмът A2 е по-бърз и неговата сложност е линейна, докато тази на A1 е квадратична.

    n g1(n) g2(n)
    1 2 200
    10 200 2000
    100 2.104 2.104
    1000 2.106 2.105
    104 2.108 2.106
    106 2.1012 2.108

     Размер на входните данни

    * Нека е дадена задача, в която размерът на входните данни е определен от дадено цяло число n.
    * Почти всички задачи, които ще разглеждаме, притежават това свойство.
    * Ще поясним последното като разгледаме няколко примера:

    Пример 1.
    Да се сортира масив с n елемента.
    Размерът на входните данни се определя от броя n на елементите на масива .

    Пример 2.
    Да се намери най-големият общ делител на a и b.
    В този пример размерът на входните данни се определя от броя на двоичните цифри (битовете) на по-голямото от числата a и b.

    Пример 3.
    Да се намери покриващо дърво на граф.
    В този случай характеризираме размера на входа с две числа: брой на върховете и брой на ребрата.

    Асимптотична нотация

    * Когато се интересуваме от сложността на алгоритъм най-често се интересуваме как ще работи при достатъчно голям размер n на входните данни.
    * При формалното оценяване на сложността на алгоритмите изследваме поведението им при "достатъчно голямо" n (клонящо към безкрайност).

    1. O(f) определя множеството от всички функции  g, които нарастват не по-бързо от f, т.е. съществува константа c > 0 такава, че g (n) <= cf(n), за всички достатъчно големи стойности на n.

    2. Theta (f) определя множеството от всички функции g, които нарастват толкова бързо, колкото и f (с точност до константен множител), т.е. съществуват константи c1 > 0 и c2 > 0 такава, че c1f(n) <=  g (n) <= c2f(n), за всички достатъчно големи стойности на n.

    3. Omega (f) определя множеството от всички функции g, които нарастват не по-бавно от f, т.е. съществува константа c > 0 такава, че g(n) >= cf(n), за всички достатъчно големи стойности на n.

    O(f): Свойства и примери

    * Нотацията О(f) е най-често използваната при оценка на сложност на алгоритми и програми.
    * По-важни свойства на О(f) (с ~ означаваме принадлежност):

    Нарастване на най-често използваните функции:

    Функция / n
    1
    2
    10
    100
    1000
    5
    5
    5
    5
    5
    5
    log n
    0
    1
    3.32
    6.64
    9.96
    n
    1
    2
    10
    100
    1000
    n log n
    0
    2
    33.2
    664
    9996
    n2
    1
    4
    100
    104
    106
    n3
    1
    8
    1000
    106
    109
    2n
    2
    4
    1024
    1030
    10300
    n!
    1
    2
    3628800
    10157
    102567
    nn
    1
    4
    1010
    10200
    103000

    ** Определяне на сложност на алгоритъм.
    * Намиране на функцията, определяща зависимостта между големината на входните данни и времето за изпълнение.
    * Винаги разглеждаме най-лошия случай - при най-неблагоприянти входни данни.

    - елементарна операция - не зависи от размера на обработваните данни - O(1) ;
    - последователност от оператори - определя се от асимтотично най-бавния -  f + g ~ max(O( f ), O(g));
    - композиция на оператори - произведение от сложностите - f (g) ~ O( f*g);
    - условни оператори - определя се от асимтотично най-бавния между условието и различните случаи;
    - цикли, два вложени цикъла, p вложени цикли - O(n), O(n2), O(np) .

    Оценка на сложността на следните цикли (колко пъти ще се изпълни цикъла в най-лошия случай):

    // 1
    for (i = 0; i < n; i++)
     for (j = 0; j < n; j++, sum++);
    // 2
    for (i = 0; i < n; i++)
     for (j = 0; j < n; j++) if (a[i] == b[j]) return;
    // 3
    for (i = 0; i < n; i++)
     for (j = 0; j < n; j++) if (a[i] != b[j]) return;
    // 4
    for (i = 0; i < n; i++)
     for (j = 0; j < n; j++) if (a[i] == a[j]) return;
    // 5
    for (i = 0; i < n; i++)
     for (j = 0; j < i; j++) sum++;
    // 6
    for (i = 0; i < n; i++)
     for (j = 0; j < n*n; j++) sum++;
    // 7
    for (i = 0; i < n; i++)
     for (j = 0; j < i*i; j++) sum++;
    // 8
    for (i = 0; i < n; i++)
     for (j = 0; j < i*i; j++)
       for (k = 0; k < j*j; k++) sum++;

    Логаритмична сложност.

    Да разгледаме цикъла:
    for (sum = 0, i = 0; i < n; i *= 2) sum++;
    Променливата i приема стойности 1, 2, 4, ..., 2k, ...  докато надмине n. Цикълът се изпълнява [log n] пъти. Сложността е O(log n).

    Изчисляване на сложност при рекурсия.
    * Двоично търсене в сортиран масив - рекурсивен алгоритъм. 

    int binary_search(vector<int> v, int from, int to, int a)
    {  
       if (from > to)
          return -1;
       int mid = (from + to) / 2;
       if (v[mid] == a)
          return mid;
       else if (v[mid] < a)
          return binary_search(v, mid + 1, to, a);
       else
          return binary_search(v, from, mid - 1, a);
    }

    * Броим обръщенията към елементите на масива.
    * В рекурсивната функция се разглежда средния елемент и се прави едно рекурсивно извикване с два пъти по-малък масив.
    * Следователно, ако T(n) е функцията, която задава броя на обръщенията, то T(n) = T(n/2) + 1.
    * От равенствата
                    T(n) = T(n/2) + 1 = T(n/4) + 2 = T(n/8) + 3 = ... = T(n/2k) + k
    получаваме за n = 2k, че  T(n) = T(1) + log n, т.е. сложността на алгоритъма е O(log n).

    Проблеми на асимптотичната нотация - зависимост от входните данни
    * Най-добър случай, най-лош случай, обща сложност (примери - алгоритми за сортиране).
    * Проблеми при асимптотичната нотация [1.4.11]
    Вижте още: Анализ на алгоритми
  • коректност;
  • бързодействие.
  •  рефлексивност: f ~ О( f );
  • транзитивност: ако f ~ О(g), g ~ О(h), то  f ~ О(h);
  • транспонирана симетрия:  ако f  ~ Omega (g), то g ~ O( f ) и обратно;
  • константите могат да бъдат игнорирани: за всяко k > 0, kF ~ О(F);
  • n, повдигнато в по-висока степен, нараства по-бързо: nr~ О(ns), за 0 < r < s.
  • нарастването на сума от функции се определя от най-бързо нарастващата от тях: f + g ~ max(O( f ), O(g));
  • ако f(n) е полином от степен d, то f ~ О(nd);
  • ако f нараства по-бързо от g, а g нараства по-бързо от h, то следва, че f нараства по-бързо от h.