Reducing Memory and Search Overhead in Grid Movement Combinatorics

I know my title is not very clear but I’m not sure how to word it.

Let’s start with a 4 by 4 grid; I eventually want to expand to an 8 by 8 or as big as I can get if 8 by 8 is too big but not any bigger. I’ve labeled the cells so it’s easier to talk about them and we need to start somewhere so I’ve added our starting position in the 0th cell in red. I’m only focusing on the 0th cell for now but you would start with a list of all the single cells. I’ll call this iteration 1.

Next, I would get all the possible positions that can be visited from here. I’ve marked cells that the starting position can visit in green. The rule is that you can only move up, down, left, or right, but you can move a variable distance. For example, you can move one tile to the right, or 3 tiles to the right. Back to this example, we could move to 1, 2, 3, 4, 8, and 12. (I would repeat this for all the 16 possible starting positions) As a set of pairs, they would be (0, 1), (0, 2), … , (0, 12). This is the second iteration.

Then, for each pair, get all the possible triplets. In the example, I’m using (0, 8). So they would be (0, 1, 8), (0, 2, 8), (0, 3, 8), (0, 4, 8), (0, 8, 9), (0, 8, 10), (0, 8, 11), (0, 8, 12)

And I would keep repeating these iterations until there’s no more empty space.

I’ve already written program that can do this. It’s fairly trivial, but it’s slow and uses a lot of memory. I’m hoping someone can give me some insight into this problem.

Code

I’m storing the positions as bitboards. It uses a breadth-first search to get all the positions. There’s a few optimizations I’m making, some I’m not sure are working, some I think might prune too much too soon leading to not all combinations being found.

internal class ValidPositionGenerator
{
    // Used to store new bitboards we need to check
    private readonly Queue<ulong> bitBoards = new Queue<ulong>();
    
    // Used to store unique bitboards we have found
    private readonly HashSet<ulong> visited = [];

    public ValidPositionGenerator(int maxTiles)
    {

        int i = 0;

    // This corresponds to the first iteration
        while (i < 64)
        {
            bitBoards.Enqueue(CanonicalizeBitboard((ulong)Math.Pow(2, i)));
            i++;
        }

        GenerateValid(bitBoards, maxTiles);
    }

    private void GenerateValid(Queue<ulong> bitBoards, int maxTiles)
    {
        int i = 0;
        while (bitBoards.Count != 0)
        {
            ulong board = bitBoards.Dequeue();

            if (visited.Contains(board))
            {
                continue;
            }

            if (BitOperations.PopCount(board) > maxTiles)
            {
                continue;
            }

            visited.Add(board);

            GetNewPositions(board, bitBoards, visited);
            
            i++;
        }
    }


    private static void GetNewPositions(ulong board, Queue<ulong> bitBoards, HashSet<ulong> visited)
    {
        // Get the active bits from board
        List<int> indices = GetIndices(board);

        int i;
        ulong canonical;

        // Loop over each one and attempt to add each one to the output
        foreach (int index in indices)
        {
            (int, int) pair = GetXYPairFromIndex(index);
            // Up
            i = 1;
            while (pair.Item2 - i > -1)
            {
                // Check if the next bit is already set: Optimization 1
                if (Util.GetBitboardCell(board, pair.Item1, pair.Item2 - i) == true)
                {
                    break;
                }
                // Optimization 2, 3, and 4 happens inside CanonicalizeBitboard()
                canonical = CanonicalizeBitboard(SetBitboardBit(board, pair.Item1, pair.Item2 - i, true));
                if (!visited.Contains(canonical))
                {
                    bitBoards.Enqueue(canonical);
                }
                i++;
            }
            // I've removed the rest to keep the code shorter
        // Left
            // Right
            // Down
            }
        }
    }

    private static List<int> GetIndices(ulong board)
    {
        ulong temp = board;
        List<int> indices = new List<int>();
        int index = 0;
        while (board > 0)
        {
            if ((board & 1) == 1)
            {
                indices.Add(index);
            }
            board >>= 1;
            index++;
        }
        return indices;
    }

    // Convert index to an x, y pair
    private static (int, int) GetXYPairFromIndex(int index)
    {
        return (index % 8, index / 8);
    }
}

Optimization 1: Don’t double check

While scanning for new positions, if we find an already active position, we can stop because we can continue from there when we get there. In the example below, I’m on (0, 2) scanning for bits to the right of 0. When we reach 2, we see that is already active so we can stop. Then we continue scanning down, left before scanning bits around 2. From here, we would continue scanning to the right getting anything we missed. I haven’t tested if this actually improves anything yet.

I think a possible optimization would be to create another bitboard with all the places we visited during this check and stop when we find something already found. Because once we get to 2, it’ll start scanning left towards 0 but it already found everything in that direction. I think I can merge this we the first optimization. It seems like an extension of it.

Optimization 2: Don’t store mirrors or rotations

Every combination has symmetry and I don’t need it. I only need to keep one of them. That’s what the canonicalizeBitboard method does. It checks it against rotations and reflections. This drastically reduces the search space. Instead of storing 16 possibilities in the first iteration, we only need to store 3: 0, 1, and 5.

Optimization 3: Don’t store similar translations

Consider the two grids below. They are essentially the same thing to me. So when I canonicalize the bitboard, I’m also shifting it so it’s flush to the top left corner.

Optimization 4: Condensed

Considered the following two bitboards. For my purposes, I only care about the unique combinations of moving up, down, left or right. If we start from 0, we can move right and down in both of them so to me, they are the same and I only need one. However, I’m not sure if I can remove it entirely. I think I’ll still need to keep it for when I check for new bitboards, but I don’t need it in my output. I’m confused about this point for my other optimizations as well. I think that even if I don’t need them in my output, I should still check them because the new position I can get from them might be unique and not found in their condensed version.

Conclusion

That’s all the optimizations I can think of for now, but writing this post has got me thinking. All I’m really doing is finding all the possible combinations/permutations of up, down, left or right, where the sum of left and right equals the size of the width of the bitboard – 1 and similarly with up and down, but with some extra rules, maybe. Maybe there’s a way to think about it differently. I think there might be a way to think about it terms of a string of up, down, left, and right and then that somehow gets mapped to a canonical bitboard.

What I’ve tried so far

I’ve tried my code and it works. I’m just wondering if there’s a better way. When I call the ValidPositionGenerator() constructor and use 6 as an input, it takes about 6 seconds and half a gig of memory. At 7, it takes about 2 minutes and uses 16 gigs of memory. When I profile it, the bitBoards queue blows up like crazy and that might very well be unavoidable. I know there are 2^64 possible bitboards but I’m hoping my rules and optimizations makes that more manageable.

New contributor

timeslidr is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.

I am not sure why you are using a breadth-first search. Usually it is easier to reason about depth-first algorithms (at least for me). Recursive depth-first back-tracking algorithms do not require a queue and the intermediate results are automatically stored temporarily on the stack.

If you stored the result as “RD” (i.e., right, down) instead of e.g. 0, 2, 14 in a HashSet<T>, then this would automatically eliminate duplicates of “original” versus “condensed” as well as duplicates given by translations. Also, since you are not interested in mirrored patterns, there is no need to consider other cells than your 0-cell as starting point. Other cells would allow you to start by going up or left and would result in a mirrored pattern compared to first going down or right.

I am not sure whether this exactly what you need, nut here is my attempt:

internal class ValidPositionGenerator
{
    const int N = 4;

    private readonly bool[,] _board = new bool[N, N];
    private readonly StringBuilder _pattern = new();

    public HashSet<string> Positions { get; } = new();

    public void Generate(int x, int y)
    {
        _board[x, y] = true;

        // Move right
        if (x < N - 1 && !_board[x + 1, y]) {
            if (_pattern.Length == 0 || _pattern[^1] != 'R') {
                _pattern.Append('R');
                Positions.Add(_pattern.ToString());
                Generate(x + 1, y);
                _pattern.Length--;
            } else {
                Generate(x + 1, y);
            }
        }

        // Move down
        if (y < N - 1 && !_board[x, y + 1]) {
            if (_pattern.Length == 0 || _pattern[^1] != 'D') {
                _pattern.Append('D');
                Positions.Add(_pattern.ToString());
                Generate(x, y + 1);
                _pattern.Length--;
            } else {
                Generate(x, y + 1);
            }
        }

        // Move left
        if (x > 0 && !_board[x - 1, y]) {
            if (_pattern.Length == 0 || _pattern[^1] != 'L') {
                _pattern.Append('L');
                Positions.Add(_pattern.ToString());
                Generate(x - 1, y);
                _pattern.Length--;
            } else {
                Generate(x - 1, y);
            }
        }

        // Move up
        if (y > 0 && !_board[x, y - 1]) {
            if (_pattern.Length == 0 || _pattern[^1] != 'U') {
                _pattern.Append('U');
                Positions.Add(_pattern.ToString());
                Generate(x, y - 1);
                _pattern.Length--;
            } else {
                Generate(x, y - 1);
            }
        }

        _board[x, y] = false; // back-track
    }
}

I tested it with

var generator = new ValidPositionGenerator();
generator.Generate(0, 0);
foreach (string position in generator.Positions) {
    Console.WriteLine(position);
}

Note that this might generate moves like RDLU where the last U bumps into the starting point. This happens because the notation is condensed. The “real” moves should either be RDDLU or RDLLU. I don’t know if you need to know this or whether knowing the turns is enough.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật