Zadatak: Igra sa Džokerom - Rešenje i objašnjenje
U ovoj igri korisnici pokušavaju da pronađu skriveni interval [L, R] koji je Džoker odabrao.
Džoker daje broj N (gde je 5 ≤ N ≤ 1000) i čuva dva broja L i R u tajnosti
(0 ≤ L ≤ R ≤ N−1).
Pravila igre
- Korisnik može da postavi pitanje tako što Džokeru pošalje niz
a₀, a₁, …, aₙ₋₁. - Džoker zatim vraća nasumično ili minimum ili maksimum elemenata unutar segmenta
[L, R]. - Džoker prihvata svako rešenje
(L', R')koje zadovoljava:|L − L'| + |R − R'| ≤ 1.
Cilj je pronaći što preciznije (L', R') koristeći što manje pitanja (query poziva).
Vrati se na tekst zadatka: Joker, vrati se na stranicu sa zadatkom Joker .
Takmičarsko okruženje vs. lokalna simulacija
U takmičarskom sistemu (npr. na onlajn platformi) funkcije query() i answer()
već su definisane od strane sistema — učesnik implementira isključivo:
void solve(int N);
Sistem zatim sam poziva query(a) i proverava rezultat answer(L, R).
U lokalnom okruženju (na sopstvenom računaru) potrebno je samostalno napraviti simulaciju
Džokera, odnosno implementirati i funkcije query() i answer() da bi se
testiralo rešenje.
Ideja rešenja
Pošto Džoker nasumično vraća minimum ili maksimum, nije moguće direktno znati gde je interval
[L, R]. Međutim, ponavljanjem više upita sa različitim nizovima i analizom rezultata
možemo proceniti u kom delu niza se nalazi interval.
Postoji više mogućih pristupa:
- Rekurzivno sužavanje opsega (slično binarnoj pretrazi)
- Stohastičko ispitivanje – više ponavljanja radi stabilnijeg rezultata
- Iterativno sužavanje bez rekurzije (deterministički pristup)
Edukativno rešenje (lokalna simulacija)
Sledeći kod implementira potpunu simulaciju: uključuje generisanje tajnog intervala, funkcije
query(), answer() i rekurzivnu funkciju search_interval() koja
postepeno sužava opseg dok ne pronađe rešenje.
#include <bits/stdc++.h>
using namespace std;
// --- Simulacija Džokera (lokalna verzija) ---
int L_secret, R_secret;
// Funkcija query – vraća nasumično minimum ili maksimum na segmentu [L,R]
int query(vector<int> a) {
vector<int> segment(a.begin() + L_secret, a.begin() + R_secret + 1);
if (rand() % 2 == 0)
return *min_element(segment.begin(), segment.end());
else
return *max_element(segment.begin(), segment.end());
}
// Funkcija answer – proverava tačnost rešenja
void answer(int L, int R) {
double greska = abs(L - L_secret) + abs(R - R_secret);
cout << "Greška: " << greska << endl;
if (greska <= 1)
cout << "✅ Correct! (Tajni interval je [" << L_secret << ", " << R_secret << "])\n";
else
cout << "❌ Wrong! (Tajni interval je [" << L_secret << ", " << R_secret << "])\n";
}
// --- Rekurzivna pretraga intervala ---
pair<int, int> search_interval(int l, int r, int N) {
if (r - l <= 1)
return {l, r};
int mid = (l + r) / 2;
vector<int> a(N);
iota(a.begin(), a.end(), 1);
int leftScore = 0, rightScore = 0;
for (int i = 0; i < 5; i++) {
int res = query(a);
if (res <= a[mid]) leftScore++;
else rightScore++;
}
if (leftScore >= rightScore)
return search_interval(l, mid, N);
else
return search_interval(mid, r, N);
}
// --- Glavna funkcija solve (kao u takmičarskom okruženju) ---
void solve(int N) {
pair<int, int> result = search_interval(0, N - 1, N);
answer(result.first, result.second);
}
// --- Lokalni main za testiranje ---
int main() {
srand(time(0));
int N;
cout << "Unesi N (5 ≤ N ≤ 1000): ";
cin >> N;
L_secret = rand() % N;
R_secret = L_secret + rand() % (N - L_secret);
cout << "□ (DEBUG) Tajni interval: [" << L_secret << ", " << R_secret << "]\n";
solve(N);
return 0;
}
Prednost: lako razumljiv i vizuelno edukativan kod koji pokazuje kako se
„emulira“ interaktivni zadatak lokalno.
Nedostatak: algoritam nije najefikasniji, jer koristi jednostavnu binarnu
pretragu i slučajne odgovore Džokera mogu uticati na tačnost.
Drugo rešenje — probabilistički pristup
Sledeći primer prikazuje jednostavniji okvir koji se koristi u takmičarskim rešenjima. Ideja je da se ponavljanjem više upita proveri stabilnost kandidata (da li se u proseku dobija očekivani odgovor).
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// Ovo je "stub" funkcija — u stvarnom sistemu bi se pozivala funkcija query(a)
int send_query(const vector<int>& a) {
return 0; // simulacija
}
bool verify_candidate(int l, int r, int K) {
int ok = 0;
for (int t = 0; t < K; ++t) {
vector<int> a;
int res = send_query(a);
if (false) ++ok; // placeholder
}
return ok * 2 > K;
}
int main() {
vector<pair<int,int>> candidates;
int K = 5;
for (auto [l, r] : candidates) {
if (verify_candidate(l, r, K)) {
cout << "Pronađen kandidat: " << l << " " << r << "\n";
break;
}
}
return 0;
}
Ovo rešenje pokazuje osnovni okvir probabilističke pretrage: ne daje tačno rešenje, ali objašnjava kako se može implementirati logika provere više kandidata kroz ponovljene upite.
Zaključak
Ovaj zadatak je tipičan primer interaktivnog problema — traži kombinaciju logike, verovatnoće i pažljivog planiranja upita. U edukativne svrhe, gornja simulacija korisniku omogućava da vidi kako Džoker „razmišlja” i da nauči kako da razvije efikasniji algoritam pre nego što rešenje preda na zvaničnoj platformi.
Napredna ideja – binarna pretraga uz višestruke probe
U ovom delu objašnjavamo napredniji pristup rešavanju zadatka „Igra sa Džokerom“. Ideja je da se kombinuje binarna pretraga i ponavljanje upita kako bi se smanjio uticaj nasumičnih odgovora Džokera.
Ideja algoritma
- Džoker vraća MIN ili MAX, ali mi ne znamo koji.
- Da bismo smanjili uticaj nasumičnosti, ponavljamo isti upit više puta (M puta).
- Kroz posebno konstruisane nizove
amožemo odrediti:- ➤ da li je ceo segment levo od određene tačke (
R ≤ k), - ➤ desno od nje (
L > k), - ➤ ili ga presijeca (
L ≤ k < R).
- ➤ da li je ceo segment levo od određene tačke (
Ove informacije nam omogućavaju da binarnom pretragom pronađemo L i R sa malom greškom.
Implementacija algoritma
#include <bits/stdc++.h>
using namespace std;
/*
============================================
□ SIMULACIJA DŽOKERA (za lokalno testiranje)
============================================
U stvarnom interaktivnom zadatku, funkcije `query` i `answer`
bi bile obezbeđene od strane sistema (judge-a),
a programer implementira samo funkciju `solve(int N)`.
Ovde ih simuliramo da bismo testirali algoritam lokalno.
*/
// Tajni interval (nepoznat igraču)
int L_secret, R_secret;
// query(a): vraća NASUMIČNO minimum ili maksimum iz segmenta [L_secret, R_secret]
int query(vector<int> a) {
int mn = INT_MAX, mx = INT_MIN;
for (int i = L_secret; i <= R_secret; ++i) {
mn = min(mn, a[i]);
mx = max(mx, a[i]);
}
// nasumično vraća ili min ili max
return (rand() % 2 == 0) ? mn : mx;
}
// answer(L, R): proverava tačnost
void answer(int L, int R) {
int greska = abs(L - L_secret) + abs(R - R_secret);
cout << "\n□ Odgovor: (" << L << ", " << R << ")";
if (greska <= 1)
cout << " ✅ Tačno! (Pravi interval: [" << L_secret << ", " << R_secret << "])\n";
else
cout << " ❌ Pogrešno! (Pravi interval: [" << L_secret << ", " << R_secret << "])\n";
}
/*
============================================
□ ALGORITAM – BINARNA PRETRAGA SA PROBAMA
============================================
Ideja:
- Džoker vraća MIN ili MAX, ali mi ne znamo koji.
- Da bismo smanjili uticaj nasumičnosti, ponavljamo isti upit M puta.
- Kroz posebno konstruisane nizove "a" određujemo:
➤ da li je ceo segment levo od određene tačke (R ≤ k),
➤ desno od nje (L > k),
➤ ili ga presijeca (L ≤ k < R).
Ove informacije nam omogućavaju da binarnom pretragom
pronađemo L i R sa malom greškom.
*/
// Broj ponavljanja po upitu (povećava tačnost)
const int M = 25;
// Pomoćna funkcija – testira položaj R granice u odnosu na indeks k
int probe_R(int N, int k) {
vector<int> a(N);
for (int i = 0; i < N; ++i)
a[i] = (i <= k ? 0 : 1); // 0 do k, 1 nakon k
int zeros = 0;
for (int t = 0; t < M; ++t) {
int res = query(a);
if (res == 0) zeros++;
}
if (zeros == M) return +1; // svi 0 → ceo segment je levo (R ≤ k)
if (zeros == 0) return -1; // svi 1 → segment je desno (L > k)
return 0; // mešano → segment "seče" k
}
// Pomoćna funkcija – testira položaj L granice u odnosu na indeks k
int probe_L(int N, int k) {
vector<int> a(N);
for (int i = 0; i < N; ++i)
a[i] = (i >= k ? 0 : 1); // 0 od k naviše, 1 pre toga
int zeros = 0;
for (int t = 0; t < M; ++t) {
int res = query(a);
if (res == 0) zeros++;
}
if (zeros == M) return +1; // svi 0 → L ≥ k
if (zeros == 0) return -1; // svi 1 → R < k
return 0; // mešano → L < k ≤ R
}
// Pronalazi R pomoću binarne pretrage
int findR(int N) {
int l = 0, r = N - 1;
while (l < r) {
int mid = (l + r) / 2;
int p = probe_R(N, mid);
if (p == +1) r = mid; // segment je levo → R ≤ mid
else l = mid + 1; // segment desno → R > mid
}
return l;
}
// Pronalazi L pomoću binarne pretrage
int findL(int N) {
int l = 0, r = N - 1;
while (l < r) {
int mid = (l + r + 1) / 2;
int p = probe_L(N, mid);
if (p == +1) l = mid; // segment je desno → L ≥ mid
else r = mid - 1; // segment levo → L < mid
}
return l;
}
/*
============================================
□ FUNKCIJA KOJU TREBA IMPLEMENTIRATI (solve)
============================================
- Ova funkcija bi bila jedina koju sistem (judge)
direktno poziva u interaktivnom zadatku.
- U ovom lokalnom testu mi simuliramo `query()` i `answer()`.
*/
void solve(int N) {
cout << "\n□ Početak traženja intervala...\n";
// prvo tražimo R, zatim L
int R = findR(N);
int L = findL(N);
cout << "➡️ Pronađeno: L = " << L << ", R = " << R << endl;
answer(L, R);
}
/*
============================================
□ MAIN (lokalni test)
============================================
Ovde inicijalizujemo nasumičan tajni interval i pozivamo solve().
*/
int main() {
srand(time(0));
int N;
cout << "Unesi N (5 ≤ N ≤ 1000): ";
cin >> N;
// nasumično postavljamo tajni interval [L_secret, R_secret]
L_secret = rand() % N;
R_secret = L_secret + rand() % (N - L_secret);
cout << "□ (DEBUG) Tajni interval: [" << L_secret << ", " << R_secret << "]\n";
solve(N);
return 0;
}
Objašnjenje algoritma
| Korak | Objašnjenje |
|---|---|
| 1. | Pravimo niz a koji ima dve vrednosti (0 i 1) — to je „senzor“ za levo/desno od granice. |
| 2. | Funkcija query(a) vraća min ili max iz segmenta [L, R], ali nasumično — zato radimo više ponavljanja (M) i posmatramo obrazac rezultata. |
| 3. | Ako su svi rezultati 0 → ceo segment unutar nula → R ≤ k. |
| 4. | Ako su svi rezultati 1 → ceo segment desno → L > k. |
| 5. | Ako ima i 0 i 1 → segment se „seče“ u k → L ≤ k < R. |
| 6. | Binarna pretraga omogućava da precizno pronađemo R (gde prestaju nule) i L (gde počinju nule). |
| 7. | Na kraju pozivamo answer(L, R) — uspeh ako je greška ≤ 1. |
Prednosti pristupa
- Robusan — ne zavisi od nasumičnosti jednog upita (ponavljanja smanjuju šum).
- Efikasan — složenost
O(log N × M), što je vrlo brzo i za N ≤ 1000. - Jasna logika — koristi čistu binarnu pretragu po indeksima.
Ideja algoritma – binarna pretraga sa ponavljanjem upita
Ovaj pristup koristi kombinaciju binarnog pretraživanja i ponovljenih upita kako bi se smanjio efekat nasumičnih odgovora Džokera (koji vraća min ili max po volji).
- Džoker vraća MIN ili MAX, ali ne znamo koji.
- Da bismo smanjili uticaj nasumičnosti, isti upit ponavljamo više puta (
M). - Posebno konstruisani nizovi
aomogućavaju da odredimo:- da li je segment potpuno levo od tačke (
R ≤ k), - potpuno desno od nje (
L > k), - ili da je presijeca (
L ≤ k < R).
- da li je segment potpuno levo od tačke (
Ove informacije koristimo u binarnoj pretrazi da postepeno suzimo opseg i pronađemo granice L i R.
#include <bits/stdc++.h>
using namespace std;
/*
============================================
□ SIMULACIJA DŽOKERA (za lokalno testiranje)
============================================
U stvarnom interaktivnom zadatku, funkcije `query` i `answer`
bi bile obezbeđene od strane sistema (judge-a),
a programer implementira samo funkciju `solve(int N)`.
Ovde ih simuliramo da bismo testirali algoritam lokalno.
*/
// Tajni interval (nepoznat igraču)
int L_secret, R_secret;
// query(a): vraća NASUMIČNO minimum ili maksimum iz segmenta [L_secret, R_secret]
int query(vector<int> a) {
int mn = INT_MAX, mx = INT_MIN;
for (int i = L_secret; i <= R_secret; ++i) {
mn = min(mn, a[i]);
mx = max(mx, a[i]);
}
// nasumično vraća ili min ili max
return (rand() % 2 == 0) ? mn : mx;
}
// answer(L, R): proverava tačnost
void answer(int L, int R) {
int greska = abs(L - L_secret) + abs(R - R_secret);
cout << "\n□ Odgovor: (" << L << ", " << R << ")";
if (greska <= 1)
cout << " ✅ Tačno! (Pravi interval: [" << L_secret << ", " << R_secret << "])\n";
else
cout << " ❌ Pogrešno! (Pravi interval: [" << L_secret << ", " << R_secret << "])\n";
}
/*
============================================
□ ALGORITAM – BINARNA PRETRAGA SA PROBAMA
============================================
Ideja:
- Džoker vraća MIN ili MAX, ali mi ne znamo koji.
- Da bismo smanjili uticaj nasumičnosti, ponavljamo isti upit M puta.
- Kroz posebno konstruisane nizove "a" određujemo:
➤ da li je ceo segment levo od određene tačke (R ≤ k),
➤ desno od nje (L > k),
➤ ili ga presijeca (L ≤ k < R).
Ove informacije nam omogućavaju da binarnom pretragom
pronađemo L i R sa malom greškom.
*/
// Broj ponavljanja po upitu (povećava tačnost)
const int M = 25;
// Testira položaj R granice u odnosu na indeks k
int probe_R(int N, int k) {
vector<int> a(N);
for (int i = 0; i < N; ++i)
a[i] = (i <= k ? 0 : 1);
int zeros = 0;
for (int t = 0; t < M; ++t) {
int res = query(a);
if (res == 0) zeros++;
}
if (zeros == M) return +1; // svi 0 → segment levo (R ≤ k)
if (zeros == 0) return -1; // svi 1 → segment desno (L > k)
return 0; // mešano → seče k
}
// Testira položaj L granice u odnosu na indeks k
int probe_L(int N, int k) {
vector<int> a(N);
for (int i = 0; i < N; ++i)
a[i] = (i >= k ? 0 : 1);
int zeros = 0;
for (int t = 0; t < M; ++t) {
int res = query(a);
if (res == 0) zeros++;
}
if (zeros == M) return +1; // svi 0 → L ≥ k
if (zeros == 0) return -1; // svi 1 → R < k
return 0; // mešano → L < k ≤ R
}
// Pronalazi R binarnom pretragom
int findR(int N) {
int l = 0, r = N - 1;
while (l < r) {
int mid = (l + r) / 2;
int p = probe_R(N, mid);
if (p == +1) r = mid;
else l = mid + 1;
}
return l;
}
// Pronalazi L binarnom pretragom
int findL(int N) {
int l = 0, r = N - 1;
while (l < r) {
int mid = (l + r + 1) / 2;
int p = probe_L(N, mid);
if (p == +1) l = mid;
else r = mid - 1;
}
return l;
}
// Funkcija koju sistem poziva u interaktivnom okruženju
void solve(int N) {
cout << "\n□ Početak traženja intervala...\n";
int R = findR(N);
int L = findL(N);
cout << "➡️ Pronađeno: L = " << L << ", R = " << R << endl;
answer(L, R);
}
// Lokalni test
int main() {
srand(time(0));
int N;
cout << "Unesi N (5 ≤ N ≤ 1000): ";
cin >> N;
L_secret = rand() % N;
R_secret = L_secret + rand() % (N - L_secret);
cout << "□ (DEBUG) Tajni interval: [" << L_secret << ", " << R_secret << "]\n";
solve(N);
return 0;
}
□ Objašnjenje algoritma
| Korak | Objašnjenje |
|---|---|
| 1. | Pravimo niz a sa vrednostima 0 i 1 — služi kao „senzor“ za granice. |
| 2. | query(a) vraća min ili max nasumično, zato ponavljamo više puta (M). |
| 3. | Ako su svi rezultati 0 → ceo segment unutar nula → R ≤ k. |
| 4. | Ako su svi 1 → segment desno → L > k. |
| 5. | Ako ima i 0 i 1 → segment „seče“ k. |
| 6. | Binarna pretraga određuje tačke gde prelaz nastaje i nalazi L i R. |
| 7. | Na kraju se poziva answer(L, R) — tačno ako je greška ≤ 1. |
Prednosti ovog pristupa
- Robustan — smanjuje uticaj slučajnosti ponavljanjem upita.
- Efikasan — složenost O(log N × M), što je vrlo brzo za N ≤ 1000.
- Didaktički jasan — lepo demonstrira princip binarne pretrage i smanjenja šuma.
Stohastički pristup rešavanju – „probabilistički detektiv“
Ovaj pristup ne koristi eksplicitnu binarnu pretragu, već se oslanja na
ponovljene nasumične eksperimente da bi statistički procenio položaj tajnog intervala
[L, R].
Ideja je da generišemo više slučajnih nizova a i posmatramo rezultate funkcije
query(a) — koja nasumično vraća min ili max iz segmenta.
Ako posmatramo dovoljan broj rezultata, možemo zaključiti gde se interval verovatno nalazi.
Ključne ideje
- Umesto sistematskog sužavanja kao kod binarne pretrage, koristimo više nasumičnih upita i analiziramo raspodelu odgovora.
- Često pojavljivanje istog minimuma ili maksimuma ukazuje na to da je segment obuhvatio tu poziciju.
- Na osnovu statistike po indeksima procenjujemo najverovatnije granice
LiR.
#include <bits/stdc++.h>
using namespace std;
// ============================================
// □ SIMULACIJA DŽOKERA (kao i ranije)
// ============================================
int L_secret, R_secret;
int query(vector<int> a) {
int mn = INT_MAX, mx = INT_MIN;
for (int i = L_secret; i <= R_secret; ++i) {
mn = min(mn, a[i]);
mx = max(mx, a[i]);
}
return (rand() % 2 == 0) ? mn : mx;
}
void answer(int L, int R) {
int greska = abs(L - L_secret) + abs(R - R_secret);
cout << "\n□ Odgovor: (" << L << ", " << R << ")";
if (greska <= 1)
cout << " ✅ Tačno! (Pravi interval: [" << L_secret << ", " << R_secret << "])\n";
else
cout << " ❌ Pogrešno! (Pravi interval: [" << L_secret << ", " << R_secret << "])\n";
}
// ============================================
// □ STOHASTIČKI PRISTUP
// ============================================
// Ideja: posmatramo koje pozicije u nizu najčešće doprinose ekstremnim vrednostima.
// To nam daje statističku procenu gde se interval [L, R] nalazi.
void solve(int N) {
const int T = 300; // broj eksperimenata
vector<int> score(N, 0);
for (int t = 0; t < T; ++t) {
vector<int> a(N);
for (int i = 0; i < N; ++i)
a[i] = rand() % 1000; // nasumične vrednosti
int res = query(a);
// Beležimo koje pozicije imaju vrednosti bliske rezultatu
for (int i = 0; i < N; ++i) {
if (a[i] == res)
score[i]++; // povećaj „verovatnoću pripadnosti segmentu“
}
}
// Pronalazimo najduži uzastopni niz pozicija sa velikim score-om
int threshold = *max_element(score.begin(), score.end()) * 0.7;
int bestL = 0, bestR = 0, L = -1;
for (int i = 0; i < N; ++i) {
if (score[i] >= threshold) {
if (L == -1) L = i;
bestR = i;
} else if (L != -1) {
if (bestR - L > bestR - bestL)
bestL = L;
L = -1;
}
}
answer(bestL, bestR);
}
// ============================================
// □ MAIN (lokalno testiranje)
// ============================================
int main() {
srand(time(0));
int N;
cout << "Unesi N (5 ≤ N ≤ 1000): ";
cin >> N;
L_secret = rand() % N;
R_secret = L_secret + rand() % (N - L_secret);
cout << "□ (DEBUG) Tajni interval: [" << L_secret << ", " << R_secret << "]\n";
solve(N);
return 0;
}
□ Objašnjenje pristupa
| Korak | Objašnjenje |
|---|---|
| 1. | Generiše se više nasumičnih nizova (T eksperimenata). |
| 2. | Za svaki niz, Džoker vraća min ili max iz segmenta [L, R]. |
| 3. | Za indekse čije su vrednosti jednake rezultatu povećava se brojač score[i]. |
| 4. | Posle mnogo ponavljanja, najveće score vrednosti se grupišu u regionu [L, R]. |
| 5. | Pronalazimo uzastopni interval gde je score iznad praga (npr. 70% maksimuma). |
| 6. | Taj region smatramo procenom tajnog segmenta, pa ga prosleđujemo u answer(). |
✅ Prednosti i mane
- Prednosti: jednostavan za implementaciju, robustan na nasumičnost, intuitivan.
- Mane: zahteva mnogo upita (O(N × T)), manje efikasan za velike N.
Ovaj pristup je koristan kada ne možemo da kontrolišemo nasumičnost Džokerovih odgovora ili kada želimo da prikažemo statistički način zaključivanja — korisno za edukativne svrhe.
|< Interaktivni Algoritmi

