Java 13.923 (tối thiểu: 11, tối đa: 17)
Cập nhật: điểm số được cải thiện (đã phá vỡ <14 / crack avg!), Mã mới
- Kiểm tra các ký tự đã biết bây giờ dày đặc hơn (bây giờ là ABABAB *, thay vì -AAAA *)
- Khi không có ký tự đã biết, hai ẩn số sẽ được tính trong một lần đoán
- Dự đoán sai được lưu trữ và sử dụng để kiểm tra các trận đấu có thể
- Một số điều chỉnh liên tục với logic mới tại chỗ
Bài gốc
Tôi đã quyết định tập trung hoàn toàn vào số lượng dự đoán thay vì hiệu suất (đưa ra các quy tắc). Điều này đã dẫn đến một chương trình thông minh rất chậm.
Thay vì ăn cắp từ các chương trình đã biết, tôi quyết định viết mọi thứ từ đầu, nhưng hóa ra một số / hầu hết các ý tưởng đều giống nhau.
Thuật toán
Đây là cách tôi làm việc:
- Thực hiện một truy vấn duy nhất dẫn đến tổng số lượng e và ký tự
- Tiếp theo, chúng tôi tìm khoảng trắng, nối thêm một số ký tự chưa biết ở cuối để lấy số ký tự
- Khi tìm thấy khoảng trắng, chúng tôi vẫn muốn tìm hiểu thêm số lượng ký tự, đồng thời tôi cũng nhận được nhiều dữ liệu hơn về các ký tự đã biết (nếu chúng ở vị trí chẵn) sẽ giúp tôi loại bỏ rất nhiều cụm từ.
- Khi chúng tôi đạt đến một giới hạn nhất định (dấu vết / lỗi), nó sẽ tạo ra tất cả các cụm từ có thể và bắt đầu tìm kiếm nhị phân, phần lớn thời gian vẫn nối thêm các ký tự không xác định ở cuối.
- Cuối cùng chúng tôi cũng đoán được!
Ví dụ đoán
Đây là một ví dụ thực tế:
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
Bởi vì mã của tôi không bao giờ thực sự tập trung vào các từ đơn lẻ và chỉ thu thập thông tin về cụm từ hoàn chỉnh, nó phải tạo ra rất nhiều cụm từ làm cho nó rất chậm.
Mã
Và cuối cùng ở đây là mã (xấu), thậm chí đừng cố gắng hiểu nó, xin lỗi:
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;
}
}