Algorithm to find map regions in c#

Viewed 212

I want to number these regions.I share my missing algorithm below.

int i = 1;

            for (int x= 0; x< height ; x++)
            {
                for (int y= 0; y< width ; y++)
                {
                    if (y>= 1)
                    {
                        if (a[x- 1, ] != "0")
                        {
                            if(a[x, y]!="0")
                            {
                                a[x, y] = a[x- 1, y];
                            }
                        }
                        else
                        {
                            if(a[x, y] != "0")
                            {
                                a[x, y] = "?";
                            }
                        }
                    }
                    else
                    {
                        if (a[x, y] != "0")
                        {
                            a[x, y] = i.ToString();
                        }
                        else
                        {
                            if (x>= 1)
                            {
                                if (a[x, y- 1] != a[x, y])
                                    i++;
                            }
                        }
                    }
                    Console.Write(string.Format("{0}  ", a[x, y]));
                }
                Console.Write(Environment.NewLine);
            }

Do you have any suggestions for a more effective algorithm or any idea how to improve this algorithm.Thank you for your help.

1 Answers

A simple but effective naive implementation would be to do this:

  1. Find any cell that isn't numbered (any algorithm fits, a simple linear search is the naive implementation)
  2. Use a flood fill algorithm to fill the area this cell is in, with a new number
  3. Rinse and repeat until step 1 can't find more unnumbered cells

Here's a simple implementation. Note that I used a int?[,] rectangular array to hold the map, you will have to adapt the code to your needs. I use LINQPad to test this in, so the .Dump() extension method there simply dumps the output of the LINQ query to the output window. Sample output below the code:

void Main()
{
    var map = new int?[30, 20];

    for (int x = 0; x < 30; x++)
    {
        map[x, 10 - x / 3] = 0;
        map[x, 5 + x / 2] = 0;
        map[x, 18 - x / 5] = 0;
    }
    ShowMap(map);
    NumberRegions(map);
    ShowMap(map);
}

public static void ShowMap(int?[,] map)
{
    Enumerable.Range(0, map.GetLength(1))
        .Select(y => string.Join("", Enumerable.Range(0, map.GetLength(0)).Select(x => map[x, y]?.ToString() ?? "?")))
        .Dump();
}

public static void NumberRegions(int?[,] map)
{
    var stack = new Stack<(int x, int y)>();
    int nextNumber = 1;

    for (int x = 0; x < map.GetLength(0); x++)
    {
        for (int y = 0; y < map.GetLength(1); y++)
        {
            if (map[x, y] is null)
            {
                stack.Push((x, y));
                while (stack.Any())
                {
                    (int cx, int cy) = stack.Pop();
                    if (map[cx, cy] is null)
                    {
                        map[cx, cy] = nextNumber;
                        if (cx > 0) stack.Push((cx - 1, cy));
                        if (cx < map.GetLength(0) - 1) stack.Push((cx + 1, cy));
                        if (cy > 0) stack.Push((cx, cy - 1));
                        if (cy < map.GetLength(1) - 1) stack.Push((cx, cy + 1));
                    }
                }

                nextNumber++;
            }
        }
    }
}

Here is the sample output:

Before:
?????????????????????????????? 
???????????????????????????000 
????????????????????????000??? 
?????????????????????000?????? 
??????????????????000????????? 
00?????????????000???????????? 
??00????????000??????????????? 
????00???000?????????????????? 
??????000????????????????????? 
???000??00???????????????????? 
000???????00?????????????????? 
????????????00???????????????? 
??????????????00?????????????? 
????????????????00???????00000 
??????????????????0000000????? 
???????????????0000000???????? 
??????????00000???????00?????? 
?????00000??????????????00???? 
00000?????????????????????00?? 
????????????????????????????00 

After:
111111111111111111111111111111 
111111111111111111111111111000 
111111111111111111111111000555 
111111111111111111111000555555 
111111111111111111000555555555 
001111111111111000555555555555 
220011111111000555555555555555 
222200111000555555555555555555 
222222000555555555555555555555 
222000330055555555555555555555 
000333333300555555555555555555 
333333333333005555555555555555 
333333333333330055555555555555 
333333333333333300555555500000 
333333333333333333000000066666 
333333333333333000000066666666 
333333333300000444444400666666 
333330000044444444444444006666 
000004444444444444444444440066 
444444444444444444444444444400 

The reason I'm saying it's a naive implementation is that if you have a very large map, the "search for the next unnumbered cell" can potentially be a big deal. If you could find a good way to keep track of 0-borders you've found so that you can hunt for unnumbered cells around those, that might work out, but there's plenty of edge cases to consider here so I didn't bother even trying.


Here's a variant that simply splits the flood fill implementation into a separate method:

public static void NumberRegions(int?[,] map)
{
    int nextNumber = 1;
    for (int x = 0; x < map.GetLength(0); x++)
        for (int y = 0; y < map.GetLength(1); y++)
            if (map[x, y] is null)
            {
                FloodFill(map, x, y, nextNumber);
                nextNumber++;
            }
}

public static void FloodFill(int?[,] map, int x, int y, int number)
{
    var work = new Stack<(int x, int y)>();
    work.Push((x, y));
    while (work.Any())
    {
        (x, y) = work.Pop();
        if (map[x, y] is not null)
            continue;
            
        map[x, y] = number;
        if (x > 0) work.Push((x - 1, y));
        if (x < map.GetLength(0) - 1) work.Push((x + 1, y));
        if (y > 0) work.Push((x, y - 1));
        if (y < map.GetLength(1) - 1) work.Push((x, y + 1));
    }
}

Since you mention some requirements in your comment, if you cannot use the Stack<> type, here's a recursive version of the FloodFill method above:

public static void FloodFill(int?[,] map, int x, int y, int number)
{
    if (map[x, y] is null)
    {
        map[x, y] = number;
        if (x > 0) FloodFill(map, x-1, y, number);
        if (x < map.GetLength(0) - 1) FloodFill(map, x+1, y, number);
        if (y > 0) FloodFill(map, x, y-1, number);
        if (y < map.GetLength(1) - 1) FloodFill(map, x, y+1, number);
    }
}
Related