Implementing the Dancing Links (DLX) Algorithm for Exact Cover in C

Fahad Ali Khan - Oct 27 - - Dev Community

Introduction

In my recent contribution to the open-source community, I worked on implementing the Dancing Links (DLX) algorithm for solving exact cover problems in C. This effort was part of addressing Issue #1131 in the AlgoGenesis/C repository. You can find my pull request here.

Understanding the Issue

What Was the Issue About?

The issue requested an implementation of the DLX algorithm in C that adheres to the C11 standard for maximum portability. The DLX algorithm is an efficient method for solving exact cover problems, such as Sudoku, N-Queens, and polyomino tiling. The challenge was to create clean, modular, and well-documented code with appropriate comments explaining key sections.

Preparation and Setup

Before diving into the code, I needed to:

  • Understand the DLX Algorithm: Although I was familiar with Algorithm X and exact cover problems, I needed a deeper understanding of how Dancing Links optimizes the search process.
  • Review Existing Implementations: I looked at implementations in other languages to grasp different approaches.
  • Set Up the Development Environment: Ensured I had a C11-compliant compiler (GCC and Clang) and configured my IDE (Visual Studio Code) for C development.

Learning and Research

What Did I Need to Learn?

  • Data Structures: How to represent the DLX matrix using circular doubly-linked lists.
  • Memory Management: Efficient allocation and deallocation of memory in C, especially when dealing with dynamic data structures.
  • Portability Concerns: Writing code that compiles and runs smoothly on both MacOS and Linux systems.

Research Conducted

  • Donald Knuth's Papers: Read "Dancing Links" by Donald Knuth to understand the theoretical foundation.
  • C11 Standard Documentation: Reviewed the C11 standard features to ensure compliance.
  • Community Resources: Checked forums and discussions on implementing DLX in C for common pitfalls and best practices.

Implementing the Fix

Code Explanation

Data Structures

  • Node Structure: Represents elements in the DLX matrix, linked in four directions (up, down, left, right).
  typedef struct Node {
      struct Node *left;
      struct Node *right;
      struct Node *up;
      struct Node *down;
      struct Column *col;
  } Node;
Enter fullscreen mode Exit fullscreen mode
  • Column Structure: Inherits from Node and includes additional information like size and name.
  typedef struct Column {
      Node node;     // Node header
      int size;      // Number of ones in the column
      char name[20]; // Identifier for debugging
  } Column;
Enter fullscreen mode Exit fullscreen mode
  • Root Node: A special Column node that serves as the starting point for traversing the matrix.
  static Column root;
Enter fullscreen mode Exit fullscreen mode

Initialization

  • initialize_dlx(int cols): Sets up the column headers linked circularly.
  void initialize_dlx(int cols) {
      root.node.right = &root.node;
      root.node.left = &root.node;

      for (int i = 0; i < cols; i++) {
          Column *col = (Column *)malloc(sizeof(Column));
          if (!col) {
              fprintf(stderr, "Memory allocation failed for column headers.\n");
              exit(EXIT_FAILURE);
          }
          col->size = 0;
          snprintf(col->name, sizeof(col->name), "C%d", i);
          col->node.up = &col->node;
          col->node.down = &col->node;
          col->node.col = col;

          // Insert the new column into the header list
          col->node.right = root.node.right;
          col->node.left = &root.node;
          root.node.right->left = &col->node;
          root.node.right = &col->node;
      }
  }
Enter fullscreen mode Exit fullscreen mode

Adding Rows

  • Challenge with Variable-Length Arrays (VLAs): Initially, I used VLAs for temporary storage when adding rows, but encountered compilation errors because VLAs are optional in C11 and not supported by all compilers.

  • Solution: Replaced VLAs with dynamic memory allocation using malloc.

  void add_row(int *row_data, int row_size, char *row_name) {
      Node **row_nodes = malloc(row_size * sizeof(Node *));
      if (!row_nodes) {
          fprintf(stderr, "Memory allocation failed for row_nodes.\n");
          exit(EXIT_FAILURE);
      }

      // ... (rest of the function)

      free(row_nodes);
  }
Enter fullscreen mode Exit fullscreen mode

Covering and Uncovering Columns

  • cover(Column *col): Removes a column and all rows containing a 1 in that column from the matrix.
  void cover(Column *col) {
      col->node.right->left = col->node.left;
      col->node.left->right = col->node.right;

      for (Node *row = col->node.down; row != &col->node; row = row->down) {
          for (Node *node = row->right; node != row; node = node->right) {
              node->down->up = node->up;
              node->up->down = node->down;
              node->col->size--;
          }
      }
  }
Enter fullscreen mode Exit fullscreen mode
  • uncover(Column *col): Restores a previously covered column and its rows.
  void uncover(Column *col) {
      for (Node *row = col->node.up; row != &col->node; row = row->up) {
          for (Node *node = row->left; node != row; node = node->left) {
              node->col->size++;
              node->down->up = node;
              node->up->down = node;
          }
      }

      col->node.right->left = &col->node;
      col->node.left->right = &col->node;
  }
Enter fullscreen mode Exit fullscreen mode

The Search Function

  • Recursive Search: Implements Algorithm X to find all solutions.
  void search(int k) {
      if (root.node.right == &root.node) {
          // Solution found
          printf("Solution %d:\n", ++solution_count);
          for (int i = 0; i < k; i++) {
              // Print the row identifiers
              printf("Row: ");
              Node *n = solution[i];
              do {
                  printf("%s ", n->col->name);
                  n = n->right;
              } while (n != solution[i]);
              printf("\n");
          }
          return;
      }

      Column *col = choose_column();
      if (!col || col->size == 0) return;

      cover(col);

      for (Node *row = col->node.down; row != &col->node; row = row->down) {
          solution[k] = row;
          for (Node *node = row->right; node != row; node = node->right) {
              cover(node->col);
          }
          search(k + 1);
          for (Node *node = row->left; node != row; node = node->left) {
              uncover(node->col);
          }
      }

      uncover(col);
  }
Enter fullscreen mode Exit fullscreen mode

Demo: Before and After the Fix

Before the Fix

  • Compilation Error: The use of VLAs caused the compiler to throw an error:
  expression must have a constant value
Enter fullscreen mode Exit fullscreen mode

This was because VLAs are not guaranteed to be supported in C11, and some compilers or IDEs like Visual Studio Code's IntelliSense might not recognize them.

After the Fix

  • Successful Compilation: Replacing VLAs with dynamic memory allocation resolved the issue, and the code compiled without errors or warnings.

  • Execution: Running the program produced the expected solutions for the exact cover problem provided.

  Solution 1:
  Row: C2 C4 C5 
  Row: C0 C3 C6 
Enter fullscreen mode Exit fullscreen mode

Challenges Faced

Compilation Errors

  • Issue: The initial use of VLAs led to compilation errors on some systems.

  • Solution: Replaced VLAs with malloc and free to allocate memory dynamically, ensuring compatibility across different compilers and systems.

Memory Management

  • Challenge: Managing dynamic memory allocation in C requires careful handling to avoid leaks and segmentation faults.

  • Approach: Ensured that every malloc had a corresponding free, and checked for allocation failures.

Understanding the Algorithm

  • Complexity: The DLX algorithm's manipulation of pointers in multiple linked lists can be intricate.

  • Overcoming It: Drew diagrams to visualize the data structures and stepped through the code with a debugger to trace the execution flow.

Interactions with Project Maintainers

I had a productive interaction with the project maintainers. After submitting my initial pull request, they provided feedback on code style and documentation. They emphasized the importance of adhering to the C11 standard and ensuring that the code is portable. Their guidance helped me refine the implementation.

Conclusion

Working on this issue was a valuable learning experience. I deepened my understanding of the DLX algorithm, improved my C programming skills, and contributed to an open-source project that can help others solve exact cover problems efficiently.


Links:

. . . . . . . . . .