这是为给定的 n 和 m 生成每个矩阵的逻辑。这有点令人费解,所以我不确定实现会比蛮力快多少;我认为对于较大的值,差异会变得更加明显。
(为方便起见,以下将生成 0 和 1 的输出,其中 0 表示加号,1 表示减号。)
每行有 m 个的方阵转换为这些折叠的行/列有 m 个的三角矩阵:
x 0 1 0 1 x 0 1 0 1 0 1 0 1
0 x 1 1 0 x 1 1 0 1 1 0
1 1 x 0 0 x 0 0 0 0
0 1 0 x 1 x 1 1
1 0 0 1 x x
这些组中的每一个都与所有其他组重叠;为前 k 个组选择值意味着组 k+1 的垂直部分已经确定。
我们首先将每行所需的数量放在对角线上;例如对于 (5,2) 即:
2 . . . .
2 . . .
2 . .
2 .
2
然后我们为第一组生成具有 m 个 1 的每个位模式;其中有 (n-1 选择 m) 个,并且可以有效地生成它们,例如与 Gosper 的 hack。
(4,2) -> 0011 0101 0110 1001 1010 1100
对于每一个,我们将它们填充到矩阵中,然后从所需的数量中减去它们:
X 0 0 1 1
2 . . .
2 . .
1 .
1
然后用较小的三角形递归:
2 . . .
2 . .
1 .
1
如果我们到达对角线上一些必需的数量为零的点,例如:
2 . . .
1 . .
0 .
1
那么我们已经可以在该列中放置一个零,并为更少的列生成可能的位模式;在示例中,将是 (2,2) 而不是 (3,2),因此只有一种可能的位模式:11。然后我们将位模式分布在其下具有非零要求计数的列上:
2 . 0 . X 1 0 1
1 . . 0 . .
0 . 0 .
1 0
但是,并非所有可能的位模式都会导致有效的解决方案;举个例子:
2 . . . . X 0 0 1 1
2 . . . 2 . . . 2 . . . X 0 1 1
2 . . 2 . . 2 . . 2 . . 2 . .
2 . 1 . 1 . 0 . 0 .
2 1 1 0 0
我们最终得到的一行需要另外 2 个,而两列都不能再占用任何一个。发现这种情况的方法是查看倒数第二步中每个选项创建的每列所需的列表:
pattern required
0 1 1 -> 2 0 0
1 0 1 -> 1 1 0
1 1 0 -> 1 0 1
如果列表中的第一个值是 x,那么它之后必须至少有 x 个非零值;这对于三个选项中的第一个是错误的。
(这里有优化的空间:在1,1,0,6,0,2,1,1这样的计数列表中,6之前只有2个非零值,这意味着6最多会减少2倍,所以它的最小值当它成为第一个元素时的值将是 4;但是,它之后只有 3 个非零值,所以在这个阶段你已经知道这个列表不会导致任何有效的解决方案。检查这会增加代码的复杂性,所以我不确定这是否会提高执行速度。)
所以 (n,m) 的完整算法是这样开始的:
- 创建一个 n 大小的列表,所有值都设置为 m(每组所需的计数)。
- 生成所有大小为 n-1 的位模式,其中有 m 个;对于其中的每一个:
- 从计数列表的副本中减去模式(不包括第一个元素)。
- 使用计数列表的模式和副本进行递归。
之后的递归步骤是:
- 收到到目前为止的序列和计数列表。
- 计数列表的长度是n,它的第一个元素是m。
- 设 k 为计数列表中非零值的数量(不包括第一个元素)。
- 生成所有大小为 k 的位模式,其中有 m 个;对于其中的每一个:
- 创建一个由 0 填充的列表,大小为 n-1。
- 在其上分配位模式,跳过计数为零的列。
- 到目前为止,将值列表添加到序列中。
- 从计数列表的副本中减去值列表(不包括第一个元素)。
- 如果计数列表副本中的第一个值大于其后的非零数,则跳过此模式。
- 在最深的递归级别,存储序列,否则:
- 使用到目前为止的序列和计数列表的副本进行递归。
这里有一个代码 sn-p 作为概念证明;用一种严肃的语言,并且使用整数而不是位图的数组,这应该更快:
function generate(n, m) {
// if ((n % 2) && (m % 2)) return; // to catch (3,1)
var counts = [], pattern = [];
for (var i = 0; i < n - 1; i++) {
counts.push(m);
pattern.push(i < m ? 1 : 0);
}
do {
var c_copy = counts.slice();
for (var i = 0; i < n - 1; i++) c_copy[i] -= pattern[i];
recurse(pattern, c_copy);
}
while (revLexi(pattern));
}
function recurse(sequence, counts) {
var n = counts.length, m = counts.shift(), k = 0;
for (var i = 0; i < n - 1; i++) if (counts[i]) ++k;
var pattern = [];
for (var i = 0; i < k; i++) pattern.push(i < m ? 1 : 0);
do {
var values = [], pos = 0;
for (var i = 0; i < n - 1; i++) {
if (counts[i]) values.push(pattern[pos++]);
else values.push(0);
}
var s_copy = sequence.concat(values);
var c_copy = counts.slice();
var nonzero = 0;
for (var i = 0; i < n - 1; i++) {
c_copy[i] -= values[i];
if (i && c_copy[i]) ++nonzero;
}
if (c_copy[0] > nonzero) continue;
if (n == 2) {
for (var i = 0; i < s_copy.length; i++) {
document.write(["+ ", "− "][s_copy[i]]);
}
document.write("<br>");
}
else recurse(s_copy, c_copy);
}
while (revLexi(pattern));
}
function revLexi(seq) { // reverse lexicographical because I had this lying around
var max = true, pos = seq.length, set = 1;
while (pos-- && (max || !seq[pos])) if (seq[pos]) ++set; else max = false;
if (pos < 0) return false;
seq[pos] = 0;
while (++pos < seq.length) seq[pos] = set-- > 0 ? 1 : 0;
return true;
}
generate(5, 2);
这里是 n 到 10 的值的结果数和递归数,因此您可以比较它们以检查正确性。当 n 和 m 都是奇数时,没有有效结果;这是正确计算的,但 (3,1) 的情况除外;抓住这些病例并立即返回当然很容易。
(n,m) results number of recursions
(4,0) (4,3) 1 2 2
(4,1) (4,2) 3 6 7
(5,0) (5,4) 1 3 3
(5,1) (5,3) 0 12 20
(5,2) 12 36
(6,0) (6,5) 1 4 4
(6,1) (6,4) 15 48 76
(6,2) (6,3) 70 226 269
(7,0) (7,6) 1 5 5
(7,1) (7,5) 0 99 257
(7,2) (7,4) 465 1,627 2,313
(7,3) 0 3,413
(8,0) (8,7) 1 6 6
(8,1) (8,6) 105 422 1,041
(8,2) (8,5) 3,507 13,180 23,302
(8,3) (8,4) 19,355 77,466 93,441
(9,0) (9,8) 1 7 7
(9,1) (9,7) 0 948 4,192
(9,2) (9,6) 30,016 119,896 270,707
(9,3) (9,5) 0 1,427,457 2,405,396
(9,4) 1,024,380 4,851,650
(10,0) (10,9) 1 8 8
(10,1) (10,8) 945 4440 18930
(10,2) (10,7) 286,884 1,210,612 3,574,257
(10,3) (10,6) 11,180,820 47,559,340 88,725,087
(10,4) (10,5) 66,462,606 313,129,003 383,079,169