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

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

Објекти се обично представљају \(n\)-торкама бројева, при чему се исти објекти могу торкама моделовати на различите начине. На пример, сваки подскуп скупа \(\{a_0, \ldots, a_{n-1}\}\) се може представити коначним низом индекса елемената који му припадају. Да би сваки подскуп био јединствено представљен, потребно је да тај низ буде канонизован (на пример, уређен строго растући). На пример, торка \((0, 2, 3)\) једнозначно одређује подскуп \(\{a_0, a_2, a_3\}\). Други начин да се подскупови представе су \(n\)-торке логичких вредности или вредности 0-1. На пример, ако је \(n=6\), и ако претпоставимо да се битови читају слева надесно, тада торка \(101100\) означава скуп \(\{a_0, a_2, a_3\}\).

Сви објекти се обично могу представити дрветом и то дрво одговара процесу њиховог генерисања тј. обиласка (оно се не прави експлицитно, у меморији, али нам помаже да разумемо и организујемо поступак претраге). Обилазак дрвета се најједноставније изводи у дубину (често рекурзивно). За прву наведену репрезентацију подскупова дрво је дато на слици 10. Сваки чвор дрвета одговара једном подскупу, при чему се одговарајућа торка очитава на гранама пута који води од корена до тог чвора.

Слика 10: Сви подскупови четворочланог скупа - сваки чвор дрвета одговара једном подскупу

За другу наведену репрезентацију подскупова дрво је дато на слици 11. На почетку се бира да ли ће елемент \(a_0\) бити укључен у подскуп, на наредном нивоу да ли ће бити укључен елемент \(a_1\), затим елемент \(a_2\) и тако даље. Само листови дрвета у којима је за сваки елемент донета одлука да ли припада или не припада подскупу, одговарају подскуповима, при чему се одговарајућа торка логичких вредности очитава на гранама пута који води од корена до тог чвора.

Слика 11: Сви подскупови четворочланог скупа - сваки лист дрвета одговара једном подскупу

Приметимо да оба дрвета садрже \(2^n\) чворова којима се представљају подскупови (у првом случају су то сви чворови дрвета, а у другом само листови).

Приликом генерисања објеката често је пожељно ређати их одређеним редом. С обзиром на то то да се сви комбинаторни објекти представљају одређеним торкама (коначним низовима), природан поредак међу њима је лексикографски поредак (који се користи за утврђивање редоследа речи у речнику). Подсетимо се, торка \(a_0\ldots a_{m-1}\) лексикографски претходи торци \(b_0\ldots b_{n-1}\) акко постоји неки индекс \(i\) такав да за свако \(0 \leq j < i\) важи \(a_j = b_j\) и важи или да је \(a_i < b_i\) или да је \(i = m < n\). На пример важи да је \(11 < 112 < 221\) (овде је \(i=2\), а затим \(i=0\)).

На пример, ако подскупове скупа \(\{1, 2, 3\}\) представимо на први начин, торкама у којима су елементи уређени растуће, лексикографски поредак би био \(\emptyset\), \(\{1\}\), \(\{1, 2\}\), \(\{1, 2, 3\}\), \(\{1, 3\}\), \(\{2\}\), \(\{2, 3\}\), \(\{3\}\). Ако бисмо их представљали на други начин, торкама у којима се нулама и јединицама одређује да ли је неки елемент укључен у подскуп, лексикографски редослед би био: 000 (\(\emptyset\)), 001 (\(\{3\}\)), 010(\(\{2\}\)), 011(\(\{2, 3\}\)), 100(\(\{1\}\)), 101(\(\{1,3\}\)), 110(\(\{1,2\}\)) и 111(\(\{1,2,3\}\)).

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

Задатак: Следећи подскуп

Написати програм који одређује подскуп скупа бројева \(\{1, \ldots, n\}\) који у лексикографском редоследу следи непосредно иза датог подскупа. Подскупови су задати у облику строго растуће сортираних низова.

Опис улаза

Прва линија садржи број \(n\) (\(1 \leq n \leq 100\)), а наредна линија садржи подскуп чији су елементи задати сортирано растуће, раздвојени по једним размаком.

Опис излаза

На стандардни излаз у једној линији исписати елементе траженог подскупа тј.  - ако је учитани подскуп лексикографски највећи.

Пример 1

Улаз

5 1 2 3 4 5

Излаз

1 2 3 5

Пример 2

Улаз

5 5

Излаз

-

Решење

Напишимо, на пример, лексикографски уређен списак свих подскупова скупа бројева од 1 до 4.

-, 1, 12, 123, 1234, 124, 13, 134, 14, 2, 23, 234, 24, 3, 34, 4

Можемо приметити да постоје два начина да се дође до наредног подскупа. Анализирајмо ове скупове у истом редоследу, груписане у колоне на основу броја елемената.

- 1 12 123 1234 124 13 134 14 2 23 234 24 3 34 4

Један начин је проширивање када се наредни подскуп добија додавањем неког елемента у претходни. То су кораци у претходној табели код којих се прелази из једне у наредну колону. Да би добијени подскуп следио непосредно иза претходног у лексикографском редоследу, додати елемент подскупу мора бити најмањи могући. Пошто је сваки подскуп сортиран, елемент мора бити за један већи од последњег елемента подскупа који се проширује (изузетак је празан скуп, који се проширује елементом 1). Једини случај када проширивање није могуће је када је последњи елемент подскупа највећи могући (у нашем примеру то је 4).

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

Подскупове можемо представити динамичким низом који нам омогућва да елементе додајемо и уклаљамо са десног краја. У језику C++ можемо употребити вектор (тј. колекцију vector).

// na osnovu datog podskupa skupa {1, ..., n} određuje
// leksikografski naredni podskup i vraca da li on postoji
bool sledeciPodskup(vector<int>& podskup, int n) {
  // specijalni slučaj proširivanja praznog skupa
  if (podskup.empty()) {
    podskup.push_back(1);
    // podskup je uspešno pronađen
    return true;
  }
  
  // proširivanje
  if (podskup.back() < n) {
    // u podskup dodajemo element koji je za 1 veći od 
    // trenutno najvećeg elementa
    podskup.push_back(podskup.back() + 1);
    // podskup je uspešno pronađen
    return true;
  }

  // skraćivanje
  // uklanjamo poslednji najveći element
  podskup.pop_back();
  // ako nema preostalih elemenata ne postoji naredni podskup
  if (podskup.empty())
    return false;
  // najveći od preostalih elemenata uvećavamo za 1
  podskup.back()++;
  // podskup je uspešno pronađen
  return true;
}
#include <iostream>
#include <vector>

using namespace std;

// na osnovu datog podskupa skupa {1, ..., n} određuje
// leksikografski naredni podskup i vraca da li on postoji
bool sledeciPodskup(vector<int>& podskup, int n) {
  // specijalni slučaj proširivanja praznog skupa
  if (podskup.empty()) {
    podskup.push_back(1);
    // podskup je uspešno pronađen
    return true;
  }
  
  // proširivanje
  if (podskup.back() < n) {
    // u podskup dodajemo element koji je za 1 veći od 
    // trenutno najvećeg elementa
    podskup.push_back(podskup.back() + 1);
    // podskup je uspešno pronađen
    return true;
  }

  // skraćivanje
  // uklanjamo poslednji najveći element
  podskup.pop_back();
  // ako nema preostalih elemenata ne postoji naredni podskup
  if (podskup.empty())
    return false;
  // najveći od preostalih elemenata uvećavamo za 1
  podskup.back()++;
  // podskup je uspešno pronađen
  return true;
}

int main() {
  int n;
  cin >> n;
  vector<int> podskup;
  int x;
  while (cin >> x)
    podskup.push_back(x);
  if (sledeciPodskup(podskup, n)) {
    for (int x : podskup)
      cout << x << " ";
    cout << endl;
  } else {
    cout << "-" << endl;
  }
  return 0;
}

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


// na osnovu datog podskupa skupa {1, ..., n} određuje
// leksikografski naredni podskup i vraća da on postoji.
// Tekući podskup je smešten u nizu dužine k
bool sledeciPodskup(int podskup[], int& k, int n) {
  // specijalni slučaj proširivanja praznog skupa
  if (k == 0) {
    podskup[k++] = 1;
    return true;
  }
  
  // proširivanje
  if (podskup[k-1] < n) {
    // u podskup dodajemo element koji je za 1 veći od 
    // trenutno najvećeg elementa
    podskup[k] = podskup[k-1] + 1;
    k++;
    return true;
  }

  // skraćivanje
  // izbacujemo najveći element iz podskupa
  k--;
  // ako nema preostalih elemenata, naredni podskup ne postoji
  if (k == 0)
    return false;
  // najveći od preostalih elemenata uvećavamo za 1
  podskup[k-1]++;
  return true;
}
#include <iostream>

using namespace std;


// na osnovu datog podskupa skupa {1, ..., n} određuje
// leksikografski naredni podskup i vraća da on postoji.
// Tekući podskup je smešten u nizu dužine k
bool sledeciPodskup(int podskup[], int& k, int n) {
  // specijalni slučaj proširivanja praznog skupa
  if (k == 0) {
    podskup[k++] = 1;
    return true;
  }
  
  // proširivanje
  if (podskup[k-1] < n) {
    // u podskup dodajemo element koji je za 1 veći od 
    // trenutno najvećeg elementa
    podskup[k] = podskup[k-1] + 1;
    k++;
    return true;
  }

  // skraćivanje
  // izbacujemo najveći element iz podskupa
  k--;
  // ako nema preostalih elemenata, naredni podskup ne postoji
  if (k == 0)
    return false;
  // najveći od preostalih elemenata uvećavamo za 1
  podskup[k-1]++;
  return true;
}


int main() {
  int n;
  cin >> n;
  // elementi podskupa
  const int MAX = 100;
  int podskup[MAX];
  // broj elemenata podskupa
  int k = 0;
  // ucitavamo elemente datog podskupa
  int x;
  while (cin >> x)
    podskup[k++] = x;
  // pokusavamo da pronadjemo naredni podskup
  if (sledeciPodskup(podskup, k, n)) {
    // ako postoji, ispisujemo ga
    for (int i = 0; i < k; i++)
      cout << podskup[i] << " ";
    cout << endl;
  } else
    // naredni podskup ne postoji
    cout << "-" << endl;
  return 0;
}

Задатак: Сви подскупови

Написати програм који исписује све подскупове датог скупа.

Опис улаза

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

Опис излаза

На стандардни излаз исписати све подскупове учитаног скупа бројева, сваки у посебном реду, са елементима раздвојеним једним размаком. Прво се ређају подскупови у којима први елемент није укључен, а затим они у којима јесте. У свакој од те две групе, прво се исписују подскупови у којима други елемент није укључен, а затим они где јесте и тако даље.

Пример

Улаз

3 1 2 3

Излаз

3 2 2 3 1 1 3 1 2 1 2 3

Решење

Рекурзивни поступак генерисања свих варијација дужине \(n\) скупа \(\{0, 1\}\)

Генерисање свих подскупова одговара генерисању свих варијација дужине \(n\) од нула и јединица (сваки елемент је или укључен или искључен). Поредак описан у поставци задатка указује на то да подскупови треба да буду уређени лексикографски, у односу на њихову репрезентацију у облику варијација.

Опишимо индуктивно-рекурзивну конструкцију функције која генерише све подскупове скупа \(S\).

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

До решења се може доћи тако што се у рекурзивној функцији прослеђује неки подсуп \(P\) скупа \(\{a_0, \ldots, a_{i-1}\}\) и који она на све могуће начине проширује елементима скупа \(\{a_i, \ldots, a_{n-1}\}\).

Иницијално је \(i=0\), а подскуп је празан (празан скуп је, заиста, једини подскуп празног супа \(\{a_0, \ldots, a_{i-1}\}\) и он се на све могуће начине проширује елементима скупа \(\{a_i, \ldots, a_{n-1}\} = \{a_0, \ldots, a_{n-1}\}\)).

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

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

Рад рекурзивне функције одговара слици 11.

// procedura određuje i obrađuje sve moguće skupove koji se
// dobijaju tako što se na elemente prosleđenog podskupa p
// dužine j, dodaju podskupovi prosleđenog skupa smeštenog u
// nizu a, od pozicije i nadalje
void obradi_sve_podskupove(const vector<int>& a, int i,
                           vector<int>& p, int j) {
  // skup preostalih elemenata u nizu a koji se mogu ubaciti u
  // podskup je prazan
  if (i == a.size())
    // obrađujemo formirani podskup
    obradi(p, j);
  else {
    // element na poziciji i ne uključujemo u podskup
    obradi_sve_podskupove(a, i + 1, p, j);
    // element na poziciji i uključujemo u podskup
    p[j] = a[i];
    obradi_sve_podskupove(a, i + 1, p, j + 1);
  }
}

void obradi_sve_podskupove(const vector<int>& a) {
  // podskup je na početku prazan, i u njega potencijalno
  // dodajemo sve elemente skupa a od pozicije 0 nadalje
  vector<int> p(a.size());
  obradi_sve_podskupove(a, 0, p, 0);
}
#include <iostream>
#include <vector>

using namespace std;

// ispis prvih n elemenata niza a
void obradi(const vector<int>& a, int n) {
  for (int k = 0; k < n; k++)
    cout << a[k] << " ";
  cout << endl;
}

// procedura određuje i obrađuje sve moguće skupove koji se
// dobijaju tako što se na elemente prosleđenog podskupa p
// dužine j, dodaju podskupovi prosleđenog skupa smeštenog u
// nizu a, od pozicije i nadalje
void obradi_sve_podskupove(const vector<int>& a, int i,
                           vector<int>& p, int j) {
  // skup preostalih elemenata u nizu a koji se mogu ubaciti u
  // podskup je prazan
  if (i == a.size())
    // obrađujemo formirani podskup
    obradi(p, j);
  else {
    // element na poziciji i ne uključujemo u podskup
    obradi_sve_podskupove(a, i + 1, p, j);
    // element na poziciji i uključujemo u podskup
    p[j] = a[i];
    obradi_sve_podskupove(a, i + 1, p, j + 1);
  }
}

void obradi_sve_podskupove(const vector<int>& a) {
  // podskup je na početku prazan, i u njega potencijalno
  // dodajemo sve elemente skupa a od pozicije 0 nadalje
  vector<int> p(a.size());
  obradi_sve_podskupove(a, 0, p, 0);
}

int main() {
  int n;
  cin >> n;
  vector<int> a(n);
  for (int i = 0; i < n; i++)
    cin >> a[i];
  obradi_sve_podskupove(a);
  return 0;
}

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

// procedura određuje i obrađuje sve moguće skupove koji se
// dobijaju tako što se na elemente prosleđenog podskupa
// dodaju podskupovi prosleđenog skupa
void obradiSvePodskupove(const vector<int>& skup,
                         const vector<int>& podskup) {
  // skup je prazan
  if (skup.size() == 0)
    // na prosleđeni podskup možemo dodati samo prazan skup
    obradi(podskup);
  else {
    // izdvajamo i uklanjamo proizvoljan element skupa
    int x = skup.back();
    vector<int> smanjenSkup = skup;
    smanjenSkup.pop_back();
    // u podskup dodajemo sve podskupove skupa bez izdvojenog 
    // elementa
    vector<int> podskupBez = podskup;
    obradiSvePodskupove(smanjenSkup, podskupBez);
    // u podskup uključujemo izdvojeni element i zatim sve 
    // podskupove skupa bez izdvojenog elementa
    vector<int> podskupSa = podskup;
    podskupSa.push_back(x);
    obradiSvePodskupove(smanjenSkup, podskupSa);
  }
}
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// ispis elemenata niza a
void obradi(const vector<int>& a) {
  for (int i = 0; i < a.size(); i++)
      cout << a[i] << " ";
  cout << endl;
}

// procedura određuje i obrađuje sve moguće skupove koji se
// dobijaju tako što se na elemente prosleđenog podskupa
// dodaju podskupovi prosleđenog skupa
void obradiSvePodskupove(const vector<int>& skup,
                         const vector<int>& podskup) {
  // skup je prazan
  if (skup.size() == 0)
    // na prosleđeni podskup možemo dodati samo prazan skup
    obradi(podskup);
  else {
    // izdvajamo i uklanjamo proizvoljan element skupa
    int x = skup.back();
    vector<int> smanjenSkup = skup;
    smanjenSkup.pop_back();
    // u podskup dodajemo sve podskupove skupa bez izdvojenog 
    // elementa
    vector<int> podskupBez = podskup;
    obradiSvePodskupove(smanjenSkup, podskupBez);
    // u podskup uključujemo izdvojeni element i zatim sve 
    // podskupove skupa bez izdvojenog elementa
    vector<int> podskupSa = podskup;
    podskupSa.push_back(x);
    obradiSvePodskupove(smanjenSkup, podskupSa);
  }
}

void obradiSvePodskupove(const vector<int>& skup) {
  // pošto elementi iz skupa u podskup prebacuju sa desnog kraja, da
  // bismo dobili traženi redosled podskupa, potrebno je da obrnemo
  // redosled elemenata skupa
  vector<int> skupObratno = skup;
  reverse(begin(skupObratno), end(skupObratno));
  // krećemo od praznog podskupa
  vector<int> podskup;
  // prazan skup proširujemo svim podskupovima datog skupa
  obradiSvePodskupove(skupObratno, podskup);
}

int main() {
  int n;
  cin >> n;
  vector<int> a(n);
  for (int i = 0; i < n; i++)
    cin >> a[i];
  obradiSvePodskupove(a);
  return 0;
}

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

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

// procedura određuje i obrađuje sve moguće skupove koji se
// dobijaju tako što se na elemente prosleđenog podskupa dodaju
// podskupovi prosleđenog skupa
void obradiSvePodskupove(vector<int>& skup,
                         vector<int>& podskup) {
  // skup je prazan
  if (skup.size() == 0)
    // na prosleđeni podskup možemo dodati samo prazan skup
    obradi(podskup);
  else {
    // izdvajamo i uklanjamo proizvoljan element skupa
    int x = skup.back();
    skup.pop_back();
    // u podskup dodajemo sve podskupove skupa bez izdvojenog 
    // elementa
    obradiSvePodskupove(skup, podskup);
    // u podskup uključujemo izdvojeni element i zatim sve 
    // podskupove skupa bez izdvojenog elementa
    podskup.push_back(x);
    obradiSvePodskupove(skup, podskup);
    // vraćamo skup i podskup u početno stanje
    podskup.pop_back();
    skup.push_back(x);
  }
}

void obradiSvePodskupove(vector<int>& skup) {
  // pošto elementi iz skupa u podskup prebacuju sa desnog
  // kraja, da bismo dobili traženi redosled podskupa,
  // potrebno je da obrnemo redosled elemenata skupa
  vector<int> skupObratno = skup;
  reverse(begin(skupObratno), end(skupObratno));
  // krećemo od praznog podskupa
  vector<int> podskup;
  // efikasnosti radi rezervišemo potrebnu memoriju za
  // najveći podskup
  podskup.reserve(skup.size());
  // prazan skup proširujemo svim podskupovima datog skupa
  obradiSvePodskupove(skupObratno, podskup);
}
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// ispis vektora
void obradi(const vector<int>& a) {
  for (int i = 0; i < a.size(); i++)
      cout << a[i] << " ";
  cout << endl;
}

// procedura određuje i obrađuje sve moguće skupove koji se
// dobijaju tako što se na elemente prosleđenog podskupa dodaju
// podskupovi prosleđenog skupa
void obradiSvePodskupove(vector<int>& skup,
                         vector<int>& podskup) {
  // skup je prazan
  if (skup.size() == 0)
    // na prosleđeni podskup možemo dodati samo prazan skup
    obradi(podskup);
  else {
    // izdvajamo i uklanjamo proizvoljan element skupa
    int x = skup.back();
    skup.pop_back();
    // u podskup dodajemo sve podskupove skupa bez izdvojenog 
    // elementa
    obradiSvePodskupove(skup, podskup);
    // u podskup uključujemo izdvojeni element i zatim sve 
    // podskupove skupa bez izdvojenog elementa
    podskup.push_back(x);
    obradiSvePodskupove(skup, podskup);
    // vraćamo skup i podskup u početno stanje
    podskup.pop_back();
    skup.push_back(x);
  }
}

void obradiSvePodskupove(vector<int>& skup) {
  // pošto elementi iz skupa u podskup prebacuju sa desnog
  // kraja, da bismo dobili traženi redosled podskupa,
  // potrebno je da obrnemo redosled elemenata skupa
  vector<int> skupObratno = skup;
  reverse(begin(skupObratno), end(skupObratno));
  // krećemo od praznog podskupa
  vector<int> podskup;
  // efikasnosti radi rezervišemo potrebnu memoriju za
  // najveći podskup
  podskup.reserve(skup.size());
  // prazan skup proširujemo svim podskupovima datog skupa
  obradiSvePodskupove(skupObratno, podskup);
}

int main() {
  int n;
  cin >> n;
  vector<int> a(n);
  for (int i = 0; i < n; i++)
    cin >> a[i];
  obradiSvePodskupove(a);
  return 0;
}

Функција за одређивање наредне варијације у лексикографском редоследу

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

void obradiSvePodskupove(const vector<int>& a) {
  vector<bool> v(a.size(), false);
  do {
    obradi(v, a);
  } while (sledecaVarijacija(v));
}
#include <iostream>
#include <vector>

using namespace std;

void obradi(const vector<bool>& v, const vector<int>& a) {
  for (int i = 0; i < v.size(); i++)
    if (v[i])
      cout << a[i] << " ";
  cout << endl;
}

bool sledecaVarijacija(vector<bool>& v) {
  int i = v.size() - 1;
  while (i >= 0 && v[i])
    v[i--] = false;
  if (i < 0) return false;
  v[i] = true;
  return true;
}

void obradiSvePodskupove(const vector<int>& a) {
  vector<bool> v(a.size(), false);
  do {
    obradi(v, a);
  } while (sledecaVarijacija(v));
}

int main() {
  int n;
  cin >> n;
  vector<int> a(n);
  for (int i = 0; i < n; i++)
    cin >> a[i];
  obradiSvePodskupove(a);
  return 0;
}

Задатак: Следећа варијација

Написати програм који одређује наредну варијацију дужине \(k\) скупа \(\{1, \ldots, n\}\) у лексикографском поретку.

Опис улаза

Прва линија стандардног улаза садржи број \(k\) (\(1 \leq k \leq 100\)), а друга број \(n\) (\(1 \leq n \leq 100\)). У трећој линији се налази варијација описана бројевима раздвојеним по једним размаком.

Опис излаза

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

Пример

Улаз

5 4 1 1 2 3 4

Излаз

1 1 2 4 1

Решење

Следећа варијација у лексикографском поретку се може генерисати тако што се увећа последњи број у варијацији који се може увећати, и што се након увећавања сви бројеви иза увећаног броја поставе на \(1\). Позиција на којој се број увећава назива се преломна тачка (енгл. turning point). На пример, ако набрајамо варијације скупа \(\{1, 2, 3\}\) дужине \(5\) наредна варијација за варијацију \(21332\) је \(21333\) (преломна тачка је позиција \(4\), која је последња позиција у низу), док је њој наредна варијација \(22111\) (преломна тачка је позиција \(1\) на којој се налазио елемент \(1\)). Низ \(33333\) нема преломну тачку, па самим тим ни лексикографски следећу варијацију.

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

bool sledecaVarijacija(int n, vector<int>& varijacija) {
  // od kraja varijacije tražimo prvi element koji se moze
  // povecati
  int i;
  int k = varijacija.size();
  for (i = k-1; i >= 0 && varijacija[i] == n; i--)
    varijacija[i] = 1;
  // svi elementi su jednaki n, pa ne postoji naredna
  // varijacija
  if (i < 0) return false;
  // uvecavamo element koji je moguće uvecati
  varijacija[i]++;
  return true;
}
#include <iostream>
#include <vector>

using namespace std;

void ispisi(const vector<int>& varijacija) {
    for (int x : varijacija)
      cout << x << " ";
    cout << endl;
}

bool sledecaVarijacija(int n, vector<int>& varijacija) {
  // od kraja varijacije tražimo prvi element koji se moze
  // povecati
  int i;
  int k = varijacija.size();
  for (i = k-1; i >= 0 && varijacija[i] == n; i--)
    varijacija[i] = 1;
  // svi elementi su jednaki n, pa ne postoji naredna
  // varijacija
  if (i < 0) return false;
  // uvecavamo element koji je moguće uvecati
  varijacija[i]++;
  return true;
}

int main() {
  int k, n;
  cin >> k >> n;
  vector<int> varijacija(k);
  for (int i = 0; i < k; i++)
    cin >> varijacija[i];
  if (sledecaVarijacija(n, varijacija))
    ispisi(varijacija);
  else
    cout << "-" << endl;
  return 0;
}

Задатак: Све варијације

Напиши програм који одређује све варијације са понављањем дужине \(k\) скупа \(\{1, \ldots, n\}\).

Опис улаза

Са стандардног улаза се учитава број \(n\) (\(1 \leq n \leq 5\)) и у наредној линији број \(k\) (\(1 \leq k \leq 5\)).

Опис излаза

На стандардни излаз исписати све варијације, сортиране лексикографски.

Пример

Улаз

2 3

Излаз

1 1 1 1 1 2 1 2 1 1 2 2 2 1 1 2 1 2 2 2 1 2 2 2

Решење

Рекурзивно генерисање варијација

Варијације се могу набројати индуктивно рекурзивном конструкцијом.

Рецимо и да је могуће на последње место постављати један по један број од \(1\) до \(n\), а затим рекурзивно попуњавати префикс, но тиме би редослед варијација био другачији од траженог.

Уместо да дефинишемо функцију која враћа колекцију варијација, дефинисаћемо рекурзивну процедуру која прима низ коме су попуњени сви елементи осим последњих \(k\), и који ће на све могуће начине допуњавати варијацијама дужине \(k\) (која ће се смањивати кроз рекурзивне позиве). Дакле, након попуњеног дела низа постављамо једну по једну вредност од \(1\) до \(n\) и затим рекурзивно позивамо процедуру да попуни остатак низа (тиме што смањујемо дужину \(k\) и тиме прелазимо на наредну позицију).

Да бисмо избегли потребу за динамичким проширивањем низова унапред ћемо алоцирати низ дужине \(k\), а онда ћемо му у рекурзији попуњавати једну по једну позицију (текућа позиција се може одредити као разлика између дужине низа и текуће вредности броја \(k\)).

Рекурзивни поступак генерисања варијација дужине \(2\) од елемената скупа \(\{1, 2, 3\}\)

// Sve varijacije duzine k elemenata skupa {1, ..., n}
// Data varijacija duzine varijacije.size() - k se
// dopunjuje svim mogucim varijacijama duzine k skupa
// {1, ..., n} i sve tako dobijene varijacije se
// obradjuju funkcijom obradi
void obradiSveVarijacije(int k, int n, 
                         vector<int>& varijacija) {
  // k je 0, pa je jedina varijacija duzine nula prazna i
  // njenim dodavanjem na polazni niz on se ne menja
  if (k == 0)
    obradi(varijacija);
  else
    // na tekucu poziciju postavljamo sve moguce vrednosti
    // od 1 do n i dobijeni niz onda rekurzivno proširujemo
    for (int nn = 1; nn <= n; nn++) {
       varijacija[varijacija.size() - k] = nn;
       obradiSveVarijacije(k-1, n, varijacija);
    }
}


// sve varijacije duzine k skupa {1, ..., n}
void obradiSveVarijacije(int k, int n) {
  vector<int> varijacija(k);
  obradiSveVarijacije(k, n, varijacija);
}
#include <iostream>
#include <vector>

using namespace std;

// ispisuje tekucu varijaciju na standardni izlaz
void obradi(const vector<int>& varijacija) {
  for (int x : varijacija)
    cout << x << " ";
  cout << endl;
}

// Sve varijacije duzine k elemenata skupa {1, ..., n}
// Data varijacija duzine varijacije.size() - k se
// dopunjuje svim mogucim varijacijama duzine k skupa
// {1, ..., n} i sve tako dobijene varijacije se
// obradjuju funkcijom obradi
void obradiSveVarijacije(int k, int n, 
                         vector<int>& varijacija) {
  // k je 0, pa je jedina varijacija duzine nula prazna i
  // njenim dodavanjem na polazni niz on se ne menja
  if (k == 0)
    obradi(varijacija);
  else
    // na tekucu poziciju postavljamo sve moguce vrednosti
    // od 1 do n i dobijeni niz onda rekurzivno proširujemo
    for (int nn = 1; nn <= n; nn++) {
       varijacija[varijacija.size() - k] = nn;
       obradiSveVarijacije(k-1, n, varijacija);
    }
}


// sve varijacije duzine k skupa {1, ..., n}
void obradiSveVarijacije(int k, int n) {
  vector<int> varijacija(k);
  obradiSveVarijacije(k, n, varijacija);
}

int main() {
  int n, k;
  cin >> n >> k;
  obradiSveVarijacije(k, n);
  return 0;
}

Проналажење лексикографски следеће варијације

Друга могућност је да се крене од лексикографски најмање варијације (то је варијација \(\underbrace{11\ldots 11}_{k}\)) и да се коришћењем функције описане у задатку [Следећа варијација] одређује наредна варијација дате варијације у односу на лексикографски редослед, све док таква постоји.

void obradiSveVarijacije(int k, int n) {
   // krećemo od varijacije 11...11
   // ona je leksikografski najmanja
   vector<int> varijacija(k, 1);
   // obradjujemo redom varijacije dok god postoji
   // leksikografski sledeca
   do {
     obradi(varijacija);
   } while(sledecaVarijacija(n, varijacija));
}
#include <iostream>
#include <vector>

using namespace std;

// ispisuje tekucu varijaciju na standardni izlaz
void obradi(const vector<int>& varijacija) {
  for (int x : varijacija)
    cout << x << " ";
  cout << endl;
}

bool sledecaVarijacija(int n, 
                       vector<int>& varijacija) {
   // od kraja varijacije tražimo prvi element koji se moze povecati
   int i;
   int k = varijacija.size();
   for (i = k-1; i >= 0 && varijacija[i] == n; i--)
      varijacija[i] = 1;
   // svi elementi su jednaki n - ne postoji naredna varijacija
   if (i < 0) return false;
   // uvecavamo element koji je moguće uvecati
   varijacija[i]++;
   return true;
}

void obradiSveVarijacije(int k, int n) {
   // krećemo od varijacije 11...11
   // ona je leksikografski najmanja
   vector<int> varijacija(k, 1);
   // obradjujemo redom varijacije dok god postoji
   // leksikografski sledeca
   do {
     obradi(varijacija);
   } while(sledecaVarijacija(n, varijacija));
}

int main() {
  int n, k;
  cin >> n >> k;
  obradiSveVarijacije(k, n);
  return 0;
}

Задатак: Све комбинације

Комбинације дужине \(k\) од \(n\) елемената подразумевају да се врши одабир \(k\) елемената скупа \(\{1, \ldots, n\}\), слично као што се, на пример, у игри лото бира 7 од 39 куглица. Напиши програм који за дате вредности \(k\) и \(n\) набраја и исписује све комбинације, поређане по лексикографском редоследу.

Опис улаза

Прва линија стандардног улаза садржи број \(k\) (\(1 \leq k \leq n\)), а наредна број \(n\) (\(2 \leq n \leq 20\)).

Опис излаза

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

Пример

Улаз

3 5

Излаз

1 2 3 1 2 4 1 2 5 1 3 4 1 3 5 1 4 5 2 3 4 2 3 5 2 4 5 3 4 5

Решење

Рекурзивни позиви по позицијама

Задатак рекурзивне функције биће да допуни низ дужине \(k\) од позиције \(i\) па до краја. Када је \(i=k\), низ је попуњен и потребно је обрадити (у нашем случају исписати) добијену комбинацију. У супротном бирамо елемент који ћемо поставити на позицију \(i\). Пошто су комбинације уређене строго растуће, он мора бити већи од претходног (ако претходни не постоји, онда може бити \(1\)) и мањи или једнак \(n\). Заправо, ово горње ограничење мора да се смањи. Пошто су елементи строго растући, а од позиције \(i\) па до краја низа треба поставити \(k-i\) елемената, на позицији \(i\) може бити \(n+i-k+1\) и тада ће на позицији \(k-1\) бити вредност \(n\). У петљи стављамо један по један од тих елемената на позицију \(i\) и рекурзивно настављамо генерисање од наредне позиције. Дакле, у општем случају, дрво рекурзивних позива неће бити бинарно (функција може да направи много више од два рекурзивна позива).

Рекурзивно генерисање комбинација дужине 3 из скупа \(\{1, 2, 3, 4\}\)

// niz kombinacije dužine k na pozicijama [0, i) sadrži uređen
// niz elemenata iz skupa [1, n-i+1). Procedura na sve moguće
// načine dopunjava elementima iz skupa [1, n) tako da niz
// bude uređen rastući
void obradiSveKombinacije(vector<int>& kombinacija,
                          int i, int n) {
  // tražena dužina kombinacije
  int k = kombinacija.size();

  // ako je popunjen ceo niz samo ispisujemo kombinaciju
  if (i == k) {
    obradi(kombinacija);
    return;
  }
  // određujemo raspon elemenata na poziciji i
  int pocetak = i == 0 ? 1 : kombinacija[i-1]+1;
  int kraj = n + i - k + 1;
  // jedan po jedan element upisujemo na poziciju i, pa
  // nastavljamo generisanje rekurzivno
  for (int x = pocetak; x <= kraj; x++) {
    kombinacija[i] = x;
    obradiSveKombinacije(kombinacija, i+1, n);
  }
}

// nabraja i obradjuje sve kombinacije dužine k skupa
// {1, 2, ..., n}
void obradiSveKombinacije(int k, int n) {
  vector<int> kombinacija(k);
  obradiSveKombinacije(kombinacija, 0, n);
}
#include <iostream>
#include <vector>

using namespace std;

// ispisuje kombinaciju na standarndi izlaz
void obradi(const vector<int>& kombinacija) {
  for (int x : kombinacija)
    cout << x << " ";
  cout << endl;
}

// niz kombinacije dužine k na pozicijama [0, i) sadrži uređen
// niz elemenata iz skupa [1, n-i+1). Procedura na sve moguće
// načine dopunjava elementima iz skupa [1, n) tako da niz
// bude uređen rastući
void obradiSveKombinacije(vector<int>& kombinacija,
                          int i, int n) {
  // tražena dužina kombinacije
  int k = kombinacija.size();

  // ako je popunjen ceo niz samo ispisujemo kombinaciju
  if (i == k) {
    obradi(kombinacija);
    return;
  }
  // određujemo raspon elemenata na poziciji i
  int pocetak = i == 0 ? 1 : kombinacija[i-1]+1;
  int kraj = n + i - k + 1;
  // jedan po jedan element upisujemo na poziciju i, pa
  // nastavljamo generisanje rekurzivno
  for (int x = pocetak; x <= kraj; x++) {
    kombinacija[i] = x;
    obradiSveKombinacije(kombinacija, i+1, n);
  }
}

// nabraja i obradjuje sve kombinacije dužine k skupa
// {1, 2, ..., n}
void obradiSveKombinacije(int k, int n) {
  vector<int> kombinacija(k);
  obradiSveKombinacije(kombinacija, 0, n);
}

int main() {
  int k, n;
  cin >> k >> n;
  obradiSveKombinacije(k, n);
  return 0;
}

Рекурзивни позиви по вредностима

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

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

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

void obradiSveKombinacije(vector<int>& kombinacija, int i, 
                          int n_min, int n_max) {
  // tražena dužina kombinacije
  int k = kombinacija.size();

  // ako je popunjen ceo niz samo ispisujemo kombinaciju
  if (i == k) {
    obradi(kombinacija);
    return;
  }

  // ako tekuću kombinaciju nije moguće popuniti do kraja
  // prekidamo ovaj pokušaj
  if (n_max - n_min + 1 < k - i)
    return;

  // vrednost n_min uključujemo na poziciju i, pa rekurzivno
  // proširujemo tako dobijenu kombinaciju
  kombinacija[i] = n_min;
  obradiSveKombinacije(kombinacija, i+1, n_min+1, n_max);
  // vrednost n_min preskačemo i isključujemo iz kombinacije
  obradiSveKombinacije(kombinacija, i, n_min+1, n_max);
}

// nabraja i obradjuje sve kombinacije dužine k skupa
// {1, 2, ..., n}
void obradiSveKombinacije(int k, int n) {
  vector<int> kombinacija(k);
  obradiSveKombinacije(kombinacija, 0, 1, n);
}
#include <iostream>
#include <vector>

using namespace std;

// ispisuje kombinaciju na standarndi izlaz
void obradi(const vector<int>& kombinacija) {
  for (int x : kombinacija)
    cout << x << " ";
  cout << endl;
}

void obradiSveKombinacije(vector<int>& kombinacija, int i, 
                          int n_min, int n_max) {
  // tražena dužina kombinacije
  int k = kombinacija.size();

  // ako je popunjen ceo niz samo ispisujemo kombinaciju
  if (i == k) {
    obradi(kombinacija);
    return;
  }

  // ako tekuću kombinaciju nije moguće popuniti do kraja
  // prekidamo ovaj pokušaj
  if (n_max - n_min + 1 < k - i)
    return;

  // vrednost n_min uključujemo na poziciju i, pa rekurzivno
  // proširujemo tako dobijenu kombinaciju
  kombinacija[i] = n_min;
  obradiSveKombinacije(kombinacija, i+1, n_min+1, n_max);
  // vrednost n_min preskačemo i isključujemo iz kombinacije
  obradiSveKombinacije(kombinacija, i, n_min+1, n_max);
}

// nabraja i obradjuje sve kombinacije dužine k skupa
// {1, 2, ..., n}
void obradiSveKombinacije(int k, int n) {
  vector<int> kombinacija(k);
  obradiSveKombinacije(kombinacija, 0, 1, n);
}

int main() {
  int k, n;
  cin >> k >> n;
  obradiSveKombinacije(k, n);
  return 0;
}

Лексикографски следећа комбинација

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

// nabraja i obradjuje sve kombinacije dužine k skupa
// {1, 2, ..., n}
void obradiSveKombinacije(int k, int n) {
  // krecemo od kombinacije 1, 2, ..., k
  vector<int> kombinacija(k);
  for (int i = 0; i < k; i++)
    kombinacija[i] = i + 1;
     
  // obradjujemo kombinacije dokle god postoji sledeca
  do {
    obradi(kombinacija);
  } while (sledecaKombinacija(n, kombinacija));
}
#include <iostream>
#include <vector>

using namespace std;

// ispisuje kombinaciju na standarndi izlaz
void obradi(const vector<int>& kombinacija) {
  for (int x : kombinacija)
    cout << x << " ";
  cout << endl;
}

// pronalazi sledecu kombinaciju u leksikografskom redosledu 
bool sledecaKombinacija(int n, vector<int>& kombinacija) {
  // tražena dužina kombinacije
  int k = kombinacija.size();

  // krećemo od kraja i tražimo prvu poziciju koja nije na maksimumu
  // tj. koja se može povećati. Maksimumi od kraja su n, n-1, n-2, ...
  int i;
  for (i = k-1; i >= 0 && kombinacija[i] == n; i--, n--)
    ;
  // ako takva pozicija ne postoji, tekuća kombinacija je maksimalna
  if (i < 0)
    return false;
  // uvećavamo poslednji element koji se može povećati
  kombinacija[i]++;
  // iza njega slažemo redom brojeve za jedan veće
  for (i++; i < k; i++)
    kombinacija[i] = kombinacija[i-1] + 1;
  return true;
}


// nabraja i obradjuje sve kombinacije dužine k skupa
// {1, 2, ..., n}
void obradiSveKombinacije(int k, int n) {
  // krecemo od kombinacije 1, 2, ..., k
  vector<int> kombinacija(k);
  for (int i = 0; i < k; i++)
    kombinacija[i] = i + 1;
     
  // obradjujemo kombinacije dokle god postoji sledeca
  do {
    obradi(kombinacija);
  } while (sledecaKombinacija(n, kombinacija));
}

int main() {
  int k, n;
  cin >> k >> n;
  obradiSveKombinacije(k, n);
  return 0;
}

Задатак: Следећа пермутација

Све пермутације бројева од \(1\) до \(n\) се могу поређати лексикографски. На пример за \(n=3\) пермутације у лексикографском поретку су

123 132 213 231 312 321

Написати програм којим се за дати природан број \(n\) и дату пермутацију бројева од \(1\) до \(n\) приказује следећа пермутација у лексикографском поретку (прва пермутација која се налази после дате пермутације).

Опис улаза

Прва линија стандардног улаза садржи природан број \(n\) (\(n < 1000\)). У свакој од \(n\) наредних линија стандардног улаза, налазе се редом елементи пермутације, сваки у посебној линији.

Опис излаза

На стандардном излазу приказати редом елементе следеће пермутације у лексикографском поретку, сваки елемент у посебној линији. Ако не постоји следећа пермутација (дата пермутација је последња) приказати у једној линији поруку ne postoji.

Пример 1

Улаз

5 3 1 4 5 2

Излаз

3 1 5 2 4

Пример 2

Улаз

3 3 2 1

Излаз

ne postoji

Решење

Алгортитам за одређивање наредне пермутације у лексикографском редоследу

Пример 6.1. Размотримо пермутацију \(13542\). Заменом елемента \(2\) и \(4\) би се добила пермутација \(13524\) која је лексикографски мања од полазне и то нам не одговара. Слично би се десило и да се замене елементи \(5\) и \(4\). Чињеница да је низ \(542\) строго опадајући нам говори да није могући ни на који начин разменити та три елемента да се добије лексикографски већа пермутација, тј. да је ово највећа пермутација која почиње са \(13\). Дакле, наредна пермутација ће бити лексикографски најмања пермутација која почиње са \(14\), а то је \(14235\).

Дакле, у првом кораку алгоритма проналазимо преломну тачку, тј. прву позицију \(i\) здесна, такву да је \(a_i < a_{i+1}\) (за све \(i+1 \leq k < n-1\) важи да је \(a_k > a_{k+1}\)). Ово радимо најјобичнијом линеарном претрагом. Ако таква позиција не постоји, наша пермутација је скроз опадајућа и самим тим лексикографски највећа. Након тога, проналазимо прву позицију \(j\) здесна такву да је \(a_i < a_j\) (опет линеарном претрагом) и размењујемо елементе на позицијама \(i\) и \(j\). Пошто је овом разменом реп иза позиције \(i\) и даље стриктно опадајући, да бисмо добили жељену пермутацију (лексикографски најмању пермутацију која почиње са \(a_0\ldots a_{i-1}a_j\)), потребно је обрнути редослед елемената репа тј. део низа од позиције \(i+1\) до краја низа.

Пример 6.2. Ако означимо позиције елемената добијамо \(1^03^15^24^32^4\). Зато је \(i=1\) и \(a_i = 3\), док је \(j=3\) и \(a_j = 4\). Након размене добијамо \(1^04^15^23^32^4\). Да бисмо добили тражену пермутацију \(1^04^12^23^35^4\) обрћемо део низа од позиције \(i+1 = 2\) до краја низа.

bool sledecaPermutacija(vector<int>& a){
  int n = a.size();

  // linearnom pretragom pronalazimo prvu poziciju i takvu da
  // je a[i] > a[i+1]
  int i = n - 2;
  while (i >= 0 && a[i] > a[i+1])
    i--;
  // ako takve pozicije nema,
  // permutacija a je leksikografski maksimalna
  if (i < 0) return false;
  // linearnom pretragom pronalazimo prvu poziciju j takvu da
  // je a[j] > a[i]
  int j = n - 1;
  while (a[j] < a[i])
    j--;
  // razmenjujemo elemente na pozicijama i i j
  swap(a[i], a[j]);
  // obrcemo deo niza od pozicije i+1 do kraja
  for (j = n - 1, i++; i < j; i++, j--)
    swap(a[i], a[j]);
  return true;
}
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

bool sledecaPermutacija(vector<int>& a){
  int n = a.size();

  // linearnom pretragom pronalazimo prvu poziciju i takvu da
  // je a[i] > a[i+1]
  int i = n - 2;
  while (i >= 0 && a[i] > a[i+1])
    i--;
  // ako takve pozicije nema,
  // permutacija a je leksikografski maksimalna
  if (i < 0) return false;
  // linearnom pretragom pronalazimo prvu poziciju j takvu da
  // je a[j] > a[i]
  int j = n - 1;
  while (a[j] < a[i])
    j--;
  // razmenjujemo elemente na pozicijama i i j
  swap(a[i], a[j]);
  // obrcemo deo niza od pozicije i+1 do kraja
  for (j = n - 1, i++; i < j; i++, j--)
    swap(a[i], a[j]);
  return true;
}

// ispisujemo elemente vektora
void prikazi(const vector<int>& a) {
  for (int x: a)
    cout << x << endl;
}

int main() {
  // ucitavamo polaznu permutaciju
  int n;
  cin >> n;
  vector<int> a(n);
  for(int i = 0; i < n; i++)
    cin >> a[i];
  // odredjujemo sledecu i ispisujemo rezultat
  if (sledecaPermutacija(a))
    prikazi(a);
  else
    cout << "ne postoji" << endl;
  return 0;
}

Библиотечка функција

У језику C++ постоји библиотечка функција next_permutation која одређује следећу пермутацију у лексикографском редоследу (и враћа информацију о томе да ли она постоји).

// ucitavamo polaznu permutaciju
int n;
cin >> n;
vector<int> a(n);
for(int i = 0; i < n; i++)
  cin >> a[i];
  
// odredjujemo sledecu i ispisujemo rezultat
if (next_permutation(begin(a), end(a)))
  obradi(a);
else
  cout << "ne postoji" << endl;
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// ispisujemo elemente vektora
void obradi(const vector<int>& a) {
  for (int x: a)
    cout << x << endl;
}

int main() {
  // ucitavamo polaznu permutaciju
  int n;
  cin >> n;
  vector<int> a(n);
  for(int i = 0; i < n; i++)
    cin >> a[i];
  
  // odredjujemo sledecu i ispisujemo rezultat
  if (next_permutation(begin(a), end(a)))
    obradi(a);
  else
    cout << "ne postoji" << endl;
  
  return 0;
}