【问题标题】:Algorithm for equiprobable random square binary matrices with two non-adjacent non-zeros in each row and column在每行和每列中具有两个非相邻非零的等概率随机平方二进制矩阵的算法
【发布时间】:2018-04-11 15:45:48
【问题描述】:

如果有人能指点我一个允许我这样做的算法,那就太好了:

  1. 创建一个包含条目 0 和 1 的随机方阵,这样
  2. 每一行和每一列都恰好包含两个非零条目,
  3. 两个非零条目不能相邻,
  4. 所有可能的矩阵都是等概率的。

现在我设法通过以下方式实现第 1 点和第 2 点:可以使用适当的行和列排列将这样的矩阵转换为具有以下形式的块的对角块矩阵

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

所以我从这样的矩阵开始,使用 [0, ..., n-1] 的分区,并通过随机排列行和列来对其进行加扰。不幸的是,我找不到整合邻接条件的方法,而且我很确定我的算法不会平等地对待所有矩阵。

更新

我已经设法实现了第 3 点。答案实际上就在我的眼皮底下:我正在创建的块矩阵包含考虑邻接条件所需的所有信息。首先是一些属性和定义:

  • 一个合适的矩阵定义了[1, ..., n] 的排列,可以像这样构建:在1 行中选择一个1。包含此条目的列在与 1 不同的行 a 上恰好包含另一个等于 1 的条目。同样,行 a 在包含行 b 的第二个条目 1 的列中包含另一个条目 1,并且很快。这将启动一个排列 1 -> a -> b ...

例如,对于以下矩阵,从标记的条目开始

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 |

我们得到排列1 -> 3 -> 5 -> 2 -> 6 -> 4 -> 1

  • 这种排列的循环导致我前面提到的块矩阵。我还提到了加扰块矩阵,使用行和列上的任意排列来重建与要求兼容的矩阵。

但我使用的是 any 排列,这导致了一些相邻的非零条目。为避免这种情况,我必须选择将块矩阵中相邻的行(和列)分开的排列。实际上,更准确地说,如果两行属于同一个块并且循环连续(块的第一行和最后一行也被认为是连续的),那么我要应用的排列必须将这些行移动到最终矩阵的非连续行中(在这种情况下,我将称两行不兼容)。

所以问题变成了:如何构建所有这些排列?

最简单的想法是通过随机添加与前一个兼容的行来逐步构建排列。例如,考虑n = 6 使用分区6 = 3 + 3 和相应的块矩阵的情况

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 |

这里的行 123 相互不兼容,456 也是如此。选择一个随机行,例如3

我们将一个排列写成一个数组:[2, 5, 6, 4, 3, 1]意思是1 -> 2,2 -> 5,3 -> 6,...这意味着块矩阵的行2将成为最终的第一行矩阵,行5将成为第二行,以此类推。

现在让我们通过随机选择一行来构建一个合适的排列,比如3

  • p = [3, ...]

然后将在与3 兼容的剩余行中随机选择下一行:456。假设我们选择4

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

必须在12 之间做出下一个选择,例如1

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

等等:p = [3, 4, 1, 5, 2, 6].

将此排列应用于块矩阵,我们得到:

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 |

这样做,我们设法垂直隔离所有非零条目。列也必须这样做,例如通过使用排列 p' = [6, 3, 5, 1, 4, 2] 最终得到

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 | 

所以这似乎很有效,但是构建这些排列需要谨慎,因为很容易卡住:例如,使用n=6 和分区6 = 2 + 2 + 2,遵循之前设置的构建规则可以导致p = [1, 3, 2, 4, ...]。不幸的是,56 是不兼容的,所以选择一个或另一个使得最后的选择不可能。我想我已经找到了所有导致死胡同的情况。我将用r 表示剩余选择的集合:

  1. p = [..., x, ?]r = {y}xy 不兼容
  2. p = [..., x, ?, ?]r = {y, z}yz 都与 x 不兼容(无法选择)
  3. p = [..., ?, ?]r = {x, y}xy 不兼容(任何选择都会导致情况 1)
  4. p = [..., ?, ?, ?]r = {x, y, z}xyz 循环连续(选择 xz 会导致情况 2,选择 y 会导致情况 3)
  5. p = [..., w, ?, ?, ?]r = {x, y, z}xwy 是一个3-cycle(xy 都不能选择,选择z 会导致情况3)
  6. p = [..., ?, ?, ?, ?]r = {w, x, y, z}wxyz 是一个 4 周期(任何选择都会导致情况 4)
  7. p = [..., ?, ?, ?, ?]r = {w, x, y, z}xyz 是一个 3 周期(选择 w 会导致情况 4,选择任何其他都会导致情况 4)

现在看来,以下算法给出了所有合适的排列:

  • 只要选择的数字严格超过 5 个,就在兼容的数字中随机选择。
  • 如果还有 5 个数字可供选择:如果剩余数字包含 3 循环或 4 循环,则打破该循环(即选择属于该循环的数字)。
  • 如果还有 4 个数字可供选择:如果剩余的数字包含三个循环连续的数字,则选择其中一个。
  • 如果还有 3 个数字可供选择:如果剩余的数字包含两个循环连续的数字,则选择其中一个。

我很确定这可以让我生成所有合适的排列,从而生成所有合适的矩阵。

不幸的是,每个矩阵都会被多次获取,具体取决于所选的分区。

【问题讨论】:

  • 我们在说什么尺寸的矩阵?
  • @m69 小,小于 20。
  • 如果你在 BDD 中编码约束并使用通常的“生成统一随机满足向量”算法怎么样?这是正确的,但由于 BDD 可能很大,因此可能效果不佳,但我无法提前很好地判断..
  • 例如见here,它可能有点长,但非常透彻
  • @harold 我用自制的 bdd 实现做了一些测试(在 python 中,我知道,不是最好的选择)。对于 n = 5,bdd 有 185 个节点,对于 n=6,有 1629 个节点。对于我有数十万个节点的耐心,n=7 的情况已经花费了太长时间来计算。毕竟,我有 n² 个变量,很快就会变大。所以这对我来说不是解决方案。

标签: algorithm matrix random


【解决方案1】:

只是一些想法。满足 n

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

很遗憾,OEIS 中没有这些数字的序列。

有一个类似的(A001499),没有条件相邻的。在这种情况下,nxn 矩阵的数量是“有序”的,因为 A001499 的 (n-1)x(n-1) 矩阵的数量。这是可以预料的,因为数字 在这种情况下填充一行的方法中,在 n 个位置中的第 2 个位置至少有一个 0 是 ((n-1) 选择 2)。与没有限制的(n-1)个位置的第2个相同。

我认为这些 n 阶矩阵和 n-1 阶 A001499 矩阵之间没有简单的联系,这意味着如果我们有 A001499 矩阵,我们就可以构造其中的一些矩阵。

这样,对于 n=20,矩阵的数量是 >10^30。很多:-/

【讨论】:

  • 是的,要列出两个很多,幸运的是,这不是我所希望的。看看你的数字(你是如何得到它们的......蛮力?)在我看来,u(n)/u(n-1) ~ n² 会导致 u(n)~(n!)²。 .. 如果这是真的,我们甚至会更接近 10^37,n = 20...
  • @ChristophFrings 是的,蛮力。 n = 10 大约需要 1 小时。计算时间(和组合数量)随着因子 ~100 增长。对于 n=11,计算将花费 >4 天。由于与 A001499 的 n-1 案例有关,我认为 n=20 的顺序为 10^33-35。
  • 仅供参考,我得到 116 不同的 5x5 矩阵
  • 我用蛮力算法得到了相同的结果(并且在 10x10 之后也放弃了,因为它花费了太长时间)。
  • 你们这些蛮力的家伙疯了...无法竞争(python...):-)
【解决方案2】:

(下面更新了测试结果、示例运行和代码 sn-ps。)

您可以使用动态编程来计算每个状态产生的解的数量(以比蛮力算法更有效的方式),并使用这些(预先计算的)值来创建等概率的随机解。

考虑一个 7x7 矩阵的例子;一开始,状态是:

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

意味着有七个相邻的未使用的列。在第一行添加两个之后,状态可以是例如:

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

现在有两列,其中有一列。将 1 添加到第二行后,状态可能是例如:

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

填满三行后,一列有可能最多有两个;这有效地将矩阵分成两个独立的区域:

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

这些区域是独立的,因为在将 1 添加到不同区域时,不相邻的规则不起作用,并且区域的顺序对解决方案的数量没有影响。

为了将这些状态用作解决方案类型的签名,我们必须将它们转换为规范符号。首先,我们必须考虑这样一个事实,即其中只有 1 个 1 的列在下一行中可能无法使用,因为它们在当前行中包含 1 个。因此,我们必须使用三进制表示法,而不是二进制表示法,例如:

2,1,1,0 + 0,1  

其中 2 表示该列已在当前行中使用(而不是该列中有 2 个)。在下一步,我们应该将两个转换回一个。

此外,我们还可以镜像单独的组以将它们放入字典中最小的符号:

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

最后,我们将单独的组从小到大排序,然后按字典顺序排列,这样更大矩阵中的状态可能是例如:

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

然后,当计算每个状态产生的解的数量时,我们可以使用每个状态的规范符号作为键来使用记忆。

只需要为每个状态创建一个字典和解决方案的数量,并且更大矩阵的表也可能用于更小的矩阵。

实际上,您会生成一个介于 0 和解决方案总数之间的随机数,然后对于每一行,您会查看可以从当前状态创建的不同状态,查看唯一解决方案的数量每个都会生成,并查看哪个选项会导致与您随机生成的数字相对应的解决方案。


请注意,每个状态和对应的键只能出现在特定的行中,因此您可以将键存储在每行单独的字典中。


测试结果

使用未优化的 JavaScript 进行的第一次测试给出了非常有希望的结果。使用动态编程,计算 10x10 矩阵的解数现在需要一秒钟,而蛮力算法需要几个小时(这是算法的一部分,只需要完成一次)。带有签名和解决方案数量的字典的大小随着大小的每一步接近 2.5 的递减因子而增长;生成它的时间增加了大约 3 倍。

这些是创建的解决方案、状态、签名(字典的总大小)和每行的最大签名数(每行最大的字典):

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

(使用 C++ 获得的附加结果,使用简单的 128 位整数实现。要计算状态,必须使用每个状态作为单独的签名运行代码,我无法为最大的尺寸。)


示例

5x5 矩阵的字典如下所示:

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

解的总数为16;如果我们随机选择一个从 0 到 15 的数字,例如13,我们可以像这样找到对应的(即第14个)解决方案:

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

这告诉我们第14个解决方案是选项00101的第2个解决方案。下一步是:

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

这告诉我们第二解是选项01010的第一解。下一步是:

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

这告诉我们第一个解决方案是选项10100的第一个解决方案。下一步是:

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

这告诉我们第一个解决方案是选项01010的第一个解决方案。最后一步是:

state:      12221  
options:    10001  

随机选择的数字13对应的5x5矩阵为:

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  

这是一个快速的'n'dirty 代码示例;运行 sn-p 生成签名和解计数字典,并生成一个随机 10x10 矩阵(生成字典需要一秒钟;一旦完成,它会在半毫秒内生成随机解):

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>");

下面的代码 sn-p 显示了大小为 10x10 的矩阵的签名字典和解决方案计数。我使用了与上面的解释略有不同的签名格式:区域由“2”而不是加号分隔,前一行中有一个的列用“3”而不是'2'。这显示了如何将密钥作为具有 2×N 位(用 2 填充)的整数存储在文件中。

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>");
    }
}

【讨论】:

  • 需要读几遍才能理解 - 但是,原理类似于非图 - 因为随着矩阵的增大,零越来越多,任何非零都会减少与该列/行的可用组合数量,一旦你得到 2 个非零列/行,就会被排除在进一步的搜索之外。然后导致创建“孤岛”,这可以(理论上)作为 DP 子问题来解决。
  • @c69 不幸的是,这里的小岛或独立区域不能被视为单独的子问题,因为每个后续行都可能在不同的区域中有它的子问题。我没有看到一种明显的方法来完全分离它们。
  • 我明白了.. 虽然小岛是分开的 - 为了找到(第二个)非零,它们仍然在一起:(例如,如果您通过放置简单的 4x4 矩阵将 100x100 网格分成 4 部分使用方格图案,则 4 个中心行和列将被剔除,但您仍然不能单独处理每个 48x48 正方形,因为丢失的像素可能在其他区域..
  • @c69 是的。它们提供了一些优势,例如能够独立镜像它们并重新排序它们,但它们永远不会完全独立。
  • 这绝对是我一直在寻找的解决方案。经验教训:对称问题不需要对称解决方案!
【解决方案3】:

简介

这是一些原型方法,试图解决更一般的任务 统一组合采样,对于我们的方法而言,这意味着:我们可以将这种方法用于我们可以表述为 SAT-problem 的所有内容。

没有直接利用您的问题,并且绕了很多弯路。这种对 SAT 问题的绕道有助于提高理论(更强大的一般理论结果)和效率(SAT 求解器)。

话虽如此,如果您想在几秒钟或更短的时间内(在我的实验中)进行采样,这不是一种方法,至少在关注均匀性的同时。

理论

该方法基于复杂性理论的结果,遵循this work

戈麦斯,卡拉 P.; SABHARWAL,阿什什;塞尔曼,巴特。使用 XOR 约束对组合空间进行近乎均匀的采样。在:神经信息处理系统的进展。 2007. S. 481-488。

基本思路:

  • 将问题表述为 SAT 问题
  • 为问题添加随机生成的异或(仅作用于决策变量!这在实践中很重要)
    • 这将减少解决方案的数量(某些解决方案将变得不可能)
    • 在循环中执行此操作(使用调整的参数),直到只剩下一个解决方案!
      • SAT-solvers 或#SAT-solvers (=model-counting) 正在搜索一些解决方案
      • 如果有多个解决方案:不会添加异或,但会完全重新启动:将随机异或添加到启动问题!

保证:

  • 在正确调整参数时,这种方法可以实现近乎均匀的采样
    • 这种调整可能代价高昂,因为它基于可能的解决方案数量的近似值
    • 根据经验,这也可能代价高昂!
    • Ante 的回答,提到数列 A001499 实际上给出了解决方案空间的一个很好的上限(因为它只是忽略了邻接约束!)

缺点:

  • 对大型问题效率低下(通常;不一定与 MCMC 和 co 等替代方案相比。)
    • 需要更改/减少参数以生成样本
    • 那些减少的参数失去了理论保证
    • 但根据经验:仍然有可能取得好的结果!

参数:

在实践中,参数是:

  • N:添加的异或数
  • L:一个异或约束的最小变量数
  • U:一个异或约束的最大变量数

N 对于减少可能解决方案的数量很重要。给定 N 常数,其他变量当然也有一定的影响。

理论说(如果我解释正确的话),我们应该使用 L = R = 0.5 * #dec-vars。

这在实践中是不可能的,因为 xor-constraints 对 SAT-solvers 伤害很大!

Here 更多关于 L 和 U 影响的科学幻灯片。

他们将大小为 8-20 的异或称为短异或,而我们以后需要使用更短的异或!

实施

最终版

这是一个在 python 中的非常 hacky 实现,使用来自here 的 XorSample 脚本。

使用的底层 SAT-solver 是 Cryptominisat

代码基本上归结为:

  • 将问题转换为合取范式
    • 作为 DIMACS-CNF
  • 实施抽样方法:
    • 调用 XorSample(基于管道 + 基于文件)
    • 调用 SAT-solver(基于文件)
  • 将样本添加到某个文件以供以后分析

代码:(我希望我已经警告过你代码质量)

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

输出看起来像(删除了一些警告):

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

核心:CNF 配方

我们为矩阵的每个单元引入一个变量。 N=20 表示 400 个二进制变量。

邻接:

预先计算所有减少对称性的冲突并添加冲突子句。

基本理论:

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

行/列基数:

这在 CNF 中很难表达,幼稚的方法需要一个指数 的约束。

我们使用了一些adder-circuit based encoding(SINZ,Carsten。实现布尔基数约束的最佳 CNF 编码),它引入了新的辅助变量。

备注:

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

这些 SAT 编码可以放入精确的模型计数器中(对于小 N;例如

还有一些有趣的近似模型计数器(也很大程度上基于 xor-constraints),例如 approxMC,它显示了我们可以使用 SAT 公式做的另一件事。但实际上我无法使用这些(approxMC = autoconf;没有评论)。

其他实验

我还使用pblib 构建了一个版本,以使用更强大的基数公式 对于 SAT-CNF 公式。我没有尝试使用基于 C++ 的 API,而是只尝试使用简化的 pbencoder,它会自动选择一些 best 编码,这比我上面使用的编码要差得多(最好的仍然是一项研究-问题;通常甚至冗余约束也有帮助)。

经验分析

为了获得一些样本大小(考虑到我的耐心),我只计算了 N=15 的样本。在这种情况下,我们使用:

  • N=70 异或
  • L,U = 4,6

我还用 (100,3,6) 计算了 N=20 的一些样本,但这需要几分钟,我们降低了下限!

可视化

这里有一些动画(加强我与matplotlib的爱恨交织):

编辑:与 N=5 (NXOR,L,U = 4, 10, 30) 的蛮力均匀采样的(简化)比较:

(我还没有决定添加绘图代码。它和上面的一样丑陋,人们可能会过多地关注我的统计混乱;归一化等等。)

理论

可能很难进行统计分析,因为潜在问题具有这种组合性质。最终的单元格 PDF 应该是什么样子甚至不完全清楚。在 N=odd 的情况下,它可能是不均匀的并且看起来像一个棋盘(我做了蛮力检查 N=5 来观察这一点)。

我们可以确定的一件事(恕我直言):对称性!

给定一个 cell-PDF 矩阵,我们应该期望该矩阵是对称的 (A = A.T)。 这在可视化中进行了检查,并绘制了随时间变化的欧几里德范数。

我们可以在其他一些观察上做同样的事情:观察到的配对。

对于 N=3,我们可以观察到以下对:

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

现在我们可以按行和按列执行此操作,并且也应该期望对称!

可悲的是,谈论方差可能并不容易,因此谈论信心所需的样本可能并不容易!

观察

根据我的简化感知,当前样本和单元格 PDF 看起来不错,尽管尚未实现收敛(或者我们离一致性还很远)。

更重要的方面可能是两个规范,很好地减少到 0。 (是的;可以通过用 prob=0.5 转置来调整一些算法;但这里没有这样做,因为它会破坏它的目的)。

可能的后续步骤

  • 调整参数
  • 查看使用 #SAT-solvers / Model-counters 而不是 SAT-solvers 的方法
  • 尝试不同的 CNF 公式,尤其是在基数编码和异或编码方面
    • XorSample 默认使用tseitin-like encoding 来绕过指数增长
      • 对于较小的异或(如使用),使用朴素编码(传播速度更快)可能是个好主意
        • XorSample 理论上支持这一点;但脚本在实践中的工作方式不同
        • Cryptominisat 以专用的 XOR 处理而闻名(因为它是为分析包括许多 xor 在内的密码学而构建的)并且可能通过简单的编码获得一些好处(因为从爆炸的 CNF 推断 xor 要困难得多)
  • 更多统计分析
  • 摆脱 XorSample 脚本(shell + perl...)

总结

  • 方法很通用
  • 此代码生成可行样本
  • 应该不难证明,每个可行解都可以被采样
  • 其他人已经证明了某些参数的一致性的理论保证
    • 不适用于我们的参数
  • 其他人从经验/理论上分析了较小的参数(在此处使用)

【讨论】:

  • 哦不,你有照片!你本可以告诉我们你正在做这件事,那么我们就不会打扰了:-)
  • @m69 我什至得到了很多图片 :-) 只是为了查看 matplotlibs 动画模块。不过,我还没有阅读和处理你的答案(但 DP 让我很紧张)。
  • 让我们看看你的图片与我的工作代码 sn-p 相比如何 :-) (这是使用 JS 的少数优势之一。)目前我仍然落后于 NAA :-)
  • 不错!我认为对是定义 PDF 的更好方法。甚至更多的行/列深度+对。解在镜像和旋转上是对称的。甚至在行交换上也没有。在我看来,对于行深度 (0-n/2),可能的对 ((n-1) 选择 2) 上存在一致的 PDF (频率)。对于较小的 n,可以精确计算 PDF,对于较大的 n,可以通过将递归遍历到某个深度来近似。
  • 我可能会在这里奖励赏金,除非有人能提供令人信服的理由不这样做
【解决方案4】:

这是一种非常快速的逐行生成矩阵的方法,用 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"));     
}

它基本上一次生成一个随机行。如果该行将违反任何条件,则会再次生成(再次随机)。我相信这也将满足条件4。

添加一个快速说明,在没有解决方案的情况下(如 N=3),它将永远旋转 N-s。

【讨论】:

  • 每行不同的选项导致解的数量不同;例如在从 10100 开始的 5×5 矩阵中导致 4 个解决方案,从 10010 开始到只有 2 个。因此,每行随机选择不会给出均匀采样。
【解决方案5】:

此解决方案使用递归来逐一设置矩阵的单元格。如果随机游走以不可能的解决方案结束,那么我们在树中回滚一步并继续随机游走。

算法效率高,我认为生成的数据是高度等概率的。

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);
    }
}

输出是:

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

【讨论】:

  • “高度”等概率有多高?你有测试结果吗? (为了比较:我在 sascha 的回答下的评论中列出了 8x8 矩阵的均匀分布)。
  • 老实说,我不知道如何准确地测试等概率因子。我已经使用函数 printProbability(..) 更新了我的代码。这是一个近似值,但总比没有好,结果看起来非常好。
  • 如果你对 8x8 矩阵运行 2215764 次并将它们加起来,总和应该是 [[556376, 553224, 552952, 553212], [553224, 554328, 553928, 554284], [552952 , 553928, 554888, 553996], [553212, 554284, 553996, 554272]]。 (这是左上象限,其他象限是镜像的。)
  • 我添加了运行 2215764 次的 8x8 矩阵的输出。你能为我解释一下结果吗?
  • 我发现了这个错误(我已经编辑了我的帖子)。该算法在生成矩阵时效率较低,大约为 0.3 毫秒,但它正在工作。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-09-20
  • 1970-01-01
  • 2020-07-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多