Modele de algoritmi în #Excel – Maximum slice problem (9)

În acest articol propun o metodă de rezolvare a algoritmilor clasici în formatul Excel modern, din categoria Maximul slice. Descrierea acestor probleme o găsiți la adresa: https://app.codility.com/programmers/lessons/9-maximum_slice_problem/

Puțină teorie

Pentru rezolvarea problemelor din acest articol am ales să folosesc cu precădere funcția MAKEARRAY() care poate avea o putere nebănuită de a transforma un vector liniar într-o matrice dacă reușești o proiecție corectă. Sintaxa funcției:

=MAKEARRAY(rows; columns; function) în care rows este numărul de linii care se vor genera cu începere de la R1 , columns la numărul de coloane din tabelul rezultat iar function este o funcție lambda() cu doi parametri: rows; columns.

Exemplificare Makearray()

După cum se observă în C2 este generată o matrice cu 9 linii și nouă coloane, pentru fiecare celulă realizându-se calculul r+c, însemnând valoarea rândului cu valoarea coloanei.

Celelalte funcții le-ați mai întâlnit. Mai multe detalii despre Filter puteți găsi în articolul de demult: Funcția Filter() din #Excel 365. Funcția Index() mi-a dat ceva de furcă la problema a treia… dar să ajungem acolo.

Să nu uităm că în algoritmică elementele unui vector încep de la 0, dar în Excel majoritatea operațiunilor de indexare/filtrare/cautare încep de la valoarea 1.

Problema MaxProfit

Face parte din categoria problemelor care analizează secvențe și se concentrează pe identificarea profitului maxim care poate fi obținut printr-o singură tranzacție de cumpărare și vânzare a acțiunilor.

Descrierea problemei: Ni se dă un vector A care conține prețurile acțiunilor în diferite zile. Trebuie să găsim profitul maxim care poate fi obținut cumpărând acțiunile într-o zi și vânzându-le într-o altă zi ulterioară. Dacă nu este posibil să obținem un profit (de exemplu, prețurile scad sau rămân constante), profitul ar trebui să fie considerat 0.

Propunerea de rezolvare:

MaxProfit în Excel

În zona de Proiecție am realizat o implementarea manuală pentru a verifica suma maximă. La urma urmei multe probleme le rezolvăm manual până să ne mai chinuim să facem tot felul de funcții, nu? :) După ce am pus zilele și valoarile pe coloană și linie, la E3 am scris formula: =IF($C3<=E$1;””;$D3-E$2) , întotdeauna zilele trebuie să fie mai mici decât cele pentru care ne raportăm (axa timpului).

Apoi în C13 am proiectat o funcție integrată pentru a face din prima același tabel dar de data aceasta raportându-mă la vectori intermediari prin intermediul valorii lui r și c din makearray. Astfel funcția din C13 este:

=LET(arr; A2:A7; 
     seq; SEQUENCE(ROWS(arr)); 
     tarr; TRANSPOSE(arr); 
     tabi; MAKEARRAY(MAX(seq); MAX(seq); LAMBDA(r;c;
                   IF(r<=c;""; INDEX(arr;r)-INDEX(tarr;c)))); tabi)

în care arr stochează vectorul cu sumele de interpretat, seq generează numărul de zile, tarr transpozează numărul de zile pe coloane ca să le pot folosi ca artificiu în tabi care este de fapt un tabel care indexează valorile din arr pe baza liniei și coloanei curente.

Ulterior în D10 facem un max din acel tabel și avem rezultatul final. Ca să obținem și zilele în care avem acele valori în cazul nostru 1 și 5, atunci trebuie să apelăm șa tehnica de identificare a unei valori într-un tabel, descrisă în articolul: Modele de algoritmi in #Excel – Prefix Sums (5.1)

Problema MaxProfit este o problemă clasică de optimizare cu aplicabilitate în diferite contexte financiare în vederea rezolvării problemelor de maximizare a câștigurilor într-o serie de tranzacții.

Problema MaxSliceSum

MaxSliceSum este o problemă clasică în informatică, care face parte din categoria problemelor de optimizare pe secvențe, adică Maximum Subarray Problem.

Descrierea Problemei: Se dă un vector A de numere întregi (pozitive, negative sau zero). Trebuie identificată suma maximă a unui „slice” (subsecvență continuă) din acest vector. Un slice este definit de două indici P și Q din vectorul A, unde P ≤ Q, și include toate elementele de la A[P] până la A[Q].
Este posibil ca toate numerele să fie negative într-un vector, caz în care slice-ul cu suma maximă va fi cel mai mic număr negativ.

La o primă lectură a problemei am crezut ca este asemanator cu: Problema MinAvgTwoSlice, doar că de data aceasta nu mai luăm perechi de câte două elemente ci pot fi toate elementele… dintr-un șir sau doar o secvență P-Q.

Uneori problemele simple ne dau cele mai multe bătăi de cap, așa că a trebuit să mă documentez mai bine, și așa am aflat că de fapt această problemă mai este cunoscută și ca problema subșir de sumă maximă sau Algoritmul lui Kadane, unul foarte cunoscut în lumea programatorilor. Mi-au plăcut foarte mult explicațiile din filmulețul: Algoritmul lui Kadane – Determinarea unui subsir de suma maxima in C++

Să revenim la Excel. După multe încercări și teste am ajuns din nou la funcția Makearray care are o putere nebănuită în bi-dimensional prin parametrii săi r și c din LAMBDA asociat.

În final implementarea algoritmului este ceva spectaculos de simplu și puternic:

Algoritmul Kadane în Excel.

În zona de proiecție matrice am creat un array în care pentru fiecare r<=c am făcut suma subvectorilor de la linia curentă până la coloana curentă.

Funcția din E3 este:

=MAKEARRAY(8;8; LAMBDA(r;c; IF(r<=c; SUM(INDEX($A$2:$A$9;r):INDEX($A$2:$A$9;c));””)))

în care pentru fiecare celulă din tabelul 8 pe 8 calculăm suma valorilor indexului dinamic de la r la c. Comparați cu complexitatea din filmulețul cu implementarea în C++ și poate dați o șansă Excelului. :) Dincolo de glumă pe mine m-a încântat mult soluția, dar vom vedea la următoarea problemă că lucrurile nu sunt chiar atât de roz în Excel….

Funcția integrată care extrage și slice-ul cu sumă maximă este:

=LET(arr; A2:A9; 
     seq; SEQUENCE(ROWS(arr)); 
     rr; ROWS(arr);
     fSlice; LAMBDA(x; LET(aria;x; 
                           ca; MIN(COLUMN(aria))-1; 
                           ra; MIN(ROW(aria))-1;
                           tc; TOCOL(aria;1);
                           unde; MATCH(MAX(aria);tc;0);
                           col; LET(cs; COLUMNS(aria);
                                    c; MOD(unde;cs);
                                     IF(c=0; cs; c));
                           row; ROUNDUP(unde/COLUMNS(aria);0); 
                                "("&row-1&","&col-1&")"));
sarr; MAKEARRAY(rr; rr; LAMBDA(r;c; 
                               IF(r<=c; SUM(INDEX(arr;r):INDEX(arr;c));""))); 
                               "Smax: "&MAX(sarr)&" - Slice: "&fSlice(sarr))

Toată construcția pare mai complicată pentru că am integrat în fSlice tehnica de identificare a locației unei coloane, în vederea afișării slice-ului maxim. Dar toată cheia soluției este în variabila sarr în care am acel MAKEARRAY descris anterior, doar că de data aceasta dinamic pe baza variabilelor definite la început.

Din păcate pentru multe numere în A, Excelul începe să gâfâie. La 1024 de numere algormitmul merge foarte bine dar la 4096 a început deja să dea semne de blocare. Pentru testare am utilizat funcția de generare array aleatoriu: =RANDARRAY(128;1;-50; 200;TRUE) în care se generează 128 de numere întrgi (TRUE) de la minim -50 până la maximum 200. Limita de numere din A este limita numărului de coloane din Excel (16384) pentru a putea opera matricea de calcule.

MaxSliceSum este util în analizarea datelor financiare (de exemplu, determinarea perioadei în care profitul a fost maxim) sau în analizarea performanței sistemelor (de exemplu, identificarea perioadelor de maximă încărcare sau activitate). Această problemă este importantă pentru înțelegerea optimizărilor pe secvențe și este un exemplu clasic de aplicare a algoritmilor de programare dinamică.

Problema MaxDoubleSliceSum

Problema MaxDoubleSliceSum este o extindere a problemei MaxSliceSum, fiind parte din categoria problemelor de optimizare pe secvențe. Scopul este să găsești suma maximă posibilă a unei subsecvențe formate din trei părți neconsecutive dintr-un vector.

Descrierea Problemei:
Se dă un vector A de numere întregi de lungime N (unde N ≥ 3). Trebuie calculată suma maximă posibilă a unui double slice (subsecvență non-consecutivă) definită de trei indici X, Y, și Z (unde 0 ≤ X < Y < Z < N).
Double slice-ul este definit astfel:
– X este începutul primei secțiuni care este exclusă.
– Y este începutul secțiunii incluse în sumă.
– Z este sfârșitul secțiunii incluse, care este exclusă din sumă.
Suma double slice-ului este calculată ca suma tuturor elementelor din A[X+1] până în A[Y-1] și din A[Y+1] până în A[Z-1].
Rezolvarea algoritmului a fost o mare provocare pentru că indexarea nu funcționează absolut deloc într-un context prea dinamic, de aceea a trebuit să găsesc o altă metodă de rezolvare.

Propunere rezolvare problema MaxDoubleSliceSum

În rezolvarea problemei am pornit de la metoda propusă în Problema MaxProductOfThree doar că de data aceasta nu mă mai refer unitar la poziția lui X, Y sau Z ci trebuie să caut în șirul A valorile echivalente din matrice și să le însumez. În rezolvarea pas cu pas am generat întâi matricea XYZ cu toate combinațiile posibile în care X<Y<Z. Apoi pe bază de indexare în I4 am calculat suma elementelor. Funcția utilizată linie cu line:

=LET(arr;$A$2:$A$9; 
     rr; E4:G4; 
     x; --TAKE(rr;;1); 
     y; --CHOOSECOLS(rr;2); 
     z; --CHOOSECOLS(rr;3); 
     SUM(IF(x+1=y;0;
           INDEX(arr;x+1):INDEX(arr;y-1));
         IF(y+1=z;0;
            INDEX(arr;y+1):INDEX(arr;z-1))))

variabilele x, y și z sunt preluate pentru fiecare linie apoi fac suma prin indexarea de la X+1 la Y-1 și de la Y+1 la Z-1. Merge destul de bine dar nu am reușit să le integrez în aceeași funcție pentru că modelul este prea elaborat pentru a putea să funcțineze funcția INDEX():INDEX().

Ca să pot identifica și studia care sunt valorile care compun rezultatul am creat coloana intermediară K în care am unificat prin aceeași tehnică de indexare dinamică a vectorului A, pe baza valorilor din matrice.

Unificarea într-o singură formulă a fost un coșmar, insistând pe INDEX():INDEX() și câteva zeci de variante de funcții recursive. Până la urmă am realizat că FILTER() cu criterii multiple este mai fezabil pentru că returrnează blocuri de celule însumabile.

Funcția finală din N5 este:

=LET(arr; A2:A9; 
     seq; SEQUENCE(ROWS(arr));
     matrix; LET(vector; arr;
                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)*(CHOOSECOLS(tabi;3)<nr));
   unicsm; CHOOSECOLS(unics;1;2;3);
   unicsm);

tabf; HSTACK(arr;seq);
slice; BYROW(matrix; LAMBDA(r; TEXTJOIN(";";;r-1)));
fcautxy; LAMBDA(a;b; 
                SUM(TAKE(IFERROR(FILTER(tabf; 
                      (TAKE(tabf;;-1)>=a+1)*(TAKE(tabf;;-1)<=b-1));0);;1)));
fcautxyz; LAMBDA(x;y;z; fcautxy(x;y)+fcautxy(y;z));

summax; BYROW(matrix; 
        LAMBDA(r; LET(x; TAKE(r;;1); 
                      y; TAKE(TAKE(r;;2);;-1); 
                      z; TAKE(r;;-1); 
                    fcautxyz(x;y;z))));
maxsum; MAX(summax);

tabfin; HSTACK(slice; summax);
FILTER(tabfin; TAKE(tabfin;;-1)=maxsum))

în care:

  • variabila matrix este definită în articolul Problema MaxProductOfThree și este utilizat doar pentru a putea genera matricea de căutare cu X<Y<Z toate combinațiile posibile.
  • tabf este de fapt tabelul cu coloana A și secvența de numere în format de start de la 1 pentru a putea să realizez indexarea pe această coloana.
  • slice este utilizată pentru a putea genera perechile de x;y;z în formatul array cu start de la 0 rezultatul fiind afișat ulterior în tabfin.
  • fcautxy este o funcție recursivă pe care o folosesc mai jos în fcautxyz pentru ca putea extrage cu filter valorile din A, stocate pe prima coloana a lui tabf pe care o preiau cu TAKE(). Simbolul * este folosit pentru condiții cumulative în filter(). Sintaxa pe cumulative este Filter(valori; (Cond1)*(Cond2) ). Filter() a fost soluția salvatoare.
  • fcautxyz este funcția recursivă care face suma prin filtrare, definită în fcautxy dar cu aplicare pe Xy și yz. Aici este artificiul suprem prin care am putut folosi o recursivă în atltă recursivă pentru a adresa din două poziții diferite vectorul A.
  • summax este un vector de valori intermediare care preia linie cu linie (BYROW) din matrix valorile lui X, Y, Z după care invocă recursiva fcautxyz pentru a calcula suma.
  • maxsum calculează valoarea maximă obținută pe summax
  • tabfin este tabelul final în care unesc valoarea maximă cu slice după care în rezultatul afișat introduc un nou FILTER de data aceasta pe tabel, în care valoarea obținută să fie egală cu maximum obținut în maxsum.

Huh… a luat ceva timp… dar merită efortul… cu toate că la testare am avut neplăcuta surpriză să constat că numărul maxim de numere din A este de 101… de ce? pentru că rădăcina cubică a numărului maxim de linii din excel (1.048.576) este aproape 101,5…

Aici ajungem la prima limitare a Excelului în rezolvarea acestui tip de probleme. Voi reveni cu o imbunătățire a matricei de triplete care funcționează până la 185 de linii. Veți vedea de ce.

Ca aplicații practice pentru această problemă mă gândesc acum la finanțe, identificarea intervalelor de creștere și scădere a valorii acțiunilor, pentru a găsi cele mai profitabile perioade de vânzare și cumpărare.
sau analiza datelor pentru găsirea secvențelor de date cu cea mai mare variabilitate pozitivă, utile în detectarea anomaliilor sau în analiza trendurilor.

Cam atât pentru astăzi.

Sper să fie util cuiva!

Modele de algoritmi in #Excel – Leader (8)

Acest articol este o continuare a seriei de articole cu propunere de rezolvare a algoritmilor clasici în Microsoft Excel ediția 365. Probelemele de astăzi fac parte din algoritmii Leader descriși la adresa: https://app.codility.com/programmers/lessons/8-leader/

Algoritmul Leader se referă la identificarea unui element dintr-o secvență de valori care apare mai mult de jumătate din numărul total al elementelor din acea secvență. Acest element este denumit „leader” sau „dominator”. Problema se poate formula astfel: având un vector de dimensiune N, să se găsească elementul care apare de cel puțin N/2 + 1 ori, dacă există unul. În mod programatic se recomandă rezolvarea acestor tipuri de probleme cu algoritmul Boyer-Moore majority vote.

Doar că Excelul este puțin diferit și este mai greu de adaptat la rezolvările clasice care implică secvențe FOR. Alternative există și unele din ele sunt foarte interesante. Dar să începem cu:

Un pic de teorie

În mod clasic dacă am avea un șir de numere sau valori ar trebui să identificăm unicele, apoi să facem un countif pe blocul inițial.

Cateva functii de cautare leaders

De exemplu în E3: folosim funcția UNIQUE() pentru a calcula elementele unice din vectorul A2:A7, după care utilizăm un COUNTIF() dinamic pe vector după rezultatul din E3#. În felul acesta determinăm numărul de apariții a fiecărui element. Există și o variantă puțin diferită, de a folosi funcția FREQUENCY() în forma: =FREQUENCY(A2:A7; UNIQUE(A2:A7)) dar ea determină doar numărul de apariții a fiecărui element, în cazul nostru 3/3.

În Excel în schimb există o altă funcție MODE cu două variante: SNGL sau MULT. MODE.SNGL() extrage dintr-un șir de valori cea prima cea mai frecventă valoare. În exemplu nostru (B3) avem două valori care au același număr de apariții, ceea ce înseamnă că va afișa doar prima valoare întâlnită în vectorul de căutare. MODE.MULT() extrage cele mai frecvente numere, în cazul în care avem egalitate la numărul de apariții le va afișa pe ambele. În cazul în care nu avem nici un leader, funcția MODE va afișa eroarea #N/A.

O abordare interesantă este în schimb la text, pentru că MODE.MULT() funcționează doar la valori numerice. În modelul propus în imaginea anterioară, ca să putem determina leaderul folosim un MODE dar pentru un MATCH care extrage pentru fiecare valoare întâlnită poziția inițială a sa. După care pe același șir extragem valoare pentru cea mai mare valoare. MODE() în formatul clasic încă este funcțional și compatibil în Excel 365.

Problema Dominator

În problema Dominator ni se cere să determinăm care este numărul dintr-un vector care apare de mai mult de jumătate din lungimea vectorului. De asemenea, se poate solicita extragerea pozițiilor din vector în cazul în care avem un număr Dominator.

Propunerea de rezvare pas cu pas:

Rezolvare problema Dominator în Excel.

Ca să determin Leader-ul este simplu să folosim un MODE.MULT() în E3, dar ca să determinăm dacă este dominiator avem nevoie de numărul de elemente din vector (B2), valoarea de dominare care este numărul de elemente împărțit la 2 (C2), numărul de apariții (F2), verificarea că Leaderul este dominator (G2), iar dacă dorim să extragem pozițiile în vector va trebui să folosim o secvență cu start de la 0 (D2) pe care să o filtrăm după pentru cazurile în care A este egal cu Leader care devine dominator.

Unificat într-o singură funcție cu LET() aceasta ar fi:

=LET(arr; A2:A9;
     nrEl; ROWS(arr);
     dominatorv; nrEl/2;
     seq; SEQUENCE(nrEl;;0);
     dominator; MODE.MULT(arr);
     verifi; ROWS(dominator)=1;
     aparitii; COUNTIF(arr; dominator);
     verifii; aparitii>dominatorv;
     IF(AND(verifi; verifii);
          TEXTJOIN("; "; ; FILTER(seq; arr=dominator));
          "Nu există dominator"))

În funcție am două verificări: verifi și verifii. În verifi verific dacă am un singur rezultat sau mai multe. În cazul în care sunt returnate mai multe valori prin MODE.MULT atunci nu avem un dominator ci mai mulți Leaderi. În rest logica funcției este descrisă în secțiunea de rezolvare pas cu pas.

Problema EquiLeader

Aceasta este o versiune mai complexă a problemei Dominator pentru că presupune compararea dinamică a leaderilor pe două secțiuni ale aceluiași vector. În această problemă, trebuie să determinăm numărul de poziții dintr-un array unde liderii celor două sub-secvențe (stânga/sus și dreapta/jos) sunt identici. Concret: Având un array A de dimensiune N, o poziție S este un echileader dacă liderul sub-secvenței A[0…S] este același cu liderul sub-secvenței A[S+1…N-1]. Trebuie să găsim numărul total de astfel de poziții în array.

După citirea enunțului am realizat că logica de rezolvare este identică cu problema TapeEquilibrium din articolul Modele de algoritmi in #Excel – Time complexity (3) .

Propunere rezolvare în Excel:

Problema EquiLeader

În care pe coloana C determinăm dacă în poziția curentă comparată cu celelaltă secțiune avem egalitate de Dominatori, iar pe coloana D afișăm combinațiile de pe secvența de pe B. Funcția de pe C și D este aceeași cu specificarea că rezultatul final este afișat diferit.

=LET(arr;A2:A7;poz;SEQUENCE(ROWS(arr);;1);
freq; LAMBDA(a; LET(x; a; cate; ROWS(x); unice; UNIQUE(x); dominatorv; cate/2; nrfiecare; COUNTIF(x; unice); tabi; SORT(HSTACK(unice; nrfiecare);2;-1); dominatorval; TAKE(tabi;1;-1); veri; dominatorval>dominatorv; dominator; IF(veri; TAKE(tabi;1;1); 0); dominator));

pp;MAP(poz;LAMBDA(v; LET(ari; INDEX(arr;1):INDEX(arr;v); freq(ari))));
ppa; LET(pozi; B2#; ppa; MAP(pozi;LAMBDA(v; TEXTJOIN(";";;INDEX(pozi;1):INDEX(pozi;v+1)))); ppa);

dp;MAP(poz;LAMBDA(v; LET(arii; (INDEX(arr;v+1):INDEX(arr;ROWS(arr))); freq(arii))));
dpa;LET(pozi; B2#; ppa; MAP(pozi;LAMBDA(v; TEXTJOIN(";";;INDEX(pozi;v+2):INDEX(pozi;ROWS(arr))))); ppa);

domins;IFERROR(pp=dp; "");
nrequi; SUM(IFERROR(--domins; 0));

care; IFERROR(IF(domins; ppa&" - "&dpa;"");"");
domins
)

Cheia în rezolvarea problemei, este funcția freq care este utilizată recursiv în MAP-ul care calculeaza valoarea pp și dp. freq este de fapt funcția de determinare a dominatorului din problema Dominator, dar care este generalizată pentru a prelua parametrul de intrare a, care se transformă în x în LET() și calculează dominatorul pe subvectorul determinat în pp și dp de index-ul dinamic.

ppa și dpa se folosesc pentru a extrage valoarea din secvență pentru a afișa pozițiile din vector și secțiunile unde întâlnim dominatori egali.

Rezultatul 2 al funcției este numărul de segmente de pe vector care înreplinesc condiția de egalitate între dominatori.

Ca aplicabilitate practică putem să ne gândim la analize de piață pentru a identifica dacă pe diferite segmente sau intervale de timp există un brand dominant.

Cam atât pentru astăzi!

Sper să vă fie util!

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!

Blog la WordPress.com.

SUS ↑