Kirjautuminen

Haku

Tehtävät

Keskustelu: Koodit: QB: Advent of Code 2024 (QBasic)

Antti Laaksonen [01.12.2024 13:32:43]

#

Koetan ratkoa tänä vuonna Advent of Coden tehtävät QBasicilla ja raportoin tässä keskustelussa kokeilun tulokset. En ole aiemmin osallistunut Advent of Codeen, eikä minulla ole tarkkaa käsitystä tehtävien tyylistä.

QBasicin ainoa valmiina oleva tietorakenne on kiinteän kokoinen taulukko. Myös esimerkiksi taulukon järjestäminen täytyy toteuttaa itse. Koetan löytää yksinkertaisia tapoja tehtävien ratkaisemiseen ja välttää muita tietorakenteita kuin taulukkoa.

Toinen haaste on, että QBasicin suurin kokonaislukutyyppi (LONG) on 32-bittinen. Tästä voi tulla ongelmia, jos tehtävissä tulee käsitellä suurempia lukuja.

Nimestään ("quick basic") huolimatta QBasic ei ole kovin nopea kieli. Tämän takia toivon, että tehtävissä ei tarvitse käsitellä suurta määrtä dataa tai tehdä raskasta laskentaa, jotta ratkaisuni toimivat riittävän nopeasti.

Käytössäni on QBasicin versio 1.1 vuodelta 1992. Käytän QBasicia DOSBoxin kautta.

Päivä 1

Tehtävän ensimmäisestä osasta tulee mieleen tehtävä Differences HIIT Open 2019:sta. Tässä tehtävässä ei kuitenkaan tarvitse valita itse listojen järjestystä, vaan riittää käsitellä alkiot pienimmästä suurimpaan.

Tehtävän ratkaiseminen alkoi hieman hitaasti, koska tiedoston lukemisessa oli ongelmia. Syyksi osoittautui, että Advent of Coden tiedostossa on unix-rivinvaihdot, kun taas QBasic pystyy käsittelemään vain dos-rivinvaihtoja. Muutin rivinvaihdot ja tiedoston lukeminen alkoi toimia.

Ratkaisin ensimmäisen osan seuraavalla koodilla:

N% = 1000
DIM A&(N%)
DIM B&(N%)

OPEN "DAY1.TXT" FOR INPUT AS #1
FOR I% = 1 TO N%
    INPUT #1, A&(I%), B&(I%)
NEXT
CLOSE #1

FOR I% = 1 TO N%
    FOR J% = 1 TO N% - 1
        IF A&(J%) > A&(J% + 1) THEN SWAP A&(J%), A&(J% + 1)
        IF B&(J%) > B&(J% + 1) THEN SWAP B&(J%), B&(J% + 1)
    NEXT
NEXT

TOTAL& = 0
FOR I% = 1 TO N%
    TOTAL& = TOTAL& + ABS(A&(I%) - B&(I%))
NEXT
PRINT TOTAL&

Koodi lukee ensin listoilla olevat luvut taulukoihin A& ja B&. Listoissa on vain 1000 lukua, mikä on mukavan pieni määrä.

Muuttujissa merkki % tarkoittaa INTEGER-tyyppiä (16-bittinen) ja & tarkoittaa LONG-tyyppiä (32-bittinen). Tehtävän luvut ovat sen verran suuria, että niiden käsittelyyn tarvitaan LONG-tyyppiä.

Järjestän taulukot kuplajärjestämisen avulla, mikä on helppo toteuttaa. Tässä on kätevä QBasicissa oleva komento SWAP, joka vaihtaa kahden muuttujan sisällön. Tämän jälkeen riittää käydä läpi taulukot ja laskea yhteen etäisyydet.

Tehtävän toinen osa on mukavampi ratkaista, koska siinä ei tarvitse järjestämistä. Ratkaisin toisen osan näin:

N% = 1000
DIM A&(N%)
DIM B&(N%)

OPEN "DAY1.TXT" FOR INPUT AS #1
FOR I% = 1 TO N%
    INPUT #1, A&(I%), B&(I%)
NEXT
CLOSE #1

TOTAL& = 0
FOR I% = 1 TO N%
    COUNT% = 0
    FOR J% = 1 TO N%
        IF A&(I%) = B&(J%) THEN COUNT% = COUNT% + 1
    NEXT
    TOTAL& = TOTAL& + A&(I%) * COUNT%
NEXT
PRINT TOTAL&

Tässä ensimmäinen silmukka käy läpi vasemman listan luvut ja toinen silmukka käy läpi jokaisen luvun kohdalla oikean listan luvut. Näin saadaan laskettua, montako kertaa vasemman listan luku esiintyy oikealla listalla.

Vaikka tässä tehtävässä on melko pieni syöte, kummankin koodin suoritus vei aikaa useita minuutteja. Tämä on hieman huolestuttavaa tulevien tehtävien kannalta. Täytyy tosiaan toivoa, että syötteet pysyvät riittävän pieninä.

Antti Laaksonen [02.12.2024 14:51:59]

#

Päivä 2

Tehtävän ratkaisemista vaikeutti, että tiedoston riveillä on vaihteleva määrä lukuja. Muuten tehtävän pystyi ratkaisemaan melko suoraviivaisesti QBasicilla.

Ratkaisin ensimmäisen osan näin:

TOTAL% = 0

OPEN "DAY2.TXT" FOR INPUT AS #1
FOR I% = 1 TO 1000
    LINE INPUT #1, LINE$
    LINE$ = LINE$ + " "

    POS1% = 1
    PREV% = 0
    FAIL1% = 0: FAIL2% = 0

    DO
        POS2% = INSTR(POS1%, LINE$, " ")
        IF POS2% = 0 THEN EXIT DO

        CUR% = VAL(MID$(LINE$, POS1%, POS2% - POS1%))
        IF PREV% <> 0 THEN
            DIFF% = CUR% - PREV%
            IF DIFF% < 1 OR DIFF% > 3 THEN FAIL1% = 1
            IF -DIFF% < 1 OR -DIFF% > 3 THEN FAIL2% = 1
        END IF

        PREV% = CUR%
        POS1% = POS2% + 1
    LOOP

    IF FAIL1% = 0 OR FAIL2% = 0 THEN TOTAL% = TOTAL% + 1
NEXT
CLOSE #1

PRINT TOTAL%

Koodi lukee rivin muuttujaan LINE$ ja etsii sitten rivillä olevat luvut funktion INSTR avulla. Ideana on etsiä riviltä kohdat, joissa on välilyönti. Funktion syntaksi on kiinnostava, koska siinä on valinnainen parametri ensimmäisenä parametrina. INSTR(A$, B$) etsii ensimmäisen kohdan, jossa B$ esiintyy A$:ssa. INSTR(P%, A$, B$) etsii puolestaan ensimmäisen kohdan, joka on aikaisintaan kohdassa P%.

Havaitsin koodatessa, että seuraava koodi ei toimi odotetulla tavalla:

FAIL1% = FAIL2% = 0

Tavoitteena oli asettaa kummankin muuttujan arvoksi 0, mutta koodi ei tee tätä. QBasicissa merkki = on sekä sijoitus että vertailu, ja koodi sijoittaa muuttujaan FAIL1% vertailun FAIL2% = 0 totuusarvon (tosi on -1 ja epätosi on 0).

Tehtävän toinen osa ratkesi laajentamalla ensimmäisen osan koodia:

TOTAL% = 0
DIM LEVEL%(10)

OPEN "DAY2.TXT" FOR INPUT AS #1
FOR I% = 1 TO 1000
    LINE INPUT #1, LINE$
    LINE$ = LINE$ + " "

    POS1% = 1
    COUNT% = 0

    DO
        POS2% = INSTR(POS1%, LINE$, " ")
        IF POS2% = 0 THEN EXIT DO

        COUNT% = COUNT% + 1
        LEVEL%(COUNT%) = VAL(MID$(LINE$, POS1%, POS2% - POS1%))
        POS1% = POS2% + 1
    LOOP

    FOR SKIP% = 0 TO COUNT%
        PREV% = 0
        FAIL1% = 0: FAIL2% = 0

        FOR J% = 1 TO COUNT%
            IF J% <> SKIP% THEN
                CUR% = LEVEL%(J%)
                IF PREV% <> 0 THEN
                    DIFF% = CUR% - PREV%
                    IF DIFF% < 1 OR DIFF% > 3 THEN FAIL1% = 1
                    IF -DIFF% < 1 OR -DIFF% > 3 THEN FAIL2% = 1
                END IF
                PREV% = CUR%
            END IF
        NEXT

        IF FAIL1% = 0 OR FAIL2% = 0 THEN
            TOTAL% = TOTAL% + 1
            EXIT FOR
        END IF
    NEXT
NEXT
CLOSE #1

PRINT TOTAL%

Päädyin tässä lukemaan aluksi rivin luvut taulukkoon, jotta olisi helpompaa käydä läpi eri tavat valita poistettava luku. Tämän olisi voinut toteuttaa myös ilman taulukkoa, mutta toteutus vaikutti melko monimutkaiselta eikä sille ollut tarvetta tässä tapauksessa.

Tässä koodissa ongelmana oli aluksi, että olin käyttänyt samaa muuttujaa I% sisäkkäisissä silmukoissa. Tämän seurauksena sisempi silmukka sekoitti ulomman silmukan toiminnan ja koodi yritti lukea liikaa rivejä tiedostosta. Ongelma ratkesi, kun otin käyttöön toisen muuttujan J% sisemmässä silmukassa.

Antti Laaksonen [03.12.2024 11:11:00]

#

Päivä 3

Eilen käyttämäni funktio INSTR soveltuu hyvin myös tähän tehtävään. Ratkaisin ensimmäisen osan näin:

TOTAL& = 0
OPEN "DAY3.TXT" FOR INPUT AS #1
WHILE NOT EOF(1)
    LINE INPUT #1, LINE$
    START% = 1
    DO
        POS1% = INSTR(START%, LINE$, "mul(")
        IF POS1% = 0 THEN EXIT DO
        POS2% = INSTR(POS1%, LINE$, ",")
        POS3% = INSTR(POS2%, LINE$, ")")

        IF POS2% > 0 AND POS3% > 0 THEN
            NUM1$ = MID$(LINE$, POS1% + 4, POS2% - POS1% - 4)
            NUM2$ = MID$(LINE$, POS2% + 1, POS3% - POS2% - 1)
            IF VALID%(NUM1$) AND VALID%(NUM2$) THEN
                TOTAL& = TOTAL& + VAL(NUM1$) * VAL(NUM2$)
            END IF
        END IF

        START% = POS1% + 1
    LOOP
WEND
CLOSE #1
PRINT TOTAL&

FUNCTION VALID% (NUM$)
    N% = LEN(NUM$)
    VALID% = 1
    IF N% < 1 OR N% > 3 THEN
        VALID% = 0
    ELSE
        FOR I% = 1 TO N%
            DIGIT$ = MID$(NUM$, I%, 1)
            IF DIGIT$ < "0" OR DIGIT$ > "9" THEN VALID% = 0
        NEXT
    END IF
END FUNCTION

Etsin funktion INSTR avulla seuraavan kohdan, jossa on merkkijono mul(. Lisäksi etsin seuraavat kohdat, joissa on merkkijonot , ja ). Näiden kohtien avulla voidaan löytää seuraava mul-komento ja sen parametrit.

Tässä tehtävässä käytin ensimmäistä kertaa funktiota. Funktio VALID% tarkastaa, onko mul-komennon parametri kelvollinen luku (muodostuu numeroista ja pituus 1–3 numeroa). QBasicissa funktion palautusarvo määritellään antamalla arvo funktion nimiselle muuttujalle.

Tehtävän toinen osa on melko samanlainen mutta hieman vaikeampi. Ratkaisin toisen osan näin:

TOTAL& = 0
MODE% = 1
OPEN "DAY3.TXT" FOR INPUT AS #1
WHILE NOT EOF(1)
    LINE INPUT #1, LINE$
    START% = 1
    DO
        POS1% = INSTR(START%, LINE$, "mul(")
        IF POS1% = 0 THEN EXIT DO

        POSX% = INSTR(START%, LINE$, "do()")
        POSY% = INSTR(START%, LINE$, "don't()")
        FIRSTX% = 0: FIRSTY% = 0
        IF POSX% <> 0 AND POSX% < POS1% THEN FIRSTX% = 1
        IF POSY% <> 0 AND POSY% < POS1% THEN FIRSTY% = 1

        IF FIRSTX% = 1 OR FIRSTY% = 1 THEN
            IF FIRSTY% = 0 OR (FIRSTX% = 1 AND POSX% < POSY%) THEN
                MODE% = 1
                START% = POSX% + 1
            ELSE
                MODE% = 0
                START% = POSY% + 1
            END IF
        ELSE
            POS2% = INSTR(POS1%, LINE$, ",")
            POS3% = INSTR(POS2%, LINE$, ")")
            IF POS2% > 0 AND POS3% > 0 THEN
                NUM1$ = MID$(LINE$, POS1% + 4, POS2% - POS1% - 4)
                NUM2$ = MID$(LINE$, POS2% + 1, POS3% - POS2% - 1)
                IF MODE% = 1 AND VALID%(NUM1$) AND VALID%(NUM2$) THEN
                    TOTAL& = TOTAL& + VAL(NUM1$) * VAL(NUM2$)
                END IF
            END IF
            START% = POS1% + 1
        END IF
    LOOP
WEND
CLOSE #1
PRINT TOTAL&

FUNCTION VALID% (NUM$)
    N% = LEN(NUM$)
    VALID% = 1
    IF N% < 1 OR N% > 3 THEN
        VALID% = 0
    ELSE
        FOR I% = 1 TO N%
            DIGIT$ = MID$(NUM$, I%, 1)
            IF DIGIT$ < "0" OR DIGIT$ > "9" THEN VALID% = 0
        NEXT
    END IF
END FUNCTION

Muuttuja MODE% pitää yllä tietoa tilasta eli tuleeko mul-komennot käsitellä vai ei. Koodi antoi ensin väärän vastauksen, koska oletin, että tila nollautuisi joka rivin alussa.

Käytin jälleen funktiota INSTR, jonka avulla etsitään nyt myös tilaa muuttavia komentoja do() ja don't(). Jos jompikumpi näistä esiintyy ennen seuraavaa mul-komentoa, tilaa muuttava komento käsitellään ensin.

Antti Laaksonen [04.12.2024 10:15:11]

#

Päivä 4

Päivän tehtävässä tulee etsiä sanoja ruudukosta. Ratkaisin ensimmäisen osan seuraavasti:

N% = 140
WORD$ = "XMAS"
DIM GRID$(N%)

OPEN "DAY4.TXT" FOR INPUT AS #1
FOR I% = 1 TO N%
    LINE INPUT #1, GRID$(I%)
NEXT
CLOSE #1

DIM DY%(8), DX%(8)
DATA 0, 1, 0, -1, 1, 0, -1, 0, 1, 1, 1, -1, -1, 1, -1, -1
FOR I% = 1 TO 8
    READ DY%(I%), DX%(I%)
NEXT

TOTAL& = 0
FOR I% = 1 TO N%
    FOR J% = 1 TO N%
        FOR D% = 1 TO 8
            FAIL% = 0
            FOR K% = 1 TO 4
                Y% = I% + DY%(D%) * (K% - 1)
                X% = J% + DX%(D%) * (K% - 1)
                IF Y% < 1 OR Y% > N% OR X% < 1 OR X% > N% THEN
                    FAIL% = 1
                    EXIT FOR
                END IF
                IF MID$(GRID$(Y%), X%, 1) <> MID$(WORD$, K%, 1) THEN
                    FAIL% = 1
                    EXIT FOR
                END IF
            NEXT
            IF FAIL% = 0 THEN TOTAL& = TOTAL& + 1
        NEXT
    NEXT
NEXT
PRINT TOTAL&

Tässä ideana on käydä läpi kaikki ruudukon kohdat ja laskea joka kohdasta alkavat sanat eri suuntiin. Suuntia on 8, koska sana voi kulkea pysty-, vaaka- tai vinosuuntaan.

Taulukot DY% ja DX% sisältävät siirtymät eri suuntiin liikuttaessa. Käytin taulukoiden alustamiseen DATA-riviä. Komento READ lukee luvut DATA-riviltä samassa järjestyksessä kuin luvut on annettu. Esimerkiksi DY%(1) saa arvon 0 ja DX%(1) saa arvon 1.

Tehtävän toisen osan ratkaisin seuraavasti:

N% = 140
DIM GRID$(N%)

OPEN "DAY4.TXT" FOR INPUT AS #1
FOR I% = 1 TO N%
    LINE INPUT #1, GRID$(I%)
NEXT
CLOSE #1

DEF FNC$ (Y%, X%) = MID$(GRID$(Y%), X%, 1)

TOTAL& = 0
FOR I% = 1 TO N% - 2
    FOR J% = 1 TO N% - 2
        WORD1$ = FNC$(I%, J%) + FNC$(I% + 1, J% + 1) + FNC$(I% + 2, J% + 2)
        WORD2$ = FNC$(I%, J% + 2) + FNC$(I% + 1, J% + 1) + FNC$(I% + 2, J%)
        FAIL% = 0
        IF WORD1$ <> "MAS" AND WORD1$ <> "SAM" THEN FAIL% = 1
        IF WORD2$ <> "MAS" AND WORD2$ <> "SAM" THEN FAIL% = 1
        IF FAIL% = 0 THEN TOTAL& = TOTAL& + 1
    NEXT
NEXT
PRINT TOTAL&

Koska koodissa täytyy viitata useita kertoja ruudukon kohtiin, määrittelin tätä varten apufunktion FNC$ käyttäen DEF FN -syntaksia. Tämä kevyt tapa määritellä funktio muistuttaa lambda-syntaksia moderneissa ohjelmointikielissä. Erikoisuutena funktion nimen tulee alkaa merkeillä FN.

Vastaus

Muista lukea kirjoitusohjeet.
Tietoa sivustosta