ACM问题:硬币翻转,帮助我确定问题的类型

时间:2008-10-24 19:40:31

标签: algorithm

我正在为即将到来的ACM编程竞赛练习一周,我对这个编程问题感到困惑。

问题如下:


你有一个由大小为4的正方形网格组成的拼图。每个网格方格都有一个硬币;每个硬币显示头部(H)和尾部(T)。这里展示了一个这样的难题:

  

H H H H
  T T T T
  H T H T
  T T H T

当前显示Tails(T)的任何硬币都可以翻转到Heads(H)。但是,每当我们翻转硬币时,我们还必须在相同的行中向上,向下和向左和向右翻转相邻的硬币。因此,如果我们在第二排翻转第二枚硬币,我们还必须翻转另外4枚硬币,给我们这个安排(改变的硬币以粗体显示)。

  

H T H H
   H H H T
  H H H T
  T T H T

如果硬币位于拼图的边缘,那么一侧或另一侧没有硬币,那么我们就会翻转更少的硬币。我们不会“缠绕”到另一边。例如,如果我们翻转上面的arragnement的右下角硬币,我们会得到:

  

H T H H
  H H H T
  H H H H
  T T T H

注意:只能选择显示(T)尾巴的硬币进行翻转。然而,无论何时我们翻转这样的硬币,相邻的硬币也会被翻转,无论其状态如何。

拼图的目标是让所有硬币显示出头部。虽然有些arragnements可能没有解决方案,但所有问题都会有解决方案。我们正在寻找的答案是,对于任何给定的4x4硬币网格,为了使网格完全成为头部,最少的翻转次数是什么。

例如网格:
H T H H
T T T H
H T H T
H H T T

这个网格的答案是:2翻转。


到目前为止我做了什么:

我将我们的网格存储为二维的布尔数组。 Heads = true,tails = false。 我有一个翻转(int row,int col)方法,它将按照上面的规则翻转相邻的硬币,我有一个 isSolved()方法,将确定拼图是否处于解决状态(所有头)。所以我们有了“机制”。

我们遇到问题的部分是我们应该如何进行循环,最少次数?

10 个答案:

答案 0 :(得分:9)

你的谜题是一个经典的Breadth-First Search候选人。这是因为您正在寻找尽可能少“移动”的解决方案。

如果你知道目标的移动次数,那么这对于Depth-First Search来说是理想的。

这些维基百科文章包含大量有关搜索工作方式的信息,甚至包含多种语言的代码示例。

如果您确定不会耗尽堆栈空间,那么搜索都可以是递归的。

答案 1 :(得分:6)

编辑:我没有注意到你不能用硬币作为主要动作,除非它显示尾巴。这确实使秩序变得重要。我会在这里留下这个答案,但也要考虑写另一个答案。

这里没有伪代码,但想一想:你能想象自己两次掷硬币吗?会有什么影响?

替代方案,写下一些任意的板(字面意思,写下来)。设置一些真实世界的硬币,并选择两个任意的硬币,X和Y.做一个“X翻转”,然后是“Y翻转”,然后是另一个“X翻转”。写下结果。现在将电路板重置为起始版本,然后执行“Y翻转”。比较结果,并考虑发生了什么。尝试几次,有时X和Y靠近,有时不靠近。对你的结论充满信心。

这种思路应该引导您找到一组有限的可能的解决方案。你可以很容易地测试所有这些。

希望这个提示不是太明显 - 我会密切关注这个问题,看看你是否需要更多的帮助。这是一个很好的谜题。

至于递归:你可以使用递归。就个人而言,我不会在这种情况下。

编辑:实际上,在第二个想法我可能会使用递归。它可以让生活变得更加简单。


好吧,也许这还不够明显。让我们标记硬币A-P,如下所示:

ABCD
EFGH
IJKL
MNOP

翻转F将始终涉及以下硬币改变状态:BEFGJ。

翻转J将始终涉及以下硬币改变状态:FIJKN。

如果你掷硬币两次怎么办?无论发生什么其他翻转,两个翻转都会相互抵消。

换句话说,翻转F然后J与翻转J然后翻转F.然后F翻转F然后再翻J然后再翻F再与J翻转开始相同。

所以任何解决方案都不是“翻转A然后F然后J”的路径 - 它是“翻转<这些硬币&gt ;;不要翻转<这些硬币>”。 (不幸的是,“翻转”这个词用于翻转的主要硬币和改变特定动作的状态的次要硬币,但没关系 - 希望我的意思很清楚。)

每枚硬币将作为主要动作使用,或者不作为主要动作,0或1.有16个硬币,因此有2 ^ 16种可能性。所以0可能代表“不做任何事情”; 1可能代表“只是A”; 2可能代表“只是B”; 3“A和B”等。

测试每种组合。如果(不知何故)有多个解决方案,请计算每个解决方案中的位数以找到最少的数字。

实施提示:“当前状态”也可以表示为16位数。使用特定硬币作为主要移动将始终使用固定数字(对于该硬币)对当前状态进行异或。这样可以很容易地计算出任何特定动作组合的效果。


好的,这是C#中的解决方案。它显示了它找到的每个解决方案需要多少次移动,但它不会跟踪那些移动的移动,或移动次数最少的移动。这是一个SMOP:)

输入是一个列表,显示哪些硬币显示尾部 - 所以对于问题中的示例,您将使用参数“BEFGJLOP”启动程序。代码:

using System;

public class CoinFlip
{
    // All ints could really be ushorts, but ints are easier 
    // to work with
    static readonly int[] MoveTransitions = CalculateMoveTransitions();

    static int[] CalculateMoveTransitions()
    {
        int[] ret = new int[16];
        for (int i=0; i < 16; i++)
        {
            int row = i / 4;
            int col = i % 4;
            ret[i] = PositionToBit(row, col) +
                PositionToBit(row-1, col) +
                PositionToBit(row+1, col) +
                PositionToBit(row, col-1) +
                PositionToBit(row, col+1);
        }
        return ret;
    }

    static int PositionToBit(int row, int col)
    {
        if (row < 0 || row > 3 || col < 0 || col > 3)
        {
            // Makes edge detection easier
            return 0;
        }
        return 1 << (row * 4 + col);
    }

    static void Main(string[] args)
    {
        int initial = 0;
        foreach (char c in args[0])
        {
            initial += 1 << (c-'A');
        }
        Console.WriteLine("Initial = {0}", initial);
        ChangeState(initial, 0, 0);
    }

    static void ChangeState(int current, int nextCoin, int currentFlips)
    {
        // Reached the end. Success?
        if (nextCoin == 16)
        {
            if (current == 0)
            {
                // More work required if we want to display the solution :)
                Console.WriteLine("Found solution with {0} flips", currentFlips);
            }
        }
        else
        {
            // Don't flip this coin
            ChangeState(current, nextCoin+1, currentFlips);
            // Or do...
            ChangeState(current ^ MoveTransitions[nextCoin], nextCoin+1, currentFlips+1);
        }
    }
}

答案 2 :(得分:4)

我会像其他人已经提到的那样建议广泛的第一次搜索。

这里最大的秘密就是拥有游戏板的多个副本。不要想到“董事会”。

我建议创建一个包含电路板表示的数据结构,以及从起始位置到达该电路板的有序移动列表。移动是中心硬币在一组翻转中的坐标。我将这个数据结构的实例称为下面的“状态”。

我的基本算法看起来像这样:

Create a queue.
Create a state that contains the start position and an empty list of moves.
Put this state into the queue.
Loop forever:
    Pull first state off of queue.
    For each coin showing tails on the board:
        Create a new state by flipping that coin and the appropriate others around it.
        Add the coordinates of that coin to the list of moves in the new state.
        If the new state shows all heads:
            Rejoice, you are done.
        Push the new state into the end of the queue.

如果您愿意,可以添加队列长度限制或移动列表长度,以选择放弃的位置。您还可以跟踪已经看过的电路板以检测环路。如果队列清空并且您没有找到任何解决方案,则不存在任何解决方案。

此外,已经提出的一些评论似乎忽略了这样一个事实,即问题只允许显示尾巴的硬币处于移动中间。这意味着订单非常重要。如果第一步将硬币从头部翻转到尾部,则该硬币可以作为第二步的中心,但它不可能是第一步的中心。同样,如果第一步将硬币从尾巴翻到头部,那么该硬币就不能成为第二步的中心,即使它可能是第一步的中心。

答案 3 :(得分:2)

以行主顺序读取的网格只不过是16位整数。由问题给出的网格16个可能的移动(或“生成器”)都可以存储为16位整数,因此问题相当于找到尽可能少的生成器,这些生成器通过均值来求和按位XOR,给出网格本身作为结果。我想知道是否有更聪明的选择,而不是尝试所有65536种可能性。

编辑:确实有一种方便的方法来进行强制执行。您可以尝试所有1-move模式,然后是所有2-move模式,依此类推。当n-moves模式与网格匹配时,您可以停止,展示获胜模式并说该解决方案至少需要n次移动。枚举所有n次移动模式 是一个递归问题。

EDIT2:您可以使用以下(可能是错误的)递归伪代码的内容来强制执行:

// Tries all the n bit patterns with k bits set to 1
tryAllPatterns(unsigned short n, unsigned short k, unsigned short commonAddend=0)
{
    if(n == 0)
        tryPattern(commonAddend);
    else
    {
        // All the patterns that have the n-th bit set to 1 and k-1 bits
        // set to 1 in the remaining
        tryAllPatterns(n-1, k-1, (2^(n-1) xor commonAddend) );

        // All the patterns that have the n-th bit set to 0 and k bits
        // set to 1 in the remaining
        tryAllPatterns(n-1, k,   commonAddend );
    }
}

答案 4 :(得分:2)

详细说明费德里科的建议,问题在于找到一组16个发电机,这些发电机组合起来给出起始位置。

但是如果我们将每个生成器视为整数模2的向量,那么就会找到一个向量的线性组合,它等于起始位置。 解决这个问题应该只是高斯消除(mod 2)。

编辑: 在思考了一点之后,我认为这会起作用: 构建所有生成器的二进制矩阵G,并让s成为起始状态。我们正在寻找满足x(mod 2)的向量Gx=s。在进行高斯消除之后,我们要么得到这样的向量x,要么我们发现没有解决方案。

问题是要找到向量y,以便Gy = 0x^y尽可能少地设置位,我认为最简单的方法是尝试所有这些y。由于它们仅依赖于G,因此可以预先计算它们。

我承认,实施蛮力搜索会更容易实现。 =)

答案 5 :(得分:2)

好的,现在这是我已经正确阅读规则的答案:)

这是一个广度优先的搜索,使用状态队列和到达那里的动作。它没有做任何阻止循环的尝试,但你必须指定要尝试的最大迭代次数,因此它不能永远持续下去。

这个实现创建了一个 lot 字符串 - 一个不可变的链接移动列表在这方面会更整洁,但我现在没有时间。

using System;
using System.Collections.Generic;

public class CoinFlip
{
    struct Position
    {
        readonly string moves;
        readonly int state;

        public Position(string moves, int state)
        {
            this.moves = moves;
            this.state = state;
        }

        public string Moves { get { return moves; } } 
        public int State { get { return state; } }

        public IEnumerable<Position> GetNextPositions()
        {
            for (int move = 0; move < 16; move++)
            {
                if ((state & (1 << move)) == 0)
                {                    
                    continue; // Not allowed - it's already heads
                }
                int newState = state ^ MoveTransitions[move];
                yield return new Position(moves + (char)(move+'A'), newState);
            }
        }
    }

    // All ints could really be ushorts, but ints are easier 
    // to work with
    static readonly int[] MoveTransitions = CalculateMoveTransitions();

    static int[] CalculateMoveTransitions()
    {
        int[] ret = new int[16];
        for (int i=0; i < 16; i++)
        {
            int row = i / 4;
            int col = i % 4;
            ret[i] = PositionToBit(row, col) +
                PositionToBit(row-1, col) +
                PositionToBit(row+1, col) +
                PositionToBit(row, col-1) +
                PositionToBit(row, col+1);
        }
        return ret;
    }

    static int PositionToBit(int row, int col)
    {
        if (row < 0 || row > 3 || col < 0 || col > 3)
        {
            return 0;
        }
        return 1 << (row * 4 + col);
    }

    static void Main(string[] args)
    {
        int initial = 0;
        foreach (char c in args[0])
        {
            initial += 1 << (c-'A');
        }

        int maxDepth = int.Parse(args[1]);

        Queue<Position> queue = new Queue<Position>();
        queue.Enqueue(new Position("", initial));

        while (queue.Count != 0)
        {
            Position current = queue.Dequeue();
            if (current.State == 0)
            {
                Console.WriteLine("Found solution in {0} moves: {1}",
                                  current.Moves.Length, current.Moves);
                return;
            }
            if (current.Moves.Length == maxDepth)
            {
                continue;
            }
            // Shame Queue<T> doesn't have EnqueueRange :(
            foreach (Position nextPosition in current.GetNextPositions())
            {
                queue.Enqueue(nextPosition);
            }
        }
        Console.WriteLine("No solutions");
    }
}

答案 6 :(得分:1)

如果你正在为ACM练习,我会认为这个难题也适用于非平凡的板,比如1000x1000。蛮力/贪婪可能仍然有效,但要小心避免指数性爆炸。

答案 7 :(得分:1)

这是经典的“熄灯”问题。实际上有一个简单的O(2^N)强力解决方案,其中N是宽度或高度,以较小者为准。

让我们假设以下工作在宽度上,因为你可以转置它。

一个观察结果是你不需要按两次相同的按钮 - 它只是取消了。

关键概念只是您只需要确定是否要按第一行中每个项目的按钮。每按一次按钮都由一件事决定 - 是否按下了考虑按钮上方的灯。如果您正在查看单元格(x,y),并且单元格(x,y-1)已启用,则只需按一种方法即可将其关闭,方法是按(x,y)。从上到下迭代行,如果最后没有灯,你可以在那里找到解决方案。然后你可以拿走所有尝试的最小值。

答案 8 :(得分:0)

它是finite state machine,其中每个“状态”是与每个硬币的值相对应的16位整数。

每个州有16个出站过渡,对应于您翻转每个硬币后的状态。

一旦绘制出所有状态和转换,就必须在图表中找到从开始状态到状态的最短路径1111 1111 1111 1111,

答案 9 :(得分:0)

我坐下来试图解决这个问题(基于我在这个帖子中收到的帮助)。我使用的是二维数组的布尔值,所以它不如使用16位整数和位操作的人一样好。

无论如何,这是我在Java中的解决方案:

import java.util.*;

class Node
{
    public boolean[][] Value;
    public Node Parent;

    public Node (boolean[][] value, Node parent)
    {
        this.Value = value;
        this.Parent = parent;
    }
}


public class CoinFlip
{
    public static void main(String[] args)
    {
        boolean[][] startState =  {{true, false, true, true},
                                   {false, false, false, true},
                                   {true, false, true, false},
                                   {true, true, false, false}};


        List<boolean[][]> solutionPath = search(startState);

        System.out.println("Solution Depth: " + solutionPath.size());
        for(int i = 0; i < solutionPath.size(); i++)
        {
            System.out.println("Transition " + (i+1) + ":");
            print2DArray(solutionPath.get(i));
        }

    }

    public static List<boolean[][]> search(boolean[][] startState)
    {
        Queue<Node> Open = new LinkedList<Node>();
        Queue<Node> Closed = new LinkedList<Node>();

        Node StartNode = new Node(startState, null);
        Open.add(StartNode);

          while(!Open.isEmpty())
          {
              Node nextState = Open.remove();

              System.out.println("Considering: ");
              print2DArray(nextState.Value);

              if (isComplete(nextState.Value))
              {
                  System.out.println("Solution Found!");
                  return constructPath(nextState);
              }
              else
              {
                List<Node> children = generateChildren(nextState);
                Closed.add(nextState);

                for(Node child : children)
                {
                    if (!Open.contains(child))
                        Open.add(child);
                }
              }

          }

          return new ArrayList<boolean[][]>();

    }

    public static List<boolean[][]> constructPath(Node node)
    {
        List<boolean[][]> solutionPath = new ArrayList<boolean[][]>();

        while(node.Parent != null)
        {
            solutionPath.add(node.Value);
            node = node.Parent;
        }
        Collections.reverse(solutionPath);

        return solutionPath;
    }

    public static List<Node> generateChildren(Node parent)
    {
        System.out.println("Generating Children...");
        List<Node> children = new ArrayList<Node>();

        boolean[][] coinState = parent.Value;

        for(int i = 0; i < coinState.length; i++)
        {
            for(int j = 0; j < coinState[i].length; j++)
            {
                if (!coinState[i][j])
                {
                    boolean[][] child = arrayDeepCopy(coinState);
                    flip(child, i, j);
                    children.add(new Node(child, parent));

                }
            }
        }

        return children;
    }

    public static boolean[][] arrayDeepCopy(boolean[][] original)
    {
         boolean[][] r = new boolean[original.length][original[0].length];
         for(int i=0; i < original.length; i++)
                 for (int j=0; j < original[0].length; j++)
                       r[i][j] = original[i][j];

         return r;
    }

    public static void flip(boolean[][] grid, int i, int j)
    {
        //System.out.println("Flip("+i+","+j+")");
        // if (i,j) is on the grid, and it is tails
        if ((i >= 0 && i < grid.length) && (j >= 0 && j <= grid[i].length))
        {
            // flip (i,j)
            grid[i][j] = !grid[i][j];
            // flip 1 to the right
            if (i+1 >= 0 && i+1 < grid.length) grid[i+1][j] = !grid[i+1][j];
            // flip 1 down
            if (j+1 >= 0 && j+1 < grid[i].length) grid[i][j+1] = !grid[i][j+1];
            // flip 1 to the left
            if (i-1 >= 0 && i-1 < grid.length) grid[i-1][j] = !grid[i-1][j];
            // flip 1 up
            if (j-1 >= 0 && j-1 < grid[i].length) grid[i][j-1] = !grid[i][j-1];
        }
    }

    public static boolean isComplete(boolean[][] coins)
    {
        boolean complete = true;

        for(int i = 0; i < coins.length; i++)
        {
            for(int j = 0; j < coins[i].length; j++)
            {
                if (coins[i][j] == false) complete = false; 
            }

        }
        return complete;
    }

    public static void print2DArray(boolean[][] array) 
    {
        for (int row=0; row < array.length; row++) 
        {
            for (int col=0; col < array[row].length; col++)
            {
                System.out.print((array[row][col] ? "H" : "T") + " ");
            }
            System.out.println();
        }
    }

}