views:

107

answers:

2

Let's say you have the following structure in C#:

struct Piece : IEquatable<Piece> {
    public readonly int size;
    public readonly bool[,] data;

    public Piece(Piece p) {
        size = p.size;

        data = (bool[,])p.data.Clone();
    }
    public Piece(int s, bool[,] d) {
        size = s;
        if (d.GetLength(0) != s || d.GetLength(1) != s) throw new ArgumentException();

        data = (bool[,])d.Clone();
    }

    public bool Equals(Piece other) {
        if (size != other.size) return false;

        return (data.Equals(other.data));
    }
}

The idea is that it represents a sizexsize set of bits which represents a shape (a bit map, if you will).

Now, not all possible combinations of bits are valid. I have a few requirements:

  1. There must be only size bits total. (This is easy, I have already implemented this)
  2. All bits must be contiguous.

So, again assuming size==4, the following is good:

..#.
..#.
.##.
....

While the following is not:

...#
...#
#...
#...

How do I determine whether all the bits are contiguous or not?

Edit: Here is the full code, incorporating Eric Lippert's answer. This can definitely be tightened up, performance-wise, but it's very readable.

struct Point : IEquatable<Point> {
    public int X, Y;

    public Point(int x, int y) {
        X = x; Y = y;
    }

    public bool Equals(Point obj) {
        return X == obj.X && Y == obj.Y;
    }

    public override bool Equals(object obj) {
        if (obj == null) return false;

        if(obj is Point)
            return Equals((Point)obj);

        return false;
    }

    public override int GetHashCode() {
        return X ^ ~Y;
    }

    public static bool operator == (Point p1, Point p2) {
        return p1.X == p2.X && p1.Y == p2.Y;
    }

    public static bool operator !=(Point p1, Point p2) {
        return p1.X != p2.X || p1.Y != p2.Y;
    }

    public static readonly Point Empty = new Point(int.MinValue, int.MinValue);
}

struct Piece : IEquatable<Piece> {
    public readonly int size;
    public readonly bool[,] data;
    private bool valid;

    public Piece(Piece p) {
        size = p.size;
        valid = p.valid;
        data = (bool[,])p.data.Clone();
    }
    public Piece(int s, bool[,] d) {
        size = s;
        if (d.GetLength(0) != s || d.GetLength(1) != s) throw new ArgumentException();

        data = (bool[,])d.Clone();
        valid = false;

        CalcValidity();
    }

    public bool IsValid {
        get {
            return valid;
        }
    }


    private enum Square {
        White,
        Black,
        Red,
        Blue,
    }

    private int NumSquares(Square[,] map, Square q) {
        int ret = 0;
        for (int y = 0; y < size; y++) {
            for(int x = 0; x < size; x++) {
                if (map[x, y] == q) ret++;
            }
        }
        return ret;
    }

    private Point PickSquare(Square[,] map, Square q) {
        for (int y = 0; y < size; y++) {
            for (int x = 0; x < size; x++) {
                if (map[x, y] == q) return new Point(x, y);
            }
        }
        return Point.Empty;
    }

    private void MakeNeighboursRed(Square[,] map, Point p) {
        if (p.X > 0 && map[p.X - 1, p.Y] == Square.Black) map[p.X - 1, p.Y] = Square.Red;
        if (p.X < size - 1 && map[p.X + 1, p.Y] == Square.Black) map[p.X + 1, p.Y] = Square.Red;

        if (p.Y > 0 && map[p.X, p.Y - 1] == Square.Black) map[p.X, p.Y - 1] = Square.Red;
        if (p.Y < size - 1 && map[p.X, p.Y + 1] == Square.Black) map[p.X, p.Y + 1] = Square.Red;
    }

    private void CalcValidity() {
        Square[,] map = new Square[size, size];

        int count = 0;
        Point square = Point.Empty;


        for (int y = 0; y < size; y++) {
            for (int x = 0; x < size; x++) {
                if (data[x, y]) {
                    map[x, y] = Square.Black;
                    square = new Point(x, y);
                } else {
                    map[x, y] = Square.White;
                }
            }
        }

        if (square != Point.Empty) {
            map[square.X, square.Y] = Square.Red;
        }

        while (true) {
            if (NumSquares(map, Square.Red) == 0) {
                if (NumSquares(map, Square.Black) == 0) {
                    valid = count == size;
                    return;
                } else {
                    valid = false;
                    return;
                }
            } else {
                square = PickSquare(map, Square.Red);

                MakeNeighboursRed(map, square);
                map[square.X, square.Y] = Square.Blue;
                count++;
            }
        } 
    }

    #region IEquatable<Piece> Members

    public bool Equals(Piece other) {
        if (size != other.size) return false;

        for (int y = 0; y < size; y++) {
            for (int x = 0; x < size; x++) {
                if (data[x, y] != other.data[x, y]) return false;
            }
        }
        return true;
    }

    #endregion
}
A: 

You start at a random "true" bit. Then you "walk" north, south, east and west one at a time. If you find a "true" bit that is not "visited", mark that node as "visited" in a separate structure and "walk" recursively in all directions from there. If the bit is "false" or "visited", do nothing and return to the previous "level". When you can not find any more non "visited" nodes, count the number of visited nodes and compare with the total number of "true" nodes.

Edit: Note that recursion will run out of stack space if the bitmaps are large.

Albin Sunnanbo
Note that this is just an ordinary floodfill algorithm.
Loren Pechtel
Note that you probably do not actually want to do it *recursively*. That is likely to blow the stack if the bitmaps are large.
Eric Lippert
@Eric, you're right, I had that in mind, but was fooled by the small example to assume this was not a problem.
Albin Sunnanbo
+6  A: 

Here's a way to think about the flood fill algorithm that does not use recursion.

Start with every square set to either white (blank) or black (filled). The question is "are the black regions contiguous or not?"

You can use this algorithm:

  1. If there are no black squares then it is true that there are no discontiguous black regions, so you're done.
  2. Otherwise, there is at least one black square. Pick any black square and turn it red.
  3. If there are no red squares and no black squares then you're done, and the black regions are contiguous.
  4. If there are no red squares but one or more black squares then you're done. The black regions are not contiguous. The region that is still black is discontiguous from the region that is blue.
  5. Otherwise, there must be at least one red square. Pick any red square. Turn all its black neighbours to red, and then turn it blue.
  6. Go back to step 3.

See how that works? The red squares are the "edges" of the region that is not flood-filled. The blue squares are the flooded region. If the blue floods all the black then it must have been contiguous.

UPDATE: Re, your comment:

Thank you so much! This is perfect. I love your blog, especially the articles on LINQ and "writing what you mean", and I tried to apply those principles here

Thanks for the kind words. If what you like is very "LINQ-y" solutions to these sorts of problems then I would not use the solution that I sketched out here. Notice that the algorithm is basically "mutate a data structure to learn facts about it". That's not a very LINQ-ish thing to do. LINQ is all about querying data structures without modifying them.

If I wanted to make a more LINQ-like solution to your problem then I would take a very different approach. Here's what I'd do.

First off, do you know what an "equivalence class" or an "equivalence relation" is? I'll briefly define them if you don't know. A relation is a function that takes two things and returns a bool. For example, "less than", "equal to", "have the same last digit in base ten" and "sum to an even number" are all relations on integers. An equivalence relation (A~B) is a relation which is reflexive (X~X is always true), symmetric (X~Y and Y~X are the same) and transitive (if X~Y and Y~Z are both true then so is X~Z).

In our examples, "less than" is transitive but not reflexive or symmetric. The other three are equivalence relations.

An equivalence relation partitions the set into equivalence classes. For example, the equivalence relation "sum to an even number" partitions the integers into two equivalence classes: the even numbers and the odd numbers. Pick any two odd numbers and X~Y is true. Pick any two even numbers and X~Y is true. But an odd number and an even number, X~Y is false. All the even numbers are "equivalent" for the purposes of this relation.

Now consider the relation "is a neighbour on this tile set" for one of your tile sets. Clearly that is not an equivalence relation; it is symmetric but not reflexive or transitive. But any relation can be extended to be an equivalence relation by defining a new relation that is the reflexive and transitive closure of the relation. This is the "is reachable from" relation.

Your question is therefore essentially "how many equivalence classes are there for the equivalence relation of reachability"? If the answer is zero then there are no black regions. If the answer is one then there is a single contiguous region. If it is more than one then there must be discontiguous regions.

Therefore another way to characterize your question is "given that there is at least one black tile, is the entire set of black tiles identical with the reflexive and transitive closure of the neighbour relation on an arbitrary tile?" If the answer is "yes" then there is a single contiguous region. If "no" then there must be a region that is not reachable.

Since you have a way to count tiles, and since the numbers are finite integers, we can do even better than that. We can just ask "is the size of the reflexive and transitive closure of the neighbour relation on an arbitrary black tile identical with the count of all black tiles?" to solve your problem.

So how to answer that question? Suppose you have a method that takes a tile and returns a sequence of its neighbours:

public static IEnumerable<Tile> GetNeighbours(Tile tile)
{
     ... yield return the north, south, east and west neighbours
     ... if they exist and they are on
}

Basically this method is "given a tile, give me all the tiles that have the neighbour relation with it". This works great if you can compute which members have a relation with a given member, which clearly in this case you can do so cheaply.

Now you can compute the transitive and reflexive closure of that relation using the code I posted here:

http://blogs.msdn.com/b/ericlippert/archive/2010/02/08/making-the-code-read-like-the-spec.aspx

And now your entire algorithm becomes very short indeed:

bool HasExactlyOneRegion()
{
    return (tilesTurnedOn.Count == 0) ? 
        false : 
        TransitiveAndReflexiveClosure(GetNeighbours, tilesTurnedOn.First()).Count == tilesTurnedOn.Count;
}

If you have the right tools at your disposal then the solution is a single statement!

Notice that the two solutions I've given (and Albin's sketch as well) are identical in their operation. In my second solution the "red tiles" of the first solution are the tiles in the "stack" data structure, and the "blue tiles" are the tiles in the "closure" data structure of the code in the link I gave.

The difference between the solutions is only in how you think about the solution. I like to think mathematically, so I would prefer the second solution. It's all about sets and relations and closures, very abstract ideas. If you're more of a visual thinker then my first solution, where you can visualize a red-edged wave spreading into a black area until it is full, might be easier to understand and reason about.

Eric Lippert
I don't think step 6 is jumping to the correct location. I think it should be go to step 5 till there are no red squares. then step 7 if there are a remaining black squares the area is not contiguous.
Scott Chamberlain
@Scott: Apparently I ran into a formatting bug in SO; if you start a numbered list with 0. in the editor, the display engine renumbers it to start with 1, so my list was off by one. I've changed my list so that the numbering is now correct.
Eric Lippert
@Eric: Thank you so much! This is perfect. I love your blog, especially the articles on LINQ and "writing what you mean", and I tried to apply those principles here.
Mike Caron
@Mike: Thanks for the kind words. Since you like LINQy solutions I've updated my answer with a second solution that is much more in keeping with LINQ principles.
Eric Lippert