一种计算整数网格数的有效算法

时间:2017-12-06 13:43:08

标签: algorithm combinatorics

考虑正方形3乘3的非负整数网格。对于每一行i,整数之和设置为r_i。同样,对于每列j,该列中的整数之和设置为c_j。因此,问题的一个实例由6非负整数描述。

  

是否有一种有效的算法来计算有多少不同的算法   对网格的整数赋值给出了行和列   总和约束?

显然,人们可以列举所有可能的非负整数矩阵,其值最大为sum r_i,并检查每个矩阵的约束,但这将非常慢。

示例

假设行约束为1 2 3,列约束为3 2 1。可能的整数网格是:

┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│0 0 1│0 0 1│0 0 1│0 1 0│0 1 0│0 1 0│0 1 0│1 0 0│1 0 0│1 0 0│1 0 0│1 0 0│
│0 2 0│1 1 0│2 0 0│0 1 1│1 0 1│1 1 0│2 0 0│0 1 1│0 2 0│1 0 1│1 1 0│2 0 0│
│3 0 0│2 1 0│1 2 0│3 0 0│2 1 0│2 0 1│1 1 1│2 1 0│2 0 1│1 2 0│1 1 1│0 2 1│
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘

在实践中,我的主要兴趣是当网格的总和最多为100时,但更通用的解决方案将非常有趣。

6 个答案:

答案 0 :(得分:5)

  

是否有一种有效的算法可以计算出给定行和列总和约束的网格中有多少个不同的整数赋值?

upd N被修复(即变为常数3)时,我对这个特定问题的回答是错误的。在这种情况下,它是多项式。对于误导性信息感到抱歉。

TL; DR :我认为这至少是NP难的。没有polinomial算法,但也许有一些启发式加速。

对于N-by-N网格,您有N个方程式用于行总和,N方程式用于总和和N^2非负数约束:

enter image description here

对于N > 2,这个系统通常有多个可能的解决方案。因为N^2个未知变量x_ij2N个等式=>对于N > 2N^2 > 2N

您只需使用2N - 1个变量得到总和K = N^2 - (2N-1)的等式,就可以消除S个变量。然后,您必须处理integer partition problem以查找K项的所有可能组合,以获取S。这个问题是NP完全的。组合数量不仅取决于术语K的数量,还取决于值S的顺序。

这个问题让我想起了Simplex method。我的第一个想法是使用类似方法找到一个解决方案,然后遍历凸边以找到所有可能的解决方案。我希望有一个最佳算法。但是,与integer linear programming相关的整数单纯形法是NP难的:(

我希望,对于可以用来加速天真蛮力解决方案的相关问题,有一些启发式方法。

答案 1 :(得分:4)

我不知道匹配的算法,但我不认为一次完成工作会很困难。给定任何一种解决方案,您可以通过选择网格矩形区域的四个角来获得另一种解决方案,将两个对角线增加一些值,并将另外两个角减少相同的值。该值的范围将受每个对角线对的最低值约束。如果确定所有此类范围的大小,您应该能够将它们相乘以确定可能的总解决方案。

假设您将网格描述为按字母顺序排列的熟悉的电子表格,以及数字表示行,您可以在以下列表中描述所有可能的区域:

A1:B2, A1:B3, A1:C2, A1:C3, B1:C2, B1:C3, A2:B3, A2:C3, B2:C3

对于每个区域,我们根据每个对角线对的最低值制表一个范围。您可以逐步减少任何一对,直到成员达到零,因为另一对没有上限。

选择示例的第一个解决方案,我们可以使用此技术推导出所有其他可能的解决方案。

   A B C
  ┌─────┐
1 │0 0 1│ sum=1
2 │0 2 0│ sum=2
3 │3 0 0│ sum=3
  └─────┘
   3 2 1 = sums

A1:B2 - 1 solution (0,0,0,2)
A1:C2 - 1 solution (0,1,0,0)
A1:B3   1 solution (0,0,3,0)
A1:C3   2 solutions (0,1,3,0), (1,0,2,1)
B1:C2   2 solutions (0,1,2,0), (1,0,1,1)
B1:C3   1 solution (0,1,0,0)
A2:B3   3 solutions (0,2,3,0), (1,1,2,1), (2,0,1,2)
A2:C3   1 solution (0,0,3,0)
B2:C3   1 solution (2,0,0,0)

将所有解决方案计数加起来,得到2 * 2 * 3 = 12个解决方案。

答案 2 :(得分:4)

如果总和很小,也许一个简单的4嵌套循环解决方案足够快?



function solve(rowsum, colsum) {
    var count = 0;
    for (var a = 0; a <= rowsum[0] && a <= colsum[0]; a++) {
        for (var b = 0; b <= rowsum[0] - a && b <= colsum[1]; b++) {
            var c = rowsum[0] - a - b;
            for (var d = 0; d <= rowsum[1] && d <= colsum[0] - a; d++) {
                var g = colsum[0] - a - d;
                for (var e = 0; e <= rowsum[1] - d && e <= colsum[1] - b; e++) {
                    var f = rowsum[1] - d - e;
                    var h = colsum[1] - b - e;
                    var i = rowsum[2] - g - h;
                    if (i >= 0 && i == colsum[2] - c - f) ++count;
                }
            }
        }
    }
    return count;
}
document.write(solve([1,2,3],[3,2,1]) + "<br>");
document.write(solve([22,33,44],[30,40,29]) + "<br>");
&#13;
&#13;
&#13;

答案 3 :(得分:3)

对于#P-hard问题没有帮助(如果你允许矩阵具有任何大小 - 请参阅下面的评论中的参考),但有一个解决方案不等于枚举所有的矩阵,而是一组称为semi-standard Young tableaux的较小对象。根据您的输入,它可能会更快,但仍然具有指数复杂性。由于它是几本代数组合学书或Knuth的AOCP 3中的完整章节,因此我不会在此处详细说明相关的维基百科页面。

这个想法是使用Robinson–Schensted–Knuth correspondence这些矩阵中的每一个都使用一对相同形状的表格进行双射,其中一个画面用行总和计算整数,另一个用列总和。填充有由V计数的数字的形状U的表格的数量被称为Kostka Number K(U,V)。因此,您最终会得到一个公式,例如

#Mat(RowSum, ColSum) = \sum_shape  K(shape, RowSum)*K(shape, ColSum) 

当然如果RowSum == ColSum == Sum:

#Mat(Sum, Sum) = \sum_shape  K(shape, Sum)^2 

以下是SageMath系统中的示例:

sage: sum(SemistandardTableaux(p, [3,2,1]).cardinality()^2 for p in  Partitions(6))
12

以下是一些较大的例子:

sage: sums = [6,5,4,3,2,1]
sage: %time sum(SemistandardTableaux(p, sums).cardinality()^2 for p in Partitions(sum(sums)))
CPU times: user 228 ms, sys: 4.77 ms, total: 233 ms
Wall time: 224 ms
8264346

sage: sums = [7,6,5,4,3,2,1]
sage: %time sum(SemistandardTableaux(p, sums).cardinality()^2 for p in Partitions(sum(sums)))
CPU times: user 1.95 s, sys: 205 µs, total: 1.95 s
Wall time: 1.94 s
13150070522

sage: sums = [5,4,4,4,4,3,2,1]
sage: %time sum(SemistandardTableaux(p, sums).cardinality()^2 for p in Partitions(sum(sums)))
CPU times: user 1.62 s, sys: 221 µs, total: 1.62 s
Wall time: 1.61 s
1769107201498

很明显,你不会得到那么快的枚举矩阵。

根据גלעדברקן@的要求,这是一个具有不同行和列总和的解决方案:

sage: rsums = [5,4,3,2,1]; colsums = [5,4,3,3]
sage: %time sum(SemistandardTableaux(p, rsums).cardinality() * SemistandardTableaux(p, colsums).cardinality() for p in Partitions(sum(rsums)))
CPU times: user 88.3 ms, sys: 8.04 ms, total: 96.3 ms
Wall time: 92.4 ms
10233

答案 4 :(得分:2)

我已经厌倦了优化慢速选项。我得到所有组合并仅更改代码以获得总计数。这是我能得到的最快的:

    private static int count(int[] rowSums, int[] colSums)
    {
        int count = 0;
        int[] row0 = new int[3];
        int sum = rowSums[0];
        for (int r0 = 0; r0 <= sum; r0++)
            for (int r1 = 0, max1 = sum - r0; r1 <= max1; r1++)
            {
                row0[0] = r0;
                row0[1] = r1;
                row0[2] = sum - r0 - r1;
                count += getCombinations(rowSums[1], row0, colSums);
            }                    
        return count;
    }
    private static int getCombinations(int sum, int[] row0, int[] colSums)
    {
        int count = 0;
        int max1 = Math.Min(colSums[1] - row0[1], sum);
        int max2 = Math.Min(colSums[2] - row0[2], sum);
        for (int r0 = 0, max0 = Math.Min(colSums[0] - row0[0], sum); r0 <= max0; r0++)
            for (int r1 = 0; r1 <= max1; r1++)
            {
                int r01 = r0 + r1;
                if (r01 <= sum)
                    if ((r01 + max2) >= sum)
                        count++;
            }
        return count;
    }




Stopwatch w2 = Stopwatch.StartNew();
int res = count(new int[] { 1, 2, 3 }, new int[] { 3, 2, 1 });//12
int res1 = count(new int[] { 22, 33, 44 }, new int[] { 30, 40, 29 });//117276
int res2 = count(new int[] { 98, 99, 100}, new int[] { 100, 99, 98});//12743775
int res3 = count(new int[] { 198, 199, 200 }, new int[] { 200, 199, 198 });//201975050
w2.Stop();
Console.WriteLine("w2:" + w2.ElapsedMilliseconds);//322 - 370 on my computer

答案 5 :(得分:1)

除了我使用Robinson-Schensted-Knuth双射的其他答案,这里是 另一种解决方案并不需要先进的组合技术,但有些技巧可用 编程为任意较大的矩阵解决了这个问题。第一个想法 应该用来解决这类问题的是使用递归,避免重新计算的东西归功于一些记忆 或更好的动态编程。特别是一旦你选择了候选人 对于第一行,您将第一行减去列总和 留下同样的问题只有少一行。避免重新计算 存储结果的东西。你可以这样做

  • 基本上都是一张大表(memoization)

  • 或以更棘手的方式存储具有n行的矩阵的所有解 并推导出n + 1行矩阵解的数量(动态规划)。

这是一个在Python中使用memoization的递归方法:

 # Generator for the rows of sum s which are smaller that maxrow
 def choose_one_row(s, maxrow):
     if not maxrow:
         if s == 0: yield []
         else: return
     else:
         for i in range(0, maxrow[0]+1):
             for res in choose_one_row(s-i, maxrow[1:]):
                 yield [i]+res


 memo = dict()
 def nmat(rsum, colsum):
     # sanity check: sum by row and column must match
     if sum(rsum) != sum(colsum): return 0
     # base case rsum is empty
     if not rsum: return 1
     # convert to immutable tuple for memoization
     rsum = tuple(rsum)
     colsum = tuple(colsum)
     # try if allready computed
     try:
         return memo[rsum, colsum]
     except KeyError:
         pass
     # apply the recursive formula
     res = 0
     for row in choose_one_row(rsum[0], colsum):
         res += nmat(rsum[1:], tuple(a - b for a, b in zip(colsum, row)))
     # memoize the result
     memo[(tuple(rsum), tuple(colsum))] = res
     return res

之后:

sage: nmat([3,2,1], [3,2,1])
12

sage: %time nmat([6,5,4,3,2,1], [6,5,4,3,2,1])
CPU times: user 1.49 s, sys: 7.16 ms, total: 1.5 s
Wall time: 1.48 s
8264346