Двобојка - Решење

Поставка

Трансформације низа “у месту” - два показивача

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

Парни лево, непарни десно - размена наопаких

Претпоставићемо да су у сваком кораку петље познати индекси \(l\) и \(d\) тако да су елементи низа груписани тако да су:

Инваријанта је, дакле, да је распоред елемената у низу облика ppp???nnn, где су l и d позиција првог тј. последњег непознатог елемента (обележеног упитником).

На почетку иницијализујемо \(l\) на нулу, а \(d\) на \(n-1\) (тада су интервали \([0, l)\) и \((d, n)\) празни, а сви елементи у интервалу \([l, d] = [0, n-1]\) су још непрегледани). Петљу у приниципу извршавамо док још има непрегледаних елемената тј. док је \(l \leq d\), међутим, у овом задатку можемо је завршити и корак раније. Петља се извршава док је \(l < d\) и завршава када је \(l=d\), јер какав год да је тај последњи непрегледани елемент на позицији \(l=d\), он ће се моћи припојити или левом или десном делу низа и неће бити потребе премештати га.

У телу петље радимо следеће:

Када се петља заврши, елементи су у жељеном редоследу.

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

l d 3 8 7 4 5 1 6 2 l d 2 8 7 4 5 1 6 3 l d 2 8 7 4 5 1 6 3 l d 2 8 6 4 5 1 7 3 l d 2 8 6 4 5 1 7 3 ld 2 8 6 4 5 1 7 3

Имплементација се може направити на следећи начин.

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
  // ubrzavamo ucitavanje i ispis
  ios_base::sync_with_stdio(false);

  // ucitavamo niz
  int n;
  cin >> n;
  vector<int> a(n);
  for (int i = 0; i < n; i++)
    cin >> a[i];

  // odrzavamo uslov
  // [0, l) - parni
  // (d, n) - neparni
  // [l, d] - nepoznati

  // u pocetku su svi nepoznati
  int l = 0, d = n-1;
  // dok jos ima nepoznatih elemenata
  while (l < d) {
    // ako je na mestu l paran, ostavljamo ga na svom mestu i pomeramo
    // se na naredni element
    if (a[l] % 2 == 0)
      l++;
    // ako je na mestu d neparan, ostavljamo ga na svom mestu i
    // pomeramo se na prethodni element
    else if (a[d] % 2 != 0)
      d--;
    else
      // na mestu l je neparan, a na mestu d je paran broj, pa ih
      // razmenjujemo i pomeramo se po oba kraja
      swap(a[l++], a[d--]);
  }

  // ispisujemo rezultat
  for (int i = 0; i < n; i++)
    cout << a[i] << endl;
}

Сложеност. Променљива l се увећава, а променљива d се умањује све док се не сусретну, што се догађа у тачно \(n\) корака, па је укупна сложеност алгоритма \(O(n)\).

Доказ коректности. Докажимо и формално коректност претходног алгоритма. Током извршавања важи раније описана инваријанта, а важи и да је \(0 \leq l \leq d + 1 \leq n\).

Иницијализацијом вредности \(l\) на нулу, а вредности \(d\) на \(n-1\) постижемо да су ови услови на почетку задовољени (јер су сви елементи у интервалу \([l, d] = [0, n-1]\) још непрегледани).

У телу петље радимо следеће:

Када се петља заврши, распоред је коректан. Наиме, пошто на крају петље услов \(l < d\) није испуњен, а пошто је \(l \leq d + 1\), тада је \(l=d\) или је \(l=d+1\).

Приметимо да смо прекидом петље када је \(l=d\), уштедели једну итерацију петље (што није нарочито значајно), али смо закомпликовали доказ коректности, па се природно поставља питање колико је та оптимизација имала смисла.

Парни, непарни, па непознати

У овом решењу инваријанта је мало другачија. Памтимо индекс \(k\) и текући индекс \(i\) и претпостављамо да су

Дакле, намећемо инваријанту да је распоред облика pppnnn???, где је \(i\) позиција првог непознатог, а \(k\) позиција првог непарног елемената.

Размотримо како да из инваријанте закључимо како треба иницијализовати променљиве. Пошто су сви елементи из интервала \([i, n)\) непрегледани, променљиву \(i\) морамо инцијализовати на 0. Пошто су сви елементи из интервала \([0, k)\) парни, а \([k, i)\) непарни, и \(k\) морамо поставити на \(0\).

Инваријанта јасно диктира и услов петље. Наиме, петља се извршава док још има непрегледаних елемената тј. док је \(i < n\). У сваком кораку петље \(i\) се увећава за 1 (користимо класичну бројачку петљу for по променљивој \(i\)), чиме се сужава интервал непрегледаних елемената.

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

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

k n 3 8 7 4 5 1 6 2 i k n 3 8 7 4 5 1 6 2 i k n 8 3 7 4 5 1 6 2 i k n 8 3 7 4 5 1 6 2 i k n 8 4 7 3 5 1 6 2 i k n 8 4 7 3 5 1 6 2 i k n 8 4 7 3 5 1 6 2 i k n 8 4 6 3 5 1 7 2 i k n 8 4 6 2 5 1 7 3 i

Имплементација се може направити на следећи начин.

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
  // ubrzavamo ucitavanje i ispis
  ios_base::sync_with_stdio(false);

  // ucitavamo elemente niza
  int n;
  cin >> n;
  vector<int> a(n);
  for (int i = 0; i < n; i++)
    cin >> a[i];

  // odrzavamo uslov
  // [0, k) - parni
  // [k, i) - neparni
  // [i, n) - nepoznati

  // u pocetku su svi elementi nepoznati
  int k = 0;
  for (int i = 0; i < n; i++)
    // ako je element paran razmenjujemo ga sa prvim neparnim
    if (a[i] % 2 == 0)
      swap(a[i], a[k++]);
    // a ako je neparan, ne pomeramo ga

  // ispisujemo rezultat
  for (int i = 0; i < n; i++)
    cout << a[i] << endl;
}

Сложеност. У програму се користи класична бројачка петља for која се завршава у \(n\) корака, па је укупна сложеност алгоритма \(O(n)\).

Доказ коректности. Докажимо и формално коректност претходног поступка. Уз описану инваријанту важи и да је \(0 \leq k \leq i \leq n\).

Пошто је на почетку \(i=k=0\), важи да су сви елементи у интервалу \([i, n) = [0, n)\) непрегледани. Интервали \([0, k) = [k, i) = [0, 0)\) су празни, па задовољавају услове, а тривијално важи и да је \(0 \leq k \leq i \leq n\).

У телу петље се врше следеће акције.

По завршетку петље услов \(i < n\) није испуњен, па пошто на основу инваријанте важи \(0 \leq k \leq i \leq n\), важи да је \(i = n\). Зато је интервал непознатих \([i, n)\) празан, сви елементи из интервала \([0, k)\) су парни, из интервала \([k, i) = [k, n)\) су непарни и постигнут је тражени распоред.

Заустављање се једноставно доказује јер се у сваком кораку сужава интервал непознатих \([i, n)\).