Skip to content

Commit

Permalink
Merge pull request #72 from jwg4/all_solutions
Browse files Browse the repository at this point in the history
Add function get_solution_count.
  • Loading branch information
jwg4 authored Dec 1, 2023
2 parents effa7a3 + 81a908f commit 56be1f4
Show file tree
Hide file tree
Showing 21 changed files with 377 additions and 73 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/c-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Tests for C code

on:
push:
branches:
branches:
- main
pull_request:
branches: '**'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/poetry-publish-sdist.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Deploy sdist to pypi
on:
push:
branches:
branches:
- main

jobs:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/valgrind.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Valgrind

on:
push:
branches:
branches:
- main
pull_request:
branches: '**'
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ Now we can call `exact_cover`:
This is telling us that the 0th row (i.e. A), the 2nd row (i.e. C),
and the 3rd row (i.e. D) together form an exact cover.

To see the total number of distinct solutions, we can use the function get_solution_count:

>>> ec.get_solution_count(S)
1

See the file examples.md for more detailed examples of use.


Expand Down Expand Up @@ -130,5 +135,3 @@ Acknowledgements
Thanks very much to Moy Easwaran (https://github.com/moygit) for his inspiring work!

Munit aka µnit (https://nemequ.github.io/munit/) is a wonderful unit testing framework for C code.


1 change: 1 addition & 0 deletions exact_cover/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .wrapper import get_exact_cover # noqa: F401
from exact_cover_impl import get_solution_count # noqa: F401
5 changes: 5 additions & 0 deletions examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ The numpy type to use is defined in the package for convenience and compatibilit
>>> T = np.array([[1,0,0,1,0],[1,1,1,0,0],[0,1,1,0,0],[0,0,0,0,1]], dtype=DTYPE_FOR_ARRAY)
>>> print(ec.get_exact_cover(T))
[0 2 3]

It's also possible to retrieve the total number of solutions to an exact cover problem:
>>> ec.get_solution_count(T)
1

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "exact_cover"
version = "1.3.0"
version = "1.4.0a0"
description = "Solve exact cover problems"
readme = "README.md"
authors = ["Moy Easwaran"]
Expand All @@ -26,7 +26,7 @@ numpy = [
setuptools = ">=51.1.2"

# When we build wheels, we always do so with an explicit pinned numpy version
#
#
[tool.poetry.group.wheel_builder]
optional = true

Expand Down
41 changes: 41 additions & 0 deletions src/dlx.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,35 @@ int search(list sparse_matrix, int k, int max, int *solution) {
return result;
}

int count(list sparse_matrix, int k, int max, int *solution) {
list col, row, next;
int result = 0;
int c = 0;

// Base cases:
// 1. There are no columns left; we've found a solution.
if (get_right(sparse_matrix) == sparse_matrix) return 1;
// 2. There's a column with only zeros. This branch of the search
// tree has no solutions and we need to backtrack.
col = choose_column_with_min_data(sparse_matrix, max);
if (get_data(col)->data == 0) return 0;

// Main algorithm:
cover_column(col);
for (row = col; (row = get_down(row)) != col; ) {
solution[k] = get_data(row)->data; // save the row number
for (next = row; (next = get_right(next)) != row; )
cover_column(get_data(next)->list_data);
result = count(sparse_matrix, k+1, max, solution);
c += result;
// If result > 0 we're done, but we should still clean up.
for (next = row; (next = get_left(next)) != row; )
uncover_column(get_data(next)->list_data);
}
uncover_column(col);
return c;
}

int dlx_get_exact_cover(int rows, int cols, char matrix[], int *solution) {
list sparse_matrix;
int solution_length;
Expand All @@ -58,4 +87,16 @@ int dlx_get_exact_cover(int rows, int cols, char matrix[], int *solution) {
return solution_length;
}

int dlx_get_solution_count(int rows, int cols, char matrix[]) {
list sparse_matrix;
// int solution_length;
int solution_count = 0;
int *solution = malloc(rows * sizeof(*solution));

sparse_matrix = create_sparse(rows, cols, matrix);
solution_count = count(sparse_matrix, 0, rows, solution);
destroy_entire_grid(sparse_matrix);

free(solution);
return solution_count;
}
2 changes: 1 addition & 1 deletion src/dlx.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@

int search(list, int, int, int *);
int dlx_get_exact_cover(int, int, char [], int*);
int dlx_get_solution_count(int, int, char []);

#endif

33 changes: 30 additions & 3 deletions src/exact_cover.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,19 @@

static bool not_2d_int_array(PyArrayObject *);
static PyObject* get_exact_cover(PyObject*, PyObject*);
static PyObject* get_solution_count(PyObject*, PyObject*);


static bool not_2d_int_array(PyArrayObject *in_array) {
if (PyArray_NDIM(in_array) != 2) {
PyErr_SetString(PyExc_ValueError, _EXACT_COVER_NP_DIM_ERROR_);
return 1;
}
if (PyArray_TYPE(in_array) != NPY_BOOL) {
if (PyArray_TYPE(in_array) != NPY_BOOL) {
PyErr_SetString(PyExc_TypeError, _EXACT_COVER_NP_TYPE_ERROR_);
return 1;
}
if (!(PyArray_FLAGS(in_array) & NPY_ARRAY_C_CONTIGUOUS)) {
if (!(PyArray_FLAGS(in_array) & NPY_ARRAY_C_CONTIGUOUS)) {
PyErr_SetString(PyExc_TypeError, _EXACT_COVER_NP_ORDER_ERROR_);
return 1;
}
Expand All @@ -47,7 +48,7 @@ static PyObject* get_exact_cover(PyObject* self, PyObject* args)

/* Check that we got a 2-dimensional array of dtype='bool'. */
if (not_2d_int_array(in_array)) return NULL;

/* Get the data. */
dims = PyArray_DIMS(in_array);
rows = (int) dims[0], cols = (int) dims[1];
Expand All @@ -65,9 +66,35 @@ static PyObject* get_exact_cover(PyObject* self, PyObject* args)
return return_solution;
}

static PyObject* get_solution_count(PyObject* self, PyObject* args)
{

PyArrayObject *in_array;
npy_intp *dims;
char *in_array_data;
int rows, cols, result;

/* Parse single numpy array argument */
if (!PyArg_ParseTuple(args, "O!", &PyArray_Type, &in_array)) return NULL;

/* Check that we got a 2-dimensional array of dtype='bool'. */
if (not_2d_int_array(in_array)) return NULL;

/* Get the data. */
dims = PyArray_DIMS(in_array);
rows = (int) dims[0], cols = (int) dims[1];
in_array_data = (char*) PyArray_DATA(in_array);

/* Get the solution count. */
result = dlx_get_solution_count(rows, cols, in_array_data);

return PyLong_FromLong(result);
}

static PyMethodDef ExactCoverMethods[] =
{
{"get_exact_cover", get_exact_cover, METH_VARARGS, "Calculate an exact cover of a set."},
{"get_solution_count", get_solution_count, METH_VARARGS, "Get the number of distinct exact cover solutions."},
{NULL, NULL, 0, NULL}
};

Expand Down
5 changes: 2 additions & 3 deletions src/quad_linked_list.c
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ list insert_horizontally(list orig_list, node_ptr new_node_ptr) {
orig_list->left->right = new_node_ptr;
orig_list->left = new_node_ptr;
}

return new_node_ptr;
}

Expand All @@ -117,7 +117,7 @@ list insert_vertically(list orig_list, node_ptr new_node_ptr) {
orig_list->up->down = new_node_ptr;
orig_list->up = new_node_ptr;
}

return new_node_ptr;
}

Expand Down Expand Up @@ -242,4 +242,3 @@ void destroy_entire_grid (list grid) {
}
return;
}

2 changes: 0 additions & 2 deletions src/quad_linked_list.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,3 @@ void destroy_column_unsafely (list);
void destroy_entire_grid (list);

#endif


2 changes: 0 additions & 2 deletions src/sparse_matrix.c
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,3 @@ void print_column(list col, int row_count) {
for (; row_num < row_count; ++row_num) printf("0,");
putchar('\n');
}


1 change: 0 additions & 1 deletion src/sparse_matrix.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,3 @@ void print_sparse_matrix_transpose(list, int);
void print_column(list, int);

#endif

24 changes: 24 additions & 0 deletions tests/bruteforce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
bruteforce algorithm to compute the expected solutions
and help write tests
"""

from itertools import chain, combinations

import numpy as np


# from the itertools module documentation
def powerset(iterable):
"powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)"
s = list(iterable)
return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1))


def bruteforce(data):
"""
Brute-force generator of all exact cover solutions
"""
for subset in powerset(range(data.shape[0])):
if np.all(data[list(subset)].sum(axis=0) == 1):
yield subset
132 changes: 132 additions & 0 deletions tests/problems.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import numpy as np

from exact_cover.io import DTYPE_FOR_ARRAY

# one specific problem that I had trouble with
# originally based on solving the trivial problem
# of arranging 2 identical triminos on a 3x3 board

# +--+
# | |
# +--+--+
# | | |
# +--+--+

# +--+--+--+
# |xx| |xx|
# +--+--+--+
# | | | |
# +--+--+--+
# |xx| | |
# +--+--+--+


# this problem has 2 solutions
# (5, 13) and (6, 12)
def small_trimino_problem():
to_cover = [
[1, 0, 0, 1, 1, 0, 1, 0],
[1, 0, 0, 0, 1, 1, 0, 1],
[1, 0, 0, 0, 1, 1, 1, 0],
[1, 0, 1, 0, 1, 1, 0, 0],
[1, 0, 0, 0, 1, 0, 1, 1],
[1, 0, 1, 1, 1, 0, 0, 0], # <- 5
[1, 0, 0, 0, 0, 1, 1, 1], # <- 6
[0, 1, 0, 1, 1, 0, 1, 0],
[0, 1, 0, 0, 1, 1, 0, 1],
[0, 1, 0, 0, 1, 1, 1, 0],
[0, 1, 1, 0, 1, 1, 0, 0],
[0, 1, 0, 0, 1, 0, 1, 1],
[0, 1, 1, 1, 1, 0, 0, 0], # <- 12
[0, 1, 0, 0, 0, 1, 1, 1], # <- 13
]
return dict(
data=np.array(to_cover, dtype=DTYPE_FOR_ARRAY),
solution1=[5, 13],
solution_count=2,
)


def small_trimino_problem_from_file():
return dict(
data=np.load("tests/files/small_trimino_problem.npy"),
solution1=[5, 13],
solution_count=2,
)


# https://en.wikipedia.org/wiki/Exact_cover#Detailed_example
def detailed_wikipedia_problem():
sets = [
{1, 4, 7},
{1, 4}, # <- 1
{4, 5, 7},
{3, 5, 6}, # <- 3
{2, 3, 6, 7},
{2, 7}, # <- 5
]
return dict(
data=np.array(
[[1 if i in s else 0 for i in range(1, 8)] for s in sets],
dtype=DTYPE_FOR_ARRAY,
),
solution1=[1, 3, 5],
solution_count=1,
)


def bruteforce_problem1():
to_cover = [
[1, 0, 0, 1, 0, 0, 1, 0], # <- sol1
[0, 1, 0, 0, 1, 0, 0, 1], # <- sol1
[0, 0, 1, 0, 0, 1, 0, 0], # <- sol1
[0, 0, 0, 1, 0, 0, 0, 0], # <- sol2
[1, 0, 1, 0, 1, 0, 0, 1], # <- sol2
[0, 1, 0, 0, 0, 1, 1, 0], # <- sol2
]
return dict(
data=np.array(to_cover, dtype=DTYPE_FOR_ARRAY),
solution1=[0, 1, 2],
solution_count=2,
)


def bruteforce_problem2():
to_cover = [
[1, 0, 0, 1, 0, 0, 1, 0], # <- sol1
[0, 1, 0, 0, 1, 0, 0, 1], # <- sol1
[0, 0, 1, 0, 0, 1, 0, 0], # <- sol1
[0, 0, 0, 1, 0, 0, 0, 0], # <- sol2
[1, 0, 1, 0, 1, 0, 0, 1], # <- sol2
[0, 1, 0, 0, 0, 1, 1, 0], # <- sol2
[1, 0, 0, 1, 0, 0, 1, 0], # <- sol1
[0, 1, 0, 0, 1, 0, 0, 1], # <- sol1
[0, 0, 1, 0, 0, 1, 0, 0], # <- sol1
]
return dict(
data=np.array(to_cover, dtype=DTYPE_FOR_ARRAY),
solution1=[0, 1, 2],
solution_count=9,
)


def bruteforce_problem3():
to_cover = [
[1, 0, 0, 1, 0, 0, 1, 0], # <- sol1
[0, 1, 0, 0, 1, 0, 0, 1], # <- sol1
[0, 0, 1, 0, 0, 1, 0, 0], # <- sol1
[0, 0, 0, 1, 0, 0, 0, 0], # <- sol2
[1, 0, 1, 0, 1, 0, 0, 1], # <- sol2
[0, 1, 0, 0, 0, 1, 1, 0], # <- sol2
[1, 0, 0, 1, 0, 0, 1, 0], # <- sol1
[0, 1, 0, 0, 1, 0, 0, 1], # <- sol1
[0, 0, 1, 0, 0, 1, 0, 0], # <- sol1
[0, 0, 0, 1, 0, 0, 0, 0], # <- sol2
[1, 0, 1, 0, 1, 0, 0, 1], # <- sol2
[0, 1, 0, 0, 0, 1, 1, 0], # <- sol2
]
return dict(
data=np.array(to_cover, dtype=DTYPE_FOR_ARRAY),
solution1=[0, 1, 2],
solution_count=16,
)
Loading

0 comments on commit 56be1f4

Please sign in to comment.