18

It would be great if someone could point me towards an algorithm that would allow me to :

  1. create a random square matrix, with entries 0 and 1, such that
  2. every row and every column contain exactly two non-zero entries,
  3. two non-zero entries cannot be adjacent,
  4. all possible matrices are equiprobable.

Right now I manage to achieve points 1 and 2 doing the following : such a matrix can be transformed, using suitable permutations of rows and columns, into a diagonal block matrix with blocks of the form

1 1 0 0 ... 0
0 1 1 0 ... 0
0 0 1 1 ... 0
.............
1 0 0 0 ... 1

So I start from such a matrix using a partition of [0, ..., n-1] and scramble it by permuting rows and columns randomly. Unfortunately, I can't find a way to integrate the adjacency condition, and I am quite sure that my algorithm won't treat all the matrices equally.

Update

I have managed to achieve point 3. The answer was actually straight under my nose : the block matrix I am creating contains all the information needed to take into account the adjacency condition. First some properties and definitions:

  • a suitable matrix defines permutations of [1, ..., n] that can be build like so: select a 1 in row 1. The column containing this entry contains exactly one other entry equal to 1 on a row a different from 1. Again, row a contains another entry 1 in a column which contains a second entry 1 on a row b, and so on. This starts a permutation 1 -> a -> b ....

For instance, with the following matrix, starting with the marked entry

v
1 0 1 0 0 0 | 1
0 1 0 0 0 1 | 2
1 0 0 1 0 0 | 3
0 0 1 0 1 0 | 4
0 0 0 1 0 1 | 5
0 1 0 0 1 0 | 6
------------+--
1 2 3 4 5 6 |

we get permutation 1 -> 3 -> 5 -> 2 -> 6 -> 4 -> 1.

  • the cycles of such a permutation lead to the block matrix I mentioned earlier. I also mentioned scrambling the block matrix using arbitrary permutations on the rows and columns to rebuild a matrix compatible with the requirements.

But I was using any permutation, which led to some adjacent non-zero entries. To avoid that, I have to choose permutations that separate rows (and columns) that are adjacent in the block matrix. Actually, to be more precise, if two rows belong to a same block and are cyclically consecutive (the first and last rows of a block are considered consecutive too), then the permutation I want to apply has to move these rows into non-consecutive rows of the final matrix (I will call two rows incompatible in that case).

So the question becomes : How to build all such permutations ?

The simplest idea is to build a permutation progressively by randomly adding rows that are compatible with the previous one. As an example, consider the case n = 6 using partition 6 = 3 + 3 and the corresponding block matrix

1 1 0 0 0 0 | 1
0 1 1 0 0 0 | 2
1 0 1 0 0 0 | 3
0 0 0 1 1 0 | 4
0 0 0 0 1 1 | 5
0 0 0 1 0 1 | 6
------------+--
1 2 3 4 5 6 |

Here rows 1, 2 and 3 are mutually incompatible, as are 4, 5 and 6. Choose a random row, say 3.

We will write a permutation as an array: [2, 5, 6, 4, 3, 1] meaning 1 -> 2, 2 -> 5, 3 -> 6, ... This means that row 2 of the block matrix will become the first row of the final matrix, row 5 will become the second row, and so on.

Now let's build a suitable permutation by choosing randomly a row, say 3:

  • p = [3, ...]

The next row will then be chosen randomly among the remaining rows that are compatible with 3 : 4, 5and 6. Say we choose 4:

  • p = [3, 4, ...]

Next choice has to be made among 1 and 2, for instance 1:

  • p = [3, 4, 1, ...]

And so on: p = [3, 4, 1, 5, 2, 6].

Applying this permutation to the block matrix, we get:

1 0 1 0 0 0 | 3
0 0 0 1 1 0 | 4
1 1 0 0 0 0 | 1
0 0 0 0 1 1 | 5
0 1 1 0 0 0 | 2
0 0 0 1 0 1 | 6
------------+--
1 2 3 4 5 6 |

Doing so, we manage to vertically isolate all non-zero entries. Same has to be done with the columns, for instance by using permutation p' = [6, 3, 5, 1, 4, 2] to finally get

0 1 0 1 0 0 | 3
0 0 1 0 1 0 | 4
0 0 0 1 0 1 | 1
1 0 1 0 0 0 | 5
0 1 0 0 0 1 | 2
1 0 0 0 1 0 | 6
------------+--
6 3 5 1 4 2 | 

So this seems to work quite efficiently, but building these permutations needs to be done with caution, because one can easily be stuck: for instance, with n=6 and partition 6 = 2 + 2 + 2, following the construction rules set up earlier can lead to p = [1, 3, 2, 4, ...]. Unfortunately, 5 and 6 are incompatible, so choosing one or the other makes the last choice impossible. I think I've found all situations that lead to a dead end. I will denote by r the set of remaining choices:

  1. p = [..., x, ?], r = {y} with x and y incompatible
  2. p = [..., x, ?, ?], r = {y, z} with y and z being both incompatible with x (no choice can be made)
  3. p = [..., ?, ?], r = {x, y} with x and y incompatible (any choice would lead to situation 1)
  4. p = [..., ?, ?, ?], r = {x, y, z} with x, y and z being cyclically consecutive (choosing x or z would lead to situation 2, choosing y to situation 3)
  5. p = [..., w, ?, ?, ?], r = {x, y, z} with xwy being a 3-cycle (neither x nor y can be chosen, choosing z would lead to situation 3)
  6. p = [..., ?, ?, ?, ?], r = {w, x, y, z} with wxyz being a 4-cycle (any choice would lead to situation 4)
  7. p = [..., ?, ?, ?, ?], r = {w, x, y, z} with xyz being a 3-cycle (choosing w would lead to situation 4, choosing any other would lead to situation 4)

Now it seems that the following algorithm gives all suitable permutations:

  • As long as there are strictly more than 5 numbers to choose, choose randomly among the compatible ones.
  • If there are 5 numbers left to choose: if the remaining numbers contain a 3-cycle or a 4-cycle, break that cycle (i.e. choose a number belonging to that cycle).
  • If there are 4 numbers left to choose: if the remaining numbers contain three cyclically consecutive numbers, choose one of them.
  • If there are 3 numbers left to choose: if the remaining numbers contain two cyclically consecutive numbers, choose one of them.

I am quite sure that this allows me to generate all suitable permutations and, hence, all suitable matrices.

Unfortunately, every matrix will be obtained several times, depending on the partition that was chosen.

Christoph Frings
  • 427
  • 4
  • 14
  • What size of matrix are we talking about? – m69's been on strike for years Oct 30 '17 at 19:08
  • @m69 small, less than 20. – Christoph Frings Oct 30 '17 at 19:09
  • I started on an algorithm that would categorize the solutions into types, and calculate the probability of each of these types, and then select a random solution in two steps, but I misjudged the difficulty of calculating the number of solutions per type, so I'm not sure if it'll turn into something useful. – m69's been on strike for years Oct 31 '17 at 19:09
  • @m69 I made some progress on my side, will update my question in a few moments. – Christoph Frings Nov 01 '17 at 13:06
  • 1
    How about if you encode the constraints in a BDD and use the usual "generate uniform-random satisfying vector" algorithm? It's correct but it may not work well since the BDD may be large, but I can't judge that very well in advance.. – harold Nov 01 '17 at 14:38
  • @harold I'm not familiar at all with what you are suggesting. Any introductory resources on the net? – Christoph Frings Nov 01 '17 at 14:42
  • 1
    See [here](http://www.cs.utsa.edu/~wagner/knuth/fasc1b.pdf) for example, it's a bit long perhaps but very thorough – harold Nov 01 '17 at 14:44
  • @harold So I finally get to read the bible... After a few pages, I am totally thrilled, tried to build my first bdd by hand (case n=4) but, yeah, starting with 16 variables promises a huge diagram. Wish me luck! A thousand thanks for pointing that out to me. – Christoph Frings Nov 01 '17 at 15:38
  • 1
    @harold I did some testing with a home-made bdd implementation (in python, I know, not the best choice). For n = 5, the bdd has 185 nodes, and 1629 nodes for n=6. The case n=7 already takes too long to compute for the patience that I have, with hundred of thousands of nodes. After all, I have n² variables, which becomes big very quickly. So this is not a solution for me. – Christoph Frings Nov 03 '17 at 12:14
  • Correction: now that the computation is done for n=7, I get "only" 13486 nodes. – Christoph Frings Nov 03 '17 at 15:11
  • @m69 I'm coming back to your previous comment: on what criteria do you define types for these matrices? – Christoph Frings Nov 03 '17 at 15:41
  • If you categorize them by whether each row uses 0, 1 or 2 columns that haven't been used before, each solution has a fingerprint like 2210221000, which always starts with 22 and ends with 00, and whose sum is N. (But not every sequence is valid, you can not start with e.g. 22000... because you only have 4 used columns, so you can't reuse 6.) There'd be fewer than a million types for 19x19. Unfortunately, the no-adjacent-ones rule complicates the calculation of the number of solutions per type, and you need these to know the probability that a random solution will be of a certain type. – m69's been on strike for years Nov 03 '17 at 19:33
  • The idea was to pre-calculate the number of solutions of each type, and then generate random solutions in two steps, as in this answer: https://stackoverflow.com/questions/45829748/python-finding-random-k-subset-partition-for-a-given-list/45970238#45970238 But I'm starting to doubt whether that strategy will work in this case. – m69's been on strike for years Nov 03 '17 at 19:59
  • It's probably hidden somewhere in that huge text, but what kind of adjacency are we talking about? 4 or 8 neighborhood? – sascha Nov 03 '17 at 21:16
  • And how much *patience do you have*? – sascha Nov 04 '17 at 00:43
  • Must the solution be a *single* cycle? if not: in case `6)` above, the {wxyz} could form a *second* cycle(at least two possibilities for 4 elements), **if** the left cycle can be closed (which it can, if its last element differs more than one from its first) – joop Nov 06 '17 at 15:16
  • @sascha It's 4. And patience is like memory, I have long term patience but very little short term patience :) Actually, I'd like the algorithm to run quickly, but there is no deadline. – Christoph Frings Nov 06 '17 at 19:58
  • @joop I'm not sure we have the same understanding : the cycles I mentioned are cycles of the permutation used to block-diagonalise the initial matrix. From that I try to build another permutation to be used to scramble the block matrix. This permutation can be anything, as long as it separates non-zero entries. – Christoph Frings Nov 06 '17 at 20:03
  • @m69 I like this approach. But yes, the non-adjacency rule is not taken into account. I was wondering if using a double signature would help (horizontal+vertical), but, at first glance, it doesn't seem so. – Christoph Frings Nov 06 '17 at 20:10
  • This looks exactly like a problem that could be implemented in a few lines using constraint solvers. – nurettin Nov 10 '17 at 11:07

5 Answers5

10

Intro

Here is some prototype-approach, trying to solve the more general task of uniform combinatorial sampling, which for our approach here means: we can use this approach for everything which we can formulate as SAT-problem.

It's not exploiting your problem directly and takes a heavy detour. This detour to the SAT-problem can help in regards to theory (more powerful general theoretical results) and efficiency (SAT-solvers).

That being said, it's not an approach if you want to sample within seconds or less (in my experiments), at least while being concerned about uniformity.

Theory

The approach, based on results from complexity-theory, follows this work:

GOMES, Carla P.; SABHARWAL, Ashish; SELMAN, Bart. Near-uniform sampling of combinatorial spaces using XOR constraints. In: Advances In Neural Information Processing Systems. 2007. S. 481-488.

The basic idea:

  • formulate the problem as SAT-problem
  • add randomly generated xors to the problem (acting on the decision-variables only! that's important in practice)
    • this will reduce the number of solutions (some solutions will get impossible)
    • do that in a loop (with tuned parameters) until only one solution is left!
      • search for some solution is being done by SAT-solvers or #SAT-solvers (=model-counting)
      • if there is more than one solution: no xors will be added but a complete restart will be done: add random-xors to the start-problem!

The guarantees:

  • when tuning the parameters right, this approach achieves near-uniform sampling
    • this tuning can be costly, as it's based on approximating the number of possible solutions
    • empirically this can also be costly!
    • Ante's answer, mentioning the number sequence A001499 actually gives a nice upper bound on the solution-space (as it's just ignoring adjacency-constraints!)

The drawbacks:

  • inefficient for large problems (in general; not necessarily compared to the alternatives like MCMC and co.)
    • need to change / reduce parameters to produce samples
    • those reduced parameters lose the theoretical guarantees
    • but empirically: good results are still possible!

Parameters:

In practice, the parameters are:

  • N: number of xors added
  • L: minimum number of variables part of one xor-constraint
  • U: maximum number of variables part of one xor-constraint

N is important to reduce the number of possible solutions. Given N constant, the other variables of course also have some effect on that.

Theory says (if i interpret correctly), that we should use L = R = 0.5 * #dec-vars.

This is impossible in practice here, as xor-constraints hurt SAT-solvers a lot!

Here some more scientific slides about the impact of L and U.

They call xors of size 8-20 short-XORS, while we will need to use even shorter ones later!

Implementation

Final version

Here is a pretty hacky implementation in python, using the XorSample scripts from here.

The underlying SAT-solver in use is Cryptominisat.

The code basically boils down to:

  • Transform the problem to conjunctive normal-form
    • as DIMACS-CNF
  • Implement the sampling-approach:
    • Calls XorSample (pipe-based + file-based)
    • Call SAT-solver (file-based)
  • Add samples to some file for later analysis

Code: (i hope i did warn you already about the code-quality)

from itertools import count
from time import time
import subprocess
import numpy as np
import os
import shelve
import uuid
import pickle
from random import SystemRandom
cryptogen = SystemRandom()

""" Helper functions """
# K-ARY CONSTRAINT GENERATION
# ###########################
# SINZ, Carsten. Towards an optimal CNF encoding of boolean cardinality constraints.
# CP, 2005, 3709. Jg., S. 827-831.

def next_var_index(start):
    next_var = start
    while(True):
        yield next_var
        next_var += 1

class s_index():
    def __init__(self, start_index):
        self.firstEnvVar = start_index

    def next(self,i,j,k):
        return self.firstEnvVar + i*k +j

def gen_seq_circuit(k, input_indices, next_var_index_gen):
    cnf_string = ''
    s_index_gen = s_index(next_var_index_gen.next())

    # write clauses of first partial sum (i.e. i=0)
    cnf_string += (str(-input_indices[0]) + ' ' + str(s_index_gen.next(0,0,k)) + ' 0\n')
    for i in range(1, k):
        cnf_string += (str(-s_index_gen.next(0, i, k)) + ' 0\n')

    # write clauses for general case (i.e. 0 < i < n-1)
    for i in range(1, len(input_indices)-1):
        cnf_string += (str(-input_indices[i]) + ' ' + str(s_index_gen.next(i, 0, k)) + ' 0\n')
        cnf_string += (str(-s_index_gen.next(i-1, 0, k)) + ' ' + str(s_index_gen.next(i, 0, k)) + ' 0\n')
        for u in range(1, k):
            cnf_string += (str(-input_indices[i]) + ' ' + str(-s_index_gen.next(i-1, u-1, k)) + ' ' + str(s_index_gen.next(i, u, k)) + ' 0\n')
            cnf_string += (str(-s_index_gen.next(i-1, u, k)) + ' ' + str(s_index_gen.next(i, u, k)) + ' 0\n')
        cnf_string += (str(-input_indices[i]) + ' ' + str(-s_index_gen.next(i-1, k-1, k)) + ' 0\n')

    # last clause for last variable
    cnf_string += (str(-input_indices[-1]) + ' ' + str(-s_index_gen.next(len(input_indices)-2, k-1, k)) + ' 0\n')

    return (cnf_string, (len(input_indices)-1)*k, 2*len(input_indices)*k + len(input_indices) - 3*k - 1)

# K=2 clause GENERATION
# #####################
def gen_at_most_2_constraints(vars, start_var):
    constraint_string = ''
    used_clauses = 0
    used_vars = 0
    index_gen = next_var_index(start_var)
    circuit = gen_seq_circuit(2, vars, index_gen)
    constraint_string += circuit[0]
    used_clauses += circuit[2]
    used_vars += circuit[1]
    start_var += circuit[1]

    return [constraint_string, used_clauses, used_vars, start_var]

def gen_at_least_2_constraints(vars, start_var):
    k = len(vars) - 2
    vars = [-var for var in vars]

    constraint_string = ''
    used_clauses = 0
    used_vars = 0
    index_gen = next_var_index(start_var)
    circuit = gen_seq_circuit(k, vars, index_gen)
    constraint_string += circuit[0]
    used_clauses += circuit[2]
    used_vars += circuit[1]
    start_var += circuit[1]

    return [constraint_string, used_clauses, used_vars, start_var]

# Adjacency conflicts
# ###################
def get_all_adjacency_conflicts_4_neighborhood(N, X):
    conflicts = set()
    for x in range(N):
        for y in range(N):
            if x < (N-1):
                conflicts.add(((x,y),(x+1,y)))
            if y < (N-1):
                conflicts.add(((x,y),(x,y+1)))

    cnf = ''  # slow string appends
    for (var_a, var_b) in conflicts:
        var_a_ = X[var_a]
        var_b_ = X[var_b]
        cnf += '-' + var_a_ + ' ' + '-' + var_b_ + ' 0 \n'

    return cnf, len(conflicts)

# Build SAT-CNF
  #############
def build_cnf(N, verbose=False):
    var_counter = count(1)
    N_CLAUSES = 0
    X = np.zeros((N, N), dtype=object)
    for a in range(N):
        for b in range(N):
            X[a,b] = str(next(var_counter))

    # Adjacency constraints
    CNF, N_CLAUSES = get_all_adjacency_conflicts_4_neighborhood(N, X)

    # k=2 constraints
    NEXT_VAR = N*N+1

    for row in range(N):
        constraint_string, used_clauses, used_vars, NEXT_VAR = gen_at_most_2_constraints(X[row, :].astype(int).tolist(), NEXT_VAR)
        N_CLAUSES += used_clauses
        CNF += constraint_string

        constraint_string, used_clauses, used_vars, NEXT_VAR = gen_at_least_2_constraints(X[row, :].astype(int).tolist(), NEXT_VAR)
        N_CLAUSES += used_clauses
        CNF += constraint_string

    for col in range(N):
        constraint_string, used_clauses, used_vars, NEXT_VAR = gen_at_most_2_constraints(X[:, col].astype(int).tolist(), NEXT_VAR)
        N_CLAUSES += used_clauses
        CNF += constraint_string

        constraint_string, used_clauses, used_vars, NEXT_VAR = gen_at_least_2_constraints(X[:, col].astype(int).tolist(), NEXT_VAR)
        N_CLAUSES += used_clauses
        CNF += constraint_string

    # build final cnf
    CNF = 'p cnf ' + str(NEXT_VAR-1) + ' ' + str(N_CLAUSES) + '\n' + CNF

    return X, CNF, NEXT_VAR-1


# External tools
# ##############
def get_random_xor_problem(CNF_IN_fp, N_DEC_VARS, N_ALL_VARS, s, min_l, max_l):
    # .cnf not part of arg!
    p = subprocess.Popen(['./gen-wff', CNF_IN_fp,
                          str(N_DEC_VARS), str(N_ALL_VARS),
                          str(s), str(min_l), str(max_l), 'xored'],
                          stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    result = p.communicate()

    os.remove(CNF_IN_fp + '-str-xored.xor')  # file not needed
    return CNF_IN_fp + '-str-xored.cnf'

def solve(CNF_IN_fp, N_DEC_VARS):
    seed = cryptogen.randint(0, 2147483647)  # actually no reason to do it; but can't hurt either
    p = subprocess.Popen(["./cryptominisat5", '-t', '4', '-r', str(seed), CNF_IN_fp], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    result = p.communicate()[0]

    sat_line = result.find('s SATISFIABLE')

    if sat_line != -1:
        # solution found!
        vars = parse_solution(result)[:N_DEC_VARS]

        # forbid solution (DeMorgan)
        negated_vars = list(map(lambda x: x*(-1), vars))
        with open(CNF_IN_fp, 'a') as f:
            f.write( (str(negated_vars)[1:-1] + ' 0\n').replace(',', ''))

        # assume solve is treating last constraint despite not changing header!
        # solve again

        seed = cryptogen.randint(0, 2147483647)
        p = subprocess.Popen(["./cryptominisat5", '-t', '4', '-r', str(seed), CNF_IN_fp], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
        result = p.communicate()[0]
        sat_line = result.find('s SATISFIABLE')
        if sat_line != -1:
            os.remove(CNF_IN_fp)  # not needed anymore
            return True, False, None
        else:
            return True, True, vars
    else:
        return False, False, None

def parse_solution(output):
    # assumes there is one
    vars = []
    for line in output.split("\n"):
        if line:
            if line[0] == 'v':
                line_vars = list(map(lambda x: int(x), line.split()[1:]))
                vars.extend(line_vars)
    return vars

# Core-algorithm
# ##############
def xorsample(X, CNF_IN_fp, N_DEC_VARS, N_VARS, s, min_l, max_l):
    start_time = time()
    while True:
        # add s random XOR constraints to F
        xored_cnf_fp = get_random_xor_problem(CNF_IN_fp, N_DEC_VARS, N_VARS, s, min_l, max_l)
        state_lvl1, state_lvl2, var_sol = solve(xored_cnf_fp, N_DEC_VARS)

        print('------------')

        if state_lvl1 and state_lvl2:
            print('FOUND')

            d = shelve.open('N_15_70_4_6_TO_PLOT')
            d[str(uuid.uuid4())] = (pickle.dumps(var_sol), time() - start_time)
            d.close()

            return True

        else:
            if state_lvl1:
                print('sol not unique')
            else:
                print('no sol found')

        print('------------')

""" Run """
N = 15
N_DEC_VARS = N*N
X, CNF, N_VARS = build_cnf(N)

with open('my_problem.cnf', 'w') as f:
    f.write(CNF)

counter = 0
while True:
    print('sample: ', counter)
    xorsample(X, 'my_problem', N_DEC_VARS, N_VARS, 70, 4, 6)
    counter += 1

Output will look like (removed some warnings):

------------
no sol found
------------
------------
no sol found
------------
------------
no sol found
------------
------------
sol not unique
------------
------------
FOUND

Core: CNF-formulation

We introduce one variable for every cell of the matrix. N=20 means 400 binary-variables.

Adjancency:

Precalculate all symmetry-reduced conflicts and add conflict-clauses.

Basic theory:

a -> !b
<->
!a v !b (propositional logic)

Row/Col-wise Cardinality:

This is tough to express in CNF and naive approaches need an exponential number of constraints.

We use some adder-circuit based encoding (SINZ, Carsten. Towards an optimal CNF encoding of boolean cardinality constraints) which introduces new auxiliary-variables.

Remark:

sum(var_set) <= k
<->
sum(negated(var_set)) >= len(var_set) - k

These SAT-encodings can be put into exact model-counters (for small N; e.g. < 9). The number of solutions equals Ante's results, which is a strong indication for a correct transformation!

There are also interesting approximate model-counters (also heavily based on xor-constraints) like approxMC which shows one more thing we can do with the SAT-formulation. But in practice i have not been able to use these (approxMC = autoconf; no comment).

Other experiments

I did also build a version using pblib, to use more powerful cardinality-formulations for the SAT-CNF formulation. I did not try to use the C++-based API, but only the reduced pbencoder, which automatically selects some best encoding, which was way worse than my encoding used above (which is best is still a research-problem; often even redundant-constraints can help).

Empirical analysis

For the sake of obtaining some sample-size (given my patience), i only computed samples for N=15. In this case we used:

  • N=70 xors
  • L,U = 4,6

I also computed some samples for N=20 with (100,3,6), but this takes a few mins and we reduced the lower bound!

Visualization

Here some animation (strengthening my love-hate relationship with matplotlib):

enter image description here

Edit: And a (reduced) comparison to brute-force uniform-sampling with N=5 (NXOR,L,U = 4, 10, 30):

enter image description here

enter image description here

(I have not yet decided on the addition of the plotting-code. It's as ugly as the above one and people might look too much into my statistical shambles; normalizations and co.)

Theory

Statistical analysis is probably hard to do as the underlying problem is of such combinatoric nature. It's even not entirely obvious how that final cell-PDF should look like. In the case of N=odd, it's probably non-uniform and looks like a chess-board (i did brute-force check N=5 to observe this).

One thing we can be sure about (imho): symmetry!

Given a cell-PDF matrix, we should expect, that the matrix is symmetric (A = A.T). This is checked in the visualization and the euclidean-norm of differences over time is plotted.

We can do the same on some other observation: observed pairings.

For N=3, we can observe the following pairs:

  • 0,1
  • 0,2
  • 1,2

Now we can do this per-row and per-column and should expect symmetry too!

Sadly, it's probably not easy to say something about the variance and therefore the needed samples to speak about confidence!

Observation

According to my simplified perception, current-samples and the cell-PDF look good, although convergence is not achieved yet (or we are far away from uniformity).

The more important aspect are probably the two norms, nicely decreasing towards 0. (yes; one could tune some algorithm for that by transposing with prob=0.5; but this is not done here as it would defeat it's purpose).

Potential next steps

  • Tune parameters
  • Check out the approach using #SAT-solvers / Model-counters instead of SAT-solvers
  • Try different CNF-formulations, especially in regards to cardinality-encodings and xor-encodings
    • XorSample is by default using tseitin-like encoding to get around exponentially grow
      • for smaller xors (as used) it might be a good idea to use naive encoding (which propagates faster)
        • XorSample supports that in theory; but the script's work differently in practice
        • Cryptominisat is known for dedicated XOR-handling (as it was build for analyzing cryptography including many xors) and might gain something by naive encoding (as inferring xors from blown-up CNFs is much harder)
  • More statistical-analysis
  • Get rid of XorSample scripts (shell + perl...)

Summary

  • The approach is very general
  • This code produces feasible samples
  • It should be not hard to prove, that every feasible solution can be sampled
  • Others have proven theoretical guarantees for uniformity for some params
    • does not hold for our params
  • Others have empirically / theoretically analyzed smaller parameters (in use here)
sascha
  • 32,238
  • 6
  • 68
  • 110
  • 2
    Oh no, you've got pictures! You could have told us you were working on this, then we wouldn't have bothered :-) – m69's been on strike for years Nov 09 '17 at 20:10
  • @m69 I even got many pictures :-) Just did that to check out matplotlibs animation module. I have yet to read and process your answer though (but DP makes me nervous). – sascha Nov 09 '17 at 20:12
  • Let's see how your pictures fare against my working code snippet :-) (That's one of the few advantages of using JS.) At the moment I'm still trailing behind the NAA :-) – m69's been on strike for years Nov 10 '17 at 06:39
  • Nice! I think pairs are better way to define PDF. Even more row/column depth + pairs. Solutions are symmetrical on mirror and rotation. Not even on row swap. It seems to me that for a row depth (0-n/2), there is consistent PDF (frequency) on possible pairs ((n-1) choose 2). For lower n's that PDFs can be calculated exactly, for larger n it can be approximated by traversing recursion to some depth. – Ante Nov 10 '17 at 12:41
  • I'll probably award the bounty here, unless someone can offer a compelling reason not to – harold Nov 10 '17 at 17:19
  • As a target for the distribution: if you sum all 8x8 solutions, you get this distribution: [[556376, 553224, 552952, 553212], [553224, 554328, 553928, 554284], [552952, 553928, 554888, 553996], [553212, 554284, 553996, 554272]]. (This is a quadrant, the others are mirrored). I can calculate other sizes if you want. – m69's been on strike for years Nov 10 '17 at 18:42
  • Read your answer twice, but there's too much that I'm not familiar with. Maybe in a few days/months/years I'll be able to understand that, but till then m69's answer suits me better. – Christoph Frings Nov 12 '17 at 13:25
5

(Updated test results, example run-through and code snippets below.)

You can use dynamic programming to calculate the number of solutions resulting from every state (in a much more efficient way than a brute-force algorithm), and use those (pre-calculated) values to create equiprobable random solutions.

Consider the example of a 7x7 matrix; at the start, the state is:

0,0,0,0,0,0,0  

meaning that there are seven adjacent unused columns. After adding two ones to the first row, the state could be e.g.:

0,1,0,0,1,0,0  

with two columns that now have a one in them. After adding ones to the second row, the state could be e.g.:

0,1,1,0,1,0,1  

After three rows are filled, there is a possibility that a column will have its maximum of two ones; this effectively splits the matrix into two independent zones:

1,1,1,0,2,0,1  ->  1,1,1,0 + 0,1  

These zones are independent in the sense that the no-adjacent-ones rule has no effect when adding ones to different zones, and the order of the zones has no effect on the number of solutions.

In order to use these states as signatures for types of solutions, we have to transform them into a canonical notation. First, we have to take into account the fact that columns with only 1 one in them may be unusable in the next row, because they contain a one in the current row. So instead of a binary notation, we have to use a ternary notation, e.g.:

2,1,1,0 + 0,1  

where the 2 means that this column was used in the current row (and not that there are 2 ones in the column). At the next step, we should then convert the twos back into ones.

Additionally, we can also mirror the seperate groups to put them into their lexicographically smallest notation:

2,1,1,0 + 0,1  ->  0,1,1,2 + 0,1  

Lastly, we sort the seperate groups from small to large, and then lexicographically, so that a state in a larger matrix may be e.g.:

0,0 + 0,1 + 0,0,2 + 0,1,0 + 0,1,0,1  

Then, when calculating the number of solutions resulting from each state, we can use memoization using the canonical notation of each state as a key.

Creating a dictionary of the states and the number of solutions for each of them only needs to be done once, and a table for larger matrices can probably be used for smaller matrices too.

Practically, you'd generate a random number between 0 and the total number of solutions, and then for every row, you'd look at the different states you could create from the current state, look at the number of unique solutions each one would generate, and see which option leads to the solution that corresponds with your randomly generated number.


Note that every state and the corresponding key can only occur in a particular row, so you can store the keys in seperate dictionaries per row.


TEST RESULTS

A first test using unoptimized JavaScript gave very promising results. With dynamic programming, calculating the number of solutions for a 10x10 matrix now takes a second, where a brute-force algorithm took several hours (and this is the part of the algorithm that only needs to be done once). The size of the dictionary with the signatures and numbers of solutions grows with a diminishing factor approaching 2.5 for each step in size; the time to generate it grows with a factor of around 3.

These are the number of solutions, states, signatures (total size of the dictionaries), and maximum number of signatures per row (largest dictionary per row) that are created:

size                  unique solutions                  states    signatures    max/row

 4x4                                               2            9          6           2
 5x5                                              16           73         26           8
 6x6                                             722          514        107          40
 7x7                                          33,988        2,870        411         152
 8x8                                       2,215,764       13,485      1,411         596
 9x9                                     179,431,924       56,375      4,510       1,983
10x10                                 17,849,077,140      218,038     13,453       5,672
11x11                              2,138,979,146,276      801,266     38,314      14,491
12x12                            304,243,884,374,412    2,847,885    104,764      35,803
13x13                         50,702,643,217,809,908    9,901,431    278,561      96,414
14x14                      9,789,567,606,147,948,364   33,911,578    723,306     238,359
15x15                  2,168,538,331,223,656,364,084  114,897,838  1,845,861     548,409
16x16                546,386,962,452,256,865,969,596          ...  4,952,501   1,444,487
17x17            155,420,047,516,794,379,573,558,433              12,837,870   3,754,040
18x18         48,614,566,676,379,251,956,711,945,475              31,452,747   8,992,972
19x19     17,139,174,923,928,277,182,879,888,254,495              74,818,773  20,929,008
20x20  6,688,262,914,418,168,812,086,412,204,858,650             175,678,000  50,094,203

(Additional results obtained with C++, using a simple 128-bit integer implementation. To count the states, the code had to be run using each state as a seperate signature, which I was unable to do for the largest sizes. )


EXAMPLE

The dictionary for a 5x5 matrix looks like this:

row 0:  00000  -> 16        row 3:  101    ->  0
                                    1112   ->  1
row 1:  20002  ->  2                1121   ->  1
        00202  ->  4                1+01   ->  0
        02002  ->  2                11+12  ->  2
        02020  ->  2                1+121  ->  1
                                    0+1+1  ->  0
row 2:  10212  ->  1                1+112  ->  1
        12012  ->  1
        12021  ->  2        row 4:  0      ->  0
        12102  ->  1                11     ->  0
        21012  ->  0                12     ->  0
        02121  ->  3                1+1    ->  1
        01212  ->  1                1+2    ->  0

The total number of solutions is 16; if we randomly pick a number from 0 to 15, e.g. 13, we can find the corresponding (i.e. the 14th) solution like this:

state:      00000  
options:    10100  10010  10001  01010  01001  00101  
signature:  00202  02002  20002  02020  02002  00202  
solutions:    4      2      2      2      2      4  

This tells us that the 14th solution is the 2nd solution of option 00101. The next step is:

state:      00101  
options:    10010  01010  
signature:  12102  02121  
solutions:    1      3  

This tells us that the 2nd solution is the 1st solution of option 01010. The next step is:

state:      01111  
options:    10100  10001  00101  
signature:  11+12  1112   1+01  
solutions:    2      1      0  

This tells us that the 1st solution is the 1st solution of option 10100. The next step is:

state:      11211  
options:    01010  01001  
signature:  1+1    1+1  
solutions:    1      1  

This tells us that the 1st solutions is the 1st solution of option 01010. The last step is:

state:      12221  
options:    10001  

And the 5x5 matrix corresponding to randomly chosen number 13 is:

0 0 1 0 1  
0 1 0 1 0  
1 0 1 0 0
0 1 0 1 0  
1 0 0 0 1  

And here's a quick'n'dirty code example; run the snippet to generate the signature and solution count dictionary, and generate a random 10x10 matrix (it takes a second to generate the dictionary; once that is done, it generates random solutions in half a millisecond):

function signature(state, prev) {
    var zones = [], zone = [];
    for (var i = 0; i < state.length; i++) {
        if (state[i] == 2) {
            if (zone.length) zones.push(mirror(zone));
            zone = [];
        }
        else if (prev[i]) zone.push(3);
        else zone.push(state[i]);
    }
    if (zone.length) zones.push(mirror(zone));
    zones.sort(function(a,b) {return a.length - b.length || a - b;});
    return zones.length ? zones.join("2") : "2";

    function mirror(zone) {
        var ltr = zone.join('');
        zone.reverse();
        var rtl = zone.join('');
        return (ltr < rtl) ? ltr : rtl;
    }
}

function memoize(n) {
    var memo = [], empty = [];
    for (var i = 0; i <= n; i++) memo[i] = [];
    for (var i = 0; i < n; i++) empty[i] = 0;
    memo[0][signature(empty, empty)] = next_row(empty, empty, 1);
    return memo;

    function next_row(state, prev, row) {
        if (row > n) return 1;
        var solutions = 0;
        for (var i = 0; i < n - 2; i++) {
            if (state[i] == 2 || prev[i] == 1) continue;
            for (var j = i + 2; j < n; j++) {
                if (state[j] == 2 || prev[j] == 1) continue;
                var s = state.slice(), p = empty.slice();
                ++s[i]; ++s[j]; ++p[i]; ++p[j];
                var sig = signature(s, p);
                var sol = memo[row][sig];
                if (sol == undefined) 
                    memo[row][sig] = sol = next_row(s, p, row + 1);
                solutions += sol;
            }
        }
        return solutions;
    }
}

function random_matrix(n, memo) {
    var matrix = [], empty = [], state = [], prev = [];
    for (var i = 0; i < n; i++) empty[i] = state[i] = prev[i] = 0;
    var total = memo[0][signature(empty, empty)];
    var pick = Math.floor(Math.random() * total);
    document.write("solution " + pick.toLocaleString('en-US') + 
        " from a total of " + total.toLocaleString('en-US') + "<br>");
    for (var row = 1; row <= n; row++) {
        var options = find_options(state, prev);
        for (var i in options) {
            var state_copy = state.slice();
            for (var j in state_copy) state_copy[j] += options[i][j];
            var sig = signature(state_copy, options[i]);
            var solutions = memo[row][sig];
            if (pick < solutions) {
                matrix.push(options[i].slice());
                prev = options[i].slice();
                state = state_copy.slice();
                break;
            }
            else pick -= solutions;
        }
    }
    return matrix;

    function find_options(state, prev) {
        var options = [];
        for (var i = 0; i < n - 2; i++) {
            if (state[i] == 2 || prev[i] == 1) continue;
            for (var j = i + 2; j < n; j++) {
                if (state[j] == 2 || prev[j] == 1) continue;
                var option = empty.slice();
                ++option[i]; ++option[j];
                options.push(option);
            }
        }
        return options;
    }
}

var size = 10;
var memo = memoize(size);
var matrix = random_matrix(size, memo);
for (var row in matrix) document.write(matrix[row] + "<br>");

The code snippet below shows the dictionary of signatures and solution counts for a matrix of size 10x10. I've used a slightly different signature format from the explanation above: the zones are delimited by a '2' instead of a plus sign, and a column which has a one in the previous row is marked with a '3' instead of a '2'. This shows how the keys could be stored in a file as integers with 2×N bits (padded with 2's).

function signature(state, prev) {
    var zones = [], zone = [];
    for (var i = 0; i < state.length; i++) {
        if (state[i] == 2) {
            if (zone.length) zones.push(mirror(zone));
            zone = [];
        }
        else if (prev[i]) zone.push(3);
        else zone.push(state[i]);
    }
    if (zone.length) zones.push(mirror(zone));
    zones.sort(function(a,b) {return a.length - b.length || a - b;});
    return zones.length ? zones.join("2") : "2";

    function mirror(zone) {
        var ltr = zone.join('');
        zone.reverse();
        var rtl = zone.join('');
        return (ltr < rtl) ? ltr : rtl;
    }
}

function memoize(n) {
    var memo = [], empty = [];
    for (var i = 0; i <= n; i++) memo[i] = [];
    for (var i = 0; i < n; i++) empty[i] = 0;
    memo[0][signature(empty, empty)] = next_row(empty, empty, 1);
    return memo;

    function next_row(state, prev, row) {
        if (row > n) return 1;
        var solutions = 0;
        for (var i = 0; i < n - 2; i++) {
            if (state[i] == 2 || prev[i] == 1) continue;
            for (var j = i + 2; j < n; j++) {
                if (state[j] == 2 || prev[j] == 1) continue;
                var s = state.slice(), p = empty.slice();
                ++s[i]; ++s[j]; ++p[i]; ++p[j];
                var sig = signature(s, p);
                var sol = memo[row][sig];
                if (sol == undefined) 
                    memo[row][sig] = sol = next_row(s, p, row + 1);
                solutions += sol;
            }
        }
        return solutions;
    }
}

var memo = memoize(10);
for (var i in memo) {
    document.write("row " + i + ":<br>");
    for (var j in memo[i]) {
        document.write("&quot;" + j + "&quot;: " + memo[i][j] + "<br>");
    }
}
  • Takes a few reads to understand - but yeah, the principle is similar to nonograms - as there are increasingly more zeroes with bigger matrixes, any non-zero reduces amount of available combination with that column/row, and once you get 2 non-zeroes column/row is excluded from further search. Which then leads to creation of "isles", which could be (in theory) solved as DP sub-problems. – c69 Nov 09 '17 at 03:23
  • 1
    @c69 Unfortunately the isles or independent zones here cannot be treated as seperate sub-problems, because each subsequent row could have its ones in different zones. I don't see an obvious way to completely seperate them. – m69's been on strike for years Nov 09 '17 at 03:30
  • I see.. while isles are sepatated - for the purpose of finding a (second) non-zero, they are still together :( e.g. if you split 100x100 grid in 4 parts by placing simple 4x4 matrix with checkered pattern, then 4 central rows and columns will be knocked out, but you still cannot process each of 48x48 squares separately, as the missing pixel might be in the other region.. – c69 Nov 09 '17 at 04:20
  • 1
    @c69 Yes. They offer some advantages, like being able to mirror them independently, and re-ordering them, but they're never fully independent. – m69's been on strike for years Nov 09 '17 at 04:23
  • 1
    This is definitely the solution I was looking for. Lesson learned : a symmetrical problem doesn't require a symmetrical solution ! – Christoph Frings Nov 12 '17 at 13:22
  • @ChristophFrings What language are you going to use for this? You'll need >64 bit integers for some of the calculations for the larger squares. – m69's been on strike for years Nov 12 '17 at 13:38
  • @m69 python, no problem. – Christoph Frings Nov 12 '17 at 21:23
  • @ChristophFrings Feel free to edit in the exact number of solutions and the size of the table for 12x12 to 19x19 when you get there. (Can I also suggest to edit the title to something like "random binary matrix with two non-adjacent non-zeros in each row and column"?) – m69's been on strike for years Nov 12 '17 at 21:46
  • @m69 My implementation is far from optimal (takes ~20 sec for n=10, only 0.73 sec for n=8). But yes, if I get these results, I add them. – Christoph Frings Nov 12 '17 at 22:18
  • @m69 Title changed. Couldn't chose what to get rid of, so I put everything in there. – Christoph Frings Nov 12 '17 at 22:21
  • @ChristophFrings Obvious optimization in my version: integrate find_options() into random_matrix(), because you may not need all options. The thing that kills speeds is probably the signature function; it's fiddly and gets called very often. Also, associative arrays my become slow if they're very large; I've been wondering about using sorted numerical keys and binary search. – m69's been on strike for years Nov 12 '17 at 22:26
  • @m69 I won't be able to complete your data: can't even complete n=12 (out of memory) :( – Christoph Frings Nov 14 '17 at 18:58
  • @ChristophFrings What sort of keys are you using ? A 32-bit integer should be enough up to 16x16. But even if you're using strings, 104764 string+integer pairs shouldn't give you a memory error. Have you checked whether your code creates the right number of keys? If your signature function doesn't correctly create the same key for different states of the same type, it could still return the correct result, but use a lot more keys. – m69's been on strike for years Nov 14 '17 at 19:25
  • @m69 I'm being a bit heavy handed on my data structures (using classes with all that's needed to generate the solutions at the end, maybe a little to much, but I'm trying to avoid multiple reprocessing of my data). Plus I'm using an additional lookup table. My signatures are strings, and my compacting function seems to work well. – Christoph Frings Nov 14 '17 at 20:33
  • @ChristophFrings I finally got round to trying out pre-calculating and storing the dictionaries for the 128-bit C++ version. For 19x19 matrices the dictionary files are 1.67GB in total, it takes 36 seconds to load them into memory, and then you can generate 6000 matrices per second (on my i5 desktop). With every step down from 19x19, the files are 2.5x smaller and load 3x faster. – m69's been on strike for years Nov 29 '17 at 02:25
3

Just few thoughts. Number of matrices satisfying conditions for n <= 10:

3  0
4  2
5  16
6  722
7  33988
8  2215764
9  179431924
10 17849077140

Unfortunatelly there is no sequence with these numbers in OEIS.

There is one similar (A001499), without condition for neighbouring one's. Number of nxn matrices in this case is 'of order' as A001499's number of (n-1)x(n-1) matrices. That is to be expected since number of ways to fill one row in this case, position 2 one's in n places with at least one zero between them is ((n-1) choose 2). Same as to position 2 one's in (n-1) places without the restriction.

I don't think there is an easy connection between these matrix of order n and A001499 matrix of order n-1, meaning that if we have A001499 matrix than we can construct some of these matrices.

With this, for n=20, number of matrices is >10^30. Quite a lot :-/

Ante
  • 5,350
  • 6
  • 23
  • 46
  • Yes, two many to be listed, it's fortunate that this is not what I am hoping for. Looking at your numbers (how did you get them... brute force ?) it seems to me that u(n)/u(n-1) ~ n² which would lead to u(n)~(n!)²... If this is true, we would even be closer to 10^37 with n = 20... – Christoph Frings Nov 06 '17 at 20:50
  • @ChristophFrings Yes, brute force. It took ~1 hour for n=10. Computation time (and number of combinations) grow with factor ~100. For n=11, computation would take >4 days. Because of connection to A001499's n-1 case, I think that n=20 is of order 10^33-35. – Ante Nov 06 '17 at 21:35
  • FYI, I get [116](https://pastebin.com/K83Fc4X8) different 5x5 matricies – maniek Nov 07 '17 at 00:17
  • I got the same results with a brute force algorithm (and also gave up after 10x10 because it was taking too long). – m69's been on strike for years Nov 07 '17 at 01:16
  • 1
    You brute-force guys are crazy... Can't compete (python...) :-) – sascha Nov 07 '17 at 01:17
  • @sascha I used JavaScript :-) – m69's been on strike for years Nov 07 '17 at 01:18
  • Even more crazy :-) – sascha Nov 07 '17 at 01:19
  • @sascha I started with python, and with pypy n=10 would take probably 6-7 hours. I didn't use symmetry on x coordinate, ie. number of matrices with first row 1010...0 is same as number of matrices with 0...0101. That reduces computation time for factor ~2. It is possible to parallelized implementation by running threads with different first row configuration. With that on my laptop it would take >half a day for n=11. It would be possible to calculate n=12 on a cluster. – Ante Nov 07 '17 at 09:28
1

This solution use recursion in order to set the cell of the matrix one by one. If the random walk finish with an impossible solution then we rollback one step in the tree and we continue the random walk.

The algorithm is efficient and i think that the generated data are highly equiprobable.

package rndsqmatrix;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.IntStream;

public class RndSqMatrix {

    /**
     * Generate a random matrix
     * @param size the size of the matrix
     * @return the matrix encoded in 1d array i=(x+y*size)
     */
    public static int[] generate(final int size) {
        return generate(size, new int[size * size], new int[size],
                new int[size]);
    }

    /**
     * Build a matrix recursivly with a random walk 
     * @param size the size of the matrix
     * @param matrix the matrix encoded in 1d array i=(x+y*size)
     * @param rowSum
     * @param colSum
     * @return 
     */
    private static int[] generate(final int size, final int[] matrix,
            final int[] rowSum, final int[] colSum) {

        // generate list of valid positions
        final List<Integer> positions = new ArrayList();
        for (int y = 0; y < size; y++) {
            if (rowSum[y] < 2) {
                for (int x = 0; x < size; x++) {
                    if (colSum[x] < 2) {
                        final int p = x + y * size;
                        if (matrix[p] == 0
                                && (x == 0 || matrix[p - 1] == 0)
                                && (x == size - 1 || matrix[p + 1] == 0)
                                && (y == 0 || matrix[p - size] == 0)
                                && (y == size - 1 || matrix[p + size] == 0)) {
                            positions.add(p);
                        }
                    }
                }
            }
        }

        // no valid positions ?
        if (positions.isEmpty()) {

            // if the matrix is incomplete => return null
            for (int i = 0; i < size; i++) {
                if (rowSum[i] != 2 || colSum[i] != 2) {
                    return null;
                }
            }

            // the matrix is complete => return it
            return matrix;
        }

        // random walk 
        Collections.shuffle(positions);
        for (int p : positions) {
            // set '1' and continue recursivly the exploration
            matrix[p] = 1;
            rowSum[p / size]++;
            colSum[p % size]++;
            final int[] solMatrix = generate(size, matrix, rowSum, colSum);
            if (solMatrix != null) {
                return solMatrix;
            }

            // rollback 
            matrix[p] = 0;
            rowSum[p / size]--;
            colSum[p % size]--;
        }

        // we can't find a valid matrix from here => return null
        return null;
    }

    public static void printMatrix(final int size, final int[] matrix) {
        for (int y = 0; y < size; y++) {
            for (int x = 0; x < size; x++) {
                System.out.print(matrix[x + y * size]);
                System.out.print(" ");
            }
            System.out.println();
        }
    }

    public static void printStatistics(final int size, final int count) {
        final int sumMatrix[] = new int[size * size];
        for (int i = 0; i < count; i++) {
            final int[] matrix = generate(size);
            for (int j = 0; j < sumMatrix.length; j++) {
                sumMatrix[j] += matrix[j];
            }
        }
        printMatrix(size, sumMatrix);
    }

    public static void checkAlgorithm() {
        final int size = 8; 
        final int count = 2215764;
        final int divisor = 122;
        final int sumMatrix[] = new int[size * size];
        for (int i = 0; i < count/divisor ; i++) {
            final int[] matrix = generate(size);
            for (int j = 0; j < sumMatrix.length; j++) {
                sumMatrix[j] += matrix[j];
            }
        }
        int total = 0;
        for(int i=0; i < sumMatrix.length; i++) {
            total += sumMatrix[i];
        }
        final double factor = (double)total / (count/divisor);
        System.out.println("Factor=" + factor + " (theory=16.0)");
    }

    public static void benchmark(final int size, final int count,
            final boolean parallel) {
        final long begin = System.currentTimeMillis();
        if (!parallel) {
            for (int i = 0; i < count; i++) {
                generate(size);
            }
        } else {
            IntStream.range(0, count).parallel().forEach(i -> generate(size));
        }
        final long end = System.currentTimeMillis();
        System.out.println("rate="
                + (double) (end - begin) / count + "ms/matrix");
    }

    public static void main(String[] args) {
        checkAlgorithm();
        benchmark(8, 10000, true);
        //printStatistics(8, 2215764/36);
        printStatistics(8, 2215764);
    }
}

The output is:

Factor=16.0 (theory=16.0)
rate=0.2835ms/matrix
552969 554643 552895 554632 555680 552753 554567 553389 
554071 554847 553441 553315 553425 553883 554485 554061 
554272 552633 555130 553699 553604 554298 553864 554028 
554118 554299 553565 552986 553786 554473 553530 554771 
554474 553604 554473 554231 553617 553556 553581 553992 
554960 554572 552861 552732 553782 554039 553921 554661 
553578 553253 555721 554235 554107 553676 553776 553182 
553086 553677 553442 555698 553527 554850 553804 553444
  • How high is "highly" equiprobable? Do you have any test results? (For comparison: I listed the uniform distribution for an 8x8 matrix in a comment under sascha's answer). – m69's been on strike for years Nov 11 '17 at 04:53
  • Honestly i don't know how to test exactly the factor of equiprobability. I have updated my code with the function printProbability(..). It's an approximation but better than nothing and the result seems very good. – Olivier Pellier-Cuit Nov 11 '17 at 04:57
  • If you run it for 8x8 matrices 2215764 times and add them all up, the sums should be [[556376, 553224, 552952, 553212], [553224, 554328, 553928, 554284], [552952, 553928, 554888, 553996], [553212, 554284, 553996, 554272]]. (This is the top-left quadrant, the others are mirrored.) – m69's been on strike for years Nov 11 '17 at 05:00
  • I have added the output for 8x8 matrices running 2215764 times. Could you interpret the result for me ? – Olivier Pellier-Cuit Nov 11 '17 at 05:08
  • If you take the total of all the numbers and divide that by 2215764 you should get 16 (because there are 16 ones in an 8x8 matrix) but I'm getting only 14.81, so either you're summing fewer than 2215764 matrices, or some of them have fewer than 16 ones. – m69's been on strike for years Nov 11 '17 at 05:20
  • thanks i am going to check my code perhaps i have a bug somewhere :) – Olivier Pellier-Cuit Nov 11 '17 at 05:21
  • If you run the test multiple times and take the average of the results, then the longer you let it run, the closer the result should get to the ideal distribution (as shown in sascha's animations). If you find that the results settle on other values, then there is a bias in your method (but it may still be close enough for Christoph, if it's fast and you can prove that every solution can be reached). – m69's been on strike for years Nov 11 '17 at 05:34
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/158746/discussion-between-olivier-pellier-cuit-and-m69). – Olivier Pellier-Cuit Nov 11 '17 at 05:44
  • 2
    I have found the bug (i have edited my post). The algorithm is less efficient arround 0.3ms for generating a matrix but it's working. – Olivier Pellier-Cuit Nov 11 '17 at 21:15
0

Here is a very fast approach of generating the matrix row by row, written in Java:

public static void main(String[] args) throws Exception {
    int n = 100;
    Random rnd = new Random();
    byte[] mat = new byte[n*n];
    byte[] colCount = new byte[n];

    //generate row by row
    for (int x = 0; x < n; x++) {               

        //generate a random first bit
        int b1 = rnd.nextInt(n);
        while ( (x > 0 && mat[(x-1)*n + b1] == 1) ||    //not adjacent to the one above
                (colCount[b1] == 2)                     //not in a column which has 2
                ) b1 = rnd.nextInt(n);


        //generate a second bit, not equal to the first one
        int b2 = rnd.nextInt(n);
        while ( (b2 == b1) ||                           //not the same as bit 1
                (x > 0 && mat[(x-1)*n + b2] == 1) ||    //not adjacent to the one above
                (colCount[b2] == 2) ||                  //not in a column which has 2
                (b2 == b1 - 1) ||                       //not adjacent to b1
                (b2 == b1 + 1)
                ) b2 = rnd.nextInt(n);


        //fill the matrix values and increment column counts
        mat[x*n + b1] = 1;
        mat[x*n + b2] = 1;              
        colCount[b1]++;
        colCount[b2]++;
    }

    String arr = Arrays.toString(mat).substring(1, n*n*3 - 1);
    System.out.println(arr.replaceAll("(.{" + n*3 + "})", "$1\n"));     
}

It essentially generates each a random row at a time. If the row will violate any of the conditions, it is generated again (again randomly). I believe this will satisfy condition 4 as well.

Adding a quick note that it will spin forever for N-s where there is no solutions (like N=3).

xpa1492
  • 1,953
  • 1
  • 10
  • 19
  • 2
    The different options in each row lead to a different number of solutions; e.g. in a 5×5 matrix starting with 10100 leads to 4 solutions, starting with 10010 to only 2. So randomly choosing per row will not give uniform sampling. – m69's been on strike for years Nov 10 '17 at 12:00