Modele de algoritmi in #Excel – Time complexity (3)

În acest articol propun un set de metode de rezolvare pentru algoritmul Time complexity, descris la adresa: https://app.codility.com/programmers/lessons/3-time_complexity/

Un pic de teorie

Este o metodă de a arăta în cât timp se execută un set de operații / calcule asupra unui set de date. Odată cu creșterea valorilor de prelucrat crește în mod liniar și timpul de execuție (linear time). Contant time, îl întâlnim în cazul în care timpul de execuție nu se modifică odată cu creșterea setului de date, iar quadratic time apare în momentul în care timpul de execuție crește (pătratic – n2) odată cu creșterea setului de date.

Time Complexity este exprimat adesea în notație Big O (O()), care indică limita superioară a timpului de execuție în funcție de dimensiunea intrării.

Bog O presupune o metodă de notație a timpului de execuție în care:

  • O(n) este folosit pentru linear time
  • O(1) este folosit pentru contant time
  • O(n2) este folosit pentru quadratic time

În care n este numărul de elemente dintr-un șir.

Logica algoritmului este de a determina tipul timpului de execuție prin determinarea expresiei cu cea mai mare creștere dintr-o ecuație.

Exemplu: a*n+b rezultă că termenul care crește cel mai repede este a*n ceea ce înseamnă că este o funcție liniară O(n).

Dacă am avea o funcție de tipul: a*n2+b*n+v, cea mai rapidă creștere este la a*n2 ceea ce înseamnă că este o funcție pătratică.

Un videoclip care explică și demonstrează foarte fain terminologia poate fi vizionat aici: https://www.youtube.com/watch?v=D6xkbGLQesk&ab_channel=CSDojo

Legat de performanțele de execuție în Excel cu referință la câteva tehnici de optimizare puteți găsi explicații detaliate aici: https://learn.microsoft.com/en-us/office/vba/excel/concepts/excel-performance/excel-improving-calculation-performance

Problema FrogJmp

În această problemă se demonstrează un algoritm de tip O(1) în care avem o singură execuție pentru că setul de date este tot timpul la fel (X, Y, D) , în seturile de test fiind diferite doar valorile acestora. În specificații ni se spune că o broască pornește de la poziția 10 presupunem pe o axă 2D. Dacă ea sare pe o distanță (D) de 30 unități, câte sărituri trebuie să facă pentru a ajunge la poziția 85. Ca idee se presupune că broasca nu sare cu virgulă :) ci sunt doar sărituri întregi.

Rezolvarea este destul de simplă în celula C5: avem formula: =ROUNDUP((C2-C1)/C3;0)

FrogJmp în Excel pentru demosntrare Time Complexity

în care funcția ROUNDUP() se folosește pentru rotunjirea în sus a rezultatului obținut. Ca să putem împărți corect distanța de parcurs (D, C3) ar trebui să scădem din destinație (Y, C2) valoarea poziției de start (X, C1).

În partea a doua am realizat un interval cu valori care să specifice unde ajunge broasca la fiecare săritură (jump). Ca să pot realiza dinamic valorile pentru săritură am folosit un scan pe coloana E, în care adun valoare acumulată anterior la valoarea distanței D.

Problema PermMissingElem

Această problemă este din categoria O(n) pentru că numărul de prelucrări depunde de numărul elementelor din șirul propus spre rezolvare.

Problema presupune un șir de numere întregi care ar fi teoretic un șir. Scopul este de a obține numărul elementelor (numerelor lipsă) din intervalul dat.

Se dă șirul {8, 3, 2,  5, 7} în celula A4.

Problema PermMissingElem din algoritmul Time Complexity

În care:

  • celula A7: avem descompunerea șirului cu TEXTSPLIT(), eliminarea spațiilor din șir cu TRIM() și sortare ascendentă cu SORT();
  • în B7: calculez valoarea minimă din șir: =MIN(A7#)
  • în B8: calculez valoarea maximă din șir: =MAX(A7#)
  • în D7: generez secvența completă de la numărul cel mai mic la cel mai mare: =SEQUENCE(C7-B7+1;;B7;1). Numărul de elemente va fi numărul maxim din șir minus numărul minim +1.
  • În celula E7 am calculat dacă numărul din secvența completă (D7#) este în secvența propusă. Pentru aceasta folosesc: =IFNA(XMATCH(D7#;A7#);”x”) în care XMATCH() face căutarea între cei doi vectori iari IFNA() pune valoarea X în căsuța numărului lipsă.
  • Rezultatul final l-am trecut în celula G7: =TEXTJOIN(„, „;;FILTER(D7#;E7#=”x”; „-„)) în care am unificat toate valorile lipsă din șir prin filtrarea secvenței complete (D7#) pentru care rezultatul căutării este egal cu valoarea „x”. Am folosit TEXTJOIN pentru a unifica rezultatele din FILTER.

Unificate toate coloanele intermediare într-o singură formulă aceasta ar fi:

=LET(arr; SORT(--TRIM(TEXTSPLIT(A4;;",")));
          all; SEQUENCE(MAX(arr)-MIN(arr)+1; ;MIN(arr); 1);
          vCheck; IFNA(XMATCH(all; arr;0 ); "x");

TEXTJOIN(", ";;FILTER(all;vCheck="x"; "-")))

Aplicabilitatea practică a acestui model poate fi destul de interesantă.

Un exemplu ar fi acela în care avem un interval de date și vrem să identificăm dacă lipsește una din ele. Alt exemplu, avem un exemplu de litere (coduri, etc) și dorim să identificăm literele care lipsesc.

Model de solutii din viata reala pentru PermMissingElem

Funcția din D3 este:

=LET(arr; SORT(A3:A22);
          all; SEQUENCE(MAX(arr)-MIN(arr)+1; ;MIN(arr); 1);
          vCheck; IFNA(XMATCH(all; arr;0 ); "x");

FILTER(all;vCheck="x"; "-"))

Funcționarea algoritmului este posibilă datorită faptului că datele în Excel sunt de fapt numere.

În celula K3 am folosit funcția:

=LET(arr; SORT(CODE(I3:I21));
          all; SEQUENCE(MAX(arr)-MIN(arr)+1; ;MIN(arr); 1);
          vCheck; IFNA(XMATCH(all; arr;0 ); "x");

TEXTJOIN(", ";;CHAR(FILTER(all;vCheck="x"; "-"))))

Funcția nu poate lucra la propriu cu litere, de aceea a trebuit să folosesc funcția CODE() în variabila arr pentru a le transforma în coduri ascii. Apoi la final am transformat din coduri ascii în caractere cu funcția CHAR().

Problema TapeEquilibrium

Problema TapeEquilibrium este următoarea: se dă un vector de numere întregi. Vectorul trebuie să împărțit în două sub-șiruri, calculând suma fiecărui sub-șir și găsind diferența absolută dintre sumele lor. Scopul este de a minimiza această diferență absolută.

Algoritmul care rezolvă această problemă are o complexitate de timp O(n), unde n este lungimea vectorului de intrare. Acesta parcurge vectorul o singură dată pentru a calcula suma tuturor elementelor și, apoi, utilizează această sumă pentru a găsi diferența minimă dintre sumele sub-șirurilor.

Într-o abordare simplă problema este foarte ușor de rezolvat:

Se dă șirul {3, 1, 2, 4, 3}. Aflați cea mai mică valoare sau mai bine spus valoarea de echilibru pe șirul de valori.

Soluția simplă la problema: TapeEquilibrium

Pentru rezolvarea simplistă la B2 nu am introdus nici o valoare, apoi am compus suma celor de deasupra până în poziția anterioară minus suma celor rămase. Funcția ABS() transformă numerele negative în pozitive.

Doar că unificarea într-o singură formulă mi-a dat ceva bătăi de cap. Unul din principiile de bază în lucrul, nu numai în Excel, este KISS – Keep It Simple and Stupid.

În prima versiune:

Abordare complexă pe coloane diferite TapeEquilibrium

Am creat un tabel intermediar cu numerele și ordinea de la 0.

Apoi am aplicat o ditamai funcția pentru a face diferența. Scopul era de a le unifica într-una singură.

Formula din F2 pentru obținerea tabelului:

=LET(arr; --TRIM(TEXTSPLIT(C2;;", "));
          poz; SEQUENCE(ROWS(arr);;0);
          tabi; HSTACK(arr; poz);
   tabi)

în care arr este lista de numere, poziția este o secvență începând de la 0 iar tabi este variabila pentru a le unifica pe cele două. Scopul era de a traversa tabelul linie cu linie și extragerea valorilor de sus și jos pe baza poziției.

Formula din H2 este cam alambicată:

=LET(arr; F2#;
cc; ROWS(arr);
pc; TAKE(arr;;1);
dc; TAKE(arr;;-1);
BYROW(arr;
LAMBDA(rr;
LET(pv; TAKE(rr;;1);
dv; TAKE(rr;;-1);
pp; TAKE(pc;dv);
dp; TAKE(pc;-(cc-dv));
sum; ABS(SUM(pp)-SUM(dp)); rez; IFERROR(sum;""); rez))))

în care:

  • arr – preia tabelul rezultat din funcția anterioatră;
  • cc – determină numărul de rânduri al șirului;
  • pc – extrage prima coloană din tabelul arr;
  • dc – extrage a doua coloană din tabelul arr;
  • BYROW() parsează tabelul linie cu linie;
  • LAMBDA() face referire la linia curentă prin variabila rr
  • În LET() definesc un alt set de variabile pentru valorile de pe linie: pv (prima valoare), dv, valoarea de pe coloana 2, pp este prima parte din prima coloana din pozitia curenta (dv)-1, dp este a doua parte a primei coloane, apoi determin suma primei părți (pp) cu a doua parte (dp).

Iese dar la unificarea celor două funcții obțin niște rezultate dubioase, în loc de 7, 5, 1, 7 obțin 2, 1, 1, 0 ceea ce înseamnă că undeva fac o greșeală. Cu cât este mai complexă abordarea cu atât este mai greu de controlat rezultatul intermediar.

Așa că am ales o abordare diferită și am ales să folosesc funcția INDEX în mod dinamic în detrimentul funcției TAKE din varianta anterioară:

Soluția finală pentru TapeEquilibrium

Minusul acestei rezolvări este că nu pot folosi o variabilă de descompunere direct în LET() ci trebuie să refer un set de celule deja transformate în valori.

Primul pas a fost descompunerea șirului în celula A7 cu: =–TRIM(TEXTSPLIT(A2;;”,”))

Funcția din D2 care îmi calculează valoarea minimă este:

=LET(arr; A7#; poz; SEQUENCE(ROWS(arr));
pp; MAP(poz; LAMBDA(v; SUM(INDEX(arr;1):INDEX(arr;v))));
dp; MAP(poz; LAMBDA(v; IFERROR(SUM(INDEX(arr;v+1):INDEX(arr;ROWS(arr)));"")));
diff; IFERROR(ABS(pp-dp);""); MIN(diff))

în care:

  • arr – preia vectorul rezultat din split;
  • poz – determină șirul de valori de lungimea vectorului, începând cu 1 de această dată (implicit);
  • pp – preia valorile din prima parte a lui arr prin declararea variabilei v în LAMBDA în așa fel încât să construiesc un idex dinamic de la poziția 1 la poziția v curent în care v este poziția curentă din pozițe;
  • dp – a doua parte preia în același stil cu INDEX() valorile de la poziția curentă a lui v de pe secvență până la finalul lui arr ROWS(arr).
  • diff – calculează diferența absolută dintre cele două numere rezultate din însumare.

Ca să pot face depanare în această funcție mi-am făcut un tabel de rezultate intermediare, utilizând o combinație de HSTACK cu VSTACK.

vCheck; HSTACK(VSTACK("Sirul"; arr);VSTACK("Pozitia"; poz);VSTACK("Partea de sus"; pp);VSTACK("Partea de jos"; dp); VSTACK("Diferenta"; diff));

Un exemplu de problemă din viața reală unde algoritmul TapeEquilibrium ar putea fi util este în domeniul financiar, în special în gestionarea portofoliilor de investiții.

Să presupunem că aveți un portofoliu de investiții format din acțiuni și alte active financiare. Diferitele active pot fluctua în valoare în timp, iar obiectivul dumneavoastră este să gestionați aceste fluctuații și să minimizați riscul. O abordare comună este să împărțiți portofoliul în două părți și să încercați să mențineți echilibrul între ele.

Algoritmul TapeEquilibrium ar putea fi utilizat pentru a determina cea mai bună modalitate de a distribui activele între cele două părți ale portofoliului într-un mod echilibrat, astfel încât să se minimizeze riscul general. Calculând diferența absolută între sumele valorilor active din cele două părți, puteți ajusta distribuția pentru a obține o diferență minimă și, implicit, un portofoliu mai stabil și mai echilibrat din punct de vedere financiar.

Cam asta este pentru astăzi. Dacă ceva este greșit vă rog să nu ezitați să-mi scrieți în comentarii.

Sper să fie util cuiva!

Modele de algoritmi in #Excel – Vectori (2.2)

Pentru acest articol am continuat partea de vectori din articolul trecut și propun o rezolvare a problemei
OddOccurrencesInArray.

În această problemă ni se propune un vector cu un set de numere naturale, unele dintre ele se repetă. Scopul este de a identifica câte din numerele respectivă se repetă de un număr impar de ori.

Nu mai detaliez mica parte de teorie din articolul precedent ci detaliez direct problema și explic soluțiile.

Se dă șirul {9, 3, 9, 3, 9, 7, 9, 1}. În acest șir sunt unice valorile {1, 3, 7, 9}. Dintre acestea, 1 și 7 apar de un număr impar de ori (o singură dată).

OddOccurrencesInArray propunere rezolvare în Excel.

Rezolvarea pas cu pas

Șirul de numere este trecut în celula A2.

Pentru a descompune șirul în A5 folosesc funcția =–TEXTSPLIT(A2;;”, „) , caracterele – vor transforma rezolvatul text al funcției de split într-un număr.

Pentru a determina valorile unice în C5 folosesc funcția: =SORT(UNIQUE(A5#)) prin care determin valorile unice de pe array A5#, cu funcția UNIQUE() și apoi le sortez în ordine crescătoare cu funcția SORT(). Tendința în Excel este de a transforma cât mai mult operațiile în funcții. Avantajul acestei abordări este că operațiunile nu sunt replicabile, sau pot fi oarecum replicate prin înregistrare și editare de macro. Funcțiile în schimb pot fi replicate și copiate.

Tehnica de apelare cu # este utilă când nu știm câte numere sunt în șir, ceea ce face dinamic rezultatul dar și apelarea sa.

Ca să determin câte valori sunt de fiecare număr folosesc în D5 un simplu COUNTIF() pentru numărarea condiționată a valorilor de pe array A5# cu condiția ca ele să fie egale cu cele din unicele de pe array C5#.

Ca să pot compara dacă valoarea rezultată pe coloana D este un număr impar folosesc funcția ISODD(). Ca să preiau valoarea impară de pe coloana din dreapta folosesc funcția TAKE() cu parametrul -1. Ca să afișez în IF() valoarea de pe coloana din stânga folosesc un TAKE cu valoarea 1. Pentru a scana tot tabelul rezultat din array C5#:D5# linie cu linie folosim funcția BYROW() care are nevoie de un tabel ca input și o funcție LAMBDA() cu parametrul de input r (echivalentul liniei) pentru interpretare.

Pentru a afișa rezultatul final al prelucrării folosesc funcția TEXTJOIN() cu delimitator „, „.

Rezolvarea dintr-o singură funcție

Rezolvarea pas cu pas a fost aproape o nimica toată, cheia fiind BYROW()-ul descris anterior.

Ca să rezolvi problema într-o singură funcție, ai aproape întotdeauna pentru probleme puțin mai complexe de funcția LET() în care poți compune gradual variabilele pe care pot fiu folosite în alte funcții din același LET(). LET()-urile se pot combina între ele cu alte funcții. Doar că una din funcții nu funcționează corect în context de LET(). Minumata funcție COUNTIF() returnează eroare de fiecare dată când încerci să o incluzi în LET().

Într-o abordare conform scenariului anterior, funcția intermediară de determinare a fiecărei apariții a numărului ar fi trebuit să arate ca în imagine:

Încercare de rezolvare OddOccurrencesInArray  prin utilizare COUNTIF() în LET()

în care funcția MAP() ar trebui să parcurgă element cu element din valorile unice, iar LAMBDA() ar trebui să execute operațiunea de numărare condiționată a vectorului rezultat arr, pentru fiecare valore. Doar că nici COUNTIF() nici COUNTIFS() nu returnează valori corecte în acest context. Am încercat să realizez o funcție personalizată ca să includă COUNTIF()-ul și MAP() nu funcționează.

În acest context a trebuit să schimb funcția și în loc de un COUNTIF() să fac un COUNT() de un FILTER().

Funcția finală:

=LET( arr;--TEXTSPLIT(A2;;", ");
unice; SORT(UNIQUE(arr));
cate; MAP(unice;
LAMBDA(v; COUNT(FILTER(arr; arr=v))));
interm; HSTACK(unice;cate);
TEXTJOIN(", ";TRUE;
BYROW(interm;
LAMBDA(r;IF(ISODD(TAKE(r;;-1));TAKE(r;;1);""))))
)

în care aplic tehnicile descrise anterior pentru a obține un șir cu valorile care apar într-un număr impar de ori într-un șir de numere naturale.

Descrierea variabilelor:

  • arr – preia șirul și-l descompune apoi îl tranformă în număr;
  • unice – sortează valorile unice de pe arr;
  • cate – pentru fiecare valoare (v) din unice (MAP), aplică o funcție LAMBDA pentru a număra rezultatul fintrării arr după v curentă. Unice și Cate sunt variabile de o singură coloană care au același număr de rânduri.
  • interm – este variabila prin care realizez o matrice din cele două variabile;
  • matricea interm este parcursă de BYROW() prin care aplic o funcție LAMBDA() pentru fiecare linie (r) în așa fel încât să determin dacă valoarea de pe coloana din dreapta (TAKE cu -1) este număr impar (funcția ISODD).

Update 24.04.2024

În cazul în care vrem totuși să optimizăm execuția în loc să folosim funcția COUNT() de FILTER() ar fi mai util să folosim SUMPRODUCT() un fel de substitut pentru COUNTIF() și care funcționează atât în LET cât și în MAP().

Pentru a determina numărul de apariții a ficărui număr atunci putem folosi funcția:

=LET( arr;--TEXTSPLIT(A2;;", ");
unice; SORT(UNIQUE(arr));
cate; MAP(unice;
LAMBDA(v; SUMPRODUCT(--(arr=v)))); cate)

Cam asta este. Nu sunt sigur că este cea mai optimă variantă. Dacă aveți alte modele de rezolvare inclusiv în alte limbaje (Phyton de exemplu, sau VBA) sau vă rog să le partajați în comentarii dacă doriți.

Sper să fie util cuiva.

Modele de algoritmi in #Excel – Iteratiile (1)

Acest articol face parte dintr-o serie de articole dedicate implementării de algoritmi clasici în Microsoft Excel 365.

Sursa de inspirație este site-ul: https://app.codility.com/programmers/lessons/1-iterations/binary_gap/

Doar puțină teorie

Iterațiile sau structurile de control clasice includ:

  • IF cu sintaxa pseudocod: dacă condiție atunci acțiune_adevăr altfel acțiune_fals
  • FOR cu sintaxa pseudocod: for variabilă in interval_de_valori acțiune
  • WHILE cu sintaxa pseudocod: while condiție acțiune

În Excel în schimb nu există nici FOR nici WHILE implementată ceea ce face oarecum diferită abordarea. O simulare a unui FOR este întâlnită uneori prin utilizarea funcției SEQUENCE() combinată cu funcția SCAN() sau REDUCE(). De exemplu dacă am avea un pseudocod de tipul: pentru fiecare număr de la 1 la 9 adunați numărul cu suma numerelor anterioare. Exemplu:


Problema supusă rezolvării este BinaryGap

Aceasta presupune transformarea unui număr zecimal în număr binar după care trebuie determinate numărul maxim de secvențe de 0 care apar în numărul binar, urmate de valoarea 1.

În exemplu este prezentat numărul 9 care în binar este 1001 și care are un număr de 2 de 0 consecutivi urmați de valoarea 1. În numărul 32 care este în binar: 100000 numărul de 0 nu este urmat de 1 ceea ce înseamnă că rezultatul va fi 0.

Problema de pe Codility oferă un set de numere de test [1..2,147,483,647]. Doar că în Excel ultimul număr zecimal care poate fi transformat în binar este numărul 511, de la 512 încolo obținem eroarea #NUM!

În imagine propuneri de rezolvare.

Menționez că localizarea pe calculatorul meu este localizarea în română ceea ce face ca separatorii de formule să fie ; (punct și virgulă) nu , (virgula).

Ca să transform numărul în binar folosesc funcția DEC2BIN(). Funcția din celula B2 este: =DEC2BIN(A2)

Dacă aș dori să văd câți de 0 sunt în total în număr în D2 am introdus funcția LEN() pentru a determina lungimea șirului rezultat în binar și SUBSTITUTE() pentru a înlocui numerele de 1 cu nimic. Funcția din D2 este: =LEN(B2)-LEN(SUBSTITUTE(B2;”0″;””)).

Dacă ar trebui să determin doar numărul maxim de 0 consecutivi atunci folosesc în E2 funcția: =MAX(SCAN(0;MID(B2;SEQUENCE(LEN(B2));1); LAMBDA(a;v; IF(v=”1″; 0; a+1))))

În care:

SCAN() scanează un array de valori rezultat prin descompunerea valoare cu valoare a numărului binar din B2 pe baza lungimii sale SEQUENCE(LEN(B2)). Funcția SCAN() are nevoie de o funcție LAMBDA pentru calculul valorii finale. În Lambda avem variabilele a – acumulator, și v- valoarea curentă. Dacă valoarea curentă este 1 ca text, pentru vă provine din MID(), atunci acumulatorul va lua valoarea 0, altfel va lua valoarea iterată cu 1. La final aplic un MAX() pentru a determina exact care este cea mai mare secvență de valori 0.

Pentru numărul 401 care în binar este 110010001 rezultatul SCAN() este:

În exemplul lui 401 voi obține valoarea finală 3.

Dacă dorim să răspundem corect la problemă și să numărăm doar valorile 0 care sunt urmate de 1 atunci formula se complică puțin.

În F2 am introdus o funcție LET() ca să pot scrie mai puține caractere. Scopul în algoritmi este să ai un cod cât mai optim și poate mai puține caractere într-o formulă / cod.

=LET(bloc; MID(B2;SEQUENCE(LEN(B2));1); zu; IF(RIGHT(B2)=”0″;0;1); cate; MAX(SCAN(0; bloc;  LAMBDA(a;v; IF(v=”1″; 0; a+1)))); conc; CONCAT(zu;”-„;cate); IF(zu=0;0;cate))

În această funcție am introdus variabila zu pentru a determina dacă la finalul șirului este sau nu valoarea 1. Această abordare are în schimb o eroare de logică pentru că nu tratează excepțiile în care avem totuși valori 0 urmate de 1 în numărul binar și se finalizează cu valoarea 1. Exemplu este numărul 486 care se termină cu 0 dar are 2 de 0 în interior urmați de valoarea 1.

Variantele cu emulare de FOR sunt interesante și pot duce la soluții destul de frumoase. Doar că în rezolvarea problemelor mai complexe de Excel important este să ai formule cât mai reduse ca dimensiune pentru a asigura un optim de execuție dar și pentru a face mai ușor depanarea formulelor. În acest sens propun o metodă de rezolvare fără iterații explicite.

Formula din B46 este: =MAX(LEN(DROP(TEXTSPLIT(B46;1);;-1)))

În care:

TEXTSPLIT() – împarte numărul binar în funcție de fiecare apariție a valorii 1 rezultând vectori cu număr de coloane diferite:

În cazul în care numărul splitat nu conține nici un 0 vom vedea o listă de celule vide.
În cazul în care în ultima coloană a fiecărui vector avem valoarea 0 înseamnă că secvența de 0 nu este urmată de valoarea 1.

DROP() – este utilizat pentru a elimina excepția când la final-ul vectorului avem valoarea 0. Elimină ultima celulă din rezultat.

LEN() – numără pentru fiecare celulă rămasă în vector cât de lungă este secvența de 0 rezultatul intermediar fiind de forma:

MAX() extrage din lista de valori valoarea maximă a numărului de 0 consecutivi urmați de valoarea 1.

În Google Spreadsheets nu există funcția TEXTSPLIT() dar există o funcție SPLIT() care nu funcționează asemănător cu cea din Excel.

Cam asta ar fi. Sper să fie util cuiva.

Blog la WordPress.com.

SUS ↑