Java 13.923 (min: 11, max: 17)
Aggiornamento: punteggio migliorato (rotto <14 / crack avg!), Nuovo codice
- Il controllo di personaggi conosciuti ora è più denso (ora ABABAB *, anziché -AAAA *)
- Quando non sono disponibili personaggi noti, due incognite verranno conteggiate in una singola ipotesi
- Le ipotesi errate vengono memorizzate e utilizzate per verificare possibili corrispondenze
- Alcune modifiche costanti con la nuova logica in atto
Posta originale
Ho deciso di concentrarmi completamente sulla quantità di ipotesi anziché sulle prestazioni (date le regole). Ciò ha comportato un programma intelligente molto lento.
Invece di rubare dai programmi conosciuti ho deciso di scrivere tutto da zero, ma risulta che alcune / la maggior parte delle idee sono le stesse.
Algoritmo
Ecco come funziona il mio:
- Esegui una singola query che determina la quantità di e e caratteri in totale
- Quindi cerchiamo gli spazi, aggiungendo alcuni personaggi sconosciuti alla fine per ottenere un conteggio dei personaggi
- Una volta trovati gli spazi, vogliamo ancora scoprire più conteggi dei caratteri, nel frattempo ottengo anche più dati sui personaggi noti (se si trovano in posizioni pari) che mi aiuteranno a eliminare molte frasi.
- Quando raggiungiamo un certo limite (percorso / errore) genera tutte le frasi possibili e inizia una ricerca binaria, il più delle volte aggiungendo ancora caratteri sconosciuti alla fine.
- Finalmente facciamo alcune ipotesi!
Esempio di ipotesi
Ecco un esempio reale:
Phase 1 (find the e's and total character count):
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbccccccccccccccccccddddddddddddddddddffffffffffffffffffgggggggggggggggggghhhhhhhhhhhhhhhhhhiiiiiiiiiiiiiiiiiijjjjjjjjjjjjjjjjjjkkkkkkkkkkkkkkkkkkllllllllllllllllllmmmmmmmmmmmmmmmmmmnnnnnnnnnnnnnnnnnnooooooooooooooooooppppppppppppppppppqqqqqqqqqqqqqqqqqqrrrrrrrrrrrrrrrrrrssssssssssssssssssttttttttttttttttttuuuuuuuuuuuuuuuuuuvvvvvvvvvvvvvvvvvvwwwwwwwwwwwwwwwwwwxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyzzzzzzzzzzzzzzzzzz
Phase 2 (find the spaces):
----------------iiiiiiiiiiiiiiiiii
----------aaaaaaaaaaaa
-------------sssssssssssssss
--------------rrrrrrrrrrrr
---------------nnnnnnnnnnn
-------ttttttttt
---------oooooooo
--------lllllll
Phase 3 (discovery of characters, collecting odd/even information):
eieieieieieieieieieieieicccccc
ararararararararararararddddd
ntntntntntntntntntntntntuuuuu
Phase 4 (binary search with single known character):
------------r------------ppppp
Phase 5 (actual guessing):
enveloper raging charter
racketeer rowing halpern
Poiché il mio codice non si concentra mai realmente su singole parole e raccoglie solo informazioni sulla frase completa, deve generare molte di quelle frasi che lo rendono molto lento.
Codice
E finalmente ecco il (brutto) codice, non provare nemmeno a capirlo, scusa:
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MastermindV3 {
// Order of characters to analyze:
// eiasrntolcdupmghbyfvkwzxjq - 97
private int[] lookup = new int[] {4, 8, 0, 18, 17, 13, 19, 14, 11, 2, 3, 20, 15, 12, 6, 7, 1, 24, 5, 21, 10, 22, 25, 23, 9, 16};
public static void main(String[] args) throws Exception {
new MastermindV3().run();
}
private void run() throws Exception {
long beforeTime = System.currentTimeMillis();
Map<Integer, List<String>> wordMap = createDictionary();
List<String> passPhrases = createPassPhrases();
int min = Integer.MAX_VALUE;
int max = 0;
for(String phrase:passPhrases) {
int before = totalGuesses;
solve(wordMap, phrase);
int amount = totalGuesses - before;
min = Math.min(min, amount);
max = Math.max(max, amount);
System.out.println("Amount of guesses: "+amount+" : min("+min+") max("+max+")");
}
System.out.println("Total guesses: " + totalGuesses);
System.out.println("Took: "+ (System.currentTimeMillis()-beforeTime)+" ms");
}
/**
* From the original question post:
* I've added a boolean for the real passphrase.
* I'm using this method to check previous guesses against my own matches (not part of Mastermind guesses)
*/
int totalGuesses = 0;
int[] guess(String in, String pw, boolean againstRealPassphrase) {
if(againstRealPassphrase) {
//Only count the guesses against the password, not against our own previous choices
totalGuesses++;
}
int chars=0, positions=0;
for(int i=0;i<in.length()&&i<pw.length();i++){
if(in.charAt(i)==pw.charAt(i))
positions++;
}
if(positions == pw.length() && pw.length()==in.length())
return new int[]{-1,positions};
for(int i=0;i<in.length();i++){
String c = String.valueOf(in.charAt(i));
if(pw.contains(c)){
pw = pw.replaceFirst(c, "");
chars++;
}
}
chars -= positions;
return new int[]{chars,positions};
}
private void solve(Map<Integer, List<String>> wordMap, String pw) {
// Do one initial guess which gives us two things:
// The amount of characters in total
// The amount of e's
int[] initialResult = guess(Facts.INITIAL_GUESS, pw, true);
// Create the object that tracks all the known facts/bounds:
Facts facts = new Facts(initialResult);
// Determine a pivot and find the spaces (binary search)
int center = ((initialResult[0] + initialResult[1]) / 3) + 1;
findSpaces(center, facts, pw);
// When finished finding the spaces (and some character information)
// We can calculate the lengths:
int length1 = (facts.spaceBounds[0]-1);
int length2 = (facts.spaceBounds[2]-facts.spaceBounds[0]-1);
int length3 = (facts.totalLength-facts.spaceBounds[2]+2);
// Next we enter a discovery loop where we find out two things:
// 1) The amount of a new character
// 2) How many of a known character are on an even spot
int oddPtr = 0;
int pairCnt = 0;
// Look for more characters, unless we have one HUGE word, which should be brute forcible easily
int maxLength = Math.max(length1, Math.max(length2, length3));
while(maxLength<17 && !facts.doneDiscovery()) { // We don't need all characters, the more unknowns the slower the code, but less guesses
// Try to generate a sequence with ABABABABAB... with two characters with known length
String testPhrase = "";
int expected = 0;
while(oddPtr < facts.charPtr && (facts.oddEvenUsed[oddPtr]!=-1 || facts.charBounds[lookup[oddPtr]] == 0)) {
oddPtr++;
}
// If no character unknown, try pattern -A-A-A-A-A-A-A... with just one known pattern
int evenPtr = oddPtr+1;
while(evenPtr < facts.charPtr && (facts.oddEvenUsed[evenPtr]!=-1 || facts.charBounds[lookup[evenPtr]] == 0)) {
evenPtr++;
}
if(facts.oddEvenUsed[oddPtr]==-1 && facts.charBounds[lookup[oddPtr]] > 0 && oddPtr < facts.charPtr) {
if(facts.oddEvenUsed[evenPtr]==-1 && facts.charBounds[lookup[evenPtr]] > 0 && evenPtr < facts.charPtr) {
for(int i = 0; i < (facts.totalLength + 3) / 2; i++) {
testPhrase += ((char)(lookup[oddPtr] + 97) +""+ ((char)(lookup[evenPtr] + 97)));
}
expected += facts.charBounds[lookup[oddPtr]] + facts.charBounds[lookup[evenPtr]];
} else {
for(int i = 0; i < (facts.totalLength + 3) / 2; i++) {
testPhrase += ((char)(lookup[oddPtr] + 97) + "-");
}
expected += facts.charBounds[lookup[oddPtr]];
}
}
// If we don't have known characters to explore, use the phrase-length part to discover the count of an unknown character
boolean usingTwoNew = false;
if(testPhrase.length() == 0 && facts.charPtr < 25) {
usingTwoNew = true;
//Fill with a new character
while(testPhrase.length() < (facts.totalLength+2)) {
testPhrase += (char)(lookup[facts.charPtr+1] + 97);
}
} else {
while(testPhrase.length() < (facts.totalLength+2)) {
testPhrase += "-";
}
}
// Use the part after the phrase-length to discover the count of an unknown character
for(int i = 0; i<facts.charBounds[lookup[facts.charPtr]];i++) {
testPhrase += (char)(lookup[facts.charPtr] + 97);
}
// Do the actual guess:
int[] result = guess(testPhrase, pw, true);
// Process the results, store the derived facts:
if(oddPtr < facts.charPtr) {
if(evenPtr < facts.charPtr) {
facts.oddEvenUsed[evenPtr] = pairCnt;
}
facts.oddEvenUsed[oddPtr] = pairCnt;
facts.oddEvenPairScore[pairCnt] = result[1];
pairCnt++;
}
if(usingTwoNew) {
facts.updateCharBounds(result[0]);
if(result[1] > 0) {
facts.updateCharBounds(result[1]);
}
} else {
facts.updateCharBounds((result[0]+result[1]) - expected);
}
}
// Next we generate a list of possible phrases for further analysis:
List<String> matchingPhrases = new ArrayList<String>();
// Hacked in for extra speed, loop over longest word first:
int[] index = sortByLength(length1, length2, length3);
@SuppressWarnings("unchecked")
List<String>[] lists = new List[3];
lists[index[0]] = wordMap.get(length1);
lists[index[1]] = wordMap.get(length2);
lists[index[2]] = wordMap.get(length3);
for(String w1:lists[0]) {
//Continue if (according to our facts) this word is a possible partial match:
if(facts.partialMatches(w1)) {
for(String w2:lists[1]) {
//Continue if (according to our facts) this word is a partial match:
if(facts.partialMatches(w1+w2)) {
for(String w3:lists[2]) {
// Reconstruct phrase in correct order:
String[] possiblePhraseParts = new String[] {w1, w2, w3};
String possiblePhrase = possiblePhraseParts[index[0]]+" "+possiblePhraseParts[index[1]]+" "+possiblePhraseParts[index[2]];
//If the facts form a complete match, continue:
if(facts.matches(possiblePhrase)) {
matchingPhrases.add(possiblePhrase);
}
}
}
}
}
}
//Sometimes we are left with too many matching phrases, do a smart match on them, binary search style:
while(matchingPhrases.size() > 8) {
int lowestError = Integer.MAX_VALUE;
boolean filterCharacterIsKnown = false;
int filterPosition = 0;
int filterValue = 0;
String filterPhrase = "";
//We need to filter some more before trying:
int targetBinaryFilter = matchingPhrases.size()/2;
int[][] usedCharacters = new int[facts.totalLength+2][26];
for(String phrase:matchingPhrases) {
for(int i = 0; i<usedCharacters.length;i++) {
if(phrase.charAt(i) != ' ') {
usedCharacters[i][phrase.charAt(i)-97]++;
}
}
}
//Locate a certain character/position combination which is closest to 50/50:
for(int i = 0; i<usedCharacters.length;i++) {
for(int x = 0; x<usedCharacters[i].length;x++) {
int error = Math.abs(usedCharacters[i][x]-targetBinaryFilter);
if(error < lowestError || (error == lowestError && !filterCharacterIsKnown)) {
//If we do the binary search with a known character we can append more information as well
//Reverse lookup if the character is known
filterCharacterIsKnown = false;
for(int f = 0; f<facts.charPtr; f++) {
if(lookup[f]==x) {
filterCharacterIsKnown = true;
}
}
filterPosition = i;
filterValue = x;
filterPhrase = "";
for(int e = 0; e<i; e++) {
filterPhrase += "-";
}
filterPhrase += ""+((char)(x+97));
lowestError = error;
}
}
}
//Append new character information as well:
while(filterPhrase.length() <= (facts.totalLength+2)) {
filterPhrase += "-";
}
if(filterCharacterIsKnown && facts.charPtr < 26) {
//Append new character to discover
for(int i = 0; i<facts.charBounds[lookup[facts.charPtr]];i++) {
filterPhrase += (char)(lookup[facts.charPtr] + 97);
}
}
//Guess with just that character:
int[] result = guess(filterPhrase, pw, true);
//Filter the 50%
List<String> inFilter = new ArrayList<String>();
for(String phrase:matchingPhrases) {
if(phrase.charAt(filterPosition) == (filterValue+97)) {
inFilter.add(phrase);
}
}
if(result[1]>0) {
//If we have a match, retain all:
matchingPhrases.retainAll(inFilter);
} else {
//No match, filter all
matchingPhrases.removeAll(inFilter);
}
if(filterCharacterIsKnown && facts.charPtr < 26) {
//Finally filter according to the discovered character:
facts.updateCharBounds((result[0]+result[1]) - 1);
List<String> toKeep = new ArrayList<String>();
for(String phrase:matchingPhrases) {
if(facts.matches(phrase)) {
toKeep.add(phrase);
}
}
matchingPhrases = toKeep;
}
}
// Finally we have some phrases left, try them!
for(String phrase:matchingPhrases) {
if(facts.matches(phrase)) {
int[] result = guess(phrase, pw, true);
System.out.println(phrase+" "+Arrays.toString(result));
if(result[0]==-1) {
return;
}
// No match, update facts:
facts.storeInvalid(phrase, result);
}
}
throw new IllegalArgumentException("Unable to solve!?");
}
private int[] sortByLength(int length1, int length2, int length3) {
//God this code is ugly, can't be bothered to fix
int[] index;
if(length3 > length2 && length2 > length1) {
index = new int[] {2, 1, 0};
} else if(length3 > length1 && length1 > length2) {
index = new int[] {2, 0, 1};
} else if(length2 > length3 && length3 > length1) {
index = new int[] {1, 2, 0};
} else if(length2 > length1 && length1 > length3) {
index = new int[] {1, 0, 2};
} else if(length2 > length3) {
index = new int[]{0, 1, 2};
} else {
index = new int[]{0, 2, 1};
}
return index;
}
private void findSpaces(int center, Facts facts, String pw) {
String testPhrase = "";
//Place spaces for analysis:
for(int i = 0; i<center; i++) {testPhrase+=" ";}while(testPhrase.length()<(facts.totalLength+2)) {testPhrase+="-";}
//Append extra characters for added information early on:
for(int i = 0; i<facts.charBounds[lookup[facts.charPtr]];i++) {
testPhrase += (char)(lookup[facts.charPtr]+97);
}
//Update space lower and upper bounds:
int[] answer = guess(testPhrase, pw, true);
if(answer[1] == 0) {
facts.spaceBounds[0] = Math.max(facts.spaceBounds[0], center+1);
facts.spaceBounds[2] = Math.max(facts.spaceBounds[2], center+3);
} else if(answer[1] == 1) {
facts.spaceBounds[1] = Math.min(facts.spaceBounds[1], center);
facts.spaceBounds[2] = Math.max(facts.spaceBounds[2], center+1);
} else {
facts.spaceBounds[3] = Math.min(facts.spaceBounds[3], center);
facts.spaceBounds[1] = Math.min(facts.spaceBounds[1], center-2);
}
int correctAmountChars = (answer[0] + answer[1]) - 2;
facts.updateCharBounds(correctAmountChars);
//System.out.println(Arrays.toString(facts.spaceBounds));
if(facts.spaceBounds[0]==facts.spaceBounds[1]) {
if(facts.spaceBounds[2]==facts.spaceBounds[3]) return;
findSpaces(facts.spaceBounds[2] + ((facts.spaceBounds[3]-facts.spaceBounds[2])/3), facts, pw);
} else {
findSpaces((facts.spaceBounds[0]+facts.spaceBounds[1])/2, facts, pw);
}
}
private class Facts {
private static final String INITIAL_GUESS = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbccccccccccccccccccddddddddddddddddddffffffffffffffffffgggggggggggggggggghhhhhhhhhhhhhhhhhhiiiiiiiiiiiiiiiiiijjjjjjjjjjjjjjjjjjkkkkkkkkkkkkkkkkkkllllllllllllllllllmmmmmmmmmmmmmmmmmmnnnnnnnnnnnnnnnnnnooooooooooooooooooppppppppppppppppppqqqqqqqqqqqqqqqqqqrrrrrrrrrrrrrrrrrrssssssssssssssssssttttttttttttttttttuuuuuuuuuuuuuuuuuuvvvvvvvvvvvvvvvvvvwwwwwwwwwwwwwwwwwwxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyzzzzzzzzzzzzzzzzzz";
private final int totalLength;
private final int[] spaceBounds;
// Pre-filled with maximum bounds obtained from dictionary:
private final int[] charBounds = new int[] {12, 9, 9, 9, 15, 9, 12, 9, 18, 6, 9, 12, 9, 12, 12, 9, 3, 12, 15, 9, 12, 6, 6, 3, 9, 6};
private final int[] oddEvenUsed = new int[] {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1};
private final int[] oddEvenPairScore = new int[26];
private int charPtr;
public Facts(int[] initialResult) {
totalLength = initialResult[0] + initialResult[1];
spaceBounds = new int[] {2, Math.min(totalLength - 2, 22), 4, Math.min(totalLength + 1, 43)};
//Eliminate firsts
charBounds[lookup[0]] = initialResult[1];
//Adjust:
for(int i = 1; i<charBounds.length; i++) {
charBounds[lookup[i]] = Math.min(charBounds[lookup[i]], totalLength-initialResult[1]);
}
charPtr = 1;
}
private List<String> previousGuesses = new ArrayList<String>();
private List<int[]> previousResults = new ArrayList<int[]>();
public void storeInvalid(String phrase, int[] result) {
previousGuesses.add(phrase);
previousResults.add(result);
}
public boolean doneDiscovery() {
if(charPtr<12) { //Always do at least N guesses (speeds up and slightly improves score)
return false;
}
return true;
}
public void updateCharBounds(int correctAmountChars) {
// Update the bounds we know for a certain character:
int knownCharBounds = 0;
charBounds[lookup[charPtr]] = correctAmountChars;
for(int i = 0; i <= charPtr;i++) {
knownCharBounds += charBounds[lookup[i]];
}
// Also update the ones we haven't checked yet, we might know something about them now:
for(int i = charPtr+1; i<charBounds.length; i++) {
charBounds[lookup[i]] = Math.min(charBounds[lookup[i]], totalLength-knownCharBounds);
}
charPtr++;
while(charPtr < 26 && charBounds[lookup[charPtr]]==0) {
charPtr++;
}
}
public boolean partialMatches(String phrase) {
//Try to match a partial phrase, we can't be too picky because we don't know what else is next
int[] cUsed = new int[26];
for(int i = 0; i<phrase.length(); i++) {
cUsed[phrase.charAt(i)-97]++;
}
for(int i = 0; i<cUsed.length; i++) {
//Only eliminate the phrases that definitely have wrong characters:
if(cUsed[lookup[i]] > charBounds[lookup[i]]) {
return false;
}
}
return true;
}
public boolean matches(String phrase) {
// Try to match a complete phrase, we can now use all information:
int[] cUsed = new int[26];
for(int i = 0; i<phrase.length(); i++) {
if(phrase.charAt(i)!=' ') {
cUsed[phrase.charAt(i)-97]++;
}
}
for(int i = 0; i<cUsed.length; i++) {
if(i < charPtr) {
if(cUsed[lookup[i]] != charBounds[lookup[i]]) {
return false;
}
} else {
if(cUsed[lookup[i]] > charBounds[lookup[i]]) {
return false;
}
}
}
//Check against what we know for odd/even
for(int pair = 0; pair < 26;pair++) {
String input = "";
for(int i = 0; i<26;i++) {
if(oddEvenUsed[i] == pair) {
input += (char)(lookup[i]+97);
}
}
if(input.length() == 1) {
input += "-";
}
String testPhrase = "";
for(int i = 0; i<=(totalLength+1)/2 ; i++) {
testPhrase += input;
}
int[] result = guess(testPhrase, phrase, false);
if(result[1] != oddEvenPairScore[pair]) {
return false;
}
}
//Check again previous guesses:
for(int i = 0; i<previousGuesses.size();i++) {
// If the input phrase is the correct phrase it should score the same against previous tries:
int[] result = guess(previousGuesses.get(i), phrase, false);
int[] expectedResult = previousResults.get(i);
if(!Arrays.equals(expectedResult, result)) {
return false;
}
}
return true;
}
}
private List<String> createPassPhrases() throws Exception {
BufferedReader reader = new BufferedReader(new FileReader(new File("pass.txt")));
List<String> phrases = new ArrayList<String>();
String input;
while((input = reader.readLine()) != null) {
phrases.add(input);
}
return phrases;
}
private Map<Integer, List<String>> createDictionary() throws Exception {
BufferedReader reader = new BufferedReader(new FileReader(new File("words.txt")));
Map<Integer, List<String>> wordMap = new HashMap<Integer, List<String>>();
String input;
while((input = reader.readLine()) != null) {
List<String> words = wordMap.get(input.length());
if(words == null) {
words = new ArrayList<String>();
}
words.add(input);
wordMap.put(input.length(), words);
}
return wordMap;
}
}