Inhalt
Die Fakten:
Plattform: | codewars.com |
Name: | The observed PIN |
Level: | 4 kyu |
Sprache: | JavaScript |
Beschreibung:
Alright, detective, one of our colleagues successfully observed our target person, Robby the robber. We followed him to a secret warehouse, where we assume to find all the stolen stuff. The door to this warehouse is secured by an electronic combination lock. Unfortunately our spy isn't sure about the PIN he saw, when Robby entered it.
The keypad has the following layout:
┌───┬───┬───┐
│ 1 │ 2 │ 3 │
├───┼───┼───┤
│ 4 │ 5 │ 6 │
├───┼───┼───┤
│ 7 │ 8 │ 9 │
└───┼───┼───┘
│ 0 │
└───┘
He noted the PIN 1357
, but he also said, it is possible that each of the digits he saw could actually be another adjacent digit (horizontally or vertically, but not diagonally). E.g. instead of the 1
it could also be the 2
or 4
. And instead of the 5
it could also be the 2
, 4
, 6
or 8
.
He also mentioned, he knows this kind of locks. You can enter an unlimited amount of wrong PINs, they never finally lock the system or sound the alarm. That's why we can try out all possible (*) variations.
* possible in sense of: the observed PIN itself and all variations considering the adjacent digits
Can you help us to find all those variations? It would be nice to have a function, that returns an array (or a list in Java/Kotlin and C#) of all variations for an observed PIN with a length of 1 to 8 digits. We could name the function getPINs
(get_pins
in python, GetPINs
in C#). But please note that all PINs, the observed one and also the results, must be strings, because of potentially leading '0's. We already prepared some test cases for you.
Detective, we are counting on you!
***For C# user:*** Do not use Mono. Mono is too slower when run your code.
Quelle: codewars.com
Lösung
Pseudo-Code
Es gibt wie immer viele Varianten, hier ist eine meiner.
Erst die Lösungsschritte in Pseudo-Code. Los geht’s:
Lösungsschritte
Schritt 1
Als Erstes brauchen wir eine Hilfsfunktion um die Nachbar-Ziffern zu bekommen. Man könnte das auch hardcoden, aber ich entscheide mich für die gecodete Version.
Schritt 2
In der Hilfsfunktion definieren wir uns erst mal das Keypad mit den Zahlen. Z.B. als 2-dimensionales Array.
Schritt 3
Dann können wir durch das Keypad loopen. Wenn wir die Reihe mit der gesuchten Zahl gefunden haben geht’s weiter.
Schritt 4
Dann schnappen wir uns die beiden Zahlen links und rechts davon und dann die Zahlen drüber und drunter.
Schritt 5
Das kommt alles in ein Array, das wir dann zurückgeben.
Schritt 6
Zurück in unserer Hauptfunktion loopen wir als Erstes durch die beobachteten Input-Ziffern.
Schritt 7
Für jede Ziffer lassen wir unsere Hilfsfunktion laufen und speichern sämtliche Ziffern mit ihren Nachbarn in einem 2-dimensionalen Array.
Schritt 8
Dann loopen wir durch dieses 2-dimensionale Array.
Schritt 9
Im ersten Schritt kombinieren wir das erste Innere-Array mit dem zweiten inneren Array. So dass wir danach ein Array mit 2-stelligen Zahlen erhalten.
Schritt 10
Im nächsten Schritt kombinieren wir dieses Array mit dem nächsten inneren Array bis wir durch alle inneren Arrays durch sind. Es bietet sich wahrscheinlich an diese Kombinations-Arie in eine weitere Hilfsfunktion auszulagern.
Schritt 11
Nach dem Loop können wir dann einfach das Ergebnis-Array mit den Kombinationen zurückgeben.
Code
Geil. Übersetzen wir unseren Pseudo-Code in JavaScript:
Lösungsschritte
Die erste Zeile meiner getNeighbours
-Hilfsfunktion:
function getNeighbours(key) {
Also Erstes also unser Keypad definieren:
const keypad = [
["1", "2", "3"],
["4", "5", "6"],
["7", "8", "9"],
["", "0", ""],
];
Jetzt der Loop durchs Keypad:
.reduce((acc, row, i) => {
Hier bietet sich ein .reduce()
mit einem Array als Startwert an. Du kannst es aber auch gerne mit einem .forEach()
und einem Ergebnis-Array machen.
Wir checken jede Reihe, ob sie die gesuchte Ziffer enthält:
if (row.includes(key)) {
Wenn ja, können wir die Reihe auseinandernehmen, wir extrahieren die Nachbar-Zahlen:
const j = row.indexOf(key);
const prevRow = keypad[i - 1];
const nextRow = keypad[i + 1];
const numLeft = row[j - 1];
const numRight = row[j + 1];
const numUp = prevRow ? prevRow[j] : null;
const numDown = nextRow ? nextRow[j] : null;
Der Lesbarkeit halber erstelle ich mir zuerst ein paar Variablen.
Den Index der gefunden Zahl in der aktuellen Reihe speichern wir uns als j
. Außerdem können wir uns die vorige und die nächste Reihe speichern.
Dann schnappen wir uns die eigentlichen Nachbarn. Wenn es keinen linken oder rechten Nachbarn gibt, bekommen wir undefined
zurück. Das ist erst mal OK und sogar so gewollt.
Für den oberen bzw. unteren Nachbarn müssen wir erst mal checken, ob die jeweilige Reihe überhaupt existiert. Wenn nicht, speichern wir uns null
an diese Stelle.
Dann können wir die aktuelle Zahl (key
) und ihre Nachbarn an den Accumulator der .reduce()
-Methode, also ans Ergebnis-Array (acc
) anhängen:
acc.push(key, numLeft, numRight, numUp, numDown);
}
Zum Schluss nicht vergessen den Accumulator zurückzugeben:
return acc;
}, [])
Wir haben oben teilweise undefined
und/oder null
zurückbekommen, wenn die Zahl nicht existierte. Diese Werte löschen wir jetzt.
.filter((num) => num);
}
Auch leere Strings (””
) sind möglich, wenn der Nachbar in der letzten Reihe gesucht wurde.
Mit .filter((num) => num) behalten wir nur die Werte, die nicht falsy
sind. Damit fliegen alle undefined
s, null
s und ””
s raus. Perfekt!
Das war das getNeighbours
-Helferlein, weiter geht’s mit der Hauptfunktion:
Die erste Zeile meiner Hauptfunktion:
function getPINs(observed) {
Als Erstes loopen wir durch die beobachteten Input-Ziffern:
const allPossibleDigits = [...observed].map((digit) => getNeighbours(digit));
Für jede Ziffer lassen wir unsere Hilfsfunktion laufen und speichern die Ziffer mit ihren Nachbarn in einem Array.
Wir erhalten also ein Array mit Arrays. Ein ein 2-dimensionales Array.
Dann können wir schnell testen, ob die beobachteten Ziffern mehr als eine sind:
if (observed.length === 1) return allPossibleDigits[0];
Wenn es nur eine war, brauchen wir gar nicht weiter rumrödeln. Dann geben wir direkt diese Zahl mit ihren Nachbarn zurück und beenden damit die Funktion. Diese stecken im ersten inneren Array unseres 2-dimensionalen Arrays.
Ansonsten geht’s weiter!
Jetzt wollen wir die Arrays miteinander in allen möglichen Varianten miteinander kombinieren. Wir brauchen erst mal ein Ergebnis-Array:
let pins;
Dann ein weiterer Loop:
for (let i = 0; i < allPossibleDigits.length - 1; i++) {
Zur Erinnerung: Wir loopen hier durch ein 2-dimensionales Array.
Da wir uns immer das aktuelle und das nächste Array schnappen und zusammenführen wollen, müssen wir 1x weniger loopen, als wir Elemente in allPossibleDigits
haben. Denn in der letzten Iteration gibt es kein nächstes Element mehr. Und das würde fieses Kuddelmuddel geben...
Das jeweils aktuelle und das nächste Element bzw. innere Array speichere ich mir wieder jeweils in einer Variablen ab:
const currArr = pins || allPossibleDigits[i];
const nextArr = allPossibleDigits[i + 1];
Wenn wir zwei Arrays miteinander kombiniert haben, wollen wir immer das neu entstandene Array als Grundlage für unsere nächste Kombination verwenden.
Das erreichen wir, indem wir unser aktuelles Array currArr
immer auf unser Ergebnis-Array setzen. Außer in der ersten Runde, wenn unser Ergebnis-Array pins
noch leer bzw. undefined
ist. Da setzen wir es auf das erste Array von unseren allPossibleDigits
.
Jetzt erstellen wir die Kombination aus beiden Arrays:
pins = getAllCombosFrom2Arrs(currArr, nextArr);
}
Und geben nach dem Loop unser Ergebnis zurück:
return pins;
}
Müssen wir also nur noch unsere zweite Hilfsfunktion getAllCombosFrom2Arrs
- basteln. Easy. Endspurt - los geht’s!
Hier die erste Zeile meiner getAllCombosFrom2Arrs
-Hilfsfunktion:
function getAllCombosFrom2Arrs(strs1, strs2) {
Ich habe die Parameter hier strs1
und strs2
genannt, weil diese Funktion beliebige Strings kombinieren kann.
Zuerst ein Ergebnis-Array initialisieren:
const newStrs = [];
Dann loopen wir durch das erste Array:
strs1.forEach((str1) => {
Für jedes Element in strs1
loopen wir dann wiederum durch das zweite Array:
strs2.forEach((str2) => newStrs.push(str1 + str2));
});
Alles klar? Gut!
Für jedes Element in strs2
hängen wir jetzt die Kombination des jeweils aktuellen Elements aus strs1
und strs2
an unser Ergebnis-Array. Cool!
Zum Schluss nur noch das Ergebnis zurückgeben:
return newStrs;
}
Komplettlösung
function getPINs(observed) {
const allPossibleDigits = [...observed].map((digit) => getNeighbours(digit));
if (observed.length === 1) return allPossibleDigits[0];
let pins;
for (let i = 0; i < allPossibleDigits.length - 1; i++) {
const currArr = pins || allPossibleDigits[i];
const nextArr = allPossibleDigits[i + 1];
pins = getAllCombosFrom2Arrs(currArr, nextArr);
}
return pins;
}
function getAllCombosFrom2Arrs(strs1, strs2) {
const newStrs = [];
strs1.forEach((str1) => {
strs2.forEach((str2) => newStrs.push(str1 + str2));
});
return newStrs;
}
function getNeighbours(key) {
const keypad = [
["1", "2", "3"],
["4", "5", "6"],
["7", "8", "9"],
["", "0", ""],
];
return keypad
.reduce((acc, row, i) => {
if (row.includes(key)) {
const j = row.indexOf(key);
const prevRow = keypad[i - 1];
const nextRow = keypad[i + 1];
const numLeft = row[j - 1];
const numRight = row[j + 1];
const numUp = prevRow ? prevRow[j] : null;
const numDown = nextRow ? nextRow[j] : null;
acc.push(key, numLeft, numRight, numUp, numDown);
}
return acc;
}, [])
.filter((num) => num);
}