Pagini: [1]   În jos
  Imprimă  
Ajutor Subiect: OJI - Text  (Citit de 9601 ori)
0 Utilizatori şi 1 Vizitator pe acest subiect.
VladBtz
Strain
*

Karma: -3
Deconectat Deconectat

Mesaje: 29



Vezi Profilul
« : Februarie 28, 2015, 17:25:24 »

Cerinţă
Pentru un text dat, se cere să se afişeze numărul de cuvinte din text, apoi numărul minim de cuvinte ce pot fi eliminate astfel încât în textul rămas orice cuvânt (cu excepţia ultimului) să se termine cu aceeaşi literă cu care începe cuvântul următor, iar în final să se afişeze cuvintele din text rămase după eliminare, fiecare cuvant fiind afişat pe câte o linie.

Date de intrare
Fişierul text.in conţine un text scris pe mai multe linii. Pe fiecare linie se află cuvinte formate din litere mici ale alfabetului latin. Cuvintele sunt despărţite între ele prin exact câte un spaţiu.

Date de iesire
Fişierul text.out va conţine pe primele doua linii două numerele x şi y, unde x va fi numărul de cuvinte din text, iar y numărul minim de cuvinte ce trebuie eliminate. Pe liniile următoare se vor afişa, în ordine, cuvintele rămase după eliminarea celor y cuvinte, câte un cuvant pe o linie.

Restricţii şi precizări
•   Numărul de cuvinte din text este maximum 20000.
•   Lungimea maximă a unui cuvânt este 20.
•   Fiecare linie de text din fişierul de intrare are cel mult 200 de caractere.
•   În fişier pot exista rânduri goale.
•   Se acordă 10 % din punctaj pentru determinarea corectă a numărului de cuvinte din text.
•   Se acordă 40 % din punctaj pentru rezolvarea corectă a primelor două cerinţe.
•   Pentru rezolvarea corectă a tuturor cerinţelor se acordă tot punctajul.

Exemplu
text.in   text.out   Explicaţii
pentru ca nu are

timp ion spune ca nu urmareste nici
emisiuni interesante si evident nici altfel
de

emisiuni   19
13
ion
nu
urmareste
emisiuni
interesante
evident   Din întregul text care este format din 19 cuvinte se elimină 13 cuvinte şi se obţin, în ordine, cuvintele: ion, nu, urmareste, emisiuni, interesante,
evident

Timp maxim de executare/test : 0.1 sec
Total memorie disponibilă : 2 MB (din care 1 MB pentru stivă)
Dimensiune maximă a sursei: 5 KB


Sa elimine cat mai putine cuvinte adica sa se determine cea mai munga secventa de cuvinte care incep cu ultima litera a celuilalt.Cum as putea sa scot aceasta secventa?Idei mai simple?
Memorat
Owlree
Strain
*

Karma: 16
Deconectat Deconectat

Mesaje: 27



Vezi Profilul
« Răspunde #1 : Februarie 28, 2015, 18:33:35 »

Cuvintele nu trebuie neapărat să fie adiacente.

Soluția din exemplu arată cam așa:
pentru ca nu are timp ion spune ca nu urmareste nici emisiuni interesante si evident nici altfel de emisiuni.

Unde cuvintele îngroșate au fost păstrate, iar celelalte eliminate.

Răspunsul este da, ni se cere secvența maximală de cuvinte (nu neapărat consecutive în datele de intrare) care are proprietatea că oricare două cuvinte consecutive din secvență îndeplinesc proprietatea cerută.

Dacă nu te descurci, problema este de la OJI 2010. Găsești soluția în arhiva OJI.

Poți găsi problema și pe infoarena.
« Ultima modificare: Februarie 28, 2015, 22:44:44 de către Robert Badea » Memorat
VladBtz
Strain
*

Karma: -3
Deconectat Deconectat

Mesaje: 29



Vezi Profilul
« Răspunde #2 : Februarie 28, 2015, 23:08:30 »

e doar varianta .pas ,eu vreau C,C++ si nu stiu cum sa traduc in limbaju pe care il vreau
Memorat
VladBtz
Strain
*

Karma: -3
Deconectat Deconectat

Mesaje: 29



Vezi Profilul
« Răspunde #3 : Martie 01, 2015, 11:37:50 »

imi traduce cineva codul in C++/C va rog?
Memorat
Owlree
Strain
*

Karma: 16
Deconectat Deconectat

Mesaje: 27



Vezi Profilul
« Răspunde #4 : Martie 04, 2015, 01:00:46 »

Da, recunosc și eu că soluțiile oficiale de la problemele de la olimpiade și implementarea lor lasă de dorit uneori.

Luând în calcul că problema s-a mai dat la olimpiadă, și o soluție este deja existentă pe internet, o să-mi iau libertatea să dau și eu o descriere a aceleiași soluție. Cu toate astea, ca să nu se supere lumea pe mine, o să dau și ceva lucruri mai generale, poate învățăm cu toții din asta.

Trecând peste partea cu determinarea numărului de cuvinte din datele de intrare, avem în față o problemă clasică de programare dinamică, dar puțin deghizată, ce-i drept.

Acum probabil întrebarea este ce este programarea dinamică și de unde vine ea?

Un lucru este sigur, nu are legătură cu alocarea dinamică sau alte chestii legate de limbajul de programare. Zic asta pentru că am văzut că mai există confuzii pe alocuri când se aude prima dată de acest termen. Termenul de programare dinamică a fost folosit prima dată de către Richard Bellman în 1953. Pe atunci nu se prea scria cod, iar termenul de programare era mai degrabă folosit cu sensul de plănuire. Programarea dinamică este doar o metodă de rezolvare a unor probleme.

Ideea e cam așa – dându-ni-se o problemă, am putea da soluția corectă dacă am ști de dinainte soluția pentru una sau mai multe probleme mai mici?

De exemplu, problema ar putea fi: să se determine f20 , al 20-lea termen al șirului lui Fibonacci. Am putea da pe loc soluția problemei dacă am ști al 18-lea și al 19-lea termen, f20 = f18 + f19 . Pentru că suntem programatori, să facem deja și un program pentru determinarea unui asemenea termen Evident, nu o să facem un program care doar ne afișează al 20-lea termen, sau doar al x-lea termen, ci o să facem un program care ne dă al n-lea termen, pentru un n luat cumva din niște date de intrare. Știm că, în general, pentru determinarea unui termen al șirului lui Fibonacci ne este de ajuns să cunoaștem cei doi termeni precedenți ai șirului. Dacă îi cunoaștem, putem folosi imediat formula fn = fn-2 + fn-1 și să obținem rezultatul. Dacă nu îi cunoaștem, atunci vrem să îi calculăm. Observăm că am obținut acum alte două probleme, dar care sunt mai mici decât cea inițială. Putem folosi iar formula de recurență a șirului lui Fibonacci ca să obținem câte alte două probleme pentru fiecare din cele două obținute precedent, mai exact fn-1 = fn-3 + fn-2 și fn-2 = fn-4 + fn-3. Putem continua în aceasă manieră până când ajungem la o problemă pe care o putem rezolva trivial, aceea se va numi cazul de bază (pot fi și mai multe). În situația noastră, cazurile de bază sunt f1 = f2 = 1. Am obținut deci un algoritm recursiv care ne rezolvă problema determinării unui termen de indice n al șirului lui Fibonacci pentru un n arbitrar. Să vedem și codul în C++ pentru un astfel de algoritm.

Cod:
int fibonacci(int n) {

  if (n == 1 || n == 2) {
    return 1;
  }

  return fibonacci(n - 2) + fibonacci(n - 1);
}


Cu toate că folosind acest algoritm recursiv obținem soluția corectă, algoritmul este ineficient. Dezvoltând după formula de recurență, putem observa că recalculăm inutil unii termeni ai șirului.

(arbore de recurență de adâncime 3 pentru formula generală fn = fn-2 + fn-1 a unui număr Fibonacci, figură realizată cu yEd)

În arborele de recurență de mai sus se observă că fn-4 este calculat deja de 4 ori. Dacă am extinde arborele, am observa că fn-4 este calculat chiar de mai multe ori. Ca exemplu, următorul tabel ne arată de câte ori calculăm fiecare din primii 5 termeni ai șirului, dacă folosim algoritmul de mai sus pentru a calcula f20.
termencount
f12584
f24181
f32584
f41597
f5987

Clar nu vrem să calculăm f2 de 4181 ori, până și pe hârtie putem face mult mai bine de atât.

Acest fapt ne conduce la o optimizare naturală, care este și o tehnică foarte folositoare la olimpiade și alte concursuri. Aceasta se numește memoizare (engleză - memoization). Termenul a fost folosit prima dată în 1968 de către Donald Michie, care, ca și Alan Turing, a lucrat la Bletchley Park în timpul celui de-Al Doilea Război Mondial. Memoizarea nu este decât faptul de a memora anumite rezultate pentru a nu le recalcula.

În exemplul de față, este ușor să facem acest lucru. Concret, vom menține un vector de n elemente în care vom memora dacă am mai calculat sau nu înainte un termen și, dacă da, care a fost rezultatul. Pentru a vedea dacă am mai calculat sau nu termenul în cauză, vom inițializa vectorul cu -1, folosindu-ne de faptul că un termen al șirului lui Fibonacci este mereu pozitiv, deci dacă vectorul la indicele termenului pe care vrem să îl calculăm este mai mare decât -1 atunci înseamnă că am mai calculat termenul înainte.  Codul în C++ este mai jos.

Cod:
int mem[100];

int fibonacci(int n) {

  if (mem[n] == -1) {
    if (n == 1 || n == 2) {
      mem[n] = 1;
    } else {
      mem[n] = fibonacci(n - 2) + fibonacci(n - 1);
    }
  }

  return mem[n];
}

Folosind memoizare, tabelul de mai sus devine
termencount
f11
f21
f31
f41
f51
și, mai mult, continuă cu 1 până la f20.

Acestă tehnică este foarte utilă, dar putem mai bine de atât! Putem chiar să scăpăm de recursivitate și să creăm un algoritm iterativ. Ce am făcut mai sus se numește o abordare top-down. Ce urmează să facem se numește, intuitiv, bottom-up.

În cazul șirului lui Fibonacci ne este ușor să trecem la abordarea dorită. Ne este ușor din două puncte de vedere. Primul este că un termen al șirului lui Fibonacci depinde doar de indicile său, este o funcție de o singură variabilă. Al doilea este că formula de recurență este simplă, nu depinde decât de două rezultate precedente.

Întrebarea mai clară aici este am putea calcula valorile vectorului mem în timp liniar, fără a folosi un algoritm recursiv? (partea cu liniar este specifică probleme pe care o rezolvăm acum)

Primul pas ar fi să punem cazurile de bază, mem[1]=1 și mem[2]=1. Următorul pas ar fi să observăm că putem calcula acum mem[3], care este mem[1]+mem[2]. Continuând tot așa pentru mem[4], mem[5], mem[6], etc, obținem un simplu algoritm iterativ pentru a calcula valorile vectorului.

Cod:
mem[1] = 1;
mem[2] = 2;

for (int i = 3; i < n; ++i) {
  mem[i] = mem[i - 2] + mem[i - 1];
}

Acum putem spune oficial că am folosit programare dinamică. Codul de mai sus mai poate fi optimizat, dar optimizarea nu aduce nimic în plus ideii în discuție.

___________________________________________________________

Să încercăm acum să ne îndreptăm acum spre problema Text, de la care a pornit cam toată discuția. Problema ne cere mai exact, cum am zis în postările anterioare, secvența maximală de cuvinte (nu neapărat consecutive în datele de intrare) care are proprietatea că oricare două cuvinte consecutive din secvență îndeplinesc proprietatea cerută. O pereche de cuvinte îndeplinește proprietatea dacă și numai dacă ultima literă a primului cuvânt este aceeași cu prima literă a celui de-al doilea.  Să ne luăm un exemplu mai ușor:

ana are caini andrei are iepuri

Putem reprezenta grafic exemplul cu diagrama de mai jos.


(reprezentarea grafică a exemplului de mai sus, figură realizată cu yEd)

Am pus o săgeată de la un cuvânt la altul dacă ce de-al doilea vine după primul în datele de intrare și ultima literă a primului este prima literă de celui de-al doilea.

Acum problema este mai ușor de văzut, ni se cere să găsim un drum de lungime maximă. Prin drum înțelegem o secvență de cuvinte legate prin săgeți ca în diagrama de mai sus. Evident, săgețile trebuie să fie orientate înspre dreapta, nu puteam schimba ordinea cuvintelor. Pentru asta, vom rezolva probleme similare cu cele în care găseam un termen al șirului lui Fibonacci, dar de data aceasta vom folosi o altă formulă de recurență. Mai exact, problemele pe care vrem să le rezolvăm sunt de tipul care este lungimea maximă a unei secvențe care se termină la al n-lea cuvânt din datele de intrare și corespunde cu cerința problemei? Nu prea sună la fel, dar în realitate este. O să notăm răspunsul la întrebări de genul acesteia cu gn.

Spre exemplu, cum am putea găsi g6? Ei bine, lungimea secvenței care se terină la cuvântul iepuri este cel puțin 1 pentru că, evident, secvența include cel puțin cuvântul iepuri. Desigur, răspunsul nu este chiar 1, dar deja începem să construim o formulă, g6 = 1 + (ceva) . Observăm acum ușor pe diagramă că singura modalitate de a ajunge la cuvântul iepuri, în caz că nu am plecasem chiar de acolo, este ori prin cuvântul andrei (indice 4), ori prin cuvântul câini (indice 3). Dar nu le putem adăuga pe ambele. Trebuie să alegem una dintre cele două modalități. Ținând cont că vrem să aflăm lungimea maximă, ar trebui să o adăugăm pe cea cu valoarea g mai mare. Deci, obținem formula pentru g6, g6 = 1 + max(g3, g4) . Este indicat să vedem prin această formulă. Practic ea zice că dintre cele două modalități de a ajunge la cuvântul 6, noi o vrem pe cea mai bună, mai bună în acest caz înseamnă mai lungă.

Putem generaliza acum ușor această formulă recurentă. De la un cuvânt de indice n vrem să alegem cel mai lung drum care ajunge la cuvântul n. Deci, mai concis gn = 1 + max(ga1, ga2, ... , gam) unde cuvintele de indici a1, a2, ... , an au ca ultimă literă prima literă a cuvântului de indice n.

Acum că știm formula, povestea este aceeași ca la șirul lui Fibonacci, putem rezolva problema recursiv top-down, unde putem optimiza folosind memoizare, sau o putem rezolva iterativ bottom-up. Ar fi câteva lucruri de menționat. Primul este că răspunsul nu va mai găsit chiar în același loc ca la Fibonacci, ci vor mai fi de făcut câteva mici calcule. Al doilea este că nu suntem mulțumiți doar cu găsirea lungimii secvenței care trebuie eliminată, ci vrem și secvența în sine, lucru care se face ușor dacă ținem minte cumva la fiecare utilizare a formulei gn = 1 + max(ga1, ga2, ... , gam) ce indice ak a ales funcția max. În exemplul de mai sus, pentru iepuri am fi ținut minte andrei, pentru că la andrei aveam g4 = 2 iar la câini g3 = 1, deci era de preferat să fi venit de la cuvântul andrei pentru că drumul era mai lung. Mai exact, ținem minte de unde am venit. Acest lucru (găsirea subsecvenței în sine) nu ține direct de programare dinamică, așa că nu o să îl descriu în detaliu.

Astea fiind zise, avem acum o direcție destul de bună ca să începem să implementăm problema. Din diferite motive, am ales să nu pun aici codul, dar problema se rezolvă, cum am zis mai sus, aproximativ la fel ca cea a șirului lui Fibonacci.

___________________________________________________________

Mai jos sunt câteva chestii puțin mai avansate și care deviază de la subiect.

Este interesant de văzut, pentru cei care sunt la început și deja au învățat despre grafuri, că sub orice problemă de dinamică (i.e. care poate fi rezolvată folosind metoda programării dinamice) stă de fapt un graf aciclic orientat, sau DAG (directed acyclic graph). Chiar dacă problema pare că nu are absolut nicio legătură cu grafurile, ea se poate modela ca o problemă pe un DAG în mod natural. Nodurile DAG-ului sunt subproblemele pe care vrem să le rezolvăm, iar muchiile sunt dependențele între probleme. Dacă alegem această reprezentare, este evident de ce vrem ca graful să fie aciclic – nu vrem dependențe circulare. În problema de mai sus se vede clar acest lucru. Diagrama aceea era de fapt un DAG, iar în formulă foloseam acel max(ga1, ga2, ... , gam) pentru indicii de forma ak dacă aveam o muchie care pleca de la cuvântul (nodul) de indice ak și ajungea în nodul pentru care făceam calculul. Găsirea celui mai lung subșir crescător, este exact aceeași problemă ca cea de mai sus, deci acesta este încă un exemplu. Alt exemplu – găsirea celui mai lung subșir comun, graful arată frumos dacă îl aranjăm ca o pânză, adică nodurile puse ca într-un tablou dibimensional, soluția ar fi atunci pe o plimbare dintr-un colț al grafului în colțul opus, fără să schimbăm sensul, soluția în sine ar fi ce obținem dacă selectăm din acea plimbare doar acele mișcări pe diagonală. Aceste grafuri ne oferă, printre altele, o posibilitate de a vizualiza mai clar problemele, scăpând de cea a tablourilor 1-, 2-, 3- sau mai știu cu cât-dimensionale unde mai toate problemele arată la fel – un tablou și-o formulă.
« Ultima modificare: Martie 12, 2015, 20:49:32 de către Robert Badea » Memorat
Pagini: [1]   În sus
  Imprimă  
 
Schimbă forumul:  

Powered by SMF 1.1.19 | SMF © 2006-2013, Simple Machines