Modele de algoritmi in #Excel – Stacks and Queues (7)

În acest articol voi propune diferite metode de rezolvare a problemelor clasice de algoritmică cu focus pe zona de stive și cozi, probabil cele mai utilizate concepte în programare și mai ales în lucrul cu datele. Cum am mai spus cu diferite ocazii, Excelul nu ar trebui să fie un instrument pentru colectarea, stocarea și gestionarea unui volum mare de date, ci mai mult un instrument de analiză și rafinare rapidă a unor seturi restrânse de date. Da, poate face asta dar uneori nu foarte eficient, sau efortul este mai mare decât dacă ai utiliza alte instrumente.

Problemele supuse rezolvării sunt descrise pe site-ul Codility: https://app.codility.com/programmers/lessons/7-stacks_and_queues/ unde de obicei se propun rezolvări programatice.

O mică delimitare conceptuală

Excelul se dezvoltă rapid datorită numărului mare de utilizatori din variate domenii de activitate, dar și creativității acestora. La ora actuală vorbim despre mai multe metode de rezolvare a diferitelor probleme în Excel dintre care amintim:

  • Excel clasic
    • utilizarea formulelor și funcțiilor clasice
    • utilizarea unor instrumente clasice de rezolvare probleme: Pivot table, Data table, Solver
    • utilizarea VBA, tehnologie pe care eu personal o consider depășită
  • Excel modern
    • utilizarea Power Pivot și Power Query (limbaj m)
    • utilizarea DAX
    • utilizarea Automate (un înlocuitor modern al VBA, dar încă la început de drum)
    • utilizarea Dynamic arrays.

Ceea ce vă propun eu prin seria de articole despre algoritmi este utilizarea Dynamic arrays, parte componentă a Excel modern. Sunt convins că aceste probleme se pot rezolva și cu celelalte metode.

Marele dezavantaj al funcțiilor actuale care lucrează cu dynamic arrays este acela că nu au integrată funcția de prelucrare iterativă for din programare clasică.

Pentru că toți învățăm de la cineva, citez și eu în acest articol, câteva metode de a rezolva diferite probleme de iterații în excel în locul for-ului propuse de Diarmuid Early, vicecampion modial Excel eSports, în videoclipul Write Excel firmulas like a programmer.

Câteva concepte teoretice

Așa cum menționam în Excel nu avem for. Cu ce facem iterațiile atunci? Nu putem face iterații adevărate dar putem simula iterațiile prin utilizarea unor combinații de funcții Sequence combinate cu scan, map, reduce, byrow, bycol.

În imagine câteva exemple de utilizare a funcției sequence():

Variante utilizare funcția sequence în Excel.

Parametrii lui sequence sunt:

  • Rows: numărul de linii pe care dorim să le generăm (exemplu în A3 avem 10 linii)
  • Columns: numărul de coloane pe care dorim să le generăm (exemplu în E2 primul parametru rows nu este completat și formula începe direct cu separatorul ; (punct și virgulă) echivalentul , (virgulă) din versiunea în engleză.
  • Start: numărul de la care să pornească numerotarea (exemplu în B3, generăm 5 numere pe o coloană (rows) pornind de la valoarea 2. În E3, pornim de la 5)
  • Step: pasul de creștere, implicit 1.

În C3 combin un sequence care generează 10 numere cu un mod prin artificiul -1/+1 pentru a putea obține 10 numere în formatul dat.

Cele mai utilizate funcții de scanare a vector sunt prezentate în imagine:

Scan, reduce si map în Excel

SCAN și REDUCE au aceeași sintaxă, numai că scan calculează pentru fiecare element din vector, adică ajungem pas cu pas la valoarea finală, rezultatul fiind un array de valori, pe când REDUCE calculează direct valoarea finală. Parametrii celor două funcții sunt:

  • valoarea inițială de la care pornim, în cazul meu 0 în ambele cazuri (B16 și C16);
  • vectorul de scanat, în cazul meu A16#;
  • funcția lambda care se ocupă cu modul de calcul efectiv. Cheia în această funcție este că are doi parametri: a și v în această ordine. A este valoarea acumulată prin operațiunile returnată de lambda până la V, valoarea curentă.

Foarte important de știut: SCAN nu lucrează cu tabele de date și nici nu returnează tabele de date ci un singur vector. Dacă dorim să lucrăm totuși cu tabele, atunci putem reduce întreg tabelul la un vectori de valori prin concatenare după care să lucrăm cu artificiul prezentat data trecută de splitare în linii și coloane:

split; IFNA(--TEXTSPLIT(TEXTJOIN("|";1;tabelrezultat);";";"|");"");

MAP este o funcție la fel de interesantă, care are doar 2 parametri: vectorul de parcurs și funcția lambda pentru calcule care are ca parametru de intrare valoarea curentă (v). Da, putem face cu un MAP ce facem cu un SCAN, vezi formula din E16, doar că într-un mod oarecum mai complicat. Personal consider că scan este recomandat, dar uneori se complică problemele când dorești să lucrezi cu valoarea anterioară nu cu acumulatorul. Veți vedea mai jos.

Combinând între ele sequence cu scan putem emula operațiuni for în Excel, chiar dacă unele variante cu stop la o anumită condiție, încă nu știu cum să le rezolv. :)

În continuarea articolului voi prezenta câteva propuneri de rezolvare a problemelor de pe Codility.

Problema Brackets

Problema „Brackets” face parte din categoria algoritmilor de tip Stacks and Queues și este folosită pentru a verifica corectitudinea unui șir de paranteze. Aceasta este o problemă clasică de validare a echilibrului și închiderii corecte a parantezelor într-un șir.

Enunțul problemei: Un șir de caractere conține următoarele tipuri de paranteze: (), {}, []. Trebuie să determinăm dacă șirul de paranteze este corect închis și echilibrat. Un șir de paranteze este considerat corect dacă:

  • Fiecare paranteză deschisă are o paranteză corespunzătoare închisă de același tip.
  • Parantezele închise sunt în ordinea corectă.

Exemplu
(), ([]{}), {[()()]} sunt șiruri corecte.
(], ([)], {{{{ sunt șiruri incorecte.

Modelul este destul de bizar, am încercat diferite abordări, clasic prin descompunerea într-un vector… dar fără succes, iar dacă cumva riscați să încercați o rezolvare cu ChatGPT… treaba e tristă rău. :) Doar încercați…

Propunerea mea de rezolvare după ceva abordări matematizate și care mai de care mai alambicate:

Propunere rezolvare problema Brackets.

În rezolvarea problemei am pornit de la generarea unei secvențe de jumătate din lungimea totală a șirului de paranteze. Practic ele vin în perechi deschis, închis deci nu ar trebui să existe mai multe rulări. După care pentru a putea scana și substitui perechile deschis închis ca să vedem dacă rămâne una închisă eronat sau neînchisă, am replicat șirul de paranteze de dimensiunea secvenței generate prin maparea șirului de secvență și utilizarea unei fake Lambda, în care parametrul de intrare, în loc de V l-am înlocuit cu _ (undescore) pentru că nu-l folosesc la nimic.

În scanul din H2, care pornește de la șirul inițial din A2 și verifică pe G2 (care este un V identic), am substituit pe rand parantezele din valoarea acumulată. Ceea ce înseamnă că el de fapt face o substituție repetitivă pentru câte rulări i-am specificat prin Size. Rezolvarea nu este optimă din punct de vedere algoritmic pentru că în fiecare pas execut un număr determinat de la început de pași. Așa cum specificam în partea de teorie nu putem să oprim execuția la îndeplinirea unei condiții ci executăm în continuare după un număr de pași prestabiliți.

În formula pentru o singură celulă am vrut să integrez și secvența de paranteze care nu se potrivește.

Funcția integrată:

=LET(sir;A2;
     size;SEQUENCE(LEN(sir)/2);
     paras;MAP(size;LAMBDA(_;sir));
     subst;SCAN(sir;paras;LAMBDA(a;v;SUBSTITUTE(SUBSTITUTE(SUBSTITUTE(a;"()";"");"[]";"");"{}";"")));
     lastval;TAKE(subst;-1);
       IF(LEN(lastval)=0;"OK";"Gresit: "&lastval))

În care în ultima parte am preluat ultima valoare a lui Scan care reprezintă parantezele care nu se potrivesc.

Având în vedere că sunt funcția propusă se aplică la toate tipurile de paranteze, aceeași soluție funcționează și pentru Problema Nesting de pe Codility.

Care este aplicabilitatea acestui tip de problemă?

Un pic forțat, un model de aplicabilitate ar fi acela de evaluare a unei secvențe de cod JSON, pentru a verifica dacă este valid din punct de vedere al parantezelor.

Model utilizare funcție de verificare paranteze.

În același exemplu, în B11 am verificat și dacă ghilimelele sunt deschise și închise corect.

De asemenea, cred că putem defini și alte simboluri pe post de închidere / deschidere care ar putea fi verificate.

Problema Fish

Problema în care peștele cel mare îl mănăncă pe cel mai mic… mi-a dat cele mai dureroase bătăi de cap până acum. De ce? Pentru că trebuie să parcurgi un tabel cu două coloane de un număr necunoscut de ori.

Descrierea Problemei: Avem un râu în care n pești înoată în direcții opuse. Fiecare pește este descris de două atribute:

  • Un număr întreg care reprezintă mărimea peștelui. (A)
  • O direcție care poate fi 0 (înoată în amonte) sau 1 (înoată în aval). (B)

Peștii din amonte și aval își urmează cursul până când se întâlnesc. Atunci când doi pești se întâlnesc:
Dacă peștele din amonte (0) este mai mic decât peștele din aval (1), peștele din amonte este mâncat.
Dacă peștele din aval (1) este mai mic decât peștele din amonte (0), peștele din aval este mâncat.
Dacă cei doi pești au aceeași mărime, peștele din aval (1) va mânca peștele din amonte (0).

Scopul problemei este să determinăm câți pești vor supraviețui după ce toți peștii care se pot întâlni și mânca au făcut acest lucru.

Propunere de rezolvare:

Propunere rezolvare problema Fish.

Ca să pot rezolva această problemă mi-am dat seama după mult timp că cea mai bună abordare este cu SCAN și căutări permanente și comparare cu valoarea anterioară sau valoarea următoare în funcție de direcția în care se mișcă peștele. Ca să ajung la funcția din G2, am trecut prin mai multe variante, una intermediară este acea a rulărilor care încep cu coloana K în care input pentru Run 01 este o concatenare a tabelului, iar pentru Run 02 încolo este rezultatul anterior. Aici a fost de fapt cheia: cum folosești un tabel ca un input recursiv?!

Funcția propusă în celula G2:

=LET(ari; A2:B6; seqc; SEQUENCE(;ROWS(ari)*2);

fscani; LAMBDA(arri; LET(arr; arri;      
     rrs; ROWS(arr); seq; SEQUENCE(ROWS(arr));
     fH; LAMBDA(x; TEXTJOIN(";";TRUE;INDEX(arr;x;1);INDEX(arr;x;2))); 
     
    fIndexP; LAMBDA(x; LET(ba; INDEX(arr;x-1;2); bc; INDEX(arr;x;2); bv; INDEX(arr;x+1;2);
                       aa; INDEX(arr;x-1;1); ac; INDEX(arr;x;1); av; INDEX(arr;x+1;1);
                       IF(bc=0; IF(ba=1; IF(ac<aa; aa&";"&ba; ac&";"&bc); ac&";"&bc);
                                IF(bv=1; ac&";"&bc; IF(ac<av; av&";"&bv; ac&";"&bc)))));
    fIndexL; LAMBDA(x; LET(ba; INDEX(arr;x-1;2); bc; INDEX(arr;x;2);
                       aa; INDEX(arr;x-1;1); ac; INDEX(arr;x;1);
                       IF(bc=0; IF(ba=1; IF(ac<aa; aa&";"&ba; ac&";"&bc); ac&";"&bc); ac&";"&bc )));

    rez; SCAN(0; seq; LAMBDA(a;v;
              IF( AND(v=1; INDEX(arr;v;2)=0);fH(v);
                       IF(v=rrs; fIndexL(v); fIndexP(v)))));
  rez));

rezfin; REDUCE(0; seqc; LAMBDA(a;v; IF(v=1; TEXTJOIN("|";TRUE;fscani(ari)); TEXTJOIN("|";TRUE;fscani(--TEXTSPLIT(a;";";"|"))))));
pestiramasi; UNIQUE(--TEXTSPLIT(rezfin;";";"|"));
nrpestiramasi; ROWS(pestiramasi);
pestiramasi
)

Rezultatul final este stocat în variabila rezfin care folosește o funcție REDUCE pe baza seqc variabilă ce definește o secvență pe coloane de numărul de elemente supuse analizei. Aici este problema de optimizare pe care nu am reușit să o rezolv în așa fel încât să determin optimul de rulări. În REDUCE folosesc funcția recursivă fscani cu parametrul de intrare ari dacă suntem la primul element din vector (v=1) sau a, dacă suntem la următoarele valori.

fscani este compus la rândul său din din mai multe funcții:

  • fH – utilizată pentru a concatena valorile celor două coloane de pe poziția 1, începutul scanului;
  • fIndexP – utilizată pentru a concatena valorile de dinainte sau de după înregistrarea curentă în funcție de 1/0 sau dimensiune, în care prin Index aducem valoarea dimensiunii peștelui: anterioară (aa), curentă (ac) sau următoare (av) și valoarea 0/1 specifică deplasării: ba – direcția de deplasare a peștelui anterior, bc, direcția peștelui curent și bv, valoarea peștelui viitor.
  • fIndexL – funcție derivată din fIndexP dar pe care o utilizez doar la ultimul element din vector. Aici ar fi mers o optimizare în fIndexP pentru a verifica cazul de eroare în cazul în care nu există valori viitoare.

Rezultatul final al funcției fscani este stocat în variabila rez care este de fapt un alt scan pe secvența inițială și aplică cele 3 funcții pe intervalul 1…n.

O variantă de implementare puțin diferită a acestui algoritm mi-a fost inspirată de un joc de pe telefoanele mobile, în care peștele mai mare care-l mănâncă pe cel mai mic acumulează punctele acestuia, spre final peștele acumulând majoritatea punctelor puse în joc.

Model în care peștele mai mare mănâncă peștele mai mic și îl acumulează.

Diferența față de funcția anterioară este că la indexare în momentul în care stabilesc peștele câștigător prin IF-ul din fIndexP în loc să afișez doar valoarea câștigătorului, adun la acesta valoarea celui care a fost mâncat.

Ca aplicabilitate practică a acestui algoritm mă gândesc în general la prioritizarea unor cozi de așteptare și apariția unui eveniment nou sau schimbarea priorității anumitor elemente. Cred că la managementul incidentelor putem utiliza prioritizare pentru a vedea care incident se va rezolva primul sau la portofoliile de investiții sau acțiuni pentru a determina care acțiuni merită vândute și ce trebuie achiziționat pentru maximizarea portofoliului.

Dincolo de toate Fish a fost pentru mine una din cele mai mari provocări tehnice de până acum. Clar, recomand metode programatice dar am demonstrat că se poate și în Excel.

Problema StoneWall

Descrierea problemei: Dându-se o secvență de înălțimi care reprezintă ziduri, trebuie să determinăm numărul minim de blocuri necesare pentru a construi aceste ziduri. Fiecare bloc are aceeași lățime și poate fi plasat în moduri diferite, dar trebuie să acopere întreaga înălțime specificată pentru fiecare zid, iar blocurile pot fi plasate unul peste altul.

Logica rezolvării problemei
Să considerăm șirul de înălțimi H = [8, 8, 5, 7, 9, 8, 7, 4, 8].

8: Adaugă 8 în stivă. (stiva: [8])
8: Este deja 8 în stivă, nu facem nimic. (stiva: [8])
5: 5 este mai mic decât 8, eliminăm 8 din stivă. Adăugăm 5. (stiva: [5])
7: 7 este mai mare decât 5, adăugăm 7. (stiva: [5, 7])
9: 9 este mai mare decât 7, adăugăm 9. (stiva: [5, 7, 9])
8: 8 este mai mic decât 9, eliminăm 9 din stivă. Adăugăm 8. (stiva: [5, 7, 8])
7: 7 este mai mic decât 8, eliminăm 8 din stivă. (stiva: [5, 7])
4: 4 este mai mic decât 7, eliminăm 7 și 5 din stivă. Adăugăm 4. (stiva: [4])
8: 8 este mai mare decât 4, adăugăm 8. (stiva: [4, 8])

Total add-uri: 7

Propunere de rezolvare

Propunere rezolvare problema StoneWall.

În prima parte (formulele afișate) am setat modelul problemei care la bază folosește un filtru dinamic pe poziția curentă (valoarea din B2) din vectorul H. Funcția din F2 este copiată apoi în jos. Vali este de fapt valoarea rezultată anterior, ceea ce ne duce cu gândul la o rezolvare integrată la acumulatorul dintr-un scan.

Valorile Iterm sunt apoi concatenate în J2, iar cu find-ul din K2, determin dacă există (1) adăugiri sau nu (0).

În P2 am stocat varianta finală integrată al cărei cod este:

=LET(arr;A2:A10;seq;SEQUENCE(ROWS(arr));
vals; SCAN(0;seq;
           LAMBDA(a;v;
               IF(v=1;""&INDEX(arr;v);
                      IF(ISERROR(XMATCH(INDEX(arr;v);--TEXTSPLIT(a;";");0));
                                 TEXTJOIN(";";TRUE;FILTER(--TEXTSPLIT(a;";"); --TEXTSPLIT(a;";")<=INDEX(arr;v);"");INDEX(arr;v));
                                 TEXTJOIN(";";TRUE;FILTER(--TEXTSPLIT(a;";"); --TEXTSPLIT(a;";")<=INDEX(arr;v);""))))));

ver; SCAN(0; seq; LAMBDA(a;v; IF(v=1;1;IF(ISERROR(FIND(INDEX(vals;v);INDEX(vals; v-1)));1;0))));
SUM(ver))

În care ver este variabila de verificare prin scanare a rezultatelor intermediare direct concatenate cu Textjoin din vals.

Ca aplicabilitate practică mă gândesc la diferite probleme de optimizare a utilizării unor elemente în obținerea celui mai bun rezultat.

Cam atât pentru acest articol!

Sper să fie util cuiva!

Modele de algoritmi in #Excel – Sorting (6)

După o perioadă de pauză în care au fost multe de făcut, dar și datorită unor probleme de optimizare a codurilor și formulelor, reiau seria de articole din categoria algoritmilor clasici cu propuneri de implementare / rezolvare în Excel.

În articolul de astăzi voi trata colecția de probleme Sorting de pe Codility: https://app.codility.com/programmers/lessons/6-sorting/

Un pic de teorie

Algoritmii de sortare sunt proceduri care rearanjează elementele unei liste (sau altor structuri de date) într-o anumită ordine, de obicei crescătoare sau descrescătoare. Acești algoritmi sunt esențiali în informatică și matematică datorită utilizării lor în diverse aplicații și probleme. Cei mai cunoscuți algoritmi de sortare, la nivel general sunt:

  1. Bubble Sort – Compara și interschimbă elementele adiacente, trecând repetat prin listă până când aceasta este sortată. Chiar dacă este simplu și ușor de înțeles el este ineficient pentru listele mari.
  2. Insertion Sort – Inserează fiecare element din lista nesortată în poziția sa corectă din lista sortată cu aceleași avantaje și dezavantaje pentru listele mari ca Bubble sort.
  3. Selection Sort – Găsește elementul minim din lista nesortată și îl pune la început. Repetă pentru fiecare element.
  4. Merge Sort – Împarte lista în două jumătăți, sortează fiecare jumătate și apoi le îmbină într-o listă sortată, un pic mai stabil pentru liste mari.
  5. Quick Sort – Alege un element pivot, împarte lista în subliste cu elemente mai mici și mai mari decât pivotul și sortează recursiv sublistele. Este unul din celele mai eficiente pentru liste mari dar performanța este una redusă.
  6. Heap Sort – Construiește un heap maxim din listă și apoi extrage elementele unul câte unul pentru a obține lista sortată.
  7. Counting Sort – Numără numărul de apariții ale fiecărui element și folosește aceste informații pentru a construi lista sortată.
  8. Radix Sort – Sortează elementele în funcție de cifrele lor, utilizând un sortare stabilă la fiecare pas.
  9. Bucket Sort – Împarte lista în mai multe segmente, sortează fiecare segment individual și apoi le îmbină.

În Excel, în versiunea clasică există doar operațiuni pentru sortare, avantajul major al Excelului modern (365) este faptul că transformă multe operațiuni în funcții. De exemplu avem funcția SORT() sau SORTBY() utilizate pentru sortarea vectorilor. De asemenea, foarte utile sunt funcțiile: UNIQUE() pentru determinarea valorilor unice dintr-un interval precum și funcțiile LARGE() prin intermediul căreia putem identifica cele mai mari n valori dintr-un vector sau funcția SMALL() care ne permite identificarea celor mai mici n valori dintr-un vector.

În imagine câteva detalii legate de utilizarea LARGE și SMALL

Funcțiile LARGE și SMALL în Excel.

Ca să putem selecta mai multe rânduri dintr-un vector putem utiliza funcția ROW() sau SEQUENCE(). A nu neglija funcția INDEX() care încă este destul de puternică și des utilizată pentru a axtrage valori de pe diferite poziții dintr-un vector.

Problema Distinct

În problema Distinct ni se solicită să determinăm numărul de elemente unice dintr-un vector de valori.

În rezolvarea problemei ar trebui să determinăm fiecare număr unic, să facem un COUNTIF() după fiecare număr pe vectorul de valori după care să însumăm valorile intermediare.

Rezolvarea în Excel este în schimb mult mai simplă:

Așa cum explicam în partea de teorie funcția UNIQUE() îmi determină valorile unice, iar cu COUNT() determin numărul lor. În cazul în care nu avem Excel 365 putem merge pe abordarea din metoda clasică cu însumarea din funcția specificată. Ca să determinăm care sunt exact numerele unice, o tehnică de multe ori necesară în verificarea și validarea seturilor de date atunci putem utiliza combinația de SORT() cu UNIQUE().

Problema MaxProductOfThree

Aceasta este o problemă destul de interesantă a cărei cerință este să determini produsul maxim a trei valori dintr-o listă dată, în care intervalul de valori este negativ și pozitiv. Ca metodă de rezolvare, ar trebui sortat crescător intervalul de valori după care se înmulțesc cele mai mari 3 numere.

Propunere de rezolvare inițială:

Funcția LARGE() din E2 și E3, preia automat cele mai mari 3 numere din șir fără să fie nevoie de sortare. După care aplicăm funcția PRODUCT() (un fel de SUM() mai puțin utilizat) pentru a înmulți cele 3 numere rezultat. Dezavantajul acestei abordări este că nu pot extrage din rezultat care sunt pozițiile numerelor din RANK decât dacă fac o nouă funcție de căutare pe intervalul de valori.

În zona G:J am încercat o abordare manuală de generare a tuturor combinațiilor posibile unice de la 1 la 6. În cazul în care ai mai multe numere această abordare devine complet ineficientă pentru a fie executată manual. Vezi zona N:O în care sunt explicate în funcție de numărul de elemente, numărul de combinații unice, dar și celelalte combinații, ridicarea la a doua și a treia care determină numărul maxim de combinații pe care le putem obține din numărul de elemente dat.

În J2 am introdus un clasic INDEX() care îmi caută în funcție de tripletă valorile pe vector și le înmulțește între ele, valoarea finală fiind 60 în imputul dat.

Doar că matematica are detaliile ei, pe care dacă nu le verificăm bine riscăm să cădem în capcana datelor de intrare. Se face că în aritmetică, două numere negative când se înmulțesc, obținem un număr pozitiv. Dar numerele negative nu sunt tratate corect cu funcția LARGE() ceea ce înseamnă că ar trebui să complicăm formulele inițiale din E2 ca să conțină și un SMALL() după care să facem un maxim între produsul celor mai mici două numere cu cel mai mare număr, cu produsul celor mai mari 3 numere.

Exemplu de schimbare date de intrare cu două valori mari negative.

În imagine se observă că modelul manual generează valori corecte, pe când modelul simplu din E2 intoarce doar produsul celor mai mari 3 numere, în cazul meu pozitive.

Având în vedere că o generare manuală a unui tabel de triplete nu este acceptabil de la un anumit număr de valori dintr-un vector, am procedat la implementarea unei funcții separate:

Funcție rezolvare problemă MacProductofThree

În care funcția concretă este:

=LET(vector; A2:A7;
     nr; ROWS(vector);
     matrix; MAKEARRAY(nr^3; 3; LAMBDA(i;j; IF(j = 1; INT((i-1)/nr^2)+1; IF(j = 2; MOD(INT((i-1)/nr); nr)+1; MOD(i-1; nr)+1))));
     fReq; LAMBDA(x; AND(INDEX(x;1)<INDEX(x;2);INDEX(x;2)<INDEX(x;3)));
     verif; BYROW(matrix; LAMBDA(r; fReq(r )));
     tabi; HSTACK(matrix; verif);
     unics; FILTER(tabi; TAKE(tabi;;-1)=TRUE);
     unicsm; CHOOSECOLS(unics;1;2;3);
     fReqv; LAMBDA(x; INDEX(vector;INDEX(x;1))*INDEX(vector;INDEX(x;2))*INDEX(vector;INDEX(x;3)));
     vals; BYROW(unicsm; LAMBDA(r; fReqv(r )));
MAX(vals))

în care variabilele matrix, verif, tabi, unics, unicsm le utilizez doar pentru a crea în mod dinamic tabelul de triplete constând în combinațiile unice pe care le pot lua pozițiile numerelor. Acest tabel îmi este necesar pentru a face ulterior indexul din funcția recursivă fReqv. Funcția o folosesc ulterior în variabila vals pentru a putea să calculez pe tabelul unicsm (tripletele unice) linie cu linie cu BYROW().

La final afișez MAX(vals) pentru a determina care este valoarea maximă a produsului a trei numere din șir.

Mi-a fost destul de greu să ajung la formula din matrix. Clar numărul de combinații optime este dat de numărul de elemente din vector, la numărul de elemente combinate. În cazul nostru 3. În schimb pentru a putea determina toate combinațiile posibile, singura metodă pe care am găsit-o este să generez un tabel care are numărul de linii egal cu fiecare număr din șir de câte poziții poate lua pe 3 coloane. De exemplu dacă vectorul A este format din valorile 1, 3, 8, atunci valoarea matrix va cuprinde toate combinațiile lui 1 cu el însăși și cu cu celelalte valori. Exemplificarea din imagine este mult mai concludentă:

Valoarea intermediară a matrix.

Observăm din imagine că funcția de generare a acestei matrici cuprinde toate valorile posibile. Matricea este doar pentru căutare ulterioară pe A, prin combinația poziției numerelor. Fiecare număr apare de 9 ori pe prima coloană reprezentând numărul de element la puterea a doua. Scopul final este să identific toate combinațiile crescătoare și să elimin duplicatele prin funcțiile următoare. Tabelul final de căutare fiind în variabila unicsm care are un număr de linii egal cu numărul de combinații: 3 combinat cu 3 = 1.

Variabila unicsm.

Acest tip de problemă are o mare aplicabilitate în diverse domenii de activitate: analiză financiară, optimizarea producției, logistică, ect cu scopul de a maximiza profitul rezultat în urma activității prin combinarea elementelor optime.

De exemplu, dacă am avea un portofoliu de acțiuni cu diferite randamente, putem calcula combinația maximă dintre ele prin utilizarea funcției propuse:

Model de problemă analiză portofoliu de acțiuni cu MaxProdofThree

În funcția de mai sus am înlocuit MAX(vals) cu FILTER(HSTACK(unicsm;vals);vals=MAX(vals)) în așa fel încât să păstrez ca rezultat valorile combinațiilor maxime ca produs.

Un artificiu foarte util pentru a nu ne trezi cu surprize pentru valorile negative, este acela de a transforma procentul în valoare prin adunarea valorii 1 la randament. De exemplu pentru prima valoare formula utilizată este: =1+VALUE(C29). Asta îmi permite să exclud valorile negative din combinație și să păstrez pentru acest caz particular doar valorile pozitive.

Problema Triangle

Problema Triangle este oarecum asemănătoare cu cea anterioară doar că de data aceasta rezultatul nu mai este maximul numerelor ci un rezultat de constatare, dacă 3 numere dintr-un șir pot forma un triunghi. Nu ne referim la cazuri specifice de triunghiuri isoscel, dreptunghic sau echilateral ci doar la cazul în care numerele prezentate (P, Q, R) respectă următoarele reguli:

0<=P<Q<R<N
P+Q>R
Q+R>P
R+P>Q

adică suma oricăror două numere trebuie să fie mai mare decât un al treilea. N reprezintă numărul de elemente din vectorul de analiză. Ca o particularitate a problemei, toate numerele din A trebuie să fie diferite între ele.

Soluția propusă de mine pornește de la funcția de la problema MaxProductOfThree.

Problema Triangle.

Rezultatul este numărul de triplete posibile și care este aceasta. Codul funcției prezentat mai jos:

=LET(vector; A2:A7;
     nr; ROWS(vector);
     matrix; MAKEARRAY(nr^3; 3; LAMBDA(i;j; IF(j = 1; INT((i-1)/nr^2)+1; IF(j = 2; MOD(INT((i-1)/nr); nr)+1; MOD(i-1; nr)+1))));
     fReq; LAMBDA(x; AND(INDEX(x;1)<>INDEX(x;2);INDEX(x;2)<>INDEX(x;3);INDEX(x;1)<>INDEX(x;3)));
     verif; BYROW(matrix; LAMBDA(r; fReq(r )));
     tabi; HSTACK(matrix; verif);
     unics; FILTER(tabi; TAKE(tabi;;-1)=TRUE);
     unicsm; CHOOSECOLS(unics;1;2;3);
     fReqv; LAMBDA(x; CONCAT(INDEX(vector;INDEX(x;1));";";INDEX(vector;INDEX(x;2));";";INDEX(vector;INDEX(x;3))));
     vals; BYROW(unicsm; LAMBDA(r; fReqv(r )));
     Split; --TEXTSPLIT(TEXTJOIN("|";1;vals);";";"|");
     fverif; LAMBDA(x;AND(INDEX(x;1)+INDEX(x;2)>INDEX(x;3);
                     INDEX(x;1)+INDEX(x;3)>INDEX(x;2);
                     INDEX(x;2)+INDEX(x;3)>INDEX(x;1)));
     verift; BYROW(Split; LAMBDA(r; fverif( r)));
     tabf; HSTACK(Split; verift );
     triplef; FILTER(tabf; TAKE(tabf;;-1)=TRUE;"0");
     rezfin; IF(ROWS(triplef)>1;"Rezultat: 1 - Tripleta:" & TEXTJOIN(";";TRUE; XMATCH(TAKE(triplef;1;3);vector)-1);"Rezultat: 0");
     rezfin)

În care tehnica de a obține toate combinațiile unice posibile de poziții din șir este dată de variabilele și funcțiile recursive până la vals. Artificiul esențial în această funcție, este acela de a împărți valorile în LET() prin unificarea inițială a lor din combinația funcțiilor din variabila Split. Această tehnică am învățat-o de la Diarmuid Early fost campion mondial la Excel.

Cheia problemei este în funcția recursivă fverif care este utilizată în BYROW-ul de la verift pentru a compara sumele valorilor din tabelul Split prin adunarea valorilor indexate de pe cele trei coloane. AND()-ul va returna TRUE sau FALSE pe care le unific în tabf cu HSTACK. Ulterior filtrez în triplef doar valorile TRUE din tabf.

Identificarea tringhiurilor dreptunghice

Cred că sunt puțini cei care nu au spus niciodată că anumite formule din matematică nu le folosește la nimic. :) Una din experiențele mele în acest sens cu copiii mei a fost să le demonstrez în nenumărate rânduri utilizatea Teoremei lui Pitagora. Nu am reușit niciodată! :)

Pe scurt în această teoremă, dacă A^2+B^2=C^2 înseamnă că triunghiul este unul dreptunghic (unghi de 90 de grade pentru cei care au uitat).

Ca să identificăm dintr-un șir de valori, care oricare trei numere pot forma un triunghi dreptunghic, am extins problema Triangle descrisă anterior și am modificat funcția recursivă fverif pentru a răspunde noii condiții de sumă a pătratelor.

Teorema lui Pitagora în Excel.

În zona GHI identificăm numerele din șirul A care pot forma tringhiuri dreptunghice.

3^2+4^2=9+16=25=5^2

5^2+12^=13^2

Funcția utilizată în acest caz este:

=LET(vector; A2:A7;
     nr; ROWS(vector);
     matrix; MAKEARRAY(nr^3; 3; LAMBDA(i;j; IF(j = 1; INT((i-1)/nr^2)+1; IF(j = 2; MOD(INT((i-1)/nr); nr)+1; MOD(i-1; nr)+1))));
     fReq; LAMBDA(x; AND(INDEX(x;1)<>INDEX(x;2);INDEX(x;2)<>INDEX(x;3);INDEX(x;1)<>INDEX(x;3)));
     verif; BYROW(matrix; LAMBDA(r; fReq(r )));
     tabi; HSTACK(matrix; verif);
     unics; FILTER(tabi; TAKE(tabi;;-1)=TRUE);
     unicsm; CHOOSECOLS(unics;1;2;3);
     fReqv; LAMBDA(x; CONCAT(INDEX(vector;INDEX(x;1));";";INDEX(vector;INDEX(x;2));";";INDEX(vector;INDEX(x;3))));
     vals; BYROW(unicsm; LAMBDA(r; fReqv(r )));
     Split; --TEXTSPLIT(TEXTJOIN("|";1;vals);";";"|");
     fverif; LAMBDA(x;OR(INDEX(x;1)^2+INDEX(x;2)^2=INDEX(x;3)^2;
                     INDEX(x;1)^2+INDEX(x;3)^2=INDEX(x;2)^2;
                     INDEX(x;2)^2+INDEX(x;3)^2=INDEX(x;1)^2));
     verift; BYROW(Split; LAMBDA(r; fverif( r)));
     tabf; HSTACK(Split; verift );
     triplef; FILTER(tabf; TAKE(tabf;;-1)=TRUE;"0");
     fSort; LAMBDA(row;TEXTJOIN(";";TRUE;SORT(TRANSPOSE(row))));
     tabs; BYROW(triplef; LAMBDA(r; fSort(r)));
     pita; TEXTSPLIT(TEXTJOIN("|";1;UNIQUE(tabs));";";"|");
     pita)

În care recunoașteți codul de la problema Triangle cu modificarea specificată pentru fverif, doar că de data aceasta am folosit funcția OR() pentru a identifica oricare din combinațiile dintre cele 3 numere. De asemenea, specific acestei probleme, ca să pot face sortare în LET cu BYROW trebuie să folosim artificiul amintit anterior cu splitarea după join. Funcția recursivă fSort îmi crează un tabel cu toate valorile sortate crescător dar ca să le pot trata am nevoie de ea în BYROW().

Înainte de splitare, variabila tabs are următoarea formă:

Forma intermediară a soluției în variabila tabs.

În cazul în care șirul de numere nu are valori care să îndeplinească condiția de triunghi dreptunghic, atunci rezultatul returnat va fi zero, ca parte a variabilei triplef.

Problema Triangle poate fi aplicată în diferite domenii de activitate cu efect de optimizare a costurilor, prețurilor sau evaluare a diferitelor riscuri. În analiza pieței imobiliare, algoritmul Triangle poate fi folosit pentru a evalua relațiile dintre prețurile proprietăților în trei locații diferite. De exemplu, dacă prețurile locuințelor în trei cartiere formează un triunghi, se poate evalua dacă aceste prețuri sunt în echilibru sau dacă există anomalii care ar putea sugera oportunități de investiție sau riscuri. În piețele financiare, portofoliile de investiții sunt adesea analizate pentru a evalua riscul și rentabilitatea. Folosind un algoritm Triangle, se pot evalua trei acțiuni sau active diferite pentru a determina dacă formarea lor (în termeni de valori sau randamente) formează un „triunghi” de risc echilibrat. Un portofoliu ideal ar trebui să aibă un echilibru între risc și rentabilitate, iar algoritmul poate ajuta la identificarea combinațiilor care nu sunt optim echilibrate.

În domeniul logisticii, determinarea rutelor optime pentru transportul mărfurilor poate beneficia de algoritmi de tip triunghi. De exemplu, dacă trebuie să transportați mărfuri între trei depozite, algoritmul poate ajuta la determinarea rutei optime care minimizează costurile și timpul, verificând dacă distanțele dintre depozite formează un triunghi optim din punct de vedere logistic. În managementul resurselor umane, performanța angajaților poate fi analizată în grupuri de câte trei pentru a vedea dacă există un echilibru în echipă. Dacă performanța a trei angajați formează un „triunghi”, managerii pot evalua dacă acest triunghi este echilibrat sau dacă există nevoi de formare suplimentară pentru a echilibra performanțele.

Problema NumberOfDiscIntersections

Problema NumberOfDiscIntersections presupune să găsești numărul de perechi de discuri care se intersectează într-un plan 1D. Fiecare disc este definit prin centrul său (o poziție pe axa x) și raza sa (o lungime care se extinde la stânga și la dreapta de la centru).

Discurile sunt date sub forma unui array A, unde fiecare element A[i] reprezintă raza discului centrat în punctul i.

Trebuie să recunosc că problema mi-a dat bătăi de cap, pentru că nu am înțeles de la început cerința. Numărând intersecțiile de pe reprezentarea grafică, luând în calcul și punctele de tangență, tot nu reușeam să ajung la valoarea 11 propusă de autori.

Propunere de rezolvare problema NumberofDiscIntersections.

Consultând diferite site-uri specializate am identificat faptul că în această problemă cei de la Codility au luat în calcul de fapt componența fie și parțială a unui disc într-un alt disc. De exemplu discul 5 nu conține nici un alt disc, dar el este numărat ca apartenență atât în discul 1 (cel mai mare cât și în discul 4.

În momentul în care înțelegi că nu este vorba de intersecția liniilor între ele, treaba devine relativ simplă. Eu am rezolvat cu un SUMPRODUCT() comparând valorile de start cu valorile de final și invers, adăugând în ecuație poziția în calcul.

Ca să ajung la rezultat a trebuit să transform comparația fiecărui element cu celălalt element într-o matrice de elemente, artificiul aici fiind funcția TRANSPOSE().

Pasii de rezolvare a problemei.

Partea de secvență o folosesc pentru a limita valorile calculate pentru intersecție doar pentru partea în care linia curentă este mai mică decât transpusul său.

După multe chinuri în înțelegerea problemei am ajuns la următoarea funcție:

=LET(sir;B1;
    raza;--TEXTSPLIT(sir;;",");
    centru;SEQUENCE(ROWS(raza);1;0;1);
    start;centru-raza;
    final;centru+raza;
    tabi;HSTACK(start;final);
    tFinal;TRANSPOSE(final);
    tStart;TRANSPOSE(start);
    intersections;BYROW(tabi;LAMBDA(_;
                        SUMPRODUCT(
                           --(start<=tFinal);
                           --(final>=tStart);
                           --(SEQUENCE(ROWS(raza))<TRANSPOSE(SEQUENCE(ROWS(raza))))
                          )
                  ));
MAX(intersections))

Din cauză că formula se execută pe un tabel, obțin valoarea 11 pentru fiecare linie din raza. Atunci la final am aplicat funcția MAX() pentru a obține o singură valoare.

În funcția prezentată nu am mai folosit un recursiv, ci am folosit o funcție LAMBDA() cu paramentru null (_) pentru că am input din variabila tabi, ci o folosesc doar pentru a parcuge linie cu linie tabelul inițial.

Nu sunt foarte mulțumit de ce a ieșit, din cauză că am lucrat prea mult la ea, iar unul din princiile mele de lucru în Excel este: Dacă ceva îți ia prea mult timp pentru a găsi o soluție, înseamnă că ceva nu faci bine.

Posibil să existe mai multe metode de aplicare a acestei probleme, dar dacă ne referim la intersecții, cel mai probabil problema ar trebui să ia în calcul amplasamentele pe o hartă care de obicei are coordonate X, Y, nu doar o axă OX ca în această problemă.

Cam atât pentru acest articol! Dacă aveți alte variante de rezolvare vă rog să utilizați secțiunea comentarii. Pentru pasionați vă aștept cu o rezolvare pentru problema intersecției cercurilor în format XY.

Sper să fie util cuiva!

Validare CNP și IBAN în #Excel

În urmă cu ceva timp scriam un articol pentru validarea numerelor CNP ale cetățenilor români cu aplicabilitate în Excel și SharePoint.

În ultimul timp a devenit necesară într-una din activitățile pe care le desfășor implementarea unui algoritm de validare a IBAN-urilor românești. Având în vedere că în ultima perioadă am început o cercetare amânunțită de rezolvare a unor probleme clasice de algoritmică în Excel, mi-am propus să restructurez și modul de validare a IBAN-ului. (Nu am renunțat la seria de articole, doar că nu este suficient timp pentru cercetare).

Reamintesc că funcțiile din acest articol sunt scrise în Excel 365 cu localizare în limba română, ceea ce înseamnă că separatorul de parametri în formule este ; (punct și virgulă).

Pentru cei care dorescă să descarce fișierul Excel de validate îl puteți descărca de aici:

În cazul în care nu aveți Excel 365, am forțat funcționarea unei versiuni în Google Docs disponibilă în editare valori la această adresa.

Validarea CNP-ului românesc

Pentru validarea CNP-ului se folosește o cifră de control cu care se obține rezultatul sumei produselor primelor 12 cifre din CNP cu numărul de control, apoi se calculează MOD 11 din suma de control și se compară cu a 13-a cifră din CNP.

Validare CNP și IBAN în Excel.

Codul funcției din A2:

=LET(
    cnp;A2;
    control; "279146358279";
    validare_lungime; LEN(cnp) = 13;
    validare_numere; SUMPRODUCT(--ISNUMBER(--MID(cnp; SEQUENCE(LEN(cnp)); 1))) = 13;
    vControl; MID(control; SEQUENCE(12); 1);
    vCNP; MID(cnp; SEQUENCE(12); 1);
    sumControl; SUM(vCNP * vControl);
    cifra_control; IF(MOD(sumControl; 11) = 10; 1; MOD(sumControl; 11));
    validare_cifra_control; cifra_control = --RIGHT(cnp; 1);
    valid; AND(validare_lungime; validare_numere; validare_cifra_control);
    IF(valid; "Valid"; "Invalid")
)

în care folosesc un tehnică de a obține valori de validare intermediare (lungime, să fie doar numere, cifra de control). rezultatul validărilor intermediare îl determin final în variabila valid în care trebuie să aibă toate verificările intermediare valoare true.

Validarea IBAN-ului românesc

Pentru validarea IBAN-urilor există o tehnică internațională descrisă pe larg cu toate cazurile în legătura de pe Wikipedia: International_Bank_Account_Number

Pe scurt, în procesul de validare al doilea grup de 4 cifre trebuie adus la început, apoi toate literele din IBAN se transformă în CODUL lor Ascii +55, pentru ca litera A să devină 10, B 11 și așa mai departe.

Pausul următor este împărțirea numărului rezultat în grupuri de 9 și aplicarea MOD 97 asupra acestor rezultate. Dacă la final obținem valoarea 1 la ultimul MOD atunci IBAN-ul este valid. Dacă nu înseamnă că este Invalid.

Funcția propusă de mine este disponibilă doar în versiunea Excel 365 sau în fișierul Google Docs în mod fortaț pus la dispoziție la începutul acestui articol.

=LET(
    iban; SUBSTITUTE(TRIM(D8);" ";"");
    rearrangedIBAN; MID(iban; 5; LEN(iban) - 4) & LEFT(iban; 4);
    charToNum; LAMBDA(ch; IF(ISNUMBER(--ch); ch; TEXT(CODE(UPPER(ch)) - 55; "0")));
    convertToNumbers; TEXTJOIN(""; TRUE; MAP(MID(rearrangedIBAN; ROW(INDIRECT("1:" & LEN(rearrangedIBAN))); 1); charToNum));
    numericIBAN; TEXTJOIN(""; TRUE; MID(convertToNumbers; ROW(INDIRECT("1:" & LEN(convertToNumbers))); 1));
  segments;  MID(numericIBAN; SEQUENCE(ROUNDUP(LEN(numericIBAN)/9; 0);;1;9); 9);
    calcModnoua; REDUCE(0; segments; LAMBDA(acc;seg; MOD(--(acc & seg); 97)));
    result; IF(AND(LEN(iban); calcModnoua = 1); "Valid"; "Invalid");
convertToNumbers)

În această funcție în variabila charToNum este o funcție lambda recursivă care o folosesc în convertToNumbers pentru a putea calcula acea transformare a caracterelor in numere prin Code() de literă -55.

În imagine sunt prezentate rezultatele intermediare:

Rezultate intermediare variabile din funcția de validare IBAN.

Variabila calcModnoua este de fapt o funcție REDUCE() dar ca să o pot prezenta în această imaginine am înlocuit-o cu un SCAN care arată rezultatul MOD 97 la fiecare număr din variabila segments.

Mi-am dat seama de omisiunea de verificare a lungimii din variabila result în timpul redactării articolului, așa că în variantele disponibile pe internet, este posibil ca anumite cuvinte să fie considerate IBAN corect. :)

Cam atât!

Sper să fie util cuiva!

Blog la WordPress.com.

SUS ↑