Моделирование шансов в игре SET (MATLAB)

Недавно я обнаружил, что пришла замечательная карта - SET. Вкратце, есть 81 карточка с четырьмя функциями: символ (овал, волнистая линия или ромб), цвет (красный, фиолетовый или зеленый), число (один, два или три) и затенение (сплошное, полосатое или открытое). Задача состоит в том, чтобы найти (из выбранных 12 карточек) НАБОР из 3 карточек, в котором каждый из четырех признаков либо одинаков на каждой карточке, либо все различен на каждой карточке (нет комбинации 2+1).

Я закодировал его в MATLAB, чтобы найти решение и оценить вероятность наличия набора в случайно выбранных картах.

Вот мой код для оценки шансов:

%% initialization
K = 12; % cards to draw
NF = 4; % number of features (usually 3 or 4)
setallcards = unique(nchoosek(repmat(1:3,1,NF),NF),'rows'); % all cards: rows - cards, columns - features
setallcomb = nchoosek(1:K,3); % index of all combinations of K cards by 3

%% test
tic
NIter=1e2; % number of test iterations
setexists = 0; % test results holder
% C = progress('init'); % if you have progress function from FileExchange
for d = 1:NIter
% C = progress(C,d/NIter);    

% cards for current test
setdrawncardidx = randi(size(setallcards,1),K,1);
setdrawncards = setallcards(setdrawncardidx,:);

% find all sets in current test iteration
for setcombidx = 1:size(setallcomb,1)
    setcomb = setdrawncards(setallcomb(setcombidx,:),:);
    if all(arrayfun(@(x) numel(unique(setcomb(:,x))), 1:NF)~=2) % test one combination
        setexists = setexists + 1;
        break % to find only the first set
    end
end
end
fprintf('Set:NoSet = %g:%g = %g:1\n', setexists, NIter-setexists, setexists/(NIter-setexists))
toc

100-1000 итераций — это быстро, но будьте осторожны с большим количеством. Один миллион итераций занимает около 15 часов на моем домашнем компьютере. В любом случае, с 12 картами и 4 функциями у меня примерно 13:1 набора. На самом деле это проблема. В инструкции сказано, что это число должно быть 33:1. Недавно это подтвердил Питер Норвиг. Он предоставляет код Python, но я еще не тестировал его.

Так можно ли найти ошибку? Любые комментарии по улучшению производительности приветствуются.


person yuk    schedule 08.05.2010    source источник
comment
У меня нет для вас расчета, и я не владею матлабом, но я наткнулся на ваш вопрос, который напомнил мне сет-игру, которую я программировал в прошлом году на scala, и которую я хотел опубликовать на freshmeat - но : нет времени на это. Теперь я нашел время, чтобы перевести некоторые немецкие вары, комментарии и сообщения на английский язык и поставить их на сайт для скачивания; Объявление о свежем мясе все равно займет несколько часов. Я посмотрю, насколько он подходит для подсчета количества наборов на странице.   -  person user unknown    schedule 24.06.2011


Ответы (4)


Я решил проблему, написав свою собственную реализацию, прежде чем смотреть ваш код. Моя первая попытка была очень похожа на то, что у вас уже было :)

%# some parameters
NUM_ITER = 100000;  %# number of simulations to run
DRAW_SZ = 12;       %# number of cards we are dealing
SET_SZ = 3;         %# number of cards in a set
FEAT_NUM = 4;       %# number of features (symbol,color,number,shading)
FEAT_SZ = 3;        %# number of values per feature (eg: red/purple/green, ...)

%# cards features
features = {
    'oval' 'squiggle' 'diamond' ;    %# symbol
    'red' 'purple' 'green' ;         %# color
    'one' 'two' 'three' ;            %# number
    'solid' 'striped' 'open'         %# shading
};
fIdx = arrayfun(@(k) grp2idx(features(k,:)), 1:FEAT_NUM, 'UniformOutput',0);

%# list of all cards. Each card: [symbol,color,number,shading]
[W X Y Z] = ndgrid(fIdx{:});
cards = [W(:) X(:) Y(:) Z(:)];

%# all possible sets: choose 3 from 12
setsInd = nchoosek(1:DRAW_SZ,SET_SZ);

%# count number of valid sets in random draws of 12 cards
counterValidSet = 0;
for i=1:NUM_ITER
    %# pick 12 cards
    ord = randperm( size(cards,1) );
    cardsDrawn = cards(ord(1:DRAW_SZ),:);

    %# check for valid sets: features are all the same or all different
    for s=1:size(setsInd,1)
        %# set of 3 cards
        set = cardsDrawn(setsInd(s,:),:);

        %# check if set is valid
        count = arrayfun(@(k) numel(unique(set(:,k))), 1:FEAT_NUM);
        isValid = (count==1|count==3);

        %# increment counter
        if isValid
            counterValidSet = counterValidSet + 1;
            break           %# break early if found valid set among candidates
        end
    end
end

%# ratio of found-to-notfound
fprintf('Size=%d, Set=%d, NoSet=%d, Set:NoSet=%g\n', ...
    DRAW_SZ, counterValidSet, (NUM_ITER-counterValidSet), ...
    counterValidSet/(NUM_ITER-counterValidSet))

После использования Профилировщика для обнаружения горячих точек некоторые улучшения могут быть достигнуты, главным образом, за счет раннего выхода из циклов, когда это возможно. Основным узким местом является вызов функции UNIQUE. Те две строки выше, где мы проверяем допустимые наборы, можно переписать как:

%# check if set is valid
isValid = true;
for k=1:FEAT_NUM
    count = numel(unique(set(:,k)));
    if count~=1 && count~=3
        isValid = false;
        break   %# break early if one of the features doesnt meet conditions
    end
end

К сожалению, симуляция все еще медленная для более крупной симуляции. Таким образом, мое следующее решение — это векторизованная версия, где для каждой итерации мы строим единую матрицу всех возможных наборов из 3 карт из руки из 12 вытянутых карт. Для всех этих наборов-кандидатов мы используем логические векторы, чтобы указать, какая функция присутствует, избегая, таким образом, вызовов UNIQUE/NUMEL (мы хотим, чтобы на каждой карте набора были все одинаковые или все разные функции).

Я признаю, что код стал менее читаемым и трудным для понимания (поэтому я выложил обе версии для сравнения). Причина в том, что я пытался максимально оптимизировать код, чтобы каждый итерационный цикл был полностью векторизован. Вот окончательный код:

%# some parameters
NUM_ITER = 100000;  %# number of simulations to run
DRAW_SZ = 12;       %# number of cards we are dealing
SET_SZ = 3;         %# number of cards in a set
FEAT_NUM = 4;       %# number of features (symbol,color,number,shading)
FEAT_SZ = 3;        %# number of values per feature (eg: red/purple/green, ...)

%# cards features
features = {
    'oval' 'squiggle' 'diamond' ;    %# symbol
    'red' 'purple' 'green' ;         %# color
    'one' 'two' 'three' ;            %# number
    'solid' 'striped' 'open'         %# shading
};
fIdx = arrayfun(@(k) grp2idx(features(k,:)), 1:FEAT_NUM, 'UniformOutput',0);

%# list of all cards. Each card: [symbol,color,number,shading]
[W X Y Z] = ndgrid(fIdx{:});
cards = [W(:) X(:) Y(:) Z(:)];

%# all possible sets: choose 3 from 12
setsInd = nchoosek(1:DRAW_SZ,SET_SZ);

%# optimizations: some calculations taken out of the loop
ss = setsInd(:);
set_sz2 = numel(ss)*FEAT_NUM/SET_SZ;
col = repmat(1:set_sz2,SET_SZ,1);
col = FEAT_SZ.*(col(:)-1);
M = false(FEAT_SZ,set_sz2);

%# progress indication
%#hWait = waitbar(0./NUM_ITER, 'Simulation...');

%# count number of valid sets in random draws of 12 cards
counterValidSet = 0;
for i=1:NUM_ITER
    %# update progress
    %#waitbar(i./NUM_ITER, hWait);

    %# pick 12 cards
    ord = randperm( size(cards,1) );
    cardsDrawn = cards(ord(1:DRAW_SZ),:);

    %# put all possible sets of 3 cards next to each other
    set = reshape(cardsDrawn(ss,:)',[],SET_SZ)';
    set = set(:);

    %# check for valid sets: features are all the same or all different
    M(:) = false;            %# if using PARFOR, it will complain about this
    M(set+col) = true;
    isValid = all(reshape(sum(M)~=2,FEAT_NUM,[]));

    %# increment counter if there is at least one valid set in all candidates
    if any(isValid)
        counterValidSet = counterValidSet + 1;
    end
end

%# ratio of found-to-notfound
fprintf('Size=%d, Set=%d, NoSet=%d, Set:NoSet=%g\n', ...
    DRAW_SZ, counterValidSet, (NUM_ITER-counterValidSet), ...
    counterValidSet/(NUM_ITER-counterValidSet))

%# close progress bar
%#close(hWait)

Если у вас есть Parallel Processing Toolbox, вы можете легко заменить простой цикл FOR параллельным PARFOR (вы можете снова переместить инициализацию матрицы M внутрь цикла: замените M(:) = false; на M = false(FEAT_SZ,set_sz2);)

Вот некоторые примеры результатов 50000 симуляций (используется PARFOR с пулом из 2 локальных экземпляров):

» tic, SET_game2, toc
Size=12, Set=48376, NoSet=1624, Set:NoSet=29.7882
Elapsed time is 5.653933 seconds.

» tic, SET_game2, toc
Size=15, Set=49981, NoSet=19, Set:NoSet=2630.58
Elapsed time is 9.414917 seconds.

И с миллионом итераций (PARFOR на 12, no-PARFOR на 15):

» tic, SET_game2, toc
Size=12, Set=967516, NoSet=32484, Set:NoSet=29.7844
Elapsed time is 110.719903 seconds.

» tic, SET_game2, toc
Size=15, Set=999630, NoSet=370, Set:NoSet=2701.7
Elapsed time is 372.110412 seconds.

Отношение шансов согласуется с результатами, представленными Петером Норвигом.

person Amro    schedule 11.07.2011

Вот векторизованная версия, где 1 миллион рук можно рассчитать примерно за минуту. Я получил с ним около 28:1, так что все еще может быть что-то не так с поиском «всех разных» наборов. Я предполагаю, что это то, с чем у вашего решения тоже проблемы.

%# initialization
K = 12; %# cards to draw
NF = 4; %# number of features (this is hard-coded to 4)
nIter = 100000; %# number of iterations

%# each card has four features. This means that a card can be represented
%# by a coordinate in 4D space. A set is a full row, column, etc in 4D
%# space. We can even parallelize the iterations, at least as long as we
%# have RAM (each hand costs 81 bytes)
%# make card space - one dimension per feature, plus one for the iterations
cardSpace = false(3,3,3,3,nIter);

%# To draw cards, we put K trues into each cardSpace. I can't think of a
%# good, fast way to draw exactly K cards that doesn't involve calling
%# unique
for i=1:nIter
    shuffle = randperm(81) + (i-1) * 81;
    cardSpace(shuffle(1:K)) = true;
end

%# to test, all we have to do is check whether there is any row, column,
%# with all 1's
isEqual = squeeze(any(any(any(all(cardSpace,1),2),3),4) | ...
    any(any(any(all(cardSpace,2),1),3),4) | ...
    any(any(any(all(cardSpace,3),2),1),4) | ...
    any(any(any(all(cardSpace,4),2),3),1));
%# to get a set of 3 cards where all symbols are different, we require that
%# no 'sub-volume' is completely empty - there may be something wrong with this
%# but since my test looked ok, I'm not going to investigate on Friday night
isDifferent = squeeze(~any(all(all(all(~cardSpace,1),2),3),4) & ...
    ~any(all(all(all(~cardSpace,1),2),4),3) & ...
    ~any(all(all(all(~cardSpace,1),3),4),2) & ...
    ~any(all(all(all(~cardSpace,4),2),3),1));

isSet = isEqual | isDifferent;

%# find the odds
fprintf('odds are %5.2f:1\n',sum(isSet)/(nIter-sum(isSet)))
person Jonas    schedule 08.05.2010
comment
Спасибо. Хотя результат выглядит лучше и скорость потрясающая, я думаю, что с тестом на сет что-то не так. Так сложно думать в четырехмерном пространстве. isEqual выглядит нормально, и это, вероятно, означает, что существуют 3 карты со всеми функциями, кроме одной. isDifferent я до сих пор не получил. Я понимаю про subvolume, но как это относится к набору правил? Если вы думаете, что все в порядке, не могли бы вы объяснить это, пожалуйста? - person yuk; 10.05.2010
comment
@yuk: Я должен был просто сделать это в 3D, но эй, это была пятница после нескольких кружек пива. В любом случае, я не думаю, что тесты правильные - я неверно истолковал правила. Я тестирую либо одну функцию, одинаковую, либо все функции разные, а не для каждого измерения, все ли функции одинаковы или все разные. Я еще не придумал логику для этого (хотя, признаюсь, я не очень старался). - person Jonas; 10.05.2010

Я нашел свою ошибку. Спасибо Джонас за подсказку с RANDPERM.

Я использовал RANDI для случайного вытягивания K карт, но вероятность повторения даже в 12 картах составляет около 50%. Когда я заменил эту строку на randperm, я получил 33,8: 1 с 10000 итераций, что очень близко к числу в инструкции.

setdrawncardidx = randperm(81);
setdrawncardidx = setdrawncardidx(1:K);

В любом случае, было бы интересно увидеть другие подходы к проблеме.

person yuk    schedule 10.05.2010
comment
Возможно, вы захотите включить это в исходный вопрос. - person MatlabDoug; 11.05.2010

Я уверен, что что-то не так с моим расчетом этих шансов, так как несколько других подтвердили с помощью моделирования, что оно близко к 33:1, как в инструкциях, но что не так со следующей логикой?

Для 12 случайных карт существует 220 возможных комбинаций из трех карт (12!/(9!3!) = 220). Каждая комбинация из трех карт имеет шанс 1/79 оказаться набором, поэтому вероятность того, что три произвольные карты не будут набором, составляет 78/79. Таким образом, если вы изучили все 220 комбинаций и вероятность того, что каждая из них не является набором, равна 78/79, то ваш шанс не найти набор при проверке всех возможных комбинаций будет равен 78/79, возведенным в 220-ю степень, или 0,0606. что составляет ок. Коэффициент 17:1.

Я должен что-то упустить...?

Кристофер

person Christopher Gronbeck    schedule 23.05.2010
comment
Если у вас есть три случайные карты, вероятность того, что они составят набор, составляет 1/79. Это потому, что если у вас есть две случайные карты, из 79 оставшихся карт ровно одна из оставшихся карт составит набор. Итак, если вы выберете три, 78 из 79 раз, у вас не будет сета. - person Christopher Gronbeck; 24.05.2010
comment
Извините, что долго не отвечал. Я думал об этом некоторое время, и я думаю, что ваша логика была бы в порядке, если бы вы случайным образом выбрали 3 карты из всей колоды 220 раз. Но поскольку вы сначала случайным образом выбираете 12 карт, а затем составляете комбинации только из этих карт, это не работает. В любом случае +1 за интересный момент. - person yuk; 29.05.2010
comment
Хотя я не мог точно определить это, ваше беспокойство казалось обоснованным. Поэтому я написал PHP-программу, которая выполняет несколько итераций сделок и проверяет наборы. Используя этот подход с 1 000 000 сделок, скрипт подсчитал, что 96,8% сделок имеют один или несколько наборов. Это согласуется с заявленным шансом 1/30, что сделка не содержит набора. Вы можете запустить код здесь (он выполняет только 10 000 итераций, так как время ожидания веб-сервера истекает через 30 секунд): susdesign.com/temp/set.php Вот удобочитаемая версия кода: susdesign.com/temp/set-code.html Есть мысли? Кристофер - person Christopher Gronbeck; 03.06.2010