Are You the One? (Mastermind Derivative)

15

4

I've got a tough one for you!

My girlfriend recently came across a new show on MTV (USA). It's a terrible show, and everyone on it is trashy, but the "game" is pretty interesting. From Wikipedia:

Are You The One? follows 20 people who are living together in Hawaii to find their perfect match. If the 10 men and 10 women are able to correctly choose all ten perfect matches in ten weeks, they will gain $1 million to split among them.

Now for the game portion (also from Wikipedia):

Each episode the cast will pair up with who they believe their perfect match is to compete in a challenge. The winners of the challenge will go on a date, and have a chance to test their match in the truth booth. The cast members will choose one of the winning couples to go to the truth booth to determine if they are a perfect match or not. This is the only way to confirm matches. Each episode ends with a matching ceremony where the couples will be told how many perfect matches they have, but not which matches are correct.

TL;DR: This is a Mastermind derivative ( M(10,10) to be specific). The rules of the game are as follows:

  1. You start with 2 sets of 10, let's call them Set A: {A,B,C,D,E,F,G,H,I,J} and Set 2: {1,2,3,4,5,6,7,8,9,10}

  2. The computer creates a solution (not visible to you) in the form of {A1,B2,C3,D4,E5,F6,G7,H8,I9,J10}, where the members in set A are mapped 1-to-1 to set 2. Another example of a solution could be {A2,B5,C10,D8,E1,F7,G6,H4,I9,J3}.

  3. Before your first turn, you get to ask if a single, particular pair of your choice is correct. Your question would be in the form of {A1} (e.g. {C8}), and you receive either a 1 (meaning correct) or 0 (meaning your guess is incorrect).

  4. Your first actual turn. You make your first guess in the form of {A1,B2,C3,D4,E5,F6,G7,H8,I9,J10}, or any permutation of your choice. Your guess cannot contain multiples of any item i.e. a guess of {A1,A2,A3,A4,A5,B6,B7,B8,B9,B10} is NOT a valid guess. After each turn, the computer tells you the number of correct matches, but NOT which matches are correct.

  5. Repeat steps 3 and 4 until you get every match correct (i.e. a response of 10), or until your 10 moves are up (whichever is sooner). If you solve it before or on your 10th turn, you win $1 million. Otherwise, you lose, and some people (or letters and numbers) go home alone to spend eternity with their 10 cats.

This is NOT a shortest code contest. The person who can solve a random matching in the least average number of guesses will be the winner. Clever game playing and calculation speed will also likely factor in. I am assuming the average number of turns will almost certainly be greater than 10, so the odds of you winning the $1 million prize (presumably paid by MTV, not me) is slim. Just how impossible is it for the cast to win the grand prize?

Note: Putting it in the {A1, B2, ...} format is not necessarily required. I simply used that form in the question to make it absolutely clear what the puzzle is. If you do not put it in this form, please just explain how to call it.

Good luck!

dberm22

Posted 2014-10-31T01:05:27.093

Reputation: 512

3If you want the person who can solve it in the least average guesses to win, why not make that the winning criteria? I can't see any reason this should be a popularity contest when a perfectly valid win condition is staring us in the face. – Geobits – 2014-10-31T01:25:07.647

As far as I can tell the question doesn't have anything to do with optimally playing Mastermind. It asks for a game that allows a user to play it. – feersum – 2014-10-31T01:28:33.013

@Geobits Wasn't sure how people would respond to that. I agree that's a better criteria though. It just might be hard to judge. – dberm22 – 2014-10-31T01:29:16.463

@Feersum I am asking for the optimal strategy. Fewest average number of turns to guess the solution will be the accepted answer. – dberm22 – 2014-10-31T01:31:03.080

1Then pop-contest is not the tag for this. – None – 2014-10-31T02:08:13.897

1@hosch250 Updated criterion for winner – dberm22 – 2014-10-31T02:24:00.957

OK, seems pretty good. – None – 2014-10-31T03:04:09.193

27 upvotes, 2 favorites, and no answers. I knew this was a tough one! – dberm22 – 2014-11-01T19:29:28.623

Answers

6

Python 2 (run faster if run using Pypy)

Believed to almost always guess the correct pairing in 10 rounds or lower

My algorithm is taken from my answer for mastermind as my hobby (see in Ideone). The idea is to find the guess which minimize the number of possibilities left in the worst case. My algorithm below just brute force it, but to save time, it just pick random guess if the number of possibilities left is larger than RANDOM_THRESHOLD. You can play around with this parameter to speed things up or to see better performance.

The algorithm is quite slow, on average 10s for one run if run using Pypy (if using normal CPython interpreter it's around 30s) so I can't test it on the whole permutations. But the performance is quite good, after around 30 tests I haven't seen any instance where it can't find the correct pairing in 10 rounds or lower.

Anyway, if this is used in real life show, it has plenty of time before the next round (one week?) so this algorithm can be used in real life =D

So I think it's safe to assume that on average this will find the correct pairings in 10 guesses or lower.

Try it yourself. I might improve the speed in the next few days (EDIT: it seems difficult to further improve, so I'll just leave the code as is. I tried only doing random pick, but even at size=7, it fails in 3 of the 5040 cases, so I decided to keep the cleverer method). You can run it as:

pypy are_you_the_one.py 10

Or, if you just want to see how it works, input smaller number (so that it runs faster)

To run a full test (warning: it'll take very long for size > 7), put a negative number.

Full test for size=7 (completed in 2m 32s):

...
(6, 5, 4, 1, 3, 2, 0): 5 guesses
(6, 5, 4, 2, 0, 1, 3): 5 guesses
(6, 5, 4, 2, 0, 3, 1): 4 guesses
(6, 5, 4, 2, 1, 0, 3): 5 guesses
(6, 5, 4, 2, 1, 3, 0): 6 guesses
(6, 5, 4, 2, 3, 0, 1): 6 guesses
(6, 5, 4, 2, 3, 1, 0): 6 guesses
(6, 5, 4, 3, 0, 1, 2): 6 guesses
(6, 5, 4, 3, 0, 2, 1): 3 guesses
(6, 5, 4, 3, 1, 0, 2): 7 guesses
(6, 5, 4, 3, 1, 2, 0): 7 guesses
(6, 5, 4, 3, 2, 0, 1): 4 guesses
(6, 5, 4, 3, 2, 1, 0): 7 guesses
Average count: 5.05
Max count    : 7
Min count    : 1
Num success  : 5040

If RANDOM_THRESHOLD and CLEVER_THRESHOLD are both set to a very high value (like 50000), it'll force the algorithm to find the optimal guess that minimizes the number of possibilities in the worst case. This is very slow, but very powerful. For example, running it on size=6 asserts that it can find the correct pairings in maximum 5 rounds.

Although the average is higher compared to using the approximation (which is 4.11 rounds on average), but it always succeeds, even more with one round left to spare. This further strengthen our hypothesis that when size=10, it should almost always find the correct pairings in 10 rounds or less.

The result (completed in 3m 9s):

(5, 4, 2, 1, 0, 3): 5 guesses
(5, 4, 2, 1, 3, 0): 5 guesses
(5, 4, 2, 3, 0, 1): 4 guesses
(5, 4, 2, 3, 1, 0): 4 guesses
(5, 4, 3, 0, 1, 2): 5 guesses
(5, 4, 3, 0, 2, 1): 5 guesses
(5, 4, 3, 1, 0, 2): 5 guesses
(5, 4, 3, 1, 2, 0): 5 guesses
(5, 4, 3, 2, 0, 1): 5 guesses
(5, 4, 3, 2, 1, 0): 5 guesses
Average count: 4.41
Max count    : 5
Min count    : 1
Num success  : 720

The code.

from itertools import permutations, combinations
import random, sys
from collections import Counter

INTERACTIVE = False
ORIG_PERMS = []
RANDOM_THRESHOLD = 100
CLEVER_THRESHOLD = 0

class Unbuffered():
    def __init__(self, stream):
        self.stream = stream

    def write(self, data):
        self.stream.write(data)
        self.stream.flush()

    def __getattr__(self, attr):
        self.stream.getattr(attr)
sys.stdout = Unbuffered(sys.stdout)

def init(size):
    global ORIG_PERMS
    ORIG_PERMS = list(permutations(range(size)))

def evaluate(solution, guess):
    if len(guess) == len(solution):
        cor = 0
        for sol, gss in zip(solution, guess):
            if sol == gss:
                cor += 1
        return cor
    else:
        return 1 if solution[guess[0]] == guess[1] else 0

def remove_perms(perms, evaluation, guess):
    return [perm for perm in perms if evaluate(perm, guess)==evaluation]

def guess_one(possible_perms, guessed_all, count):
    if count == 1:
        return (0,0)
    pairs = Counter()
    for perm in possible_perms:
        for pair in enumerate(perm):
            pairs[pair] += 1
    perm_cnt = len(possible_perms)
    return sorted(pairs.items(), key=lambda x: (abs(perm_cnt-x[1]) if x[1]<perm_cnt else perm_cnt,x[0]) )[0][0]

def guess_all(possible_perms, guessed_all, count):
    size = len(possible_perms[0])
    if count == 1:
        fact = 1
        for i in range(2, size):
            fact *= i
        if len(possible_perms) == fact:
            return tuple(range(size))
        else:
            return tuple([1,0]+range(2,size))
    if len(possible_perms) == 1:
        return possible_perms[0]
    if count < size and len(possible_perms) > RANDOM_THRESHOLD:
        return possible_perms[random.randint(0, len(possible_perms)-1)]
    elif count == size or len(possible_perms) > CLEVER_THRESHOLD:
        (_, next_guess) = min((max(((len(remove_perms(possible_perms, evaluation, next_guess)), next_guess) for evaluation in range(len(next_guess))), key=lambda x: x[0])
                               for next_guess in possible_perms if next_guess not in guessed_all), key=lambda x: x[0])
        return next_guess
    else:
        (_, next_guess) = min((max(((len(remove_perms(possible_perms, evaluation, next_guess)), next_guess) for evaluation in range(len(next_guess))), key=lambda x: x[0])
                               for next_guess in ORIG_PERMS if next_guess not in guessed_all), key=lambda x: x[0])
        return next_guess

def main(size=4):
    if size < 0:
        size = -size
        init(size)
        counts = []
        for solution in ORIG_PERMS:
            count = run_one(solution, False)
            counts.append(count)
            print '%s: %d guesses' % (solution, count)
        sum_count = float(sum(counts))
        print 'Average count: %.2f' % (sum_count/len(counts))
        print 'Max count    : %d' % max(counts)
        print 'Min count    : %d' % min(counts)
        print 'Num success  : %d' % sum(1 for count in counts if count <= size)
    else:
        init(size)
        solution = ORIG_PERMS[random.randint(0,len(ORIG_PERMS)-1)]
        run_one(solution, True)

def run_one(solution, should_print):
    if should_print:
        print solution
    size = len(solution)
    cur_guess = None
    possible_perms = list(ORIG_PERMS)
    count = 0
    guessed_one = []
    guessed_all = []
    while True:
        count += 1
        # Round A, guess one pair
        if should_print:
            print 'Round %dA' % count
        if should_print:
            print 'Num of possibilities: %d' % len(possible_perms)
        cur_guess = guess_one(possible_perms, guessed_all, count)
        if should_print:
            print 'Guess: %s' % str(cur_guess)
        if INTERACTIVE:
            evaluation = int(raw_input('Number of correct pairs: '))
        else:
            evaluation = evaluate(solution, cur_guess)
            if should_print:
                print 'Evaluation: %s' % str(evaluation)
        possible_perms = remove_perms(possible_perms, evaluation, cur_guess)

        # Round B, guess all pairs
        if should_print:
            print 'Round %dB' % count
            print 'Num of possibilities: %d' % len(possible_perms)
        cur_guess = guess_all(possible_perms, guessed_all, count)
        if should_print:
            print 'Guess: %s' % str(cur_guess)
        guessed_all.append(cur_guess)
        if INTERACTIVE:
            evaluation = int(raw_input('Number of correct pairs: '))
        else:
            evaluation = evaluate(solution, cur_guess)
            if should_print: print 'Evaluation: %s' % str(evaluation)
        if evaluation == size:
            if should_print:
                print 'Found %s in %d guesses' % (str(cur_guess), count)
            else:
                return count
            break
        possible_perms = remove_perms(possible_perms, evaluation, cur_guess)

if __name__=='__main__':
    size = 4
    if len(sys.argv) >= 2:
        size = int(sys.argv[1])
        if len(sys.argv) >= 3:
            INTERACTIVE = bool(int(sys.argv[2]))
    main(size)

justhalf

Posted 2014-10-31T01:05:27.093

Reputation: 2 070

This is truly incredible. I have ran it 100 more times, and it has yet to take more than 10 guesses to find the solution. I've seen a couple 10's, and even a couple 6's. (You say you haven't seen any instance where it can't find the correct pairing under 10 rounds. That should probably say "in 10 or less rounds", but that's just semantics.) This is fantastic! I'd assume your lambda value is some sort of measurement of entropy which allows you to make the optimal guess, but I don't see how or where it is set. This is just me being dense, not an indictment of your program. Incredible work! – dberm22 – 2014-11-04T15:58:37.027

It's trying to minimize the number of possibilities left in the worst case (the len(remove_perms ...) part). And yes, I meant in <=10 rounds =). Btw in the code above the really optimal guess is never made, since I put CLEVER_THRESHOLD=0, which means it'll only try to guess from the list of possibilities, although the optimal guess might be outside of this set. But I decided to disable that to save time. I added full test for size=7, showing that it always succeeds. – justhalf – 2014-11-04T16:13:45.177

1I have been running your code overnight with 'Clever Threshold = 0' (starting from (9,8,7,6,5,4,3,2,1,0) and working backwards through all the permutations). I am only 2050 through so far, but there has been 13 cases where it has taken 11 turns. Sample print out - (9, 8, 7, 4, 0, 6, 3, 2, 1, 5): 9 guesses, Average count: 8.29, Max count : 11, Min count : 4, Num success : 2037, Num evaluated: 2050. If 'Clever Threshold' was properly set, I bet those 11's would become 10's. Still, on average, 8.3 is pretty magnificent. – dberm22 – 2014-11-05T12:36:25.953

@dberm22: Yes, thank you for running this slow algorithm overnight! I ran full test on size=8 and found that the latest version has only 40308 successes (instead of 40320) if this setting is used. But that's still 99.97% success rate! Guess we can make the TV show organizer goes bankrupt. – justhalf – 2014-11-05T12:40:09.923

3

CJam -19 turns- An Idiot's Strategy

This is not a serious answer but a demonstration. This is an idiot's solution where he does not take into account the number of correct pairings information provided from the second part of the turn. With completely random pairings, this takes an average of 27 weeks. This answer is insuffienct as I have said but indicates that the odds for an intellegent group (much more intellegent that this program) is likely not as slim as you might expect. The more intellegent algorithms i've written, however take alot more time to run so I can' actually get answers from them.

Update: The code below was updated to use state that it should remove ones that don't work if the only correct ones are ones we already knew were correct. It was also editted to show my random "correct answer" generator. The average result is now only 19. It is still a dumb solution but it is better than the previous marginally.

A,{__,mr=_@@-}A*;]sedS*:Z;

ZS/:i:G;                               "Set the input (goal) to G";
{UU@{G2$==@+\)}%~;}:C;                 "This is the tool to count how many of an array agree with G";
{:Y;1$1$<{Y-}%Yaa+@@)>{Y-}%+}:S;       "for stack A X Y, sets the Xth value in the array to Y";
{:Y;1$1$<2$2$=Y-a+@@)>+}:R;            "for stack A X Y, removes Y from the Xth value in the array";

1:D;                                   "Set turn counter to one. if zero exits loop";

A,]A*                                  "array of arrays that has all possible values for an ordering";

{                                      "start of loop";

_V=(\;_GV=={V\SV):V;}{V\R}?            "Guesses a number for the first unknown. If right sets the pair; else erases it";

_[{(_,_{mr=}{;;11}?:Y\{Y-}%}A*;]_C     "guesses random possible arrangement and determines how many are right, error=11";
\_{+}*45-:Y{Y;{_11={;BY-}{}?}%}{}?\    "error correct by including the missing number";

_V={;V:X>{X\RX):X;}%~LV}{}?            "if all new are wrong, makes sure they aren't guessed again";
_A={Dp0:D;;p;}{D):D;;;}?               "If all are right, prints it an tells loop to exit.  Else increments counter";

D}g                                    "repeat from start of loop";

kaine

Posted 2014-10-31T01:05:27.093

Reputation: 536

Also note: the sloppy error handling is because it is easier to program that then a more intellegent method. – kaine – 2014-11-03T19:12:14.137

+1 for actually being brave enough to implement a solution. I am actually quite shocked that it only takes 27 turns on average to guess the correct solution. I'd assume that as you guess correctly, subsequent matches are exponentially (well, factorially) easier to find. Thanks! I'd be interested to see if anyone can get less than 10. You have given me hope! – dberm22 – 2014-11-03T19:45:09.790

If 27 is surprising look at that! All joking aside I think that a solution which guarentees 10 or at least gets it on average is possible. I just can't get such an algorithm to work in a reasonable time frame. – kaine – 2014-11-03T20:22:55.067

19...I'm even more shocked. Just a question though... in your program, where you say "Guesses a number for the first unknown. If right sets the pair; else erases it". If it's wrong...you should be adding it to the list of matches you know aren't correct, so you don't guess it again (either in the permutation, or as the separate guess). – dberm22 – 2014-11-03T20:30:11.490

It means erase it from the list of possibilities; I have a list of possible ones not a list of impossible ones. Oh, and I forgot to mention that this has the male be position in array and female being numbers 0-9 (or vice versa). I would use A5 B2 etc. format if it were a more serious submission. – kaine – 2014-11-03T20:31:51.273

Oh, I see. Never mind then. The format is not important to me...I just used that form because I thought it would be the most clear. I will include that in the question. – dberm22 – 2014-11-03T20:38:55.543

Actually 27 guesses is precisely the theoretical upper bound of how many turns on average this will take. Consider the first man, it'll take maximum 9 guesses to know the correct match, and an average of 5 guesses ((1+2+...+9)/9). Then it'll take maximum 8 turns for the second man to get the correct match (with average 4.5). This numbers decrease by one until we have 2 pairs left, which can be determined in 1 guess. Total we need 9+8+7+6+5+4+3+2+1 = 45 guesses at maximum, and an average of 27. – justhalf – 2014-11-04T06:44:14.833

3

Fast Multi-Threaded C++ Version

I know it's been a while since this thread was active, but I have a cool addition to share: Here's a C++ implementation of the minimax algorithm for Are You The One?, which uses multi-threading to speed up the evaluation of each possible guess.

This version is much faster than the Python version (over 100x faster when original Python version is set to maximum RANDOM_THRESHOLD and CLEVER_THRESHOLD). It doesn't use any random guessing, but rather evaluates all permutations and submits as its guess the permutation that eliminates the greatest number of possible solutions (given the worst-case response).

For smaller games, calling "ayto -n" will run the game on all n! possible hidden matchings, and will give you a short summary of the results at the end.

Since it's still intractable to evaluate all 10! possible hidden matchings, if you call "ayto 10", for example, the simulator makes its fixed first three guesses, then runs minimax to choose its guess and assumes that it was given the worst-case evaluation. This leads us down a "worst-case path" to a hidden vector that is presumably in the class of vectors that takes the algorithm a maximum number of guesses to identify. This conjecture has not been tested.

Up to n = 9, there hasn't been a simulation that has taken more than n turns to solve.

To test this yourself, an example compilation would be the following:

g++ -std=c++11 -lpthread -o ayto ayto.cpp

Here's a small example with output:

$ ./ayto -4
Found (0, 1, 2, 3) in 2 guesses.
Found (0, 1, 3, 2) in 3 guesses.
Found (0, 2, 1, 3) in 2 guesses.
Found (0, 2, 3, 1) in 3 guesses.
Found (0, 3, 1, 2) in 2 guesses.
Found (0, 3, 2, 1) in 2 guesses.
Found (1, 0, 2, 3) in 1 guesses.
Found (1, 0, 3, 2) in 3 guesses.
Found (1, 2, 0, 3) in 3 guesses.
Found (1, 2, 3, 0) in 3 guesses.
Found (1, 3, 0, 2) in 3 guesses.
Found (1, 3, 2, 0) in 3 guesses.
Found (2, 0, 1, 3) in 3 guesses.
Found (2, 0, 3, 1) in 3 guesses.
Found (2, 1, 0, 3) in 3 guesses.
Found (2, 1, 3, 0) in 3 guesses.
Found (2, 3, 0, 1) in 3 guesses.
Found (2, 3, 1, 0) in 3 guesses.
Found (3, 0, 1, 2) in 3 guesses.
Found (3, 0, 2, 1) in 3 guesses.
Found (3, 1, 0, 2) in 3 guesses.
Found (3, 1, 2, 0) in 3 guesses.
Found (3, 2, 0, 1) in 3 guesses.
Found (3, 2, 1, 0) in 3 guesses.
***** SUMMARY *****
Avg. Turns: 2.75
Worst Hidden Vector: (0, 1, 3, 2) in 3 turns.

Code

/* Multithreaded Mini-max Solver for MTV's Are You The One? */

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <cassert>
#include <algorithm>
#include <numeric>
#include <string>
#include <vector>
#include <map>
#include <thread>
#include <cmath>

#define TEN_FACT (3628800)
#define NUM_CHUNKS (8)

using std::cout;
using std::cin;
using std::endl;
using std::vector;
using std::string;
using std::map;
using std::pair;
using std::find;
using std::abs;
using std::atoi;
using std::next_permutation;
using std::max_element;
using std::accumulate;
using std::reverse;
using std::thread;

struct args {
    vector<string> *perms;
    vector<string> *chunk;
    pair<string, int> *cd;
    int thread_id;
};

void simulate_game(const string &hidden, map<string, int> &turns_taken,
                   bool running_all);
bool picmp(const pair<string, int> &p1, const pair<string, int> &p2);
double map_avg(const map<string, int> &mp);
int nrand(int n);
int evaluate(const string &sol, const string &query);
vector<string> remove_perms(vector<string> &perms, int eval, string &query);
pair<string, int> guess_tb(vector<string> &perms, vector<string> &guessed_tb, int turn);
pair<string, int> guess_pm(vector<string> &perms, vector<string> &guessed, int turn);
void make_chunks(vector<string> &orig, vector<vector<string> > &chunks, int n);
string min_candidate(pair<string, int> *candidates, int n);
void get_score(struct args *args);
int wc_response(string &guess, vector<string> &perms);
bool prcmp(pair<int, int> x, pair<int, int> y);
void sequence_print(string s);
struct args **create_args(vector<string> &perms, pair<string, int> *cd, vector<string> &chunk, int thread_id);


vector<string> ORIGPERMS;

int main(int argc, char **argv)
{
    int sz;
    map<string, int> turns_taken;
    const string digits = "0123456789";
    bool running_all = false;

    if (argc != 2) {
        cout << "usage: 'ayto npairs'" << endl;
        return 1;
    } else {
        if ((sz = atoi(argv[1])) < 0) {
            sz = -sz;
            running_all = true;
        }
        if (sz < 3 || sz > 10) {
            cout << "usage: 'ayto npairs' where 3 <= npairs <= 10" << endl;;
            return 1;
        }
    }

    // initialize ORIGPERMS and possible_perms
    string range = digits.substr(0, sz);
    do {
        ORIGPERMS.push_back(range);
    } while (next_permutation(range.begin(), range.end()));

    if (running_all) {
        for (vector<string>::const_iterator it = ORIGPERMS.begin();
             it != ORIGPERMS.end(); ++it) {
            simulate_game(*it, turns_taken, running_all);
        }
        cout << "***** SUMMARY *****\n";
        cout << "Avg. Turns: " << map_avg(turns_taken) << endl;
        pair<string, int> wc = *max_element(turns_taken.begin(),
                                            turns_taken.end(), picmp);
        cout << "Worst Hidden Vector: ";
        sequence_print(wc.first);
        cout << " in " << wc.second << " turns." << endl;
    } else {
        string hidden = ORIGPERMS[nrand(ORIGPERMS.size())];
        simulate_game(hidden, turns_taken, running_all);
    }

    return 0;
}

// simulate_game:  run a single round of AYTO on hidden vector
void simulate_game(const string &hidden, map<string, int> &turns_taken,
                   bool running_all)
{
    vector<string> possible_perms = ORIGPERMS;
    pair<string, int> tbguess;
    pair<string, int> pmguess;
    vector<string> guessed;
    vector<string> guessed_tb;
    int e;
    int sz = hidden.size();

    if (!running_all) {
        cout << "Running AYTO Simulator on Hidden Vector: ";
        sequence_print(hidden);
        cout << endl;
    }

    for (int turn = 1; ; ++turn) {
        // stage one: truth booth
        if (!running_all) {
            cout << "**** Round " << turn << "A ****" << endl;
            cout << "Num. Possibilities: " << possible_perms.size() << endl;
        }
        tbguess = guess_tb(possible_perms, guessed_tb, turn);
        if (!running_all) {
            cout << "Guess: ";
            sequence_print(tbguess.first);
            cout << endl;
            e = tbguess.second;
            cout << "Worst-Case Evaluation: " << e << endl;
        } else {
            e = evaluate(hidden, tbguess.first);
        }
        possible_perms = remove_perms(possible_perms, e, tbguess.first);

        // stage two: perfect matching
        if (!running_all) {
            cout << "Round " << turn << "B" << endl;
            cout << "Num. Possibilities: " << possible_perms.size() << endl;
        }
        pmguess = guess_pm(possible_perms, guessed, turn);

        if (!running_all) {
            cout << "Guess: ";
            sequence_print(pmguess.first);
            cout << endl;
            e = pmguess.second;
            cout << "Worst-Case Evaluation: " << e << endl;
        } else {
            e = evaluate(hidden, pmguess.first);
        }
        if (e == sz) {
            cout << "Found ";
            sequence_print(pmguess.first);
            cout << " in " << turn << " guesses." << endl;
            turns_taken[pmguess.first] = turn;
            break;
        }

        possible_perms = remove_perms(possible_perms, e, pmguess.first);
    }
}

// map_avg:  returns average int component of a map<string, int> type
double map_avg(const map<string, int> &mp)
{
    double sum = 0.0;

    for (map<string, int>::const_iterator it = mp.begin(); 
         it != mp.end(); ++it) {
        sum += it->second;
    }

    return sum / mp.size();
}

// picmp:  comparison function for pair<string, int> types, via int component
bool picmp(const pair<string, int> &p1, const pair<string, int> &p2)
{
    return p1.second < p2.second;
}

// nrand:  random integer in range [0, n)
int nrand(int n)
{
    srand(time(NULL));

    return rand() % n;
}

// evaluate:  number of black hits from permutation or truth booth query
int evaluate(const string &sol, const string &query)
{
    int hits = 0;

    if (sol.size() == query.size()) {
        // permutation query
        int s = sol.size();
        for (int i = 0; i < s; i++) {
            if (sol[i] == query[i])
                ++hits;
        }
    } else {
        // truth booth query
        if (sol[atoi(query.substr(0, 1).c_str())] == query[1])
            ++hits;
    }

    return hits;
}

// remove_perms:  remove solutions that are no longer possible after an eval
vector<string> remove_perms(vector<string> &perms, int eval, string &query)
{
    vector<string> new_perms;

    for (vector<string>::iterator i = perms.begin(); i != perms.end(); i++) {
        if (evaluate(*i, query) == eval) {
            new_perms.push_back(*i);
        }
    }

    return new_perms;
}

// guess_tb:  guesses best pair (pos, val) to go to the truth booth
pair<string, int> guess_tb(vector<string> &possible_perms,
                           vector<string> &guessed_tb, int turn)
{
    static const string digits = "0123456789";
    int n = possible_perms[0].size();
    pair<string, int> next_guess;

    if (turn == 1) {
        next_guess.first = "00";
        next_guess.second = 0;
    } else if (possible_perms.size() == 1) {
        next_guess.first = "0" + possible_perms[0].substr(0, 1);
        next_guess.second = 1;
    } else {
        map<string, double> pair_to_count;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                pair_to_count[digits.substr(i, 1) + digits.substr(j, 1)] = 0;
            }
        }

        // count up the occurrences of each pair in the possible perms
        for (vector<string>::iterator p = possible_perms.begin();
             p != possible_perms.end(); p++) {
            int len = possible_perms[0].size();
            for (int i = 0; i < len; i++) {
                pair_to_count[digits.substr(i, 1) + (*p).substr(i, 1)] += 1;
            }
        }

        double best_dist = 1;
        int perm_cnt = possible_perms.size();
        for (map<string, double>::iterator i = pair_to_count.begin();
             i != pair_to_count.end(); i++) {
            if (find(guessed_tb.begin(), guessed_tb.end(), i->first)
                == guessed_tb.end()) {
                // hasn't been guessed yet
                if (abs(i->second/perm_cnt - .5) < best_dist) {
                    next_guess.first = i->first;
                    best_dist = abs(i->second/perm_cnt - .5);
                    if (i->second / perm_cnt < 0.5) // occurs in < half perms
                        next_guess.second = 0;
                    else                            // occurs in >= half perms
                        next_guess.second = 1;
                }
            }
        }
    }

    guessed_tb.push_back(next_guess.first);

    return next_guess;
}

// guess_pm:  guess a full permutation using minimax
pair<string, int> guess_pm(vector<string> &possible_perms,
                           vector<string> &guessed, int turn)
{
    static const string digits = "0123456789";
    pair<string, int> next_guess;
    vector<vector<string> > chunks;
    int sz = possible_perms[0].size();

    // on first turn, we guess "0, 1, ..., n-1" if truth booth was correct
    // or "1, 0, ..., n-1" if truth booth was incorrect
    if (turn == 1) {
        int fact, i;
        for (i = 2, fact = 1; i <= sz; fact *= i++)
            ;
        if (possible_perms.size() == fact) {
            next_guess.first = digits.substr(0, sz);
            next_guess.second = 1;
        } else {
            next_guess.first = "10" + digits.substr(2, sz - 2);
            next_guess.second = 1;
        }
    } else if (possible_perms.size() == 1) {
        next_guess.first = possible_perms[0];
        next_guess.second = possible_perms[0].size();
    } else {
        // run multi-threaded minimax to get next guess
        pair<string, int> candidates[NUM_CHUNKS];
        vector<thread> jobs;
        make_chunks(ORIGPERMS, chunks, NUM_CHUNKS);
        struct args **args = create_args(possible_perms, candidates, chunks[0], 0);

        for (int j = 0; j < NUM_CHUNKS; j++) {
            args[j]->chunk = &(chunks[j]);
            args[j]->thread_id = j;
            jobs.push_back(thread(get_score, args[j]));
        }
        for (int j = 0; j < NUM_CHUNKS; j++) {
            jobs[j].join();
        }

        next_guess.first = min_candidate(candidates, NUM_CHUNKS);
        next_guess.second = wc_response(next_guess.first, possible_perms);

        for (int j = 0; j < NUM_CHUNKS; j++)
            free(args[j]);
        free(args);
    }

    guessed.push_back(next_guess.first);

    return next_guess;
}

struct args **create_args(vector<string> &perms, pair<string, int> *cd, vector<string> &chunk, int thread_id)
{
    struct args **args = (struct args **) malloc(sizeof(struct args*)*NUM_CHUNKS);
    assert(args);
    for (int i = 0; i < NUM_CHUNKS; i++) {
        args[i] = (struct args *) malloc(sizeof(struct args));
        assert(args[i]);
        args[i]->perms = &perms;
        args[i]->cd = cd;
    }

    return args;
}

// make_chunks:  return pointers to n (nearly) equally sized vectors
//                from the original vector
void make_chunks(vector<string> &orig, vector<vector<string> > &chunks, int n)
{
    int sz = orig.size();
    int chunk_sz = sz / n;
    int n_with_extra = sz % n;
    vector<string>::iterator b = orig.begin();
    vector<string>::iterator e;

    for (int i = 0; i < n; i++) {
        int m = chunk_sz;    // size of this chunk
        if (n_with_extra) {
            ++m;
            --n_with_extra;
        }
        e = b + m;
        vector<string> subvec(b, e);
        chunks.push_back(subvec);
        b = e;
    }
}

// min_candidate:  string with min int from array of pair<string, ints>
string min_candidate(pair<string, int> *candidates, int n)
{
    int i, minsofar;
    string minstring;

    minstring = candidates[0].first;
    minsofar = candidates[0].second;
    for (i = 1; i < n; ++i) {
        if (candidates[i].second < minsofar) {
            minsofar = candidates[i].second;
            minstring = candidates[i].first;
        }
    }

    return minstring;
}

// get_score:  find the maximum number of remaining solutions over all
//             possible responses to the query s
//             this version takes a chunk and finds the guess with lowest score
//             from that chunk
void get_score(struct args *args)
{
    // parse the args struct
    vector<string> &chunk = *(args->chunk);
    vector<string> &perms = *(args->perms);
    pair<string, int> *cd = args->cd;
    int thread_id = args->thread_id;

    typedef vector<string>::const_iterator vec_iter;
    int sz = perms[0].size();

    pair<string, int> best_guess;
    best_guess.second = perms.size();
    int wc_num_remaining;
    for (vec_iter s = chunk.begin(); s != chunk.end(); ++s) {
        vector<int> matches(sz + 1, 0);
        for (vec_iter p = perms.begin(); p != perms.end(); ++p) {
            ++matches[evaluate(*s, *p)];
        }
        wc_num_remaining = *max_element(matches.begin(), matches.end());
        if (wc_num_remaining < best_guess.second) {
            best_guess.first = *s;
            best_guess.second = wc_num_remaining;
        }
    }

    cd[thread_id] = best_guess;

    return;
}

// wc_response:  the response to guess that eliminates the least solutions
int wc_response(string &guess, vector<string> &perms)
{
    map<int, int> matches_eval;

    for (vector<string>::iterator it = perms.begin(); it!=perms.end(); ++it) {
        ++matches_eval[evaluate(guess, *it)];
    }

    return max_element(matches_eval.begin(), matches_eval.end(), prcmp)->first;
}

// prcmp:  comparison function for pair<int, int> types in map
bool prcmp(pair<int, int> x, pair<int, int> y)
{
    return x.second < y.second;
}

void sequence_print(const string s)
{
    for (string::const_iterator i = s.begin(); i != s.end(); i++) {
        if (i == s.begin())
            cout << "(";
        cout << *i;
        if (i != s.end() - 1)
            cout << ", ";
        else
            cout << ")";
    }
}

Chris Chute

Posted 2014-10-31T01:05:27.093

Reputation: 131

I've moved this to Are You The One? on GitHub with updated, faster code.

– Chris Chute – 2016-07-26T21:14:35.820