Метод решета в игре «Быки и коровы»

в 9:49, , рубрики: игры, логика, Программирование, метки: , ,

Здравствуйте.
Еще осенью на 2 курсе в качестве лабораторной работы по «Теории автоматов» преподаватель на ходу придумывал нам задания, ориентируяюсь на наши пожелания в оценке. В основном это были игры. Кому-то достался хоккей, кому-то теннис, мне же досталась не столь известная логическая игра «Быки и коровы».

image

Нужно было реализовать хоть какое-то обоснованное поведение компьютера в игре с человеком. Но я пошел дальше и уже через месяц компьютер в большинстве случаев легко обыгрывал моих однокурсников. А по предмету был получен автомат. Программу получите после описания алгоритмов.

Суть игры

Игрок и компьютер загадывают четырехзначные числа, используя цифры от 0 до 9. Игроки пытаются разгадать числа соперника, посылая ему возможные варианты чисел, в ответ получая два числа — число «быков» и число «коров». Что же это за числа такие?

  • «Быки» — правильные цифры на правильных местах. Четыре «быка» — залог победы, мечта каждого фермера.
  • «Коровы» — правильные цифры на неправильных местах.

Для понимания приведу пример:

Загадано число 1622. Если мы предположим 6112, то в ответ придет: 1 бык(четвертая цифра «2» на своем месте), 2 коровы(шестерка и единица не на своих местах).

Оперируя данными о «скоте» противника, нужно угадать число быстрей него.

Первый же тривиальный алгоритм, который так и напрашивается, — это перебирать наборы «1111», «2222», «3333»... до тех пор, пока не будет получен полный набор, а затем генерировать перемещения этого набора.

Например, загадано то же число 1622, мы предполагаем «1111», получаем в ответ «быка», затем «2222» — получаем в ответ уже 2 «быков», «3333» — пусто, «4444» — пусто, «5555» — пусто, «6666»1 бык.
Дальше продолжать не будем, так как набралось уже 4 быка в сумме. Осталось найти нужную комбинацию. Будем генерировать перестановки, пока не получим Та-дааам: «1226», «1262», «1226», «1262», «1622». Все.

Очевидно, что алгоритм не очень хорош, зато понятный и не запутаться. Максимальное число ходов для угадывания — 10(«7890»)+24 перестановки. 34 — это в самом худшем случае. Конечно возможно перебор и перестановки всячески оптимизировать, например перебирать поочередно с двух концов — «1111», «0000», «2222», «9999»... так же оптимизировать генерацию перестановок при наличии одинаковых цифр(как в нашем примере — несколько раз спрашиваем одно и то же).
Но не будем этим заниматься. Пусть данная стратегия будет у нас легким уровнем сложности компьютера.

Много я сидел на парах и играл сам с собой, пытаясь придумать какой то свой крутой алгоритм. Но приходили только единичные идеи, из которой я не мог составить единой стратегии. Начал изучать литературу. Наткнулся на статьи, рода «угадывание за 7 ходов». Они меня не привлекли, поскольку там очень много ветвления. Но прочитав книгу нашего Кировского профессора(но не из нашего университета) С.М. Окулова «Программирование в алгоритмах», я нашел описание довольно простого и достаточно эффективного алгоритма. В нем используется так называемый «метод решета» (примером может служить известное «решето Эрастофена» — классическая задача поиска простых чисел). Мы рассматриваем конечное множество всех возможных чисел и каждый ход исключаем все элементы множества, не представляющие интереса.

Например, для загаданного числа 1234 мы предположили 5678, и получили 0 быков и 0 коров, чего думать — мы исключаем все числа, содержащие 5, 6, 7, 8. Сразу можете прикинуть, сколько отнимется из 10000. Не стоит пугаться множества из 10000 элементов. За 5-6 ходов останется всего несколько чисел.

Начнем со структур данных. Код на паскале:

Const Pmax=10000;
Type Post=string[4];
Var A:array[1..Pmax] of Post; //множество
      B:array[1..Pmax] of boolean; // массив флажков, 1 - значит подходит, 0 - исключено

Инициализация:


t:=1;
 for i:=0 to 9 do
  for j:=0 to 9 do
   for k:=0 to 9 do
    for l:=0 to 9 do begin
     a[t]:=inttostr(i)+inttostr(j)+inttostr(k)+inttostr(l); // формируем множество
     inc(t); end;
 for i:=1 to pmax do  
       b[i]:=true; // по умолчанию все числа подходят

Функция, реализующая анализ элемента множества по значениям переменных (bk,kr — быки и коровы)

function pr(a,b:post;bk,kr:integer):boolean; //b - наш ход, a- элемент "тестируемого" множества
var i,x:integer;
begin
 x:=0;
 for i:=1 to 4 do // проверка по быкам
  if a[i]=b[i] then inc(x);
 if x<>bk then
  begin
   pr:=false;
   exit;
  end;
 x:=0;
 for i:=1 to 4 do // проверка по коровам
  if (a[i]<>b[i]) and (pos(b[i],a)<>0)
   then inc(x);
  if x<kr then
   begin
    pr:=false;
    exit;
   end;
  pr:=true;
end;

таким образом после каждого нашего хода запускаем решето


for i:=1 to Pmax do
if b[i] and not pr(a[i],hod,bk,kr) then  b[i]:=false;

В заключение можно сказать, что алгоритм затратен к памяти и по сравнению со стандартными алгоритмами будет думать «дольше», но насколько же он проще и вполне оптимизируется.
Ну вот и все, совсем не сложно. Первая моя статья, судить строго.

Как и обещал, ссылка на мою игрульку со второго курса, чтоб на себе почувствовать метод «решета»:
Быки и коровы

Автор: allswell

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js