(下面更新了测试结果、示例运行和代码 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(""" + j + "": " + memo[i][j] + "<br>");
}
}