0% au considerat acest document util (0 voturi)
59 vizualizări12 pagini

Lectia10.Probleme OJI 2017

Documentul prezintă două probleme de programare din concursul Olimpiadei de Informatică din 2017 pentru clasa a 10-a. Prima problemă se referă la numărarea șirurilor care încep cu 1 și cresc cu cel mult 1, iar a doua la deplasarea unui rover într-o matrice cu zone sigure și periculoase.

Încărcat de

Stefan
Drepturi de autor
© © All Rights Reserved
Respectăm cu strictețe drepturile privind conținutul. Dacă suspectați că acesta este conținutul dumneavoastră, reclamați-l aici.
Formate disponibile
Descărcați ca PDF, TXT sau citiți online pe Scribd
0% au considerat acest document util (0 voturi)
59 vizualizări12 pagini

Lectia10.Probleme OJI 2017

Documentul prezintă două probleme de programare din concursul Olimpiadei de Informatică din 2017 pentru clasa a 10-a. Prima problemă se referă la numărarea șirurilor care încep cu 1 și cresc cu cel mult 1, iar a doua la deplasarea unui rover într-o matrice cu zone sigure și periculoase.

Încărcat de

Stefan
Drepturi de autor
© © All Rights Reserved
Respectăm cu strictețe drepturile privind conținutul. Dacă suspectați că acesta este conținutul dumneavoastră, reclamați-l aici.
Formate disponibile
Descărcați ca PDF, TXT sau citiți online pe Scribd
Sunteți pe pagina 1/ 12

Lectia nr. 10.

– Probleme concurs

OJI 2017, Clasa a 10-a

Problema Șir

• Dificultate: 3
• Autor: Rodica Pintea
• Online: InfoArena, PbInfo

Rezumat
Considerăm șirurile de lungime n, care încep cu 1, cu proprietatea că fiecare
element este mai mare decât predecesorul lui cu cel mult 1. Să se determine numărul
de șiruri ce se termină în u, și respectiv numărul de șiruri în care un element apare
de cel mult r ori.

Soluție
𝑢−1
Răspunsul pentru prima cerință este 𝐶𝑛−1 , iar explicația este exact cea de la
numărul de partiții ordonate ale unui număr natural, din acest articol. Șirul nostru
este partiționat în u secvențe, iar noi trebuie să alegem capetele acestora. Cum
ultima secvență are capătul fixat în poziția n, rămâne să alegem capetele doar pentru
primele u−1 secvențe. Acestea trebuie să ia valori distincte din
mulțimea {1,2,…,n−1}, așa că numărul lor este 𝐶𝑛−1 𝑢−1
. Având nevoie de o singură
combinare, o vom calcula folosind invers modular, pe baza formulei:

𝑢−1
𝐶𝑛−1 =(n−1)!⋅(u−1)!−1⋅(n−u)!−1

La a doua cerință vom folosi programare dinamică astfel: Notăm cu dp[i] numărul
de șiruri de lungime i cu proprietatea dată. Putem obține un șir de
lungime i adăugând j valori egale la un șir de lungime i−j, unde 1≤j≤r. Valorile
adăugate sunt unic determinate de ultimul element al șirului de lungime i−j. Adică,
dacă acesta se termină în x, atunci șirul nou se va termina în j de x+1. Deci,
recurența este:

dp[i]=dp[i−1]+dp[i−2]+⋯+dp[i−r]

Dinamica poate fi calculată imediat în O(n⋅r), însă poate fi optimizată foarte ușor
făcând următoarea observație. Scriem una sub alta recurențele pentru
dp[i] și dp[i−1]:
Lectia nr. 10. – Probleme concurs

dp[i] =dp[i−1]+dp[i−2]+⋯+dp[i−r]
dp[i−1]=dp[i−2]+dp[i−3]+⋯+dp[i−r−1]
Dacă scădem cele două relații, se vor reduce o grămadă de termeni și vom obține:

dp[i]−dp[i−1]=dp[i−1]−dp[i−r−1]
De unde:

dp[i]=2⋅dp[i−1]−dp[i−r−1]

Evident, complexitatea pentru calcularea noii recurențe este O(n).

Sursă C++
Problema Șir
#include <bits/stdc++.h>
using namespace std;

const int NMAX = 100010;


const int MOD = 20173333;

ifstream fin("sir9.in");
ofstream fout("sir9.out");

int p, n, u;
int dp[NMAX];

int pwr(int a, int b) {


if (!b)
return 1;
if (b & 1)
return 1LL * a * pwr(1LL * a * a % MOD, b >> 1) %
MOD;
return pwr(1LL * a * a % MOD, b >> 1);
}

int modInv(int n) {
return pwr(n, MOD - 2);
}

int fact(int n) {
int f = 1;
for (int i = 2; i <= n; i++)
f = 1LL * f * i % MOD;
return f;
}
Lectia nr. 10. – Probleme concurs

int main() {
fin >> p >> n >> u;
if (p == 1)
fout << 1LL * fact(n - 1) * modInv(fact(u - 1)) % MOD
* modInv(fact(n - u)) % MOD << '\n';
else {
dp[0] = dp[1] = 1;
for (int i = 2; i <= n; i++)
dp[i] = (2 * dp[i - 1] - (i >= u - 1 ? dp[i - u -
1] : 0) + MOD) % MOD;
fout << dp[n] << '\n';
}
return 0;
}

Problema Rover

• Dificultate: 3
• Autor: Mircea Lupșe-Turpan
• Online: InfoArena, PbInfo

Rezumat
Avem un rover care se poate deplasa (cu o celulă în nord, sud, est sau vest)
într-o matrice pătratică cu n linii și n coloane. Fiecare celulă (zonă) a
matricei are o stabilitate reprezentată printr-un număr natural, iar rover-ul
are greutatea g. O zonă cu stabilitatea mai mică decât g este considerată o
zonă periculoasă pentru rover.

La prima cerință trebuie să determinăm numărul minim de zone periculoase


pe care trebuie să le traverseze rover-ul pentru a ajunge de la zona (1,1) la
(n,n). La a doua cerință se cere greutatea maximă pe care o poate avea rover-
ul pentru a putea ajunge din (1,1) în (n,n) fără a traversa nicio zonă
periculoasă.

Soluție: Cerința 1
Pentru prima cerință putem defini costul unei celule drept numărul minim de
zone periculoase pe care rover-ul trebuie să le parcurgă pentru a ajunge la ea.
Se observă ușor că atunci când trecem din celulaA în celula B, avem relația
de recurență cost(B)=cost(A)+zonaSigura(B). Evident, zonaSigura(B)
Lectia nr. 10. – Probleme concurs

este 1 dacă B este o zonă sigură, și 0 dacă nu. Această recurență ne indică
faptul că putem aplica algoritmul lui Lee cu costuri. Adică, în loc să calculăm
o distanță minimă, calculăm un cost minim, iar pentru a obține de fiecare
dată un cost minim pentru celula curentă, ne vom expanda mai întâi din
zonele sigure, iar abia apoi din cele periculoase.

Pentru a respecta această ordine a vizitării zonelor, putem folosi un deque în


loc de coadă. În față adăugăm zone sigure, iar în spate zone periculoase. Asta
ne garantează că întotdeauna zonele din deque vor fi ordonate descrescător
în funcție de costul lor, și deci la fiecare pas continuăm cu cea mai sigură
Pentru a înțelege mai bine, puteți urmări mai jos cum arată deque-ul după
fiecare dintre primii șase pași, pentru exemplul din enunțul problemei. Am
folosit triplete de forma (a,b,c) cu semnificația „zona de
coordonate (a,b), ce are costul c”.

(1, 1, 0)
(1, 2, 1) (2, 1, 0)
(2, 2, 1) (3, 1, 1) (1, 2, 1)
(1, 3, 2) (2, 2, 1) (3, 1, 1)
(4, 1, 2) (1, 3, 2) (2, 2, 1) (3, 2, 1)
(4, 2, 2) (4, 1, 2) (1, 3, 2) (2, 2, 1) (3, 3, 1)

Soluție: Cerința 2
Pentru a doua cerință putem folosi tehnica căutării binare pe rezultat. La
fiecare pas testăm dacă mijlocul intervalului curent reprezintă o greutate
pentru care se poate ajunge din zona (1,1) în zona (n,n). Pentru verificare
putem folosi atât algoritmul lui Lee, cât și un algoritm de fill. Eu am ales fill
recursiv pentru că e mai scurt și oricum nu avem nevoie de lungimea
drumului minim. Apoi, dacă greutatea este bună, continuăm căutarea binară
în dreapta, în vederea găsirii unei greutăți mai mari. Dacă greutatea nu este
bună, înseamnă că este prea mare, așa că vom căuta în stânga una mai mică.

Sursă C++
Problema Rover
#include <deque>
#include <fstream>

#define DMAX 510

std::ifstream fin("rover.in");
std::ofstream fout("rover.out");

// struct pentru celule:


struct Cell {
Lectia nr. 10. – Probleme concurs

int lin;
int col;
};

// Vectorii de deplasare:
const int dL[] = {-1, 0, 0, 1};
const int dC[] = { 0, -1, 1, 0};

// Datele problemei:
int c, n, g;
int mat[DMAX][DMAX];

// Pentru cerința 1:
int dp[DMAX][DMAX];
std::deque dq;

// Pentru cerința 2:
int sol;
bool aux[DMAX][DMAX];

/// Funcția ce face Lee cu deque


void leeDeque() {
Cell cell, nghb;
dq.push_front({1, 1}); // Introducem în deque prima
celulă.

// dp[i][j] = numărul minim de zone periculoase pe care


// le traversează rover-ul pentru a ajunge în celula (i,
j)

// Marcăm toate celulele drept nevizitate:


for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
dp[i][j] = -1;

dp[1][1] = 0;
while (!dq.empty()) {
// Scoatem din deque celula cu costul minim:
cell = dq.front();
dq.pop_front();

// Parcurgem vecinii acestei celule:


for (int i = 0; i < 4; i++) {
nghb.lin = cell.lin + dL[i];
nghb.col = cell.col + dC[i];

// Celula nu e vizitată...
if (dp[nghb.lin][nghb.col] == -1) {
// Dacă celula e sigură, o punem în față:
if (mat[nghb.lin][nghb.col] >= g) {
Lectia nr. 10. – Probleme concurs

dp[nghb.lin][nghb.col] =
dp[cell.lin][cell.col];
dq.push_front(nghb);
}
else { // dacă nu, în spate:
dp[nghb.lin][nghb.col] =
dp[cell.lin][cell.col] + 1;
dq.push_back(nghb);
}
}
}
}
}

/// Funcția recursivă de fill


void fill(int i, int j, int g) {
// Ne expandăm doar în zonele sigure:
if (!aux[i][j] && mat[i][j] >= g) {
aux[i][j] = true;
fill(i - 1, j , g);
fill(i + 1, j , g);
fill(i , j - 1, g);
fill(i , j + 1, g);
}
}

/// Funcția care testează dacă greutatea g e fezabilă


bool fillMat(int g) {
// Bordăm matricea:
for (int i = 0; i <= n + 1; i++)
aux[i][0] = aux[i][n + 1] =
aux[0][i] = aux[n + 1][i] = true;

// Marcăm celulele ca nevizitate:


for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
aux[i][j] = false;

fill(1, 1, g); // Începem fill-ul.


return aux[n][n]; // Returnăm dacă s-a ajuns în (n, n).
}

int main() {
fin >> c >> n;
if (c == 1)
fin >> g;

for (int i = 1; i <= n; i++)


for (int j = 1; j <= n; j++)
fin >> mat[i][j];
Lectia nr. 10. – Probleme concurs

if (c == 1) {
leeDeque();
fout << dp[n][n] << '\n';
}
else {
// Căutare binară pe greutatea maximă:
int md, lo = 0, hi = 10001;
while (hi - lo > 1) {
md = (lo + hi) / 2;
if (fillMat(md)) {
lo = md;
sol = md > sol ? md : sol;
}
else
hi = md;
}
fout << sol << '\n';
}
return 0;
}

Anexa

Ce sunt partițiile unui număr natural?


Numim partiție a lui n∈N o secvență de numere naturale
nenule P=⟨p1,p2,…,pk⟩ cu proprietatea că p1+p2+⋯+pk=n. Dacă
secvența P este ordonată, partiția este ordonată, iar în caz contrar,
neordonată.

De exemplu, partițiile ordonate ale lui 4 sunt:

(1,1,1,1),(1,1,2),(1,2,1),(1,3),(2,1,1),(2,2),(3,1),(4)

Însă, partițiile neordonate ale lui 4 sunt:

[1,1,1,1],[1,1,2],[1,3],[2,2],[4]

Așadar, partițiile ordonate (1,1,2) și (1,2,1) sunt diferite, pe când


partițiile neordonate [1,1,2] și [1,2,1] sunt egale.
Lectia nr. 10. – Probleme concurs

Prin convenție, numărul de partiții ale lui 0 este 1. Convenția este


justificată, deoarece putem considera că mulțimea vidă este o
partiție a lui 0, suma elementelor ei fiind 0.

Numărul de partiții ordonate


Problema asta e partea simplă a articolului, pentru că s-a dovedit a
fi mult mai ușor să numeri partițiile ordonate decât pe cele
neordonate. Să vedem mai întâi cum putem număra partițiile în
funcție de lungimea lor. Fie p(n,k) numărul de partiții ordonate ale
lui nn, de lungime k. Pentru a ne ușura munca, vom reduce problema
la una mai ușor de abordat:

Să se determine numărul modurilor de a împărți un șir de


lungime n în k secvențe de lungimi nenule. De exemplu, șirul xxxxx poate fi
împărțit în 3 secvențe astfel:
x x xxx
x xx xx
x xxx x
xx x xx
xx xx x
xxx x x

Este clar că o astfel de împărțire a unui șir este de fapt o partiție a


lui n, în care lungimea fiecărei secvențe reprezintă valoarea unui
element din partiție.

Practic, trebuie să găsim numărul de moduri de a alege


cele k−1 puncte de split, adică acele poziții pe care se termină fiecare
secvență, mai puțin ultima (pentru că ea are poziția de sfârșit fixată).
Aceste poziții iau valori din mulțimea {1,2,…,n−1}. Cum ele trebuie
să fie distincte, se observă ușor că soluția e dată de 𝐶𝑛−1
𝑢−1
.
Acum, dacă vrem să calculăm numărul total de partiții ordonate ale
lui n, nu avem decât să însumăm niște combinări. Mai exact, dacă
notăm cu p(n) numărul tuturor partițiilor ordonate ale
lui n, obținem:

p(n)= 𝐶𝑛−1
0 1
+𝐶𝑛−1 𝑛−1
+⋯+𝐶𝑛−1 =2n−1
Lectia nr. 10. – Probleme concurs

Numărul de partiții neordonate de lungime dată


Păstrăm notațiile precedente, doar că de data asta se vor referi la
partiții neordonate. Din nou, vom număra mai întâi partițiile după
lungimea lor. Pentru a calcula p(n,k), vom folosi programare
dinamică astfel:

Se observă că putem obține o partiție de lungime k a lui n în două


moduri. Fie luăm o partiție de lungime k−1 a lui n−1 și adăugăm
elementul 1 la începutul ei, fie luăm o partiție de lungime kk a
lui n−k și incrementăm toate elementele acesteia. De exemplu,
partiția [1,2,2] se obține adăugându-l pe 1 la începutul lui [2,2], pe
când [2,2] se obține incrementând elementele lui [1,1]. Astfel, orice
partiție se poate obține pornind de la mulțimea vidă, aplicând asupra
acesteia, pe rând, mai multe operații de aceste două feluri. De pildă,
pentru a-l obține pe [3,4,4], procedăm astfel:

Așadar, recurența dinamicii noastre este următoarea:

Complexitatea pentru calcularea lui p(n,k) este, evident, O(n2). Nu


putem optimiza nimic, și probabil nu există o soluție mai bună.
Putem folosi această dinamică și pentru a calcula p(n):

p(n)=p(n,1)+p(n,2)+⋯+p(n,n) pentru n≥1


Însă, pentru cerința asta există o soluție mai bună decât cea
în O(n2), ce se bazează pe recurența liniară despre care vorbeam la
începutul articolului.
Lectia nr. 10. – Probleme concurs

Numărul total de partiții neordonate


În primul rând, trebuie să definim noțiunea de funcție generatoare.
Funcția generatoare a unui șir (an)n≥0 este o funcție ce poate fi scrisă
ca o serie de forma:

O serie este un șir infinit între elementele căruia se pune semnul ++.

Vom demonstra mai întâi că următoarea funcție este funcția


generatoare a șirului f:

Dacă desfacem parantezele, înainte să grupăm termenii asemenea, fiecare


coeficient al lui xn va fi practic o partiție a lui n. De ce?

Ei bine, exponentul n provine dintr-o expresie de forma a11+a22+a3


3+⋯. Aici, ai reprezintă practic frecvența lui i în cadrul partiției. De
exemplu, 1⋅1+3⋅2+0⋅3+2⋅4+0⋅5+0⋅6+⋯ este partiția [1,2,2,2,4,4]. Deci,
coeficientul lui n în seria noastră va fi într-adevăr p(n).

Aici începe partea interesantă. Acum trebuie să calculăm eficient coeficienții


seriei de mai sus. Observăm că fiecare paranteză k este suma unei progresii
geometrice de rație xk. Evident, dacă x>1, progresia e divergentă. Dar dacă
nu, aceasta converge la (1−xk)−1. Înlocuim seriile cu limitele lor și obținem:
Lectia nr. 10. – Probleme concurs

Aici aplicăm Teorema numerelor pentagonale, formulată de Euler, care ne


spune că:

Semnele alternează din 2 în 2, iar exponenții sunt dați de șirul numerelor


pentagonale generalizate, adică numerele de forma g(k)=k(3k−1)/2, unde k ia pe
rând valorile +1,−1,+2,−2,…

Implementare

Mai jos aveți o sursa la problema Crescător2 de pe InfoArena, care, după cum
am spus și la început, cere determinarea sumei p(1)+p(2)+⋯+p(n). Mai
întâi am precalculat numerele pentagonale până la n, iar apoi am calculat
dinamica mergând la fiecare pas până la cel mai apropiat număr pentagonal
de i.

Problema Crescător2
#include <bits/stdc++.h>
using namespace std;

ifstream fin("crescator2.in");
ofstream fout("crescator2.out");

const int MOD = 700001;

inline void add(int& x, int y)


{ x += y; if (x >= MOD) x -= MOD; }
inline void sub(int& x, int y)
{ x -= y; if (x < 0) x += MOD; }

int main() {
int n; fin >> n;
vector<int> pent(n);
Lectia nr. 10. – Probleme concurs

for (int i = 0, j = 1; i < n; i++, j = -j + (j <= 0))


pent[i] = (3 * j * j - j) / 2;

vector<int> dp(n + 1);


dp[0] = 1;
int sol = 0;
for (int i = 1; i <= n; i++) {
for (int j = 0; pent[j] <= i; j++)
if (j & 2)
sub(dp[i], dp[i - pent[j]]);
else
add(dp[i], dp[i - pent[j]]);
add(sol, dp[i]);
}
fout << sol << '\n';
return 0;
}

Complexitate
Complexitatea soluției este O(n), pentru că numărul pentagonal maxim
până la care se iterează la fiecare pas este aproximativ g(2n/3). Asta se
înmulțește cu 2, deoarece pentru fiecare k luăm în considerare și g(+k) și
g(−k). Așadar, constanta din spatele complexității este de
aproximativ .

S-ar putea să vă placă și