Za neki prirodan broj kažemo da je prost, ako je deljiv samo sa 1 i sa samim sobom. Posmatrajmo sledeći problem:Odrediti sve proste brojeve na intervalu od a do b.
Ovaj problem se često javlja u rešavanju nekih složenijih zadataka. Jedan od mogućih algoritama za rešenje:
Posle učitanih brojeva a i b prolazi se redom kroz sve brojeve na ovom intervalu koristeći for ciklus. Za svaki od tih brojeva proverava se da li je prost i ako jeste ispisuje se. Provera da li je broj prost(metoda): Ako je 1 ili 2 broj je prost. Ako nije uvedemo bool promenljivu koja se u početku postavi na true. To znači da pretpostavljamo da će broj biti prost. Kroz ciklus menjamo delioc počev od dva pa sve dok ne naiđemo na onaj, sa kojim će ispitivani broj biti deljiv. Kad naiđemo na takav, prekidamo ciklus. Ako je poslednji delioc manji od ispitivanog broja, onda znači da broj nije prost, jer je deljiv sa još nekim brojem osim sa jedinicom i sa samim sobom(definicija prostih brojeva). U tom slučaju bool promenljivu treba promeniti na false. Dakle povratna vrednost metode će biti true, ako je broj prost ili false ako nije.
int main()
{
long long a,b,c=0;
cin >> a >> b;
for(int i=a;i<=b;i++){
if(isProst(i)){
cout << i << " ";
c++;
if(c%10==0)cout << endl;
}
}
return 0;
}
#include < iostream >
using namespace std ;
bool isProst(long long b){ if(b==1 || b==2) return true;// 1 i 2 su prosti brojevi bool r=true; long d=2; /* Trazi broj koji je delilac broja b na segmentu od 2 do b*/ while(b % d != 0 )// Uslov petlje je da b nije deljiv sa d
{
d++;
} if(d < b)
r=false; return r;
}
int main()
{ long long a, b ,c=0; for(int i=a; i <= b; i++)
{ if(isProst(i))
{ cout << i << " ";
c++; if(c % 10 == 0) cout << endl;
}
}
}
Ovaj algoritam je previše spor za velike brojeve > 10^9. Najveći broj iteracija je ovde n, a može biti i manji ako ranije naiđemo na broj koji je deljiv sa ispitivanim brojem, a da nije 1 ili n, U tom slučaju se ranije završava while petlja u kojoj se vrši provera. Postoji način da se određivanje da li je broj prost ubrza, što je objašnjeno dalje u tekstu.
Svođenje broja iteracija sa n na √n
Ako malo bolje analiziramo delioce nekog broja možemo uočiti da ako je d delioc nekog broja n i n/d će takođe biti delioc tog istog broja. Npr. broj 6 je delioc broja 42, ali takođe je i broj 7=42/6. Može se dalje uočiti da bar jedan od ova dva delioca mora biti manji ili jednaki od √n.
U slučaju da su oba delioca jednaka, broj n je njihov potpun kvadrat, a vrednost oba delioca je tačno jednaka √n
Na osnovu ovog možemo zaključiti da je potreban broj provera da li je broj n prost najviše jednak √n
#include < iostream >
using namespace std ;
bool isProst(long long n)
{ if(n==1) return false; long long d=2; while(d*d<=n){ if(n % d == 0)
{ return false;
}
d=d+1;
} return true;
}
int main()
{ long long a, b ,c=0; for(int i=a; i <= b; i++)
{ if(isProst(i))
{ cout << i << " ";
c++; if(c % 10 == 0) cout << endl;
}
}
}
Pogledajmo funkciju koja proverava da li je poslat broj n prost. Prvo se proverava da li je n jednako 1. Ako jeste izlazi se iz funkcije i vraća odgovor false. Ostali potencijalni delioci za koje se vrši provera su od 2 dok se ne naiđe na broj koji je deljiv sa n ili u najgorem slučaju do n ^ 1/2 uključujući i n ^ 1/2. Ako se kroz while naiđe na broj koji je deljiv izlazi se iz funkcije sa povratnom vrednošću jednakoj false, jer u tom slučaju broj nije prost.
Algoritam ispitivanje da li je broj prost koristeći 6k+1 teoremu
Ova teorema kaže da je svaki prost broj veći od 3 oblika ili 6k+1 ili 6k-1. Ovo može ubrzati prethodni algoritam tako da se provera da li je broj prost vrši samo za brojeve oblika 6k+1 i 6k-1.
#include < iostream >
using namespace std ;
bool isProst(long long n)
{ if(n==1) return false;
if(n==2 || n==3) return true; long long k=1; while((6*k-1)*(6*k-1)<=n)
{ if(n % (6*k-1) == 0 || n % (6*k+1) == 0)//Ispituje samo oblike 6k+1 i 6k-1, k=1,2,3...
{ return false;
}
k=k+1;
} return true;
}
int main()
{ long long a, b ,c=0; for(int i=a; i <= b; i++)
{ if(isProst(i))
{ cout << i << " ";
c++; if(c % 10 == 0) cout << endl;
}
}
}
Određivanje svih delioca nekog broja
Odrediti sve delioce nekog broja može se takođe rešiti ispitavanjem redom svih brojeva, koji su manji od n. Ovo je kao smo u prethodnom tekstu rekli, ispravno samo za brojeve do maksimalno 10 ^9. Određivanje svih delioca treba razlikovati od faktorizacije. U prvom slučaju određujemo brojeve sa kojim je deljiv neki broj n i delioce pišemo svaki po jedan put. Npr za n=12 bi bilo: Delioci broja 12: 1,2,3,4,6,12 Faktorizacija bi bila da napišemo proizvod delilaca koji bi dali broj n. Za n=12, faktorizacija bi značila: 12=1*2*2*3
Kod određivanja delioca, kao i kod određivanja prostih brojeva, važi zaključak da je dovoljno ispitati brojeve maksimalno do vrednosti n^1/2. Svaki delioc d ima svog para n/d. Ovo se vodi na slici 1. Na primer delioci broja 12: 1 i 12, 2 i 6, 3 i 4. Delioci broja 16 su: 1 i 16, 2 i 8, 4 i 4. Dakle ovde vidimo da je 4 u paru sa samim sobom i da je 4=16^1/2.
Slika 1:Delioci broja n, prikaz parova delioca brojeva 12 i 16
Program koji određuje sve delioce broja n:
#include < iostream >
#define DIM 100
using namespace std ;
int d[DIM]; long long sviDelioci(long long n)
{ long long m=0; long long i=1; while(i*i < n)
{ if(n % i == 0)
{
d[m+1]=i;
d[m+2]=n/i;
m=m+2;
}
i=i+1;
} if(i*i == n)
{
m=m+1;
d[m]=i;
} return m;
}
int main()
{ long long N,m; cin >> N;
m=sviDelioci(N); for(int i=1;i<=m;i++){ cout << d[i] << endl;
}
}
Faktorizacija broja N
Faktorizacija nekog broja je njegovo rastavljanje na proste činioce tj. Neki broj N napisati u obliku:
n=p1a1*p2a2*...*pkak, za k=1,2,...
Za razliku od određivanja svih delioca kod koga se ispituje deljivost svih brojeva od 2 do n ^ 1/2 i ako je potencijalni delilac deljiv, prelazi se na sledeći delilac, što se odrađuje u delu koda koji je prethodno prikazan:
while(i*i < n)
{
if(n%i == 0)
{
d[m+1]=i;
d[m+2]=n/i;
m=m+2;
}
i=i+1;
}
if(i*i == n)
{
m=m+1;
d[m]=i;
}
kod faktorizacije je potrebno ponavljati proces deljenja sa istim deliocem dokle god je ostatak deljenja broja n deljiv sa potencijalnim deliocem d. Rešenje algoritma bi moglo biti:
#include < iostream >
#define DIM 100
using namespace std ;
int a[DIM]; int p[DIM];
int faktorizacija(int n)
{ int k=0; int d=2; while (d*d<=n)
{ if(n % d == 0){
k=k+1;
a[k]=0;//Izlozilac koji se pamti u nizu
p[k]=d; while(n % d==0){
n=n/d;
a[k]=a[k]+1;
}
}
d=d+1;
} if(n>1){
k=k+1;
p[k]=n;
a[k]=1;
} return k;
Zadatak 1: Kvadratni parovi — primena faktorizacije
Dat je niz a dužine n. Potrebno je odrediti broj parova indeksa
(i, j), gde je 1 ≤ i < j ≤ n, za koje važi da je proizvod
ai · aj jednak kvadratu nekog prirodnog broja.
Drugim rečima, tražimo koliko parova elemenata iz niza daje proizvod koji je
savršeni kvadrat.
Uputstvo — kako razmišljati
Na prvi pogled deluje da treba proveriti sve parove elemenata i za svaki par
ispitati da li je njihov proizvod kvadrat.
To bi bilo previše sporo za velike ulaze.
Zato je potrebno da problem posmatramo kroz faktorizaciju na proste činioce.
Ključna ideja
Ako je neki broj zapisan kao:
x = p1^e1 * p2^e2 * ... * pk^ek
onda je x savršen kvadrat ako i samo ako su svi eksponenti
e1, e2, ..., ek parni.
Zbog toga za svaki broj možemo izdvojiti samo one proste faktore koji se
pojavljuju sa neparnim eksponentom.
Taj deo broja se često naziva squarefree jezgro ili samo jezgro.
Na primer:
72 = 2^3 * 3^2 -> jezgro je 2
18 = 2^1 * 3^2 -> jezgro je 2
16 = 2^4 -> jezgro je 1
Sada dolazimo do najvažnijeg zapažanja:
a_i * a_j je kvadrat ⇔ jezgra brojeva a_i i a_j su ista
Zašto zadržavamo samo neparne eksponente?
Posmatrajmo primer:
72 = 2^3 * 3^2
Eksponent uz broj 3 je paran:
3^2
što znači da je taj deo već savršen kvadrat i ne pravi problem.
Međutim:
2^3 = 2^2 * 2
Ovde deo 2^2 jeste kvadrat, ali ostaje još jedna „neuparena” dvojka:
2^3 = (2^2) * 2
Baš taj „neupareni” deo je važan. Zato iz svakog broja zadržavamo samo proste faktore sa neparnim eksponentom.
Drugim rečima:
parni eksponenti već formiraju kvadrat i mogu se zanemariti
neparni eksponenti predstavljaju „višak” koji mora da se dopuni drugim brojem
Na primer:
72 = 2^3 * 3^2 -> jezgro je 2
18 = 2^1 * 3^2 -> jezgro je 2
Kada ih pomnožimo:
72 * 18 =
2^(3+1) * 3^(2+2) =
2^4 * 3^4
Svi eksponenti sada postaju parni, pa je proizvod savršen kvadrat.
Zašto to radi?
Kada pomnožimo dva broja, eksponenti istih prostih faktora se sabiraju.
Da bi proizvod bio kvadrat, svi ti zbirni eksponenti moraju biti parni.
To se dešava tačno onda kada oba broja imaju isti „neparni deo” u faktorizaciji.
Dakle, umesto da proveravamo sve parove, dovoljno je da:
za svaki broj izračunamo njegovo jezgro
prebrojimo koliko puta se svako jezgro pojavljuje
za svako jezgro sa brojem pojavljivanja c dodamo
c * (c - 1) / 2 u odgovor
Kako brzo naći jezgro broja?
Ako bismo svaki broj faktorisali deljenjem prostim brojevima od početka,
rešenje bi bilo presporo za velike ulaze.
Zato se koristi Eratostenovo sito, odnosno niz najmanjih prostih delilaca
spf (smallest prime factor).
Tada svaki broj možemo brzo rastaviti na proste činioce i samo pratiti
parnost eksponenata.
Koraci algoritma
izgraditi niz najmanjih prostih delilaca pomoću sita
za svaki broj iz niza izračunati njegovo jezgro
prebrojati koliko puta se svako jezgro pojavljuje
sabirati cnt[x] * (cnt[x] - 1) / 2 za svako jezgro
Zašto koristimo formulu c * (c - 1) / 2 ?
Pretpostavimo da se neko jezgro pojavljuje c puta.
To znači da imamo:
c brojeva koji mogu međusobno da formiraju kvadratni proizvod
Potrebno je izabrati bilo koja dva od njih.
Broj načina da izaberemo 2 elementa od ukupno c jednak je:
c * (c - 1) / 2
Na primer, ako se neko jezgro pojavljuje 4 puta:
a, b, c, d
mogući parovi su:
(a,b) (a,c) (a,d) (b,c) (b,d) (c,d)
Ukupno:
4 * 3 / 2 = 6
Dakle, umesto da proveravamo svaki par posebno, dovoljno je da za svako jezgro znamo koliko puta se pojavljuje.
Složenost
Ako je najveći mogući broj 10^6, onda se sito pravi dovoljno brzo,
a svaka faktorizacija se zatim obavlja efikasno.
Vremenska složenost: O(M log log M + n log M) ili približno O(M + n log M)
Memorijska složenost: O(M)
Ovde je M najveća vrednost elementa niza.
⚠ Pre nego što pogledaš rešenje, pokušaj samostalno!
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
/*
Pravimo niz najmanjih prostih delilaca (spf).
spf[x] = najmanji prost broj koji deli x.
*/
vector<int> napravi_spf(int maxV) {
vector<int> spf(maxV + 1, 0);
for (int i = 2; i <= maxV; i++) {
if (spf[i] == 0) {
spf[i] = i;
if ((ll)i * i <= maxV) {
for (ll j = 1LL * i * i; j <= maxV; j += i) {
if (spf[j] == 0) spf[j] = i;
}
}
}
}
return spf;
}
/*
Funkcija koja vraća squarefree jezgro broja x.
Tokom faktorizacije pratimo parnost eksponenata:
- ako se prost faktor pojavljuje neparan broj puta, on ostaje u jezgru
- ako se pojavljuje paran broj puta, on se "poništava"
*/
int jezgro(int x, const vector<int> &spf) {
int rezultat = 1;
while (x > 1) {
int p = spf[x];
int cnt = 0;
while (x % p == 0) {
x /= p;
cnt++;
}
// Ako je eksponent neparan, zadržavamo p u jezgru
if (cnt % 2 == 1) {
rezultat *= p;
}
}
return rezultat;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n);
int maxV = 0;
for (int i = 0; i < n; i++) {
cin >> a[i];
maxV = max(maxV, a[i]);
}
// Pravimo SPF niz samo do najvećeg broja iz ulaza
vector<int> spf = napravi_spf(maxV);
/*
cnt[jezgro] = koliko puta se određeno jezgro pojavilo
Ako su dva broja istog jezgra, njihov proizvod je savršen kvadrat.
*/
unordered_map<int, long long> cnt;
for (int x : a) {
int k = jezgro(x, spf);
cnt[k]++;
}
ll odgovor = 0;
for (auto &par : cnt) {
ll c = par.second;
odgovor += c * (c - 1) / 2;
}
cout << odgovor << '\n';
return 0;
}
Objašnjenje rešenja
Umesto da proveravamo sve parove elemenata, posmatramo samo njihove
faktorizacije.
Za svaki broj izdvajamo deo sa neparnim eksponentima u prostom rastavu.
Taj deo je njegovo jezgro.
Ako dva broja imaju isto jezgro, njihov proizvod je kvadrat, jer se svi
prosti faktori u proizvodu pojavljuju sa parnim ukupnim eksponentom.
Zato je dovoljno da:
izračunamo jezgro svakog broja
prebrojimo koliko puta se svako jezgro pojavljuje
za svako jezgro izračunamo broj parova
Zašto je ovo takmičarski dobar trik?
Zadatak izgleda kao običan problem sa parovima, ali se zapravo rešava
elegantnim korišćenjem faktorizacije.
Ključ je da se broj ne posmatra sam po sebi, već kroz strukturu njegovih
prostih faktora.
To je tipičan primer zadatka iz teorije brojeva koji se često pojavljuje
na takmičenjima.
Mini intuicija za učenike
Zapamti sledeće:
kvadrat ima sve parne eksponente u faktorizaciji
za proizvod dva broja bitna je parnost eksponenata
brojeve sa istim „neparnim delom” treba grupisati zajedno
Ova ideja se često koristi i u drugim zadacima gde treba prepoznati
da li je neki proizvod ili odnos brojeva „kvadratan”.