Noudata näitä vaiheita ratkaistaksesi kaikki dynaamisen ohjelmoinnin haastatteluongelmat

Huolimatta siitä, että hänellä on huomattava kokemus ohjelmistotuotteiden rakentamisesta, monet insinöörit tuntevat järkyttävän ajatuksen käydä läpi algoritmeihin keskittyvän koodaushaastattelun. Olen haastatellut satoja insinöörejä Refdashissa, Googlessa, ja olen ollut mukana aloittavissa yrityksissä, ja jotkut yleisimmistä kysymyksistä, jotka tekevät insinööreistä vaivaa, ovat niitä, joihin liittyy dynaaminen ohjelmointi (DP).

Monet teknologiayritykset haluavat kysyä DP: ltä kysymyksiä haastatteluissaan. Vaikka voimme keskustella siitä, ovatko he tehokkaita arvioimaan jonkun kykyä toimia suunnittelijaroolissa, DP on edelleen alue, joka vie insinöörejä matkalle löytääkseen rakastamansa työn.

Dynaaminen ohjelmointi - ennakoitavissa ja valmisteltavissa

Yksi syy siihen, miksi uskon henkilökohtaisesti, että DP-kysymykset eivät ehkä ole paras tapa testata suunnittelukykyä, on, että ne ovat ennustettavissa ja helppo sovittaa yhteen. Niiden avulla voimme suodattaa paljon enemmän valmistautumista tekniikan kykyjen sijasta.

Nämä kysymykset vaikuttavat tyypillisesti melko monimutkaisilta ulkopuolelta, ja saattavat antaa sinulle kuvan siitä, että niitä ratkaiseva henkilö on erittäin hyvä algoritmeissa. Samoin ihmiset, jotka eivät ehkä pääse yli joihinkin mielenkiintoisiin käsityksiin DP: stä, saattavat tuntua melko heikolta algoritmien tuntemukseltaan.

Todellisuus on erilainen, ja suurin tekijä heidän suorituksissaan on valmius. Varmista siis, että kaikki ovat siihen valmiita. Kerta kaikkiaan.

7 vaihetta dynaamisen ohjelmoinnin ongelman ratkaisemiseksi

Lopussa tästä viesistä käsittelen reseptiä, jota voit seurata selvittääksesi, onko ongelma ”DP-ongelma”, sekä selvittää ratkaisu tällaiseen ongelmaan. Tarkastelen erityisesti seuraavia vaiheita:

  1. Kuinka tunnistaa DP-ongelma
  2. Tunnista ongelmamuuttujat
  3. Ilmaise selvästi toistosuhde
  4. Tunnista perustiedot
  5. Päätä, haluatko toteuttaa sen toistuvasti tai rekursiivisesti
  6. Lisää muisti
  7. Määritä ajan monimutkaisuus

Näyte DP -ongelma

Annan esitellä näyteongelman, jotta voin saada esimerkin abstraktioista, joita aion tehdä. Kussakin osiossa viittaan ongelmaan, mutta voit myös lukea osiot ongelmasta riippumatta.

Ongelman selvitys:

Tässä ongelmassa olemme hulluilla hyppypalloilla, yrittäen pysähtyä välttäen piikkejä matkan varrella.

Tässä ovat säännöt:

1) Sinulle annetaan tasainen kiitotie, jossa on joukko piikkejä. Kiitotietä edustaa looginen taulukko, joka ilmaisee, onko tietystä (erillisestä) pisteestä piikkejä. Se on totta selkeälle ja vääriä ei selkeälle.

Esimerkki taulukon esityksestä:

2) Sinulle annetaan aloitusnopeus S. S on ei-negatiivinen kokonaisluku missä tahansa pisteessä, ja se osoittaa, kuinka paljon siirryt eteenpäin seuraavalla hyppyllä.

3) Aina kun laskeudut paikalle, voit säätää nopeutta jopa yhdellä yksiköllä ennen seuraavaa hyppyä.

4) Haluat pysähtyä turvallisesti missä tahansa kiitotien varrella (sen ei tarvitse olla ryhmän päässä). Voit lopettaa, kun nopeutesi muuttuu nollaksi. Jos kuitenkin laskeudut piikkiin missä tahansa vaiheessa, hullu pomppiva pallo räjähtää ja se on ohi.

Toimintosi ulostulon tulisi olla looginen, joka osoittaa, voimmeko pysähtyä turvallisesti missä tahansa kiitotien varrella.

Vaihe 1: Kuinka tunnistaa dynaamisen ohjelmoinnin ongelma

Ensin tehdään selväksi, että DP on käytännössä vain optimointitekniikka. DP on menetelmä ongelmien ratkaisemiseksi jakamalla ne kokoelmaan yksinkertaisempia alioikeuksia, ratkaisemalla jokainen näistä alioikeuksista vain kerran ja tallentamalla niiden ratkaisut. Seuraavan kerran, kun sama osa-ongelma ilmenee, sen ratkaisun laskemisen sijasta etsit yksinkertaisesti aiemmin laskettua ratkaisua. Tämä säästää laskenta-aikaa (toivottavasti) vaatimatonta tallennustilan kulutusta.

Ensimmäinen ja usein vaikein vaihe sen ratkaisemisessa on tunnistaa, että ongelma voidaan ratkaista DP: n avulla. Kysy itseltäsi sitä, voidaanko ongelmaratkaisusi ilmaista vastaavien pienempien ongelmien ratkaisujen funktiona.

Esimerkki-ongelmamme tapauksessa, kun annettiin piste kiitotielle, nopeus ja edessä oleva kiitotie, voimme määrittää paikat, joissa voimme hypätä seuraavaksi. Lisäksi näyttää siltä, ​​että pystymmekö pysähtymään nykyisestä pisteestä nykyisellä nopeudella, riippuu vain siitä, voisimmeko pysähtyä valitsemastamme kohdasta seuraavaan.

Se on hieno asia, koska eteenpäin siirtymällä lyhentämme edessä olevaa kiitotietä ja pienennämme ongelmaamme. Meidän pitäisi pystyä toistamaan tämä prosessi kokonaan, kunnes pääsemme pisteeseen, jossa on selvää, voimmeko pysähtyä.

Dynaamisen ohjelmoinnin ongelman tunnistaminen on usein vaikein vaihe sen ratkaisemisessa. Voidaanko ongelmaratkaisu ilmaista vastaavien pienempien ongelmien ratkaisujen funktiona?

Vaihe 2: Tunnista ongelmamuuttujat

Nyt olemme todenneet, että alioikeidemme välillä on jotain rekursiivista rakennetta. Seuraavaksi meidän on ilmaistava ongelma funktioparametrien avulla ja tarkastettava, mitkä näistä parametreista muuttuvat.

Tyypillisesti haastatteluissa sinulla on yksi tai kaksi muuttuvaa parametria, mutta teknisesti tämä voi olla mikä tahansa luku. Klassinen esimerkki yhden muuttuvan parametriongelmasta on ”määritä n-s Fibonacci-luku”. Tällainen esimerkki kahden muuttuvan parametrin ongelmasta on ”Laske muokkausetäisyys merkkijonojen välillä”. Jos et tunne näitä ongelmia, älä ole huolissasi siitä.

Tapa määrittää muuttuvien parametrien lukumäärä on listata esimerkkejä useista alioikeuksista ja vertailla parametreja. Muuttuvien parametrien lukumäärän laskeminen on arvokasta ratkaistavien alioikeuksien lukumäärän määrittämiseksi. Se on myös itsessään tärkeää, koska se auttaa meitä vahvistamaan toistumissuhteen ymmärtämistä vaiheesta 1 lähtien.

Esimerkissämme kaksi parametria, jotka voivat muuttua jokaisessa alaongelmassa, ovat:

  1. Matriisin sijainti (P)
  2. Nopeus (S)

Voitaisiin sanoa, että myös edessä oleva kiitotie muuttuu, mutta se olisi turhaa ottaen huomioon, että koko muuttumaton kiitotie ja sijainti (P) kuljettavat kyseisen tiedon jo.

Nyt näiden 2 muuttuvan parametrin ja muiden staattisten parametrien avulla meillä on täydellinen kuvaus alaongelmista.

Tunnista muuttuvat parametrit ja määritä alioikeuksien lukumäärä.

Vaihe 3: Ilmaise toistosuhde selvästi

Tämä on tärkeä askel, jonka monet suorittavat läpi päästäkseen koodaamaan. Toistamissuhteen ilmaiseminen mahdollisimman selkeästi vahvistaa ongelmasi ymmärtämistä ja tekee kaiken muun huomattavasti helpommaksi.

Kun huomaat, että toistosuhde on olemassa, ja määrittelet ongelmat parametrien suhteen, tämän pitäisi tulla luonnollisena vaiheena. Kuinka ongelmat liittyvät toisiinsa? Oletetaan siis, että olet laskenut aliohjelmat. Kuinka laskutat pääongelman?

Näin ajattelemme sitä näyteongelmassamme:

Koska voit säätää nopeutta yhdellä, ennen kuin hyppäät seuraavaan sijaintiin, on vain 3 mahdollista nopeutta, ja siksi 3 pistettä, joissa voisimme olla seuraavina.

Muodollisemmin, jos nopeus on S, sijainti P, voisimme siirtyä (S, P):

  1. (S, P + S); # jos emme muuta nopeutta
  2. (S - 1, P + S - 1); # jos muutamme nopeutta -1: llä
  3. (S + 1, P + S + 1); # jos muutamme nopeutta +1

Jos löydämme tavan pysähtyä missä tahansa yllä olevista alioikeuksista, voimme pysähtyä myös kohdasta (S, P). Tämä johtuu siitä, että voimme siirtyä (S, P) mihin tahansa edellä mainituista kolmesta vaihtoehdosta.

Tämä on tyypillisesti hieno tietämys ongelmasta (selkeä englanninkielinen selitys), mutta joskus haluat ehkä ilmaista suhteen myös matemaattisesti. Soitetaan toiminnolle, jota yritämme laskea canStop. Sitten:

canStop (S, P) = canStop (S, P + S) || canStop (S - 1, P + S - 1) || canStop (S + 1, P + S + 1)

Woohoo, näyttää siltä, ​​että meillä olisi toistosuhteemme!

Toistuvuussuhde: Mikäli olet laskenut aliohjelmat, miten laskutat pääongelman?

Vaihe 4: Tunnista perustiedot

Perustapaus on alaongelma, joka ei riipu muusta alaongelmasta. Tällaisten alioikeuksien löytämiseksi haluat yleensä kokeilla muutamia esimerkkejä, nähdä kuinka ongelmasi yksinkertaistuu pienemmiksi alioikeiksi ja tunnistaa missä vaiheessa sitä ei voida yksinkertaistaa edelleen.

Syy, että ongelmaa ei voida edelleen yksinkertaistaa, on se, että yhdestä parametrista tulee arvo, joka ei ole mahdollista ongelman rajoitteiden vuoksi.

Esimerkki-ongelmassamme on kaksi muuttuvaa parametria, S ja P. Ajattellaanpa mitä S: n ja P: n mahdolliset arvot eivät ehkä ole laillisia:

  1. P: n tulisi olla annetun kiitotien rajoissa
  2. P ei voi olla sellainen, että kiitotie [P] on väärä, koska se tarkoittaisi, että seisomme piikillä
  3. S ei voi olla negatiivinen, ja S == 0 tarkoittaa, että olemme valmiita

Joskus voi olla vähän haastavaa muuntaa parametreista tehdyt väitteet ohjelmoitaviksi perustapauksiksi. Tämä johtuu siitä, että väitteiden luettelon lisäksi, jos haluat tehdä koodistasi ytimekäs etkä tarkistaa tarpeettomia ehtoja, sinun on myös mietittävä, mitkä näistä ehdoista ovat jopa mahdollisia.

Esimerkissämme:

  1. P <0 || P> = kiitotien pituus näyttää oikealta tehdä. Vaihtoehtona voisi olla harkita kiitotien P == tekemistä perustana. On kuitenkin mahdollista, että ongelma hajoaa osaongelmaan, joka ylittää kiitotien lopun, joten meidän on todella tarkistettava epätasa-arvoisuus.
  2. Tämä vaikuttaa melko itsestään selvältä. Voimme yksinkertaisesti tarkistaa, onko kiitotie [P] väärä.
  3. Samoin kuin numero 1, voisimme yksinkertaisesti tarkistaa, onko S <0 ja S == 0. Kuitenkin, tässä voidaan syyttää, että S: n on mahdotonta olla <0, koska S laskee korkeintaan yhdellä, joten sen pitäisi käydä läpi S == 0 tapaus etukäteen. Siksi S == 0 on riittävä perustapaus S-parametrille.

Vaihe 5: Päätä, haluatko toteuttaa sen toistuvasti tai rekursiivisesti

Tapa, jolla puhuimme vaiheista toistaiseksi, saattaa johtaa siihen, että ajattelet, että meidän pitäisi panna ongelma täytäntöön rekursiivisesti. Kaikki, mitä olemme puhuneet toistaiseksi, on kuitenkin täysin agnostinen siitä, päätätkö toteuttaa ongelman toistuvasti vai toistuvasti. Molemmissa lähestymistavoissa joudut määrittämään toistosuhteen ja perustapaukset.

Jotta päättäisit jatkaa toistavasti vai rekursiivisesti, sinun kannattaa miettiä kompromisseja huolellisesti.

Pinojen ylivuotoongelmat ovat tyypillisesti kaupan katkaisija ja syy siihen, miksi et halua rekursiota (taustaohjelmassa) tuotantojärjestelmässä. Haastattelua varten, kunhan kuitenkin mainitaan kompromissit, sinun tulee tyypillisesti olla hieno jommallakummalla toteutuksista. Sinun pitäisi tuntea olosi mukavaksi toteuttamalla molemmat.

Erityisessä ongelmassamme toteutin molemmat versiot. Tässä on python-koodi siihen:
 Rekursiivinen ratkaisu: (alkuperäiset koodinpätkät löytyvät täältä)

Toistuva ratkaisu: (alkuperäiset koodinpätkät löytyvät täältä)

Vaihe 6: Lisää muistio

Muistaminen on tekniikka, joka liittyy läheisesti DP: hen. Sitä käytetään kalliiden toimintopuhelujen tulosten tallentamiseen ja välimuistilistan palauttamiseen, kun samat tulot tapahtuvat uudelleen.

Miksi lisäämme muistion rekurssiamme? Kohtaamme samat aliohjelmat, jotka lasketaan toistamatta ilman muistamista. Nämä toistot johtavat usein eksponentiaalisiin aikakomplekseihin.

Rekursiivisissa ratkaisuissa muistion lisäämisen tulisi tuntua suoraviivaiselta. Katsotaanpa miksi. Muista, että muistaminen on vain välimuisti toiminnon tuloksista. Toisinaan haluat poiketa tästä määritelmästä pienempien optimointien purkamiseksi, mutta muistion käsitteleminen funktion tulosvälimuistina on intuitiivisin tapa toteuttaa se.

Tämä tarkoittaa, että sinun pitäisi:

  1. Tallenna toimintotulos muistiin ennen jokaista paluulauseketta
  2. Etsi muistista funktion tulos ennen kuin aloitat muun laskennan

Tässä on yllä oleva koodi, johon on lisätty muisti (lisätyt rivit on korostettu): (alkuperäiset koodinpätkät löytyvät täältä)

Tehdään joitain nopeita testejä muistamisen ja erilaisten lähestymistapojen tehokkuuden havainnollistamiseksi. Aion stressitestistää kaikkia kolme menetelmää, joita olemme tähän mennessä nähneet. Tässä on asennus:

  1. Luin kiitotien, jonka pituus on 1000, piikkien avulla satunnaisiin paikkoihin (päätin, että piikin todennäköisyys missä tahansa pisteessä on 20%)
  2. initSpeed ​​= 30
  3. Suoritin kaikkia toimintoja 10 kertaa ja mittasin suorituksen keskimääräisen ajan

Tässä ovat tulokset (sekunneissa):

Voit nähdä, että puhdas rekursiivinen lähestymistapa vie noin 500x enemmän aikaa kuin iteratiivinen lähestymistapa ja noin 1300x enemmän aikaa kuin rekursiivinen lähestymistapa muistion kanssa. Huomaa, että tämä ero kasvaa nopeasti kiitotien pituuden myötä. Kannustan sinua kokeilemaan sitä itse.

Vaihe 7: Määritä ajan monimutkaisuus

On olemassa joitain yksinkertaisia ​​sääntöjä, jotka voivat helpottaa dynaamisen ohjelmointi-ongelman laskennan ajan monimutkaisuutta. Tässä on kaksi vaihetta, jotka sinun on tehtävä:

  1. Laske tilojen lukumäärä - tämä riippuu ongelmasi muuttuvien parametrien lukumäärästä
  2. Ajattele kunkin valtion kohden tehtyä työtä. Toisin sanoen, jos kaikki muu kuin yksi tila on laskettu, kuinka paljon työtä sinun on tehtävä tämän viimeisen tilan laskemiseksi?

Esimerkki-ongelmassamme tilojen lukumäärä on | P | * | S |, missä

  • P on kaikkien paikkojen joukko (| P | ilmaisee elementtien lukumäärän P: ssä)
  • S on kaikkien nopeuksien sarja

Kullakin tilalla tehty työ on tässä ongelmassa O (1), koska ottaen huomioon kaikki muut tilat, meidän on yksinkertaisesti tarkasteltava 3 aliohjelmaa tuloksena olevan tilan määrittämiseksi.

Kuten aiemmassa koodissa huomautimme, | S | on rajoitettu kiitotien pituudella (| P |), joten voisimme sanoa, että tilojen lukumäärä on | P | ² ja koska kunkin tilan kohdalla tehty työ on O (1), niin kokonaiskestoaika on O (| P) | ²).

Näyttää kuitenkin siltä, ​​että | S | voidaan rajoittaa edelleen, koska jos se todella olisi | P |, on erittäin selvää, että pysähtyminen ei olisi mahdollista, koska joudut hyppäämään koko kiitotien pituuden ensimmäisellä liikkeellä.

Katsotaanpa, kuinka voimme laittaa tiukemman sidoksen | S | Soitetaan maksiminopeudeksi S. Oletetaan, että olemme alkamassa paikasta 0. Kuinka nopeasti voisimme pysähtyä, jos yrittäisimme pysähtyä mahdollisimman pian ja jättäisimme huomioimatta mahdolliset piikit?

Ensimmäisessä iteraatiossa meidän olisi tultava ainakin pisteeseen (S-1) säätämällä nopeutemme nollaan -1. Sieltä siirrymme vähintään (S-2) askeleen eteenpäin ja niin edelleen.

Kiitotielle, jonka pituus on L, seuraavan on oltava voimassa:

=> (S-1) + (S-2) + (S-3) +…. + 1

=> S * (S-1) / 2

=> S² - S - 2L <0

Jos löydät yllä olevan funktion juuret, ne ovat:

r1 = 1/2 + sqrt (1/4 + 2L) ja r2 = 1/2 - sqrt (1/4 + 2L)

Voimme kirjoittaa eriarvoisuutemme seuraavasti:

(S - r1) * (S - r2) <0

Ottaen huomioon, että S - r2> 0 jokaiselle S> 0 ja L> 0, tarvitsemme seuraavan:

S - 1/2 - sqrt (1/4 + 2L) <0

=> S <1/2 + sqrt (1/4 + 2L)

Se on suurin sallittu nopeus, joka meillä mahdollisesti voi olla pituudella L. Jos meillä olisi suurempi nopeus, emme pystyneet pysähtymään edes teoreettisesti piikkien sijainnista riippumatta.

Tämä tarkoittaa, että kokonaiskestoajan monimutkaisuus riippuu vain kiitotien L pituudesta seuraavassa muodossa:

O (L * sqrt (L)), joka on parempi kuin O (L²)

O (L * sqrt (L)) on ajan monimutkaisuuden yläraja

Mahtavaa, teit sen läpi! :)

Seitsemän vaihetta, jotka läpikäyimme, pitäisi antaa sinulle puitteet systemaattisesti ratkaista kaikki dynaamiset ohjelmointiongelmat. Suosittelen, että käytät tätä lähestymistapaa vielä muutamissa muissa ongelmissa lähestymistavan parantamiseksi.

Tässä on joitain seuraavia vaiheita, jotka voit tehdä

  1. Laajenna näyteongelmaa yrittämällä löytää polku pysähdyskohtaan. Ratkaisimme ongelman, joka kertoo, voitko pysähtyä, mutta entä jos haluat tietää myös tarvittavat toimenpiteet pysähtyäksesi lopulta kiitotielle? Kuinka muokkaat olemassa olevaa toteutusta tehdäksesi niin?
  2. Jos haluat vahvistaa ymmärrystäsi muistamisesta ja ymmärtää, että se on vain funktiotulosvälimuisti, sinun pitäisi lukea Python-koristeista tai vastaavista käsitteistä muilla kielillä. Ajattele, kuinka niiden avulla voit toteuttaa muistion yleensä jokaiselle toiminnolle, jonka haluat tallentaa.
  3. Työskentele lisää DP-ongelmien kanssa seuraamalla läpikäymiemme vaiheita. Voit aina löytää joukon heitä verkossa (esim. LeetCode tai GeeksForGeeks). Harjoittaessasi muista yksi asia: oppia ideoita, älä oppi ongelmia. Ideoiden määrä on huomattavasti pienempi ja se on helpompi valloittaa, mikä palvelee myös sinua paljon paremmin.

Kun sinusta tuntuu kuin olet valloittanut nämä ideat, tutustu Refdashiin, jossa vanhempi insinööri haastattelee, ja saat yksityiskohtaisen palautteen koodauksesta, algoritmeista ja järjestelmän suunnittelusta.

Alun perin julkaistu Refdash-blogissa. Refdash on haastattelualusta, joka auttaa insinöörejä haastattelemaan nimettömästi kokeneiden insinöörien kanssa huippua tarjoavista yrityksistä, kuten Google, Facebook tai Palantir, ja saada yksityiskohtaista palautetta. Refdash auttaa myös insinöörejä löytämään uskomattomia työmahdollisuuksia taitojensa ja kiinnostuksensa perusteella.