Генерисање комбинаторних објеката

Увод

Генерисање комбинаторних објеката или набрајање је скуп алгоритама којима се могу креирати сви објекти који имају неку задату комбинаторну структуру. Сви алгоритми овог типа се могу поделити у две категорије:

У оквиру ове теме позабавићемо се генерисањем варијација, комбинација и пермутација скупова, као и генерисањем комбинаторних објеката са унапред задатом структуром. У већини задатака прво ћемо објаснити итеративни алгоритам којим се добија наредни комбинаторни објекат, а затим и рекурзивни поступак којим се могу генерисати сви комбинаторни објекти са задатим својствима.

Алгоритми су једноставни, али се често разликују само у детаљима, па је потребно учити их са разумевањем.

Напомена: У свим решењима бавићемо се искључиво алгоритмима, док учитавање и штампање резултата препуштамо читаоцу за вежбу.

Задатак 1

Корисник уноси природне бројеве kk и nn. Написати програм који одређује све варијације дужине kk скупа {1,2,…,n}\{ 1, 2, \ldots, n\}.

Улаз: У првој линији стандардног улаза налази се природан број kk (1≤k≤101 \le k \le 10), а у другој је природан број nn (3≤n≤10)(3 \leq n \leq 10).

Излаз: На стандардни излаз исписати све варијације дужине kk скупа {1,2,…,n}\{ 1, 2, \ldots, n\}, сваку у свом реду.

Решење

Задатак можемо да урадимо на два начина:

Прво ћемо развити алгоритам који се заснива на одређивању лексикографски следеће варијације. Следећа варијација у лексикографском поретку се може генерисати на следећи начин:

  1. Одредити прву позицију од позади у текућој варијацији која се може увећати.
  2. Увећати елемент на тој позицији.
  3. Све елементе иза те позиције поставити на 1.

Позиција на којој се број увећава назива се преломна тачка (енгл. turning point). На пример, ако набрајамо варијације скупа {1,2,3}\{ 1, 2, 3 \} дужине 55 наредна варијација за варијацију 2133221332 је 2133321333, јер је преломна тачка на позицији 44, што је последња позиција у низу. Ако сада посматрамо варијацију 2133321333, њена наредна варијација ће бити 2211122111, јер је преломна тачка позиција 11 на којој се налазио елемент 11. Низ 3333333333 нема преломну тачку, па самим тим ни лексикографски следећу варијацију.

Један начин имплементације је да преломну тачку нађемо линеарном претрагом од краја низа, ако преломна тачка постоји да увећамо елемент и да од следеће позиције до краја низ попунимо јединицама. Међутим, те две фазе можемо објединити. Варијацију обилазимо од краја постављајући на 11 сваки елемент у варијацији који је једнак броју nn. Ако се зауставимо пре него што смо стигли до почетка низа, значи да смо пронашли елемент који се може увећати и увећавамо га. У супротном, варијација има све елементе једнаке nn и максимална је у лексикографском редоследу. Да бисмо генерисали све варијације, потребно је само да овај поступак понављам све док можемо.

Варијације се могу набројати и индуктивно-рекурзивном конструкцијом. Једина варијација дужине нула је празна. Све варијације дужине kk се могу добити тако што се на прво место упише било који од бројева од 11 до nn, а затим се преостала места допуне свим варијацијама дужине k−1k − 1. Имплементацију ћемо организовати тако да уместо да враћа колекцију варијација, рекурзивна функција прима делимично попуњен низ који ће на све могуће начине допуњавати варијацијама текуће дужине kk (која ће се смањивати кроз рекурзивне позиве). Дакле, на текућу позицију у низу постављамо једну по једну вредност од 11 до nn и затим рекурзивно позивамо функцију да попуни остатак низа (тако што увећавамо бројач и тиме прелазимо на наредну позицију).

Варијације је могуће попуњавати и уназад, тј. на последње место постављати један по један број од 11 до nn, а затим рекурзивно попуњавати префикс, но тиме би редослед варијација био другачији од траженог, тј. не би био лексикографски.

Задатак 2

Корисник уноси природан број nn и затим nn природних бројева. Написати програм који одређује све подскупове датог скупа.

Улаз: У првој линији стандардног улаза налази се природан број nn (3≤n≤10)(3 \leq n \leq 10), затим се у следећих nn линија уносе елементи скупа.

Излаз: На стандардном излазу исписати све подскупове датог скупа, сваки у свом реду.

Решење

Генерисање свих подскупова можемо да извршимо свођењем на већ познати проблем. Наиме, подскуп скупа од nn елемената можемо да кодирамо са варијацијом скупа {0,1}\{0, 1\} дужине nn. Да бисмо добили све подскупове, потребно је да генеришемо све варијације. Применићемо познати алгоритам за генерисање свих варијација са понављањем дужине nn:

Задатак можемо да решимо и без експлицитног генерисања варијација, тј. можемо директно да конструишемо подскупове. Проблем који је очигледан јесте то што су подскупови променљиве дужине. На овом месту, можемо да се послужимо једноставним триком. Уместо да користимо динамичку структуру података за представљање подскупа који ћемо током генерисања проширивати додавањем нових елемената и скраћивати брисањем старих елемената, много је економичније да користимо низ. Трик се огледа у томе да ћемо унапред да алоцирамо довољно простора у низу за цео скуп, тј. највећи подскуп, и уместо да му непрестано додајемо и одузимамо елементе, увешћемо додатни бројач у којем ћемо чувати текућу величину подскупа.

Конструкција алгоритма је врло једноставна и прати принцип који ћемо наивно звати укључи-искључи. Нека је дат скуп SS са nn елемената. Да бисмо генерисали све подскупове можемо да применимо следећу индуктивно-рекурзивну конструкцију која се ослања на принцип смањи за један:

  1. Базни случај - Ако је скуп SS празан, тада је једини његов подскуп празан скуп.
  2. Индуктивна хипотеза - Умемо да генеришемо подскупове скупа од n−1n-1 елемената.
  3. Индуктивни корак (конструкција) - Ако скуп SS није празан (|S|=n\vert S \vert = n), тада га можемо разложити на произвољни елемент x∈Sx \in S и скуп S′=S\xS' = S \setminus x, |S′|=n−1\vert S' \vert = n - 1. Према индуктивној хипотези, већ умемо да генеришемо подскупове скупа S′S', јер је његова величина n−1n - 1. Остаје нам само да размотримо шта се дешава са елементом xx. У једном случају, елемент xx можемо укључити у подскупове, а у другом случају елемент xx можемо искључити из подскупова.

Одавде лако можемо да закључимо како да имплементирамо описани поступак. Дефинисаћемо рекурзивну функцију која на сваком наредном нивоу рекурзије обрађује наредни елемент полазног скупа који је представљен низом. У првом случају га не додајемо у резултујући подскуп и прелазимо на наредни ниво рекурзије, а у другом га додајемо на крај тренутног резултујућег подскупа и прелазимо на наредни ниво рекурзије. Да бисмо постигли описано, потребно је и да резултујући подскуп представимо низом и да рекурзивној функцији као додатне параметре прослеђујемо тренутну дужину подскупа и индекс елемента скупа који се тренутно разматра. Када се цео полазни низ исцрпи, односно када је бројач елемената скупа, тј. дубина рекурзије, једнака дужини полазног низа, тада се тренутно акумулирани подскуп исписује.

Описани поступак је имплементиран у наставку:

Приметимо да се у оба приказана решења нисмо посебно бавили међусобним уређењем комбинаторних објеката, тј. подскупова. Описани поступак ће генерисати подскупове, али не у лексикографском поретку. Да бисмо подскупове генерисали у лексикографском поретку потребно је прво да осмислимо поступак којим ћемо од постојећег подскупа добити лексикографски наредни. Све подскупове генерисаћемо лексикографски сукцесивном применом поступка који следи.

Нека је дат скуп 1,2,3,4{1,2,3,4}, тада ће лексикографски поредак свих подскупова бити:

_112123123412413134142234243344 \begin{aligned} \_ \\ 1 \\ 12 \\ 123 \\ 1234 \\ 124 \\ 13 \\ 134 \\ 14 \\ 2 \\ 234 \\ 24 \\ 3 \\ 34 \\ 4 \end{aligned}

Да бисмо лакше уочили правило, преписаћемо подскупове груписане на основу броја елемената:

_11212312341241313414223234243344 \begin{aligned} & \_ & 1 & 12 & 123 & 1234 \\ & & & & 124 & \\ & & & 13 & 134 & \\ & & & 14 & & \\ & & 2 & 23 & 234 & \\ & & & 24 & & \\ & & 3 & 34 & & \\ & & 4 & & & \end{aligned}

Из овог записа се лако види да постоје два начина да се дође до наредног подскупа:

С обзиром да у овом алгоритму додајемо на крај и бришемо само са краја, не морамо да користимо низове и бројимо елементе, већ је згодније да користимо динамичку структуру података List<>. Додавање на крај и брисање са краја листе су константне операције. Описани итеративни поступак је у наставку:

Лексикографски све подскупове ћемо лако добити сукцесивном применом претходне функције. Имплементација је у наставку:

Задатак 3

Написати програм којим се приказују декадни записи свих природних бројева који у бинарном систему имају највише nn бинарних цифара и немају две узастопне нуле.

Улаз: Прва линија стандардног улаза садржи природан број nn (1≤n≤201 \le n \le 20).

Излаз: На стандардном излазу приказати тражене бројеве у растућем поретку, сваки број у посебној линији.

Решење

Задатак од нас захтева да креирамо све бинарне бројеве са највише nn цифара који немају две узастопне нуле. Проблем ћемо да упростимо и описаћемо метод којим можемо да креирамо све бинарне бројеве са тачно nn цифара који немају две нуле. Јасно је да ћемо полазни проблем решити тако што ћемо позвати описани метод за све вредности 1≤i≤n1 \le i \le n.

Прво креирамо све једноцифрене бинарне бројеве који задовољавају услов, па затим двоцифрене, па троцифрене итд. Одавде можемо да закључимо да наши бројеви могу почињати само цифром 1, тј. водеће нуле нећемо разматрати, јер бисмо тиме добили дупликате у генерисаном низу.

Слично претходним алгоритмима и овде ћемо направити помоћни низ који ће представљати бинарни запис броја. У тај низ ћемо додавати једну по једну цифру, све док цео низ не попунимо. Приликом додавања цифара имамо два случаја:

Након додавања цифре у низ, рекурзивно ћемо обрадити суфикс низа. Алгоритам ћемо комплетирати увођењем бројача позиција. Крећемо од позиције 00 и описани поступак додавања цифара спроводимо све док не дођемо до позиције која је једнака дужини помоћног низа. Тада треба да прикажемо креирамо број, али у основи 1010. У томе ће нам помоћи Хорнерова шема. Имплементација је у наставку:

static int bin_to_dec(int[] bin_zapis)
{
    // стандардна хорнерова шема, којом пребацујемо број у декадни
    int v = 0;
    for (int i = 0; i < bin_zapis.Length; i++)
        v = v*2 + bin_zapis[i];
    return v;
}
static void generisi_sve_brojeve(int[] bin_zapis, int i) 
{
    // ако смо генерисали цео комбинаторни објекат
    if (i == bin_zapis.Length) 
    {
        // приказујемо га у декадном облику
        Console.WriteLine(bin_to_dec(bin_zapis));
    }
    // ако нисмо завршили са креирањем објекта
    else 
    {
        // нулу постављамо само ако претходна цифра није 0
        if ((i > 0 && bin_zapis[i-1] != 0)) 
        {
            bin_zapis[i] = 0;
            // рекурзивно попуњавамо суфикс
            generisi_sve_brojeve(bin_zapis, i + 1);
        }
        // јединицу можемо увек да поставимо
        bin_zapis[i] = 1;
        // рекурзивно попуњавамо суфикс
        generisi_sve_brojeve(bin_zapis, i + 1);
    }
}
static void generisi_sve_brojeve(int n)
{
    // у петљи генеришемо све бринарне бројеве
    // са задатим бројем цифара
    for (int i = 1; i <= n; i++) 
    {
        // помоћни низ у којем чувамо бинарни запис броја
        int[] bin_zapis = new int[i];
        // помоћна функција која генерише бројеве са траженим особинама
        generisi_sve_brojeve(bin_zapis, 0);
    }
}

Неочигледан детаљ је израчунавање декадне вредности на основу бинарног записа броја. Декадну вредност можемо лако да израчунамо применом Хорнерове шеме. Треба само обратити пажњу да је цифра највише тежине на првом месту, па Хорнерову шему морамо да применимо од првог ка последњем месту, а не уобичајено како смо навикли са полиномима чији се коефицијенти најчешће представљају у супротном редоследу од онога који ми овде имамо.

Задатак 4

Корисник уноси природне бројеве kk и nn. Написати програм који одређује све комбинације без понављања дужине kk скупа {1,2,…,n}\{ 1, 2, \ldots, n\}.

Улаз: У првој линији стандардног улаза налази се природан број kk (1≤k≤101 \le k \le 10), а у другој је природан број nn (3≤n≤10)(3 \leq n \leq 10).

Излаз: На стандардном излазу исписати све комбинације без понављања дужине kk скупа {1,2,…,n}\{ 1, 2, \ldots, n\} у лексикографском поретку сваку у свом реду.

Решење

Уобичајени поступак који смо до сада примењивали је био да поступак раздвојимо на две методе. Једну којом ћемо од постојећег комбинаторног објекта да генеришемо лексикографски следећи и другу којом ћемо рекурзивним поступком да генеришемо све комбинаторне објекте са траженим својством.

Размотримо скуп {1,2,3,4,5}\{1,2,3,4,5\}, тј. n=5n=5 и комбинације дужине три, тј. k=3k=3. Размотрићемо неколико примера:

Одавде треба да уочимо да смо увек прво тражили онај елемент комбинације који можемо да увећамо, тј. тражили смо преломну тачку. Да бисмо одредили да ли се елемент може увећати или не, треба да имамо на уму да су елементи у комбинацији лексикографски сортирани и да нема дупликата. Ако позиције у комбинацији обележимо бројевима 00 до k−1k-1, тада на последњем месту у комбинацији може бити највише вредност nn, на претпоследњем n−1n-1 итд. Дакле, преломна тачка ће бити онај елемент комбинације који је мањи од максимума који се на тој позицији може наћи. Максималну дозвољену вредност на позицији ii можемо одредити помоћу израза (n−k+1−i)(n - k + 1 - i). Јасно је да ако преломна тачка не постоји, да не постоји ни наредна комбинација. Након што одредимо преломну тачку, увећевамо елемент на тој позицији за 11 и све елементе иза те позиције постављамо на најмање могуће вредности чиме добијамо лексикографски следећу комбинацију. С обзиром да комбинација мора бити сортирана строго растуће, након увећања преломне вредности све елементе иза ње постављамо на вредност која је за један већа од њој претходне вредности у низу.

Имплементација алгоритма који одређује лексикографски следећу комбинацију је у наставку:

Описани поступак можемо да искористмо за креирање свих комбинација дужине kk у лексикографском поретку. Довољно је да кренемо од прве комбинације и да у петљи редом генеришемо следећу комбинацију све док следећа постоји. Имплементација је у наставку:

Све комбинације можемо креирати и рекурзивним поступком. Једно могуће решење прати до сада уведени оквир. Направићемо помоћни низ у којем ћемо чувати комбинацију. Низ попуњавамо један по један елемент и рекурзивно примењујемо исти поступак на суфикс низа. У сваком тренутку потребно је да знамо која је текућа позиција коју разматрамо и колика је величина скупа. Описани поступак се назива рекурзија по позицијама

Све комбинације лексикографски можемо креирати и мало другачијим поступком. Уместо да се фокусирамо на позиције, можемо да се фокусирамо на дозвољене вредности на позицијама. У пракси, желимо да избацимо петљу у else грани претходно приказаног рекурзивног алгоритма.

Током рекурзије можемо да чувамо информацију о томе који је распон елемената којима се проширује низ. Знамо да су то елементи скупа {1,…,n}\{1, \ldots , n\}, међутим, пошто су комбинације сортиране растуће скуп кандидата је ужи. У претходном програму смо најмању вредност за позицију ii одређивали на основу вредности са позиције i−1i − 1, међутим, алтернативно можемо и експлицитно да одржавамо променљиве min\text{min} и max\text{max} које одређују скуп {min,…,max}\{ \text{min} , \ldots , \text{max} \} чији се елементи распоређују у комбинацији на позицијама из интервала [i,k)[i, k). Ако је тај интервал празан, комбинација је попуњена и може се приказати. У супротном, ако је min>max\text{min} > \text{max} , тада не постоји вредност коју је могуће ставити на позицију ii, па можемо изаћи из рекурзије, јер се тренутна комбинација не може попунити до краја. У супротном можемо размотрити две могућности. Прво на позицију ii можемо поставити елемент min\text{min} и рекурзивно извршити попуњавање низа од позиције i+1i + 1, а друга могућност je да тај елемент прескочимо и у рекурзивном позиву поново захтевамо да се попуни позиција ii. У оба случаја се скуп елемената сужава на {min+1,...,max}\{ \text{min} + 1, . . . , \text{max}\}.

Претрагу можемо сасећи и мало раније. Наиме, пошто су понављања забрањена када је број елемената тог скупа , тј. (n−min+1)(n − \text{min} + 1) мањи од броја преосталих позиција које треба попунити, тј. (k−i)(k − i), већ тада можемо сасећи претрагу, јер не постоји могућност да се комбинација успешно допуни до краја.

Описани поступак се назива рекурзија по вредностима и припада класи укључи-искључи алгоритама које смо раније увели. Такође, ово је први алгоритам у којем смо користили одсецање, да бисмо из разматрања избацили оне гране које не могу да доведу до комплетне комбинације.

Задатак 5

Корисник уноси природне бројеве kk и nn. Написати програм који одређује све комбинације са понављањем дужине kk скупа {1,2,…,n}\{ 1, 2, \ldots, n\}.

Улаз: У првој линији стандардног улаза налази се природан број kk (1≤k≤101 \le k \le 10), а у другој је природан број nn (3≤n≤10)(3 \leq n \leq 10).

Излаз: На стандардном излазу исписати све комбинације са понављањем дужине kk скупа {1,2,…,n}\{ 1, 2, \ldots, n\} сваку у свом реду.

Решење

Лексикографско генерисање свих кобинација са понављањем је минимална модификација претходно приказаног алгоритма за генерисање свих комбинација рекурзијом по вредности. Уместо да генеришемо растуће низове комбинација, треба да генеришемо неопадајуће низове. Прецизније, приликом укључивања елемента у скуп, у следећем рекурзивном позиву треба да допустимо да се тај исти елемент поново укључи у комбинацију.

Пажљиво погледајте услов у линији 15 и рекурзивни позив у линији 21. То су кључне измене у односу на претходно уведени алгоритам, које омогућавају креирање комбинација са понављањем.

Итеративне верзије решења и решење рекурзијом по позицијама се препуштају читаоцу за вежбу.

Задатак 6

Корисник уноси природан број nn. Написати програм који одређује све пермурације скупа {1,2,…,n}\{ 1, 2, \ldots, n\}.

Улаз: У првој линији стандардног улаза налази се природан број nn (3≤n≤10)(3 \leq n \leq 10).

Излаз: На стандардном излазу исписати све пермутације скупа {1,2,…,n}\{ 1, 2, \ldots, n\} сваку у свом реду.

Решење

Као и до сада размотрићемо два приступа решавању задатка. Први приступ је итеративни и заснован је на могућности одређивања лексикографски следеће пермутације, а други поступак је рекурзивни који ће креирати све пермутације датог скупа, али нажалост не лексикографски.

Размотримо пермутацију 1354213542. Заменом елемента 22 и 44 би се добила пермутација 1352413524 која је лексикографски мања од полазне и то нам не одговара. Слично би се десило и да се замене елементи 55 и 44. Чињеница да је подниз 542542 строго опадајући нам говори да није могуће ни на који начин разменити та три елемента да се добије лексикографски већа пермутација, тј. да је ово највећа пермутација која почиње са 1313. Дакле, наредна пермутација ће бити лексикографски најмања пермутација која почиње са 1414, а то је 1423514235.

Дакле, у првом кораку алгоритма проналазимо прву позицију ii сдесна, такву да је ai<ai+1a_i < a_{i+1} (за све i+1≤k<n−1i+1 ≤ k < n − 1 важи да је ak>ak+1a_k > a_{k+1}). Ово радимо најобичнијом линеарном претрагом. У нашем примеру ai=3a_i = 3. Ако таква позиција не постоји, наша пермутација је скроз опадајућа и самим тим лексикографски највећа. Након тога, проналазимо прву позицију jj сдесна такву да је ai<aja_i < a_j, опет линеарном претрагом и размењујемо елементе на позицијама ii и jj. У нашем примеру aj=4a_j = 4 и након размене добијамо пермутацију 1453214532. Пошто је овом разменом реп иза позиције ii и даље стрogo опадајући, да бисмо добили жељену лексикографски најмању пермутацију која почиње са 1414, потребно је обрнути редослед елемената репа што можемо учинити познатим алгоритмом заснованим на техници два показивача.

Имплементација описаног поступка је у наставку:

Описани поступак можемо да искористимо да на једноставан начин генеришемо све пермутације лексикографски. Кренућемо од лексикографски прве пермутације и понављаћемо поступак све док постоји следећа пермутација. Имплементација је у наставку:

Све пермутације скупа можемо генерисати и рекурзивним поступком. У том случају, не можемо их генерисати у лексикографском поретку. Алгоритам можемо конструисати на следећи начин:

  1. Базни случај - Скуп са једним елементом има само једну пермутацију, самог себе.
  2. Индуктивна хипотеза - Умемо да генеришемо све пермутације скупа од n−1n-1 елемената.
  3. Индуктивни корак (конструкција) - Ако фиксирамо последњи елемент скупа ana_n, тада према индуктивној хипотези можемо да генеришемо све пермутације префикса. Прецизније добићемо све пермутације скупа који на последљем месту има фиксиран елемент ana_n. Да бисмо генерисали све могуће пермутације скупа дужине nn, потребно је да на последње место у скупу доведемо сваки могући елемент скупа и да према индуктивној хипотези генеришемо све пермутације префикса, тј. скупа од n−1n-1 елемената.

На крају, морамо још једном да нагласимо да ће описани рекурзивни поступак генерисати све пермутације датог скупа, али не лексикографски. Рекурзивно генерисање свих пермутација у лексикографском поретку је алгоритам којим се нећемо бавити.

Задатак 7

Варијација класе kk без понављања елемената скупа SS je сваки уређена kk-торка од kk различитих елемената скупа SS. Написати програм који за дато nn и kk приказује све варијације без понављања класе kk скупа бројева {1,2,…,n}\{ 1, 2, \ldots, n \}, у лексикографском поретку.

Улаз: Прва линија стандардног улаза садржи природан број nn (n≤8n \le 8), у другој линији налази се природан број kk (0<k≤n0 < k ≤ n).

Излаз: На стандрадном излазу приказати у лексикографском поретку све варијације без понављања класе kk бројева од 11 до nn (сваку у посебном реду).

Решење

Један начин да се задатак реши је да се дефинише функција која за дату варијацију проналази следећу варијацију у лексикографском поретку. Алгоритам представља модификацију алгоритма за генерисање следеће варијације са понављањем. Полазимо од краја варијације тражећи позицију на којој се налази неки елемент који се може увећати. Да би увећавање елемента aia_i било могуће, мора постојати неки елемент који је строго већи од aia_i, а мањи или једнак nn, који се не јавља пре позиције ii, јер дупликати нису дозвољени. Ако таква позиција не постоји, тада је варијација лексикографски највећа. У супротном увећавамо елемент на позицији aia_i на најмању могућу вредност, а елементе иза њега попуњавамо редом што мањим елементима скупа {1,…,n}\{ 1, \ldots , n \}, који се нису појављивали у дотадашњем делу низа. Да бисмо ефикасније одређивали елементе који су већ употребљени у првом делу варијације, посебно ћемо одржавати скуп тих елемената, што можемо извести помоћу низа логичких вредности тако да вредност на месту ii говори да ли је елемент ii већ употребљен. Имплементација описаног алгоритма је у наставку:

// помоћна функција која открива први већи неупотребљен елемент
// скупа, ако такав постоји
static int veci_neupotrebljen(int n, int x, bool[] upotrebljen) 
{
    // у петљи пролазимо само кроз веће елементе
    for (int i = x + 1; i <= n; i++)
    {
        // ако постоји неупотребљен, враћамо његову вредност
        if (!upotrebljen[i])
            return i;
    }
    // ако не постоји, враћамо -1
    return -1;
}
static bool sledeca_varijacija_bez_ponavljanja(int n, int[] varijacija, bool[] upotrebljen) 
{
    // памтимо величину варијације
    int k = varijacija.Length;
    int i = 0, novi = 0;
    // са десне стране тражимо прву позицију на којој можемо да увећамо 
    // елемент
    for (i = k - 1; i >= 0; i--) 
    {
        // проверавамо у скупу индикатора да ли постоји већи број од текућег
        novi = veci_neupotrebljen(n, varijacija[i], upotrebljen);
        // све док не постоји, ресетујемо већ употребљене
        if (novi == -1)
            upotrebljen[varijacija[i]] = false;
        else
            break;
    }
    // ако нисмо нашли позицију на којој се може увећати елемент,
    // прекидамо извршавање
    if (i < 0)
        return false;
    // ако смо нашли, ресетујемо индикатор старе вредности
    upotrebljen[varijacija[i]] = false;
    // постављамо индикатор нове вредности
    upotrebljen[novi] = true;
    // и додајемо је у варијацију
    varijacija[i++] = novi;
    // суфикс низа редом попуњавамо неупотребљеним вредностима
    // од мање ка већој
    for (int x = 1; x < k; x++)
    {
        if (!upotrebljen[x]) 
        {
            a[i++] = x;
            upotrebljen[x] = true;
        }
    }
    return true;
}
static void sve_varijacije_bez_ponavljanja(int n, int k) 
{
    // креирамо низ индикатора
    bool[] upotrebljen = new int[n+1];
    // креирамо лексикографски прву варијацију без понављања
    int[] varijacija = new int[k];
    for (int i = 0; i < k; i++) 
    {
        varijacija[i] = i + 1;
        upotrebljen[i+1] = true;
    }
    // у петљи
    do {
        // штампамо текућу варијацију
        for (int i = 0; i < k; i++)
            Console.Write("{0} ", varijacija[i]);
        Console.WriteLine();
    // све док постоји следећа
    } while (sledeca_varijacija_bez_ponavljanja(n, varijacija, upotrebljen));
}

Као и до сада можемо дефинисати и рекурзивну функцију која генерише све варијације без понављања. Поступак је сличан као раније уведени поступак за рекурзивно генерисање свих варијација са понављањем. Функција прима до сада попуњени део варијације и покушава да постави елемент на позицију ii. Ако је i=ki = k, тада је цела варијација попуњена и исписујемо је. У супротном на место ii редом постављамо један по један елемент скупа {1,…,n}\{ 1, \ldots , n \} који није раније употребљен у варијацији и рекурзивно прелазимо на попуњавање варијације од позиције i+1i + 1.

// помоћна функција која открива први већи неупотребљен елемент
// скупа, ако такав постоји
static int veci_neupotrebljen(int n, int x, bool[] upotrebljen) 
{
    // у петљи пролазимо само кроз веће елементе
    for (int i = x + 1; i <= n; i++)
    {
        // ако постоји неупотребљен, враћамо његову вредност
        if (!upotrebljen[i])
            return i;
    }
    // ако не постоји, враћамо -1
    return -1;
}
static bool sledeca_varijacija_bez_ponavljanja(int n, int[] varijacija, bool[] upotrebljen, int i) 
{   
    // ако смо поунили велу варијацију
    if (i == varijacija.Length) 
    {
        // штампамо текућу варијацију
        for (int j = 0; j < k; j++)
            Console.Write("{0} ", varijacija[j]);
        Console.WriteLine();
    }
    // ако нисмо
    else 
    {
        // у петљи
        for (int x = 1; x <= n; x++) 
        {
            // ако текући елемент није употребљен
            if (!upotrebljen[x]) 
            {
                // додајемо га у варијацију
                a[i] = x;
                // обележавамо га као употребљеног (укључи)
                upotrebljen[x] = true;
                // рекурзивно креирамо све могуће суфиксе
                sve_varijacije_bez_ponavljanja(n, varijacija, upotrebljen, i + 1);
                // обележавамо га као неупотребљеног (искључи)
                upotrebljen[x] = false;
            }
        }
    }
}
static void sve_varijacije_bez_ponavljanja(int n, int k) 
{
    // креирамо низ индикатора
    bool[] upotrebljen = new int[n+1];
    // креирамо помоћни низ у којем чувамо текућу варијацију
    int[] varijacija = new int[k];
    // рекурзивно креирамо све варијације вез понављања
    sve_varijacije_bez_ponavljanja(n, varijacija, upotrebljen, 0);
}

Задатак 8

Датa је реч rr написана малим словима енглеске абецеде. Написати програм којим се приказују у лексикографском поретку сви палиндроми који се могу добити размештањем слова дате речи.

Улаз: Прва и једина линија стандардног улаза садржи реч rr са највише 2020 малих слова енглеске абецеде.

Излаз: На стандардном излазу приказати тражене палиндроме. Ако се не може формирати ни један палиндром приказати цртицу -.

Решење

Задатак можемо решити тако што прво приметимо да се од датих слова може формирати палиндром ако и само се сва слова у речи јављају паран број пута, осим у случају речи непарне дужине у којој се средишње слово јавља непаран број пута. Дакле, пребројаћемо појављивања свих слова у речи помоћу низа бројача или мапе, тј. речника, а затим ћемо пронаћи слова која се јављају непаран број пута. Палиндром ће бити могуће направити ако и само ако је реч парне дужине и нема слова која се појављују непаран број пута или је реч непарне дужине и постоји тачно једно слово које се појављује непаран број пута.

Ако се од речи може направити палиндром, тада њена слова можемо поделити на две половине, када се евен- туално изузме средишње слово, сва се слова јављају паран број пута и једну половину њихових појављивања ћемо поставити у леви, а другу у десни део палиндрома. Било која пермутација слова леве половине речи даје неки палиндром - само је потребно да слова у десној половини речи буду распоређена у обрнутом редоследу. Стога крећемо од најмање пермутације у лексикографском редоследу (то је она у којој су слова сортирана) у левој половини речи и затим формирамо један по један палиндром одређујући следећу пермутацију, све док оне постоје. Да бисмо решили задатак довољно је да применимо алгоритам за генерисање следеће пермутације.

static void razmeni(char[] niz, int i, int j) 
{
    int tmp = niz[i];
    niz[i] = niz[j];
    niz[j] = tmp;
}
static SortedDictionary<char,int> prebrojSlova(string s) 
{
    // конструишемо сортирани речник слова и њихових појављивања
    SortedDictionary<char,int> slova = new SortedDictionary<char,int>();
    for (int i = 0; i < s.Length; i++) 
    {
        int br = 0;
        slova.TryGetValue(s[i]. out br);
        slova[s[i]] = br + 1;
    }
    return slova;
}
static List<char> neparnaSlova(SortedDictionary<char,int> slova) 
{
    // из речника издвајамо слова која се јављају непаран број пута
    List<char> neparni = new List<char>();
    foreach (KeyValuePair<char,int> kvp in slova) 
    {
        if (kvp.Value % 2 == 1)
            neparni.Add(kvp.Key);
    }
    return neparni;
}
static bool sledeca_permutacija(char[] permutacija, int n) 
{
    int i, j;
    // тражимо преломну тачку
    int i = permutacija.Length - 2;
    while (i >= 0 && permutacija[i] > permutacija[i+1])
        i--;
    // ако преломна тачка не постоји, стигли смо до краја
    if (i < 0)
        return slova;
    // ако постоји, тражимо у суфиксу први већи елемент
    j = n - 1;
    while (permutacija[i] > permutacija[j])
        j--;
    // размењујемо их
    razmeni(permutacija, i, j);
    // обрћемо суфикс као у огледалу
    for (j = n - 1, i++; i < j; i++, j--)
        razmeni(permutacija, i, j);
    return true;
}
static void svi_palindromi(string s) {
    // из полазне речи извлачимо слова
    SortedDictionary<char, int> recnik = prebrojSlova(s);
    List<int> neparni = neparnaSlova(recnik);
    // испитујемо да ли можемо да генеришемо палиндроме
    bool postojiResenje = 
        (recnik.Count % 2 == 0 && neparni.Count == 0) ||
        (recnik.Count % 2 == 1 && neparni.Console == 1);
    // ако решење не постоји, прекидамо извршавање
    if (!postojiResenje) 
    {
        Console.WriteLine("-");
        return;
    }
    // прву половину резултата, попуњавамо словима из речника
    char[] palindrom = new char[s.Length];
    int i = 0;
    foreach (KeyValuePair<char,int> kvp in recnik)
    {
        for (int j = 0; j < kvp.Value/2; j++)
        {
            palindrom[i++] = kvp.Key;
        }
    }
    // памтимо колико слова има у префиксу
    int d = i;
    // додајемо непарно слово ако треба
    if (neparni.Count > 0)
        palindrom[i++] = neparni[0];
    // памтимо где почиње суфикс
    int p = i;
    // у петљи
    do {
        // копирамо префикс у суфикс
        Array.Copy(palindrom, 0, palindrom, p, d);
        // окрећемо суфикс као у огледалу
        Array.Reverse(palindrom, p, d);
        // приказујемо текући палиндомр
        Console.WriteLine(new string(palindrom));
    // све док постоји следећа пермутација
    } while (sledeca_permutacija(palindrom, d));
}

Палиндроме можемо генерисати и посебно дизајнираном рекурзивном функцијом. Ако знамо скуп слова која треба распоредити ван евентуалне средишње позиције, можемо редом анализирати могућности за прво (а уједно и последње слово). Пошто палиндроми треба да буду генерисани у лексикографском редоследу, на прво место ћемо стављати слова из тог скупа, редом, у абецедном редоследу. Након постављања неког слова на прво и последње место, изузећемо га из скупа и рекурзивно ћемо наставити попуњавање унутрашњости палиндрома. Излаз из рекурзије је када се скуп слова испразни (тада је реч попуњена и можемо је исписати).

static bool svi_palindromi(char[] permutacija, int i, SortedDictionary<char, int> recnik) 
{
    // ако смо генерисали целу пермутацију
    if (i == permutacija.Length/2) 
    {
        // приказујемо је
        Console.WriteLine(new string(permutacija));
    }
    // ако нисмо
    else 
    {
        // издвајамо сва слова
        List<char> slova = recnik.Keys.ToList();
        // пролазимо кроз листу слово по слово
        foreach (slovo in slova) 
        {
            // ако у речнику постоје бар две копије слова
            if (recnik[slovo] >= 2) 
            {
                // уписујемо слово на почетак и на крај палиндрома
                permutacija[i] = permutacija[permutacija.Length - 1 - i] = slovo;
                // умањујемо број доступних копија датог слова у речнику
                recnik[slovo] -= 2;
                // рекурзивно генеришемо палиндром почевши од наредне позиције
                svi_palindromi(permutacija, i + 1, recnik);
                // враћамо назад број појављивања слова, јер их више не користимо
                recnik[slovo] += 2;
            }
        }
    }
}
static void svi_palindromi(string s) {
    // из полазне речи извлачимо слова
    SortedDictionary<char, int> recnik = prebrojSlova(s);
    List<int> neparni = neparnaSlova(recnik);
    // испитујемо да ли можемо да генеришемо палиндроме
    bool postojiResenje = 
        (recnik.Count % 2 == 0 && neparni.Count == 0) ||
        (recnik.Count % 2 == 1 && neparni.Console == 1);
    // ако решење не постоји, прекидамо извршавање
    if (!postojiResenje) 
    {
        Console.WriteLine("-");
        return;
    }
    // прву половину резултата, попуњавамо словима из речника
    char[] palindrom = new char[s.Length];
    // додајемо непарно слово ако треба
    if (neparni.Count > 0)
        palindrom[palindrom.Length/2] = neparni[0];
    // генеришемо све палиндроме
    svi_palindromi(palindrom, 0, recnik);
}

Задатак 9

Партиције броја nn представљају разлагање тог броја на сабирке чија је вредност између 11 и nn. На пример, број 1010 се може партиционисати као 5+2+2+15 + 2 + 2 + 1. Свака партиција се може нормализовати тако што се претпостави, на пример, да су сабирци сортирани нерастуће. Написати програм који исписује све партиције датог броја.

Улаз: Са стандардног улаза се учитава број nn (1≤n≤251 \le n \le 25).

Излаз: На стандардни излаз исписати све нормализоване партиције броја nn, сортиране лексикографски растуће.

Решење

С обзиром да се ради о лексикографском уређењу и овде можемо да применимо до сада уведени модел. Прво ћемо описати алгоритам којим можемо добити следећу партицију, а затим и рекурзивни поступак којим добијамо све партиције датог броја. Ради једноставности, подразумеваћемо да су елементи партиције сортирани нерастуће, тј да је партиција дата у нормализованом облику.

Прво ћемо размотрити алгоритам за креирање лексикографски следеће партиције за дату партицију. Нека је задати број 1010 и нека је партиција 5,2,2,15, 2, 2, 1. Тада ће лексикографски следећа партиција бити 5,3,1,15,3,1,1. Одавде видимо да је потребно да неки елемент партиције, који је што је могуће више ближи крају, увећамо за један, док префикс остаје непромењен. Након увећавања изабраног броја, треба да ажурирамо суфикс партиције тако да збир остане једнак полазном броју.

Ако је партиција једночлана, тада је она лексикографски највећа. Последњи елемент низа није могуће повећати за један, јер би због очувања збира елемената партиције неки елемент пре њега морао бити смањен. чиме бисмо нарушили нормализованост елемената партиције. Наредни кандидат за повећање је претпоследњи елемент, а он се може увећати за један само ако се на месту испред њега не налази елемент који му је једнак, јер би се тада повећањем претпоследњег елемента добила партиција која није нормализована, тј. није уређена нерастуће. У супротном разматрамо елемент пре претпоследњег и тако редом, све док не наиђемо на елемент испред којег не стоји елемент који му је једнак. Тај елемент повећавамо за 1. Након тога потребно је поправити елементе иза тог увећаног елемента, тако да партиција буде лексикографски што мања. То ће се десити ако се иза увећаног елемента поставе само јединице. Да се збир не би променио, број постављених јединица треба да буде за један мањи од збира свих елемената иза елемента који смо увећали за један. Овај збир можемо израчунавати док обилазимо низ уназад тражећи најдешњи елемент који се може увећати за 1.

Пошто се дужина партиције може променити приликом преласка на следећу партицију, уместо класичног низа, партицију можемо представити неким обликом низа који допушта додавање елемената на крај. У је- зику C# то може бити листа List<>. Брисање елемената иза дате позиције можемо остварити методом RemoveRange(), а додавање елемената на крај методом Add(). Имплементација је у наставку:

Описани поступак можемо искористити да генеришемо све партиције лексикографски. Довољно је да кренемо од лексикографски најмање партиције и да понављамо поступак све док постоји наредна партиција. Имплементација је у наставку:

Све партиције можемо генерисати и рекурзивним поступком. Свака партиција има свој први сабирак. Свакој партицији броја nn којој је први сабирак ss (1≤s≤n1 \le s \le n) једнозначно одговара нека партиција броја n−sn − s, што указује да се проблем може решавати индуктивно-рекурзивном конструкцијом. Пошто је сабирање комутативно, да не бисмо суштински исте партиције понављали више пута наметнућемо услов да сабирци у свакој партицији буду сортирани нерастуће.

Дакле, ако је први сабирак ss, сви сабирци иза њега морају да буду мањи или једнаки од ss. Зато нам није довољно само да умемо да генеришемо све партиције броја n−sn − s, већ је потребно да ојачамо индуктивну хипотезу. Претпоставићемо да се у датом вектору на позицијама [0,i)[0, i) налазе раније постављени елементи партиције и да је задатак процедуре да тај низ допуни на све могуће начине партицијама броја nn у којима су сви сабирци мањи или једнаки smaxs_{\text{max}}. Излаз из рекурзије представљаће случај n=0n = 0 у ком је једина могућа партиција броја 00 празан скуп, у коме нема сабирака. Тада сматрамо да је партиција успешно формирана и обрађујемо садржај комбинаторног објекта.

Као и у претходним задацима, све партиције можемо генерисати рекурзијом по позицијама или рекурзијом по вредностима.

Рекурзија по позицијама је стандардни поступак у којем је циљ размотрити све могуће дозвољене вредности сабирка на позицији ii. На основу услова задатка и индуктивне хипотезе, дозвољене вредности морају бити веће од ii и мање или једнаке smaxs_{\text{max}}, уз природан услов да морају бити и мањи или једнаки од nn. Ако са mm обележимо maxn,smax\max {n, s_{\text{max}}}, тада ће могући први сабирци бити вредности s′s' у опсегу 1≤s′≤m1 \le s' \le m. Када фиксирамо сабирак s′s', низ рекурзивно допуњавамо свим партицијама n−s′n-s' у којима су сви сабирци мањи или једнаки s′s', јер је због нормализованости партиције потребно да преостали део збира буде представљен као партиција бројева који нису већи од s′s'.

У главној функцији ћемо алоцирати низ дужине nn, јер најдужа партиција има nn сабирака који су сви једнаки 11 и захтеваћемо да се тај низ попуни почевши од позиције 00 партицијама броја nn у којима су сви сабирци мањи или једнаки nn. Имплементација је у наставку:

Други поступак којим можемо генерисати све партиције броја јесте рекурзија по вредностима. Уместо да се анализирају све могуће вредности сабирка на позицији ii, могуће је разматрати само две могућности: прву да се на позицији ii јавља сабирак smaxs_{\text{max}}, а другу да се на позицији ii јавља неки сабирак строго мањи од smaxs_{\text{max}}. Први случај је могућ само ако је n≥smaxn \ge s_{\text{max}} и када се на позицију ii постави smaxs_{\text{max}} низ допуњујемо од позиције i+1i + 1 партицијама броја n−smaxn − s_{\text{max}} у којима су сви сабирци мањи или једнаки smaxs_{\text{max}}. Други случај је увек могућ и тада партицију допуњујемо партицијама броја nn у којима је највећи сабирак smax−1s_{\text{max}} − 1. У зависности од редоследа ова два рекурзивна позива одређује се да ли ће пермутације бити сортиране лексикографски растуће или опадајуће.

Приметимо кључну разлику између два приказана рекурзивна поступка:

Задатак 10

Написати програм који исписује све nn-тоцифрене бројеве који имају дати збир цифара.

Улаз: Прва линија садржи збир kk (1≤k≤9n1 \le k \le 9n), а друга број цифара nn (2≤n≤1002 \le n \le 100).

Излаз: На стандардни излаз исписати све тражене бројеве, уређене по величини.

Решење

Наивно решење у ком би се генерисали сви nn-тоцифрени бројеви, а затим филтрирали они чији је збир цифара једнак датом броју је прилично неефикасно. Овакво решење се своди на генерисање свих варијација са понављањем дужине nn скупа цифара {0,1,2,…,9}\{ 0, 1, 2, \dots, 9 \}.

Можемо да применимо идеју из претходног задатка, само што овде наша партиција мора бити фиксне дужине, јер се ради о nn-тоцифреним бројевима. Други услов који морамо да проверавамо јесте да је збир елемената партиције тачно kk. Уколико партицију нисмо попунили, тада на текућу позицију можемо да постовимо било коју цифру која је мања или једнака од минимума бројева 99 и nn. Такође, морамо да водимо рачуна да на првом месту не сме бити цифра 00. Из описаног поступка, очигледно је да се ради о рекурзију по позицијама.

У претходном решењу се на сваку позицију постављају све цифре од нуле или евентуално 11, на почетној позицији, па до 99 уз евентуално одсецање када је број nn мањи од 99. Тако, на пример, може да се деси да приликом покушаја генерисања троцифрених бројева са збиром цифара 2727 на месту прве цифре испробавамо вредности од 11 до 88, а да заправо ни са једном од њих не можемо да попунимо партицију до краја, јер је једино решење 999999. Много ефикасније решење добијамо ако применимо још једно одсецање и одредимо доњу границу вредности текуће цифре. Наиме, максимални могући збир цифара иза текуће се лако добије као 9(m−1)9(m − 1), где је mm број тренутно непопуњених цифара у партицији. Ако је текућа цифра једнака cc, тада мора да важи да је преостали збир цифара nn мањи или једнак c+9(m−1)c + 9(m − 1), одакле се добија граница да је c≥n−9(m−1)c \ge n−9(m−1). Дакле, у петљи која поставља текућу цифру крећемо од веће од вредности c+9(m−1)c+9(m−1) и вредности 00, тј. 11 на почетној позицији и завршавамо са мањом од вредности nn и 99. С обзиром на овако одређене границе, имамо гаранцију да ће свака партиција моћи успешно да се попуни и при изласку из рекурзије само треба да контролишемо да ли су све цифре партиције потпуно попуњене. Aко јесу, тада ће вредност преосталог збира nn сигурно бити једнака нули.

Задатак 11

Написати програм којим се за дато nn и kk, приказују сви природни nn-тоцифрени бројеви такви да им је разлика две суседне цифре једнака датом броју kk (0≤k≤40 \le k \le 4). На пример у броју 57535753 разлика сваке две суседне цифре једнака је 22.

Улаз: Прва линија стандардног улаза садржи природан број nn (0<n<100 < n < 10), друга линија садржи природан број kk (0≤k≤90 \le k \le 9).

Излаз: Приказати тражене бројеве у растућем поретку, сваки број у посебној линија.

Решење

Задатак је могуће решити дефинисањем рекурзивне функције која генерише све тражене бројеве. Функција прима низ цифара, чијих је првих ii елемената већ попуњено и попуњава остатак низа. Ако је ii једнако дужини низа, цео низ је попуњен, па се тренутни низ само исписује. У супротном се анализирају могућности за цифру на позицији ii. Претпоставићемо да је i>0i > 0, тј. да знамо вредност претходне цифре cc. Тада се на текућу позицију може ставити или вредност c−kc−k (ако је већа или једнака од 00) или вредност c+kc+k (ако је мања или једнака од 99). У случају када је k=0k = 0, цифре c−kc − k и c+kc + k се поклапају (обе су једнаке cc) па је довољно анализирати само једну од њих. Након постављања цифре на позицију ii, рекурзивно попуњавамо остатак низа (прослеђујући вредност i+1i + 1). Прва цифра је специфична и њу ћемо попунити пре првог рекурзивног позива. На прво место можемо ставити било коју цифру осим нуле и за сваки избор те прве цифре вршимо посебан рекурзивни позив за i=1i = 1.