Индуктивно-рекурзивна конструкција алгоритама је један од основних приступа у конструкцији алгоритама који се ослања на решавање проблема тако што га сведемо на мање инстанце истог проблема. На овом месту треба нагласити да ово звучи као наивна или школска дефиниција рекурзије, али суштина је потпуно другачија. Уместо на рекурзију, при конструкцији алгоритма ми се ослањамо на математичку индукцију, тј. на веру да ако знамо да решимо проблем димензије онда га можемо решити и за случај димензије .
Концептуално, приликом дизајнирања алгоритма овом техником ми се ослањамо на индуктивни начин размишљања, док рекурзија представља најчешће техничко решење којим имплементирамо алгоритам. Формално:
Практично правило: Никада се покушавамо да решимо проблем одједном, већ се увек фокусирамо само на то како да повежемо последњи корак са оним што је већ урађено.
У пракси, имамо три кључна корака у конструкцији алгоритма:
Илустроваћемо рекурзивну конструкцију алгоритама на примеру сортирања уметањем (енгл. insertion sort). Формално, потребно је да спроведемо следеће кораке:
Имплементација која прати описани поступак је у наставку.
static void insertion_sort(int[] niz, int n) {
// провера базног случаја
if (n == 0)
return;
// примена индуктивне хипотезе
insertion_sort(niz, n - 1);
// конструкција алгоритма (индуктивни корак)
int poslednji = niz[n-1]
int j = n - 2;
while (j >= 0 && niz[j] > poslednji) {
niz[j+1] = niz[j];
j--;
}
niz[j+1] = poslednji;
}Индуктивно-рекурзивна конструкција алгоритама има неколико предности и мана:
У пракси је индуктивно-рекурзивна конструкција алгоритама прва идеја којом се тешки проблеми решавају. Касније се та решења пребацују у итеративни облик да би се смањила меморијска сложеност алгоритма и уколико постоји потреба даље се унапређују неким другим техникама за конструкцију алгоритама. Индуктивно-рекурзивну конструкцију алгоритама можемо грубо да поделимо према три уобичајена приступа у конструкцији алгоритама:
Репна рекурзија је посебан облик рекурзије у којој је рекурзивни позив последња ствар коју функција ради пре него што врати вредности. Предност репне рекурзије у односу на обичну рекурзију огледа се у томе што нема преосталих операција након рекурзивног позива, тј. рекурзивни позив је уједно и коначан резултат функције.
Да бисмо илустровали разлику размотрићемо рекурзивно израчунавање факторијела датог природног броја . Израчунавање факторијела можемо описaти следећом рекурентном релацијом:
Имплементација директно следи:
На пример, да бисмо израчунали потребно је да извршимо следећи низ функционалних позива:
Тек у последњем функционалном позиву, тј. тек када низом свођење на проблеме мање димензије стигнемо до базног случаја, сазнајемо неку конкретну вредност. Сада се ходом уназад, тј. одмотавањем рекурзивних позива, обједињавају решења мањих проблема у решења већих:
Иако синтаксно елегантан, наш код није претерано ефикасан ни у временском ни у просторном смислу. Сваки рекурзивни позив отвара нови стек оквир, па је укупна просторна сложеност . Када је временска сложеност у питању треба приметити да извршњавање нашег програма има две одвојене фазе. Прва фаза је свођење проблеме до соновног случаја, што је шематски обележено стрелицама на доле, а друга фаза је одмотавање рекурзије, тј. конструкција решења, што је шематски обележено стрелицама на горе. Одавде се лако закључује да је временска сложеност решења .
Временска сложеност се може снизити тако што бисмо рачунање могли да изменимо. Уместо да рачунамо у фази одмотавања, можемо да сваком следећем рекурзивном позиву проследимо до сада израчунати фрагмент решења. Тиме ћемо добити комплетно решење проблема када стигнемо до базног случаја. Да бисмо то постигли, треба да уведемо помоћну променљиву (акумулатор) коју ћемо прослеђивати рекурзивном позиву. Решење сада постаје:
static int faktorijel_2(int n, int acc = 1) {
if (n == 1)
return acc;
return faktorijel_2(n-1, n * acc);
}Приметимо да у овом решењу нема никакве потребе за одмотавањем рекурзије. Заједно са последњим стек оквиром смо добили коначно решење, тј. резултат. За функцију faktorijel_2 се каже да је репно рекурзивна. Шематски, извршавање репно рекурзивне функције можемо приказати на следећи начин:
Репна рекурзија може да донесе значајно снижавање просторне сложености, јер одређени компајлери могу да уоче својство репне рекурзије и да уместо отварања нових стек оквира за сваки рекурзивни позив, користе постојећи. Тиме просторна сложеност постаје константна, а временска постаје , јер неће бити потребе за одмотавањем рекурзије. Овакво понашање компајлера је карактеристично за функционалне програмске језике (F#, Haskell, Scala) и није толико често када се ради о програмском језику C#.
Суштинске разлике измећу обичних и репно рекурзивних функција су следеће:
Напомена: У свим решењима бавићемо се искључиво алгоритмима, док учитавање и штампање резултата препуштамо читаоцу за вежбу. Формално, треба да
Ако су дати цели бројеви и , написати рекурзивну функцију која исписује редом све целе бројеве у задатом интервалу у:
Улаз: У првој линији стандардног улаза налази се цео број , а у другој је цео број .
Излаз: На стандардном излазу исписују се редом сви цели бројеви из интервала, у свакој линији по један број, у траженом редоследу.
Да бисмо решили задатак полазни проблем можемо да поделимо на последњи број и на префикс. тј. смањићемо величину проблема за један. Да бисмо одштампали вредности у сегменту растуће, то значи следеће:
Код који прати описану конструкцију је у наставку:
static void zad1_rastuci(int a, int b)
{
// провера основног случаја
if (a > b)
return;
// прво штампамо све бројеве пре текућег броја
zad1_rastuci(a, b - 1);
// затим штампамо текући број
Console.WriteLine(b);
}Да бисмо одштампали вредности у сегменту опадајуће, то значи следеће:
Код који прати описану конструкцију је у наставку:
static void zad1_opadajuci(int a, int b)
{
// провера основног случаја
if (a > b)
return;
// прво штампамо текући број
Console.WriteLine(b);
// затим штампамо све бројеве пре текућег броја
zad1_opadajuci(a, b - 1);
}Посебно обратите пажњу на редослед извршњавања рекурзивног позива и текуће операције. Ако је рекурзивни позив пре текуће операције, то је еквивалентно обрађивању података унапред. Ако је рекурзивни позив након текуће операције, то је еквивалентно обрађивању података уназад.
Написати рекурзивну функцију која израчунава -ти степен реалног број . Рекурзивна функција треба да одреди тражени степен:
Улаз: У првој линији стандардног улаза уноси се реалан број . У следећој линији стандардног улаза уноси се природан број .
Излаз: На стандардни излаз исписати тражени степен .
Задатак можемо врло лако да решимо смањивањем величине проблема за један. Тиме ћемо добити следећу рекурентну релацију која описује степеновање:
Рекурзивна имплементација директно прати рекурентну релацију.
static double zad2(double x, int n)
{
// базни случај
if (n == 0)
return 1;
// конструкција решења
return x * zad2(x, n - 1);
}Знајући за повећану меморијску сложеност и повећану временску сложеност као последице рекурзивне имплементације решења, можемо да имплементирамо репно-рекурзивну верзију функције. Уместо да вредности међурезултата чувамо имплицитно на системском стеку, можемо их експлицитно прослеђивати функционалним позивима кроз аргументе функције. Репно-рекурзивна имплементација је у наставку:
static double zad2_repna_rek(double x, int n, double s = 1)
{
// базни случај
if (n == 0)
return s;
// конструкција решења
return zad2_repna_rek(x, n - 1, s * x);
}Временска и просторна сложеност приказаних решења су .
Задатак можемо да решимо и другачијом поделом проблема на подпроблеме, тј. можемо добити решење које је логаритамске сложености. Покушаћемо да уочимо правилност која постоји приликом израчунавања . Досадашња идеја би рачунала овај резултат као:
Број множења у израчунавању степена се може значајно смањити. На пример, можемо приметити следеће
Дакле, ако бисмо знали , тада бисмо добили у само једном множењу. На сличан начин, могли бисмо да применимо низ свођења:
Имајући ово у виду, алгоритам можемо да конструишемо на следећи начин:
Имплементација директно следи из рекурентне релације:
static double zad2_efikasno(double x, int n)
{
// вазни случај
if (n == 0)
return 1;
// конструкција решења
double x2 = zad2_efikasno(x, n/2);
double rez = x2*x2;
// провера парности експонента
return (n % 2 == 0) ? rez : (x*rez);
}Приметите да у имплементацији само једном рекурзивно рачунамо и затим користимо ту вредност приликом рачунања . Временска и просторна сложеност решења су .
Корисник уноси један природан број . Написати рекурзивну функцију која одређује:
Улаз: Са стандардног улаза се уноси природан број ().
Излаз: На стандардни излаз исписати тражене вредности.
Да бисмо одредили збир цифара, можемо да применимо исти поступак као и до сада. Смањићемо величину полазног проблема за један, тј. претпоставићемо да знамо да израчунамо збир префикса и решење ћемо добити сабирањем тог резултата са цифром најмање тежине у броју. Описани поступак можемо да опишемо рекурентном релацијом:
Имплементација директно следи из рекурентне релације:
static int zbir_cifara(int n)
{
// базни случај
if (n == 0)
return 0;
// рекурзивно одређујемо збир цифара
return n%10 + zbir_cifara(n/10);
}Да бисмо одредили број цифара, можемо да применимо сличан поступак као за рачунање збира цифара. Смањићемо величину полазног проблема за један, тј. претпоставићемо да знамо да израчунамо број цифара у префиксу и решење ћемо добити сабирањем тог резултата са јединицом. Описани поступак можемо да опишемо рекурентном релацијом:
Имплементација директно следи из рекурентне релације:
static int broj_cifara(int n)
{
// базни случај
if (n == 0)
return 0;
// рекурзивно одређујемо број цифара
return 1 + broj_cifara(n/10);
}Да бисмо одредили максималну цифру броја, смањићемо величину полазног проблема за један, тј. претпоставићемо да знамо да израчунамо максимум у префиксу и решење ћемо добити упоређивањем тог резултата са цифром најмање тежине. Решење проблема ће бити већи од та два броја. Треба само водити рачуна да је базни случај једноцифрени број. Описани поступак можемо да опишемо рекурентном релацијом:
Имплементација директно следи из рекурентне релације:
static int max_cifra(int n)
{
// базни случај
if (0 <= n && n < 10)
return n;
// рекурзивно рачунамо решење префикса
int max = max_cifra(n / 10);
int c = n % 10;
// упоређујемо текућу цифру са резултатом префикса
return c > max ? c : max;
}Аналогно одређујемо минималну цифру.
static int min_cifra(int n)
{
// базни случај
if (0 <= n && n < 10)
return n;
// рекурзивно рачунамо решење префикса
int min = min_cifra(n / 10);
int c = n % 10;
// упоређујемо текућу цифру са резултатом префикса
return c < min ? c : max;
}Да бисмо одредили колико различитих цифара има у запису броја, треба нам елегантан начин да одржавамо информацију колико смо до сада нашли различитих цифара. Најефикаснија структура података за тако нешто је HashSet(), јер омогућава додавање и проверу да ли је елемент у скупу у константном времену.
С обзриом да решење конструишемо од цифре најмање тежине ка цифри највеће тежине, природно је да текући резултат пропагирамо кроз аргументе функције, па ће имплементација бити репно-рекурзивна.
static int broj_razlicitih_cifara(int n)
{
// помоћна структура у којој чувамо цифре на које смо наишли
HashSet<int> cifre = new HashSet<int>();
// позив помоћне функције која рачуна број различитих цифара
return broj_razlicitih_cifara(n, cifre, 0);
}
static int broj_razlicitih_cifara(int n, HashSet<int> cifre, int s)
{
// када искористимо све цифре, резултат је у аргументу s
if (n == 0)
return s;
// издвајамо цифру и проверавамо да ли је у скупу
int c = n / 10;
if (cifre.Contains(c) == false)
{
// ако није, додајемо је у скуп и увећавамо бројач
cifre.Add(c);
s = s + 1;
}
// рекурзивно одређујемо број различитих цифара у префиксу
return broj_razlicitih_cifara(n/10, cifre, s);
}Временска и просторна сложеност свих приказаних решења су линеарне.
Перици недостаје само сличица са бројем да би попунио читав албум. Перица је у својој колекцији поседовао један изузетно редак дупликат и договорио се са колегом скупљачем сличица да ће му дати свој вредан дупликат ако му колега преда све своје дупликате у замену. Колега је пристао на такву замену и Перица је добио гомилу сличица. Замолио нас је да напишемо програм који треба да утврди да ли се у добијеном скупу сличица налази тражена сличица .
Улаз: У првој линији стандардног улаза налази учитава се природан број , у следећој линији се учитава природан број () који представља укупан број сличица које је Перица добио у замени, а затим се у свакој од наредних линија стандардног улаза учитава по један природан број.
Излаз: У једној линији стандарног излаза исписати Da ако је Перица добио тражену сличицу, иначе исписати Ne.
Из текста задатка је јасно да се ради о обичној линеарној претрази. Проблем можемо решити свођењем на проблем који је истог типа, али за један мањи. Упоређујемо последњи елемент низа са траженом вредности. Ако смо нашли тражену вредност, прекидамо даљу претрагу. Ако нисмо нашли, претрагу рекурзивно настављамо у префиксу низа и враћамо резултат претраге. Базни случај је празан низ.
Рекурзивна имплементација линеарне претраге је у наставку:
static int linearna_pretraga(int[] niz, int n, int x)
{
// базни случај, празан низ
if (n == 0)
return -1;
// ако смо нашли елемент, прекидамо претрагу
if (niz[n-1] == x)
return n - 1;
// ако нисмо, резултат ће бити исход претраге у префиксу
return linearna_pretraga(niz, n-1, x);
}Временска и просторна сложеност су линеарне.
Перица је у продавници купио артикала. Написати програм који ће одредити укупан износ рачуна, као и цену најскупљег артикла који је Перица купио.
Улаз: Са стандардног улаза се учитава број (), а затим се у наредних линија учитавају реални бројеви који представљају цене артикала које је Перица купио.
Излаз: На стандардни излаз исписати два реална броја раздвојена једним размаком, укупан рачун и цену најскупљег артикла редом.
Очигледан и исправан приступ јесте да напишемо две рекурзивне функције. Једну за израчунавање износа рачуна, тј. збира целог низа и другу за одређивање најскупљег артикла. Поступак којим то радимо је идентичан поступку којим смо рачунали збир цифара и максималну цифру броја. Разлика је само у томе, што као аргумент функције морамо да проследимо текућу дужину префикса за који треба да израчунамо тражене статистике. Решење је у наставку:
static double max(double[] niz, int n)
{
// базни случај
if (n == 1)
return niz[0];
// конструкција решења
int m = max(niz, n - 1);
return niz[n-1] > m ? niz[n-1] : m;
}
static double zbir(double[] niz, int n)
{
// базни случај
if (n == 0)
return 0;
// констукција решења
return niz[n-1] + zbir(niz, n - 1);
}С обзиром да смо упознати са концептом репно-рекурзивних функција можемо да напишемо једну репно-рекурзивну функцију која ће у једном пролазу одредити обе статистике. Као и да сада, у случају репно-рекурзивних функција, међурезултате ћемо прослеђивати као аргументе функционалним позивима. Решење је у прилогу:
static Tuple<double, double> zbir_max(double[] niz, int n,
double zbir = 0, double max = double.MinValue)
{
// базни сллучај у којем враћамо комплетно решење
if (n == 0)
return Tuple.Create(zbir, max);
// смањивање проблема за један и пропагација међурезултата
int m = niz[n-1] > max ? niz[n-1] : max;
return zbir_max(niz, ni-1, zbir + niz[n-1], m);
}Напомена: Иако репно-рекурзивно решење на први поглед делује елегантније, оно није исправно, јер не прати правила лепог кодирања. Функције које пишемо треба да решавају искључиво један проблем, а не да буду “франкенштајни” који решавају више проблема истовремено. Иако примамљива, франкенштајн решења су тешка за дебаговање и одржавање.
У школи малих жутих мрава наставник је прегледао контролни задатак. Прво је прегледао ђаке који су радили групу , а затим оне који су радили групу , средио је резултате за сваку групу и мраве поређао на основу броја поена који су освојили. Напиши програм који му помаже да од уређеног списка ученика који су радили задатке из групе и од уређеног списка ученика који су радили задатке из групе добије јединствен уређен списак свих ученика.
Улаз: Са стандардног улаза се уноси број ђака који су радили групу (), а затим неопадајуће сортиран низ поена тих ђака (елементи су у једној линији, раздвојени са по једним размаком). Након тога се уноси број ђака који су радили групу (), a затим неопадајуће сортиран низ поена тих ђака (елементи су у једној линији, раздвојени са по једним размаком).
Излаз: На стандардни излаз исписати неопадајуће сортирани низ поена свих ђака заједно, раздвојених са по једним размаком.
Итеративна имплементација алгоритма за обједињавање сортираних низова нам је позната од раније, јер смо овај проблем већ решили уз помоћ два показивача. Вежбе ради, решићемо исти проблем индуктивно-рекурзивном конструкцијом алгоритама.
Нека је са обележена дужина низа и нека је са обележена дужина низа , тада ће укупна дужина низа бити , што је заправо величина нашег проблема. Конструкција решења може бити следећа:
Да бисмо имплементирали алгоритам, потребна су нам три бројача:
У сваком рекурзивном позиву увећаваћемо за један и тачно један од бројача и . Рекурзивно решење је у наставку:
static void objedini(int[] a, int[] b, int[] c, int i = 0, int j = 0, int k = 0)
{
if (i < a.Length && j < b.Length)
{
if (a[i] < b[j]) {
c[k] = a[i];
objedini(a, b, c, i + 1, j, k + 1);
}
else {
c[k] = b[j];
objedini(a, b, c, i, j + 1, k + 1);
}
}
else if (i < a.Length)
{
c[k] = a[i];
objedini(a, b, c, i + 1, j, k + 1);
}
else if (j < b.Length)
{
c[k] = b[j];
objedini(a, b, c, i, j + 1, k + 1);
}
}Треба приметити да у рекурзивној верзији решења, имплицитно користимо системски стек да бисмо симулирали извршавање петље. У пракси, наравно ово никада не бисмо радили. Рекуривно решење је овде наведено само као илустрација размишљања.
Уносе се масе предмета, одредити разлику суме маса предмета до првог појављивања предмета највеће масе и суме маса предмета после првог појављивања предмета највеће масе (предмет највеће масе није укуључен ни у једну суму).
Улаз: У првој линији стандардног улаза налази се број предмета . Свака од наредних линија садржи по један природан број из интервала , ти бројеви представљају масе сваког од предмета.
Излаз: У првој линији стандарног излаза приказати тражену разлику маса.
Као и до сада, задатак можемо решити свођењем на проблем чија је димензија за један мања од полазног проблема. Потребно је да приликом тог свођења одржавамо текући максимум и збирове пре и после максимума. Дакле, кроз низ ћемо пролазити уназад и резултате елемената које смо до сада прошли ћемо одржавати кроз аргументе функције. Задатак можемо врло елегантно решити репно-рекурзивном функцијом.
Приликом разматрања текућег елемента на позицији у низу, имамо следеће познате информације:
Конструкција је следећа:
Репно-рекурзивно решење је у наставку:
static Tuple<int,int> zbirPrePosleMax(int[] niz, int n, int max, int zbirPre, int zbirPosle)
{
// ако смо прошли кроз све елементе, решили смо проблем и враћамо резултат
if (n == 0)
return Tuple.Create(zbirPre, zbirPosle);
// упоређујемо последњи елемент и максимум суфикса.
// ако је текући елемент мањи или једнак
if (niz[n-1] <= max)
// ажурирамо вредност променљивих и рекурзивно обрађујемо префикс низа
return zbirPrePosleMax(niz, n - 1, max, zbirPre + niz[n-1], zbirPosle);
else
// ако је текуђи елемент већи од максимума суфикса, ажурирамо максимум,
// ажурирамо вредности збирова и рекурзивно обрађујемо префикс низа
return zbirPrePosleMax(niz, n-1, niz[n-1], 0, zbirPre + niz[n-1] + zbirPosle)ч
}
static Tuple<int,int> zbirPrePosleMax(int[] niz, int n)
{
// на почетку, максимум је једнак последњем елементу низа и збирови су 0
return zbirPrePosleMax(niz, n-1, niz[n-1], 0, 0);
}Дате су просечне оцене ученикa једног одељења. Написати програм којим се одређује просек просечних оцена свих одличних ученика тог одељења.
Улаз: Прва линија стандарног улаза садржи природан број који представља број ученика. У наредних линија налази се по један реалан број из интервала . Ти бројеви представљају просеке ученика.
Излаз: У првој линији стандарног излаза приказати просек просечних оцена одличних ученика одељења заокружен nа две децимале. Ако одличних ученика нема приказати -.
Примењујемо стандардну конструкцију смањи за један. Уколико последњи елемент низа представља одличан просек, додајемо га на текући збир и рекурзивно обрађујемо префикс. Ако последњи елемент није одличан просек, задржавамо стари збир и прелазимо на обрађивање префикса. Како смо прошли кроз цео низ, добијени збир треба да поделимо бројем одличних.
С обзиром да низ обрађујемо уназад и да све време ажурирамо текуђе резултате, репно-рекурзивна функција је решење.
static double prosek_odlicnih(double[] niz, int n, double zbir = 0, int brojOdlicnih = 0)
{
// када прођемо кроз цео низ, рачунамо просек.
if (n == 0)
return n != 0 ? (zbir/brojOdlicnih) : 0;
// ако је последњи елемент одличан
if (niz[n-1] >= 4.5)
// увећавамо збир и број одличних и рекурзивно обрађујемо префикс
return prosek_odlicnih(niz, n - 1, zbir + niz[n-1], brojOdlicnih + 1);
else
// ако није, не мењамо старе вредност и рекурзивно обрађујемо префикс
return prosek_odlicnih(niz, n - 1, zbir, brojOdlicnih);
}Становници једне дугачке улице желе да одреде положај на којем ће бити направљена антена за мобилну телефонију. Пошто желе да локацију одреде на најправеднији могући начин, договорили су се да антену саграде на месту на ком ће збир растојања свих оних који се налазе лево од антене до ње, бити једнак збиру растојања свих оних који се налазе десно од антене до ње. Ако су познате координате свих кућа у улици (можемо замислити да су то координате тачака на једној правој), написати програм који одређује положај антене.
Улаз: Са стандардног улаза у првој линији се уноси природан број који представља број станара, а у наредних линија реални бројеви (од до ) који представљају координате станара ( координате тачака на оси).
Излаз: На стандардни излаз исписати један реалан број који представља тражени положај антене (допуштена је толеранција грешке ).
Једно могуће решење је уз помоћ бинарне претраге оптималне вредности. Треба да поставимо антену тако да разлика растојања са леве и растојања са десне стране буде што је могуће мања. Прво треба да одредимо интервал у којем вршимо претрагу. Лева граница интервала биће најмања координате куће, а десна граница интервала ће бити максимална координата куће.
Након одређивања граница, потребно је да проблем прво преведемо у проблем одлучивања. Дакле, за дати положај антене треба да одредимо да ли је то решење исправно или не. Проблем који постоји овде јесте на који начин усмерити претрагу. Можемо да користимо разлику збира растојања лево од антене и збира растојања десно од антене. Ако је та разлика позитивна, тада антену треба да померимо у лево, ако је разлика негативна, тада антену треба да померимо у десно. Овим добијамо поделу простора претраге која има следећи облик:
И наше решење ће заправо бити преломна тачка. Решење проблема које користи бинарну претрагу је у наставку:
static double max_niza(double[] niz, int n)
{
if (n == 1)
return niz[0];
double max = max_niza(niz, n-1);
return max > niz[n-1] > max ? niz[n-1];
}
static double min_niza(double[] niz, int n)
{
if (n == 1)
return niz[0];
double min = min_niza(niz, n-1);
return min <> niz[n-1] > min ? niz[n-1];
}
static double razlike_rastojanja(double[] niz, int n, double m, double zbirLevo, double zbirDesno)
{
// ако смо прошли кроз цео низ, враћамо разлику збирова
if (n == 0)
return zbirLevo - zbirDesno;
// у зависности од положаја антене и куће, увећавамо одговарајући збир
if (niz[n-1] < m)
return razlike_rastojanja(niz, n - 1, zbirLevo + (m - niz[n-1]), zbirDesno);
else
return razlike_rastojanja(niz, n-1, zbirLevo, zbirDesno + (niz[n-1] - m));
}
static double polozaj_antene(double[] niz, double l, double d)
{
// ако су лева и десна граница довољно близу, одредили смо решење
if (Math.Abs(l - d) < 10e-6)
return (l + d)/2;
// одређујемо средину интервала
double m = l + (d-l)/2;
// рачунамо разлику растојања за дати положај антене
double razlika = razlike_rastojanja(niz, niz.Length, m, 0, 0);
// ако је разлика довољно мала, одредили смо решење
if (Math.Abs(razlika) < 10e-6)
return m;
// ако је разлика негативна, претрагу настављамо у десној половини
else if (razlika < 0)
return polozaj_antene(niz, m, d);
// ако је разлика позитивна, претрагу настављамо у левој половини.
else
return polozaj_antene(niz, l, m);
}
static double polozaj_antene(double[] niz)
{
// одређујемо границе интервала претраге
double min = min_niza(niz, n);
deouble max = max_niza(niz, n);
// решавамо проблем
return polozaj_antene(niz, min, max);
}Све функције у претходном решењу су написане рекурзивно ради вежбе. Укупна временска сложеност приказаног решења је , а просторна је . Итеративно решење је просторно ефикасније, док временска сложеност остаје непромењена.
Задатак можемо да решимо и значајно једноставније, ако се мало детаљније позабавимо анализом проблема. Ако са обележимо тренутни положај антене, наш задатак је да одредимо такво да разлика растојања лево и десно буде 0, тј.
Збирови су коначни, па можемо сва појављивања да пребацимо на једну страну и сва појављивања да пребацимо на другу страну. Ако број сабирака са леве стране обележимо са и број сабирака са десне стране обележимо са , добићемо:
Јасно је да важи и да са десне стране имамо збир свих елемената низа, тј.
Прецизније, које је решење проблема је заправо просек координата:
Репно-рекурзивна функција која одређује просек је дата у наставку:
static double polozaj_antene(int[] niz, int n, double zbir = 0) {
if (n == 0)
return zbir / niz.Length;
return polozaj_antene(niz, n - 1, zbir + niz[n-1]);
}Временска и просторна сложеност упрошћеног решења су линеарне.
Грејов кôд реда подразумева ређање свих -тоцифрених бинарних записа тако да се свака два суседна записа разликују тачно у једном биту (при чему ово важи и за први и последњи запис, тако да се може сматрати да су сви записи поређани у круг). Грејов кôд дужине садржи само један елемент и то празну ниску. Грејов кôд дужине се може добити од кода дужине тако што се испред сваког броја у коду дужине допише цифра , затим се редослед елемената у коду дужине обрне и на сваком броју се на почетак допише цифра и два тако добијена низа бројева се споје. Нпр. Грејов кôд реда је:
На основу претходног поступка добијамо Грејов кôд реда .
Написати програм који за дату дужину кода и дату позицију одређује бинарни број који се налази на позицији у коду дужине .
Улаз: Са стандардног улаза се учитава дужина кода и позиција .
Излаз: На стандардни излаз исписати тражени бинарни број.
За унето , Грејов код ће бити дужине , па мора важити . Функцију која одређује -ти елемент Грејовог кода можемо лако дефинисати рекурзивно:
Ради једноставности, израчунавање степена двојке ћемо радити битовским операцијама и резултате ћемо представљати као стрингове. Стрингови са којима радимо су кратки, па надовезивање можемо посматрати као константне операције. Рекурзивно решење је у наставку:
Од три дата штапа, на једном је дискова различитих величина, а остала два су празна. Дискови на првом штапу су поређани по величини, то јест тако да је диск величине на дну, на њему је диск величине итд. све до диска величине , који је на врху. Потребно је дискове преместити са првог на трећи штап користећи што мање премештања. При томе треба премештати дискове један по један и стављати само мањи диск на већи, а никако обрнуто (није дозвољено стављати већи диск преко мањег). На пример, ако је , редослед премештања треба да буде:
Написати програм који за дати број дискова исписује редослед премештања.
Улаз: Са стандардног улаза се учитава број штапова .
Излаз: На стандардни излаз за свако премештање једног диска исписати по један ред, у коме се наводи редни број штапа са чијег врха се диск премешта и редни број штапа на чији врх се диск премешта, раздвојене једним размаком.
Као и до сада, задатак можемо да решимо рекурзивно техником смањивањa за један. Претпоставићемо да знамо да решимо задатака за мање од дискова. С обзиром да су нам дата три штапа, на једном од њих морају бити поређани дискови, на други треба да пребацимо дискове и трећи штап ћемо користити као помоћни.
Нека су дискови на штапу 1 и нека је задатак да их пребацимо на штап 3 користећи штап 2 као помоћни. Ослонићемо се на индуктивну претпоставку која каже да умемо да пребацимо дискова и применићемо следећи поступак:
Функција мора имати 4 аргумента: број дискова, редни број штапа са којег пребацујемо дискове, редни број штапа на који пребацујемо штапове, редни број штапа који користимо као помоћни.
Рекурзивна функција која решава проблем је у наставку:
static void hanojske_kule(int n, int sa, int pom, int na)
{
if (n > 0)
{
hanojske_kule(n - 1, sa, na, pom);
Console.WriteLine("{0} {1}", sa, na);
hanojske_kule(n-1, pom, sa, na);
}
}Временска сложеност приказаног алгоритма је експоненцијална.