【问题标题】:Logic game, 9 square cards and one big square逻辑游戏,9张方牌和一个大方格
【发布时间】:2011-02-20 12:36:48
【问题描述】:

我不确定这是否是正确的去处,也许是其他一些 stackexchange,告诉我我会在其他地方发布。

这是我的问题,我在朋友家发现了一个老游戏,应该是一个智力游戏:9张小方块卡,你必须把它们放在一起,让它们全部融合在一起,这是一张图片:

在游戏前几个小时后,我发现没有真正简单公平的方式来完成游戏,所以我采用了编程方式。

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

这是我写的代码,暂时没用: 每次我进入循环时,我都会洗牌,随机旋转我的卡片并检查拼图是否正确,浪费了很多循环,但我不知道从哪里开始让它更有效率。

编辑: 固定代码,我得到了一些 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)

【问题讨论】:

  • 没有解决办法。我总是以遗漏卡片 3 或卡片 8 结束。他们要么需要先红后下(顺时针)、绿/蓝上加红底、绿/蓝上加黄底、红底后红顶或绿/蓝底后红底。
  • 好吧,所有这些想法都是为了一个没有解决方案的游戏,真可惜;)至少我学到了很多!谢谢

标签: algorithm logic


【解决方案1】:

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

像这样:

    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);

【讨论】:

  • 好的,我知道你用 used[card] 做了什么,我不应该从我的阵列中移除卡,以便在我到达最后一张卡时我的循环缩小吗?
  • 好的,第二期,这个程序不是要把第一张卡添加为第一张卡吗?
  • 不,whichPos 是位置,卡片是单独的变量。尝试实现它并理解它。以上代码为伪代码。
  • 是的,我现在正在实现,但是如果你用 card = 1 初始化你的 for 循环,我猜你需要洗牌一次,否则你总是会尝试第一张卡位置 0 的甲板,它会通过,因为还没有什么可以尝试的。
  • 当 whichPos 为 1 时,您尝试每张卡片 - 参见循环 (int card = 1; card &lt;= 9; card++)
【解决方案2】:

最近我一直以做约束规划研究为生,我想我可以提供一些建议。

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

这样想问题:你有九个逻辑变量要赋值,{x1, ..., x9},其中 x1, x2, x3 是底行,x4, x5, x6 是中间行,和 x7, x8, x9 最上面一行。

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

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

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

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

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

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

干杯!

【讨论】:

  • 接下来:首先为中心图块选择一个分配,因为这将对搜索树产生最强的修剪效果。
【解决方案3】:

我认为您的错误在于检查有效性。您不需要检查位置 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}],

【讨论】:

  • 我在几个圣诞节前在 perl 中做了这些之一,我会看看我是否可以挖掘出非常相似的解决方案。编辑:快速搜索一无所获,我猜我删除了那个旧脚本。
  • 你说得对!谢谢,我现在得到了几副 8 张牌,但仍然没有 9 张牌。
  • 编辑了我的评论,添加了一个额外的修复,即您的文章中的一个错字。
  • 你又是对的,我修复了我的代码,我现在找到了 10 副左右的 8 张牌,但仍然没有 9 张牌。我倾向于无解决方案?
  • 我现在想知道是否有人把一块弄混了,因为第 2 块和第 8 块是一样的。取 8 个解决方案中的一个,并将剩余的部分更改为可以验证您的代码是否良好的一个(您可以获得 4 个解决方案,每个起始角一个)。编辑:还有第 4 和第 9 件。
【解决方案4】:

图像中的方块实际上没有解。我通过将最后一块右侧的绿色/蓝色底部更改为红色/白色底部来使其工作(就像它上面的右侧一样)。

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

0   1   2
3   4   5
6   7   8

最初我是通过填充位置 4 来实现的,然后是 1、3、5、7,最后是角落。这在 3.5 毫秒内通过 2371 次递归调用找到了 4 个解决方案。通过将顺序更改为 4、1、5、2、7、8、3、0、6,它下降到 1.2 毫秒,只有 813 次递归调用,因为角落里的选项更少。现在考虑一下,按顺序进行会介于两者之间,但修改后的顺序会同样快(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 是黄色。 I xor 与 0x10 表示它是相同颜色的底部。这样,您可以对 2 种类型进行异或并与 0x10 进行比较以查看它们是否匹配,或者您可以将类型与 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 万次调用。

【讨论】:

    【解决方案5】:

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

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

    算法 X 在解决谜题方面非常高效(通常在几毫秒内解决数独问题)的关键原因在于有效地从搜索空间中删除了无用的配置。

    例如,对于您的拼图,如果左上角的棋子在南边有一个 Getafix 头,那么所有在北边没有 Getafix 脚的解决方案配置都可以消除。

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

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

    更新

    这是开始约束矩阵的一个尝试。我们来看看OP图片中的第一块,它有这四个部分:

    • Getafix 头(北)
    • 罗马头(东)
    • Asterix 脚(南)
    • Obelix 脚(西)

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

    • 9 列位置(TopLeft、TopCentre、... MiddleCentre、... BottomRight)
    • 9 列用于片段 (Piece1 ... Piece9)
    • 96 列的交互点

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

    • 位置:左上角,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
      (在 MiddleLeftNorth 只有 Asterix-Head 可以)

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

    • 位置:左上角,Piece1
    • 包括:TopLeftEast-Getafix-Head、TopLeftSouth-Roman-Head
    • ...

    • 位置:左上角,Piece1

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

    • 位置:左上角,Piece1

    • 包括:TopLeftEast-Asterix-Feet、TopLeftSouth-Obelix-Feet
    • ...

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

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

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

    【讨论】:

    • 你能举个更多的例子吗,也许矩阵看起来像 4 块甚至 2 块?
    【解决方案6】:

    我确信知道prolog 在这里会很有帮助,但我认为我没有资格回答这个问题。不过,我或许可以帮助您确定您当前的解决方案有多大可能奏效:

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

    它们每个都可以旋转 4 次 -> 4^9 = 262114

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

    在找到解决方案之前,平均有 23,781,703,680 种可能的安排可供尝试:(

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-12-13
      • 2019-07-13
      • 2011-12-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多