逻辑游戏,9个方形卡和一个大方块

时间:2011-02-20 12:36:49

标签: algorithm logic

我不确定这是不是正确的地方,也许是其他一些堆栈交换,告诉我我会在其他地方发帖。

这是我的问题,我在朋友的地方发现了一个旧游戏,它应该是一个心灵游戏:9张小方卡,你必须放置它们以便它们全部融合在一起,这是一张图片:< / p>

the game to solve

在比赛前几个小时后,我认为没有真正简单的公平方式来完成比赛,所以我采用了程序化的方式。

这是我遇到困难的地方,我虽然可以使用一些随机函数,一个大循环,然后将其完成。但是有一些类似(4 * 9)^ 9的解决方案,所以看起来并不那么容易。

这是我写的代码,现在这个代码很无用: 每次我进入循环,我都会乱洗我的阵列,按随机值旋转我的牌并检查拼图是否正确,浪费了很多周期,但我不知道从哪里开始使它更有效率。< / p>

编辑: 固定代码,我得到了一些带有8张牌的牌组,但没有9张牌,如果有人修复我的代码,或者可能没有解决方案?

require 'json'

class Array
    def rotate n
        a =dup
        n.times do a << a.shift end
        a
    end
end


@grid = [[{"type"=>"p", "head" => 1},{"type"=>"c", "head" => 1},{"type"=>"a", "head" => 2},{"type"=>"o", "head" => 2}],
[{"type"=>"o", "head" => 1},{"type"=>"a", "head" => 2},{"type"=>"c", "head" => 2},{"type"=>"p", "head" => 1}],
[{"type"=>"c", "head" => 1},{"type"=>"p", "head" => 2},{"type"=>"o", "head" => 2},{"type"=>"a", "head" => 1}],
[{"type"=>"p", "head" => 1},{"type"=>"c", "head" => 2},{"type"=>"o", "head" => 2},{"type"=>"a", "head" => 1}],
[{"type"=>"p", "head" => 2},{"type"=>"c", "head" => 2},{"type"=>"a", "head" => 1},{"type"=>"c", "head" => 1}],
[{"type"=>"a", "head" => 1},{"type"=>"p", "head" => 2},{"type"=>"o", "head" => 2},{"type"=>"p", "head" => 1}],
[{"type"=>"a", "head" => 1},{"type"=>"o", "head" => 1},{"type"=>"a", "head" => 2},{"type"=>"c", "head" => 2}],
[{"type"=>"o", "head" => 1},{"type"=>"a", "head" => 2},{"type"=>"c", "head" => 2},{"type"=>"p", "head" => 1}],
[{"type"=>"p", "head" => 1},{"type"=>"c", "head" => 2},{"type"=>"o", "head" => 2},{"type"=>"a", "head" => 1}]]
@new_grid = [nil, nil, nil,nil, nil, nil,nil, nil, nil]
@used = [false, false, false,false, false, false,false, false, false]

def check_validity(card, position, orientation)
    # since I'm adding from top left to bottom, I only need to check top and left           
    try_card = @grid[card].rotate orientation     
    valid = true
    # top
    if (@new_grid[position-3])
        if (try_card[0]["type"] != @new_grid[position-3][2]["type"] || try_card[0]["head"] == @new_grid[position-3][2]["head"])
            valid = false
        end
    end
    # left
    if (@new_grid[position-1] && (position % 3) != 0)
        if (try_card[3]["type"] != @new_grid[position-1][1]["type"] || try_card[3]["head"] == @new_grid[position-1][1]["head"])
            valid = false       
        end
    end
    return valid
end


def solve_puzzle(position)
    (0..8).each do |card|
        unless (@used[card])
            (0..3).each do |orientation|
                if (check_validity(card, position, orientation))
                    @used[card] = true
                    @new_grid[position] = @grid[card].rotate orientation
                                        if position == 7 
                                            puts @new_grid.to_json                                      
                                        end
                    if (position < 8)
                        solve_puzzle(position + 1)
                    else
                        puts "I WON"
                        puts @new_grid.to_json
                    end
                    @new_grid[position] = nil
                    @used[card] = false
                end
            end
        end
    end
end


solve_puzzle(0)

6 个答案:

答案 0 :(得分:2)

使用带有修剪的递归。我的意思是当你把当前卡片放入时,它必须与你已经放置的卡片的方向相匹配。所以你消除了许多不可能的情况:)

像这样:

    void generate(int whichPos) //whichPos is from 1 to 9
    {
       for (int card = 1; card <= 9; card++)
       {
          if (used[card]) continue;

          for (int orientation = 0; orientation < 4; orientation++)
          {
              if (orientation does match other cards from 1 to whichPos - 1 in the grid)
              { 
                  used[card] = true;
                  saveInGrid();
                  generate(whichPos + 1);
                  used[card] = false;
              }       
          }  
        }
    }

    generate(1);

答案 1 :(得分:2)

我曾经做过限制性编程研究直到最近才开始生活,我想我可以提供一些建议。

您最好的选择是尝试使用一些合理的搜索启发式方法进行生成和测试,并尽量减少浪费的搜索工作量。

以这种方式思考问题:你要分配九个逻辑变量,{x1,...,x9},其中x1,x2,x3是底行,x4,x5,x6是中间行,和x7,x8,x9在顶行。

每个变量可以取自集合D = {(p,r)|的三十六个可能值中的一个p是一块{p1,p2,...,p9},r是一个旋转{0,90,180,270}}。

解决方案是从D分配到x1,...,x9,使得每个部分仅用于一个分配,并且每对相邻的分区具有兼容的分配(即,边缘匹配)。

您的搜索应跟踪每个变量的可能分配的域。特别是:

  • 如果你将一个片段pi分配给变量xj,那么你必须从所有其他变量的域中删除所有可能的pi值;
  • 如果为变量xj分配值(pi,r),则必须从xj的邻居中删除所有不兼容的赋值;
  • 如果您删除了变量域中的所有可能分配,那么您就知道自己已经走到了死胡同并且必须回溯;
  • 如果您将变量的可能分配集合减少为单个值,那么您就知道必须赋值给该变量;
  • 如果你想要花哨,你可以使用回跳而不是简单的回溯(这是你回到最近的冲突决定失败的地方,阻止你分配一个变量,而不是只是回溯到前面的决定)

一个好的搜索策略是始终选择剩余域最小的变量来尝试下一步分配。这样,您始终可以查看受搜索分支决策影响最大的变量。

无论如何,希望这有帮助。

干杯!

答案 2 :(得分:1)

我认为你的错误是有效的。您无需检查位置3和6的左侧,因为这些位于拼图的左侧,不需要与前一行的右侧匹配。

编辑:这是我想到的路线:

# left
if (@new_grid[position-1] && (position % 3) != 0)
    if (try_card[3]["type"] != @new_grid[position-1][1]["type"] || try_card[3]["head"] == @new_grid[position-1][1]["head"])
        valid = false       
    end
end

编辑2:检查你的作品,我看到以下作为中心部分:

[{"type"=>"p", "head" => 2},{"type"=>"c", "head" => 2},{"type"=>"a", "head" => 1},{"type"=>"c", "head" => 2}],

我相信应该

[{"type"=>"p", "head" => 2},{"type"=>"c", "head" => 2},{"type"=>"a", "head" => 1},{"type"=>"c", "head" => 1}],

答案 3 :(得分:1)

图像中的方块实际上没有解决方案。我通过将最后一块的右侧的绿色/蓝色底部改为红色/白色底部(就像右上方的那个部分一样)来实现它。

随机化是一个坏主意,最好做一个详尽的搜索,你尝试像Petar Minchev回答的每一种可能性。您可以通过预处理加快速度。除了第一部分,您将始终知道您需要匹配的一个或两个边。每种类型在所有9个中仅有3到6个实例。想象一下,你有这些空间:

0   1   2
3   4   5
6   7   8

最初我通过填充位置4,然后填充1,3,5,7,最后填充角落来完成此操作。这在3.5毫秒内发现了4个解决方案,其中2371个递归调用。通过将顺序改为4,1,5,2,7,8,3,0,6,只有813个递归调用,它降至1.2毫秒,因为在拐角处放置的选项较少。现在考虑一下,按顺序排在中间,但修改后的顺序也会一样快(0,1,3,4,2,5,6,7,8)。重要的是,必须检查两场比赛会缩短您进行昂贵的递归通话的次数。

Step[] Steps = {
    new Step() { Type = 0, Position = 4 },
    new Step() { Type = 1, Position = 1, MatchP1 = 4, MatchO1 = 0 },
    new Step() { Type = 1, Position = 5, MatchP1 = 4, MatchO1 = 1 },
    new Step() { Type = 2, Position = 2, MatchP1 = 5, MatchO1 = 0, MatchP2 = 1, MatchO2 = 1 },
    new Step() { Type = 1, Position = 7, MatchP1 = 4, MatchO1 = 2 },
    new Step() { Type = 2, Position = 8, MatchP1 = 7, MatchO1 = 1, MatchP2 = 5, MatchO2 = 2 },
    new Step() { Type = 1, Position = 3, MatchP1 = 4, MatchO1 = 3 },
    new Step() { Type = 2, Position = 0, MatchP1 = 1, MatchO1 = 3, MatchP2 = 3, MatchO2 = 0 },
    new Step() { Type = 2, Position = 6, MatchP1 = 3, MatchO1 = 2, MatchP2 = 7, MatchO2 = 3 },
};

以下是我的卡片的设置方式,请注意我更改了最后一张卡片的一面以获得解决方案。 1为红色,2为脂肪,3为蓝色/绿色,4为黄色。我用0x10来表示它是同一颜色的底部。这样你可以xor 2类型和0x10比较,看看它们是否匹配,或者你可以xor类型与0x10找到你要找的类型。

Card[] cards = {
    new Card(0x01, 0x03, 0x14, 0x12),
    new Card(0x02, 0x14, 0x13, 0x01),
    new Card(0x03, 0x11, 0x12, 0x04),

    new Card(0x01, 0x13, 0x12, 0x04),
    new Card(0x11, 0x13, 0x04, 0x03),
    new Card(0x04, 0x11, 0x12, 0x01),

    new Card(0x04, 0x02, 0x14, 0x13),
    new Card(0x02, 0x14, 0x13, 0x01),
//              new Card(0x01, 0x13, 0x12, 0x04) // no solution
    new Card(0x01, 0x11, 0x12, 0x04) // 4 solutions
};

当进行预处理时,我希望有一个按我正在寻找的类型索引的数组,这将给我所有具有该类型的卡以及该类型所在的方向。我还在那里抛出NextType(顺时针),以便更容易地比较角落:

public CardImageOrientation[][] Orientations { get; set; }

public struct CardImageOrientation
{
    public int CardIndex;
    public int TypePosition;
    public int NextType;
}

// Orientations[1] is an array of CardImageOrientation structs
// that tell me what cards contain a side with type 1, what position
// it is in, and what the next type clockwise is

这是我的主要递归方法:

public bool Method1Step(int stepIndex)
{
    StepCalls++;
    if (stepIndex > 8) // found a match
    {
        FindCount++;
        return !Exhaustive; // false return value will keep going if exhaustive flag is true
    }

    Step step = Steps[stepIndex];
    switch (step.Type)
    {
        case 0:
            // step 0 we just loop through all cards and try them in position 4 with orientation 0
            for (int i = 0; i < 9; i++)
            {
                PlaceCard(i, 4, 0);
                steppedUp = true;
                if (Method1Step(stepIndex + 1))
                    // found a solution, return true (will be false below for exhaustive)
                    return true;
                RemoveCard(4);
            }
            break;
        case 1:
        case 2:
            // step type 1 we simply try to match one edge with another, find card in position we are matching with
            Card card = Cards[CardIndices[step.MatchP1]];

            // to find the actual type in that position, we take the position we are looking for, subtract card orientation, add 4 and take the lowest two bits
            int type = card.Types[(step.MatchO1 - card.Orientation + 4) & 0x03];

            // find opposite orientation where we need to put the match in the empty spot
            int orientation2 = (step.MatchO1 + 2) & 0x03;

            // looking for type that is the opposite of the existing type
            int searchType = type ^ 0x10;
            for (int i = 0; i < Orientations[searchType].Length; i++)
            {
                // try one card value that matches
                CardImageOrientation cio = Orientations[searchType][i];

                // make sure it isn't in use
                if (Cards[cio.CardIndex].Position < 0)
                {
                    // check either we are step 1 or that second type matches as well
                    if (step.Type == 1 || (
                            step.Type == 2 && 
                            (Cards[CardIndices[step.MatchP2]].Types[(step.MatchO2 - Cards[CardIndices[step.MatchP2]].Orientation + 4) & 0x3] ^ cio.NextType) == 0x10)
                        ) {
                        // get new orientation for card
                        int newOrientation = (orientation2 - cio.TypePosition + 4) & 0x03;

                        PlaceCard(cio.CardIndex, step.Position, newOrientation);
                        if (Method1Step(stepIndex + 1))
                            // found solution and non-exhaustive so return true
                            return true;
                        RemoveCard(step.Position);
                    }
                }
            }
            break;
    }
    return false; // dead end or exhaustive search
}

有趣的说明:我编写了代码,使用现有的改进套牌随机化卡片,其中4个解决方案和9999个其他套牌使用1到9999之间的随机种子创建。我发现3,465个解决方案在7.2秒内分布在1,555个起始布局上,因此它平均为每次运行约0.72毫秒。它总共对Method1Step()进行了416万次调用。

答案 4 :(得分:0)

我确信知道prolog在这里会有很大的帮助,但我认为我没有资格回答这个问题。但是,我可以帮助您了解当前解决方案的可行性:

9件可放9件! = 362880个排列(仅限位置)

它们每个都可以旋转4种方式 - &gt; 4 ^ 9 = 262114

但是你将有4个解决方案,它们只是彼此的旋转,所以/ = 4。

在找到解决方案之前,平均可以尝试23,781,703,680个安排:(

答案 5 :(得分:0)

此问题是tiling puzzle的另一种形式,其他形式包括PentominoesSoduku

解决这类谜题的最有效算法之一是由Algorithm X开发的Donald Knuth。算法X的最佳实现技术之一称为Dancing Links

算法X在解决谜题方面非常有效的关键原因(在几毫秒内解决Suduku是典型的)是无用的配置被有效地从搜索空间中移除。

例如,对于你的拼图,如果左上角的片段在南边有一个Getafix头,则可以消除在其下方的北边缘没有Getafix脚的每个解决方案配置。

在为算法X制定约束矩阵方面,您有9个部件,每个部件可以放置在9个具有4个方向的位置,因此您的约束矩阵将有324行,每个可能的部件放置一个。

识别约束列更复杂。您将需要9个约束列 - 每个位置一个,以确保碎片不在同一位置。您还需要一堆额外的列来捕获跨越每条边的字符必须兼容的约束。有十二个交互点,四个字符,每个字符分成两部分 - 另外96列。

<强>更新

这是开始约束矩阵的一个尝试。让我们看一下OP图片中显示的第一部分,它包含以下四部分:

  • Getafix Head(北)
  • 罗马头(东)
  • Asterix Feet(South)
  • Obelix Feet(West)

对于我们的约束矩阵,我们需要

  • 9个职位列(TopLeft,TopCentre,... MiddleCentre,... BottomRight)
  • 9列(Piece1 ... Piece9)
  • 交互点的96列

我们通过列出从特定位置获得的东西以及该位置阻止的东西来构建一个约束行。

  • 地点:TopLeft,Piece1
  • 包括:TopLeftEast-Roman-Head,TopLeftSouth-Asterix-Feet
  • 排除:TopCentreWest-Getafix-Head,TopCentreWest-Getafix-Feet,TopCentreWest-Obelix-Head,TopCentreWest-Obelix-Feet,TopCentreWest-Asterix-Feet,TopCentreWest-Asterix-Head,TopCentreWest-Roman-Head
    (基本上,在TopCentreWest只有罗马脚是好的)
  • 排除:MiddleLeftNorth-Obelix-Head,MiddleLeftNorth-Obelix-Feet,MiddleLeftNorth-Getafix-Head,MiddleLeftNorth-Getafix-Feet,MiddleLeftNorth-Roman-Head,MiddleLeftNorth-Roman-Feet,MiddleLeftNorth-Asterix-Feet
    (只有Asterix-Head在MiddleLeftNorth没问题)

现在,在TopLeft位置再对此作品的其他三个方向再重复3次......

  • 地点:TopLeft,Piece1
  • 包括:TopLeftEast-Getafix-Head,TopLeftSouth-Roman-Head
  • ...

  • 地点:TopLeft,Piece1

  • 包括:TopLeftEast-Obelix-Feet,TopLeftSouth-Getafix-Head
  • ...

  • 地点:TopLeft,Piece1

  • 包括:TopLeftEast-Asterix-Feet,TopLeftSouth-ObelixFeet
  • ...

对8个剩余空间中的4个方向中的每个方向重复这4行8次(另外32行,总共36行)。

然后,为剩余的8件(每288行)的每个位置/方向再创建36行。

您可能希望编写代码来生成矩阵,而不是手动计算它!