p = ^(11+)\1+$

Tämän kirjoituksen aihe vaatii ehkä enemmän taustatietoja ja on sisällöltään teknisempi. Alkuperäinen ajatus ei myöskään ole minun, vaan on pyörinyt internetissä jo kymmeniä vuosia.

Alkuluvut ja säännölliset lausekkeet

Otsikko näyttää lähinnä siltä, että kissa olisi pomppinut näppäimistöllä. Ymmärtääkseen pitäisi tietää mitä ovat säännölliset lausekkeet ja alkuluvut. Aloitetaan helpommasta.

Kuvan kissa tai näppäimistö eivät liity tapaukseen

Alkuluku on sellainen luku, jota ei voi jakaa millään muulla luvulla, kuin ykkösellä ja itsellään. Esimerkiksi luku 5. Jos sen koittaa jakaa luvulla 2, 3 tai 4, jako ei mene tasan. Tällöin ainoat jakajat ovat 1 ja 5 ja tämä tekee luvusta 5 alkuluvun. Myös luku 23022023 on alkuluku: sitä ei voi jakaa millään muulla, kuin ykkösellä ja itsellään. Alkuluvut ovat loputtoman mielenkiintoisia jo itsessään, mutta ei käsitellä niitä nyt sen tarkemmin. Alkuluja täytyy etsiä, sillä moderni tiedon salaaminen perustuu niihin: ilman alkulukuja ei pääse verkkopankkiin, eikä sähköpostiin. Tämäkin olisi hyvin kiinnostava aihe, mutta tämän kirjoituksen varsinainen aihe on alkulukujen etsintä vähän epätavallisemmalla menetelmällä.

Miten ihmeessä sitten otsikon mysteerisotku ^(11+)\1+$ löytää alkulukuja?

Unaariset numerot

Tavallisia lukuja kirjoitamme kymmenellä merkillä, eli kaikille tutuilla numeroilla 0-9. Kun numerot loppuvat kesken, uusia lukuja merkataan yhdistelemällä näitä kymmentä symbolia. Numeron 9 jälkeen ei siis tule mikään uusi merkki, vaan yhdistellään vanhoja, joten seuraava luku on 10. Aika selkeää.

Binääriluvuissa käytössä on vain kaksi merkkiä, 0 ja 1. Tämä on tietokoneelle sopiva muoto, koska nämä kaksi lukua on helppo toteuttaa sähköisesti: virta pois = 0, virta päällä = 1. Jos mennään vielä pidemmälle, niin unaarisessa järjestelmässä käytössä olisi vain yksi merkki, eli 1. Tällöin luvut esitettäisiin siis vain ykkösten jonoina, niin monta ykköstä kuin luvun arvo on. Järjestyksessä ensimmäiset luvut olisivat siis 1, 11, 111, 1111. Vähän kuin roomalaiset numerot, mutta tässä muita merkkejä ei siis tulisikaan. Tässä vielä lisää lukuja unaarijärjestelmällä, yksi kullakin rivillä:

1
11
111
1111
11111
111111
1111111
11111111
111111111
1111111111
11111111111
111111111111
1111111111111
11111111111111

Ennen kuin ihmetellään säännöllisiä lausekkeita, tutkitaan esimerkiksi lukua 12. Unaarijärjestelmässä se kirjoitettaisiin muodossa 111111111111. Kuin lapsi lukuja opetellessaan, voisimme mekin ottaa esiin vaikka tulitikkuja, palikoita tai tässä sähköisesti ihan vain peräkkäisiä ykkösiä ja järjestellä niitä eri kokoisiin ryhmiin. Luvun 12 voisi jakaa monellakin eri tavalla: 3x4 tai 4x3 tai 2x6 tai 6x2:

3 x 4

1111 1111 1111

4 x 3

111 111 111 111

2 x 6

111111 111111

6 x 2

11 11 11 11 11 11

Luku 17 on puolestaan alkuluku. Sitä ei voi jakaa muulla kuin itsellään ja ykkösellä. Ei siis ole mahdollista muodostaa keskenään yhtä suuria ryhmiä, paitsi tietenkin nämä:

1x17

11111111111111111

17x1

1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

Säännöllinen lauseke

Säännöllinen lauseke on työkalu tekstin täsmäykseen: onko teksti tietyssä muodossa? Kyseessä on esimerkiksi lähes kaikkiin ohjelmointikieliin toteutettu ominaisuus. Aihe on varsin laaja, mutta katsotaan pari esimerkkiä.

Paljon muitakin ominaisuuksia löytyy. Aidosti hyödyllinen esimerkki voisi olla seuraava: .+@.+\..+. Tämä tai vastaava löytynee kaikilta sivuilta, joihin pitää syöttää kelvollinen sähköpostiosoite. Katsotaan pala kerrallaan:

Esimerkiksi siis erkki@esimerkki.fi voidaan tunnistaa tämän lausekkeen avulla: jotain, @-merkki, jotain, piste, jotain. Toisaalta tämä tunnistaa myös tekstin a£%aa@asd.7 joka ei kyllä ole kelvollinen osoite. Tämän takia sivustot lähettävät sähköpostia: ne haluavat varmistaa, että sähköpostiosoite on oikea ja että sinulla on pääsy siihen. Säännöllisellä lausekkeella voisi toki myös vaatia käyttämään pelkkiä kirjaimia, esimerkiksi [a-z]@[a-z]\.[a-z], vaan entäpä sitten numerot tai muut osoitteessa kelvolliset merkit? Ei jumiuduta liiaksi tähän, vaan siirrytään aiheeseen.

^(11+)\1+$

Miten tämä sitten tunnistaa alkuluvun? No ei se tunnistakaan, mutta se tunnistaa ei-alkuluvut, eli komposiittiluvut (yhdistetyt luvut). Koska käsittelemme alkulukuja unaarijärjestelmässä, voimme ajatella niitä vaan tekstinpätkinä, joiden tunnistamiseen säännöllinen lauseke sopii. Varsinaisia laskuoperaatioita ei tarvita.

Sekaannusten välttämiseksi muutetaan alkuperäistä lauseketta hiukan: ^(AA+)\1+$. Esitetään luvut vastaavasti myös A-kirjaimina:

A
AA
AAA
AAAA
AAAAA
AAAAAA
.........

Aiemmassa kappaleessa todettiin, että luvut, jotka eivät ole alkulukuja, voidaan jakaa yhtä suurin ryhmiin. Tässä piilee säännöllisen lausekkeen ajatus alkulukujen tunnistamiseen. Käsitellään lauseke vaiheittain.

A

Aloitetaan kirjaimellisesta A-merkistä.

AA+

Lisätään toinen A ja sille + -merkki, eli tämä jälkimmäinen A voi olla vähintään kerran, mutta enintään loputtomasti.

Tässä kohtaa lausekkeemme tunnistaa esimerkiksi AA, AAA, AAAA mutta ei A.

Määritellään tämä pätkä ryhmäksi, lisäämällä sulkeet:

(AA+)

Ryhmän tarkoitus on esimerkiksi poimia täsmätystä tekstistä vain tietty pätkä, mutta siihen voi myös viitata lausekkeen myöhemmässä vaiheessa. Koska kyseessä on vasemmalta laskettuna ensimmäinen määritelty ryhmä, siihen voidaan viitata merkitsemällä \1:

(AA+)\1.

Nyt tekstissä pitää siis olla vähintään kaksi A:ta ja sen jälkeen täsmälleen sama ryhmä uudelleen. Esimerkiksi jonossa AAAAAA tämä toteutuu:

AAAAAA

Mutta jonossa AAAAA ei:

AAAAA

Vaan entä AAAAAAAAA? Lisätään vielä yksi asia:

(AA+)\1+

Nyt löydetty ryhmä saa esiintyä tarvittaessa monta kertaa, mutta vähintään kerran.

AAAAAAAAA

Nyt lauseke tunnistaa siis A-jonot, jotka voidaan jakaa kahteen tai useampaan yhtä pitkään osaan, siten, että jokaisen osion pituus on vähintään 2 merkkiä pitkä. Sen kummempaa ei tarvita: alkuluja ei voida jakaa, muut luvut voidaan. Jos säännöllinen lauseke täsmää tiettyyn jonoon, sen jonon kuvaama luku ei ole alkuluku.

Jos unaarilukuja haluaa merkata ykkösellä, voi A:n tilanne vaihtaa 1, kuten otsikossa ja aiemmissa esimerkeissä. Jätin sen pois siksi, että myöhemmin tuleva \1 voi aiheuttaa sekaannusta, kun nyt kyse ei olekaan ykkösestä, vaan ryhmäviittauksesta numero 1.

Pari juttua vielä

Parempi muoto lausekkeelle olisi otsikossakin oleva ^(11+)\1+$ tai vielä mieluummin ^1$|^(11+)\1+$

^ viittaa rivin alkuun ja $ viittaa rivin loppuun. Tämä voi olla oleellista jossakin tilanteissa. Säännöllistä lauseketta suorittava ohjelma voi etsiä vastaavuuksia koko syötteestä, ja löytää esimerkiksi luvusta 1111111 vastaavuuden: 1111111. Merkeillä ^$ varmistetaan siis, että etsitään nimenomaan rivin alusta ja loppuun, jolloin koko “sanan” täytyy täsmätä.

Jälkimmäisessä sitten taas huomioidaan poikkeustapaus. Lauseke löytää ne luvut, jotka eivät ole alkulukuja, eikä luku 1 ole alkuluku. Tässä | merkki tarkoittaa “tai”. Etsitään siis ykköstä — joka on yksinään koko rivillä, eli ^1$ — tai sitten toistuvia sarjoja.

Säännöllisiä lausekkeita voi testata esimerkiksi osoitteessa regex101.com, josta löytyy kätevä testausnäkymä selityksineen.

regex101.com on hyvä työkalu säännöllisten lausekkeiden testailuun ja harjoitteluun.

Kaikessa huvittavuudessaan tämä on todella epätehokas tapa etsiä alkulukuja, mutta kiinnostava esimerkki siitä, miten on mahdollista “laskea” järjestelmällä, jota ei varsinaisesti ole suunniteltu laskemiseen.

FTL-Peli: loputtomat rahat ilman rahakoodia →