【问题标题】:Optimize: Restricted integer partioning with max value优化:具有最大值的受限整数分区
【发布时间】:2020-10-12 03:58:02
【问题描述】:

使用下面的代码,我用k每个分区中的数字来计算受限整数分区(每个数字在每个分区中只能出现一次),每个数字等于或大于1且不大于@987654323 @。这段代码会生成大量缓存值,以便快速耗尽内存。

例子:

sum := 15, k := 4, m:= 10 预期结果是 6

具有以下受限整数分区:

1,2,3,9,1,2,4,8,1,2,5,7,1,3,4,7,1,3,5,7,2,3,4,6

public class Key{
  private final int sum;
  private final short k1;
  private final short start;
  private final short end;

  public Key(int sum, short k1, short start, short end){
    this.sum = sum;
    this.k1 = k1;
    this.start = start;
    this.end = end;
  }
  // + hashcode and equals
}

public BigInteger calcRestrictedIntegerPartitions(int sum,short k,short m){
  return calcRestrictedIntegerPartitionsHelper(sum,(short)0,k,(short)1,m,new HashMap<>());
}

private BigInteger calcRestrictedIntegerPartitionsHelper(int sum, short k1, short k, short start, short end, Map<Key,BigInteger> cache){
  if(sum < 0){
    return BigInteger.ZERO;
  }
  if(k1 == k){
    if(sum ==0){
      return BigInteger.ONE;
    }
    return BigInteger.ZERO;
  }
  if(end*(k-k1) < sum){
    return BigInteger.ZERO;
  }

  final Key key = new Key(sum,(short)(k-k1),start,end);

  BigInteger fetched = cache.get(key);

  if(fetched == null){
    BigInteger tmp = BigInteger.ZERO;

    for(short i=start; i <= end;i++){
      tmp = tmp.add(calcRestrictedIntegerPartitionsHelper(sum-i,(short)(k1+1),k,(short)(i+1),end,cache));
    }

    cache.put(key, tmp);
    return tmp;
  }

  return fetched;
}

是否有避免/减少缓存的公式?或者如何使用k and m 计算受限整数部分?

【问题讨论】:

  • 显示一个小值示例,以确保在问题制定中。是否允许零和? n、k 有什么限制?
  • 更新了问题,不允许使用零加法。 sum, k 没有限制
  • 好的,问题很清楚。
  • 您想要提示还是完整的解决方案?我可以提供两者,但完整的解决方案需要一些时间来编写。自己弄清楚更大的部分也可能很有趣:)
  • 一个有据可查的提示会很好,但只有轻微的数学背景。

标签: java math combinations mathematical-optimization integer-partition


【解决方案1】:

您的问题可以转置,因此您的缓存中只需要 3 个键,并且启动时间要少得多。更少不同的键意味着更好的缓存(比我聪明的人可能仍然会找到更便宜的解决方案)。

让我们将分区视为集合。每组的元素应按顺序排列(升序)。 当您将 sum := 15, k := 4, m:= 10 的预期结果声明为 [1, 2, 3, 9]; [1, 2, 4, 8] ... 时,您已经隐含地做到了这一点。

您为分区定义的限制是:

  • 正好是k 每组元素
  • 最大m 作为元素
  • 不同的值
  • 非零正整数

区分的限制其实有点麻烦,所以我们会解除它。 为此,我们需要稍微改变一下问题。因为你的集合中的元素是升序的(并且是不同的),我们知道,每个元素的最小值是一个升序序列(如果我们忽略总和必须是sum),所以最小值是:[1, 2, 3, ...]。 例如,如果m 小于k,则可能的分区数将始终为零。同样,如果[1, 2, 3, ... k] 的总和大于sum,则结果也为零。我们在一开始就排除了这些边缘情况,以确保转换是合法的。

让我们看一下“法律分区”的几何表示以及我们希望如何对其进行转换。我们有k 列、m 行和sum 正方形填充蓝色(浅蓝色或深蓝色)。

红色和深蓝色方块无关紧要,正如我们已经知道的,深蓝色方块必须始终填充,红色方块必须始终为空。因此,我们可以将它们排除在我们的计算之外,并在我们进行时假设它们各自的状态。生成的框显示在右侧。每列都被它的位置“向下移动”,红色和深蓝色区域被切断。 我们现在有一个较小的整体框,一列现在可以是空的(相邻列之间可能有相同数量的蓝色框)。

在算法上,转换现在的工作方式如下: 对于合法分区中的每个元素,我们减去它的位置(从 1 开始)。因此对于 [1, 2, 4, 8] 我们得到[0, 0, 1, 4]。此外,我们必须相应地调整我们的界限(summ):

// from the sum, we subtract the sum of [1, 2, 3, ... k], which is (k * (k + 1) / 2)
sum_2 = sum - (k * (k + 1) / 2)

// from m we subtract the maximum position (which is k)
m_2 = m - k

现在我们已经将我们的分区问题转换为另一个分区问题,一个有不同元素的限制!此外,这个分区可以包含元素0,而我们原来的不能。 (我们保持内部升序)。

现在我们需要稍微改进一下递归。如果我们知道元素是升序的,不一定是不同的并且总是不等于m_2,那么我们已经将可能的元素绑定到一个范围内。示例:

[0, 1, 3, n1, n2]
=> 3 <= n1 <= m_2
=> 3 <= n2 <= m_2

因为我们知道示例中的n1n23 或更大,所以在调用递归时,我们也可以将它们都减少3 并将sum_2 减少2 * 3(一个是“打开”元素的数量,一个是最后一个“固定”元素的值)。这样,我们在递归中传递的内容没有上限和下限,而只有一个上限,这就是我们之前的 (m)。

因此,我们可以抛出 1 个缓存键值:start。相反,在解决这个简化的问题时,我们现在只有 3 个:summk

以下实现可以达到这种效果:

@Test
public void test() {
    calcNumRIPdistinctElementsSpecificKmaxM(600, (short) 25, (short) 200);
}

public BigInteger calcNumRIPdistinctElementsSpecificKmaxM(int sum, short k, short m) {
    // If the biggest allowed number in a partition is less than the number of parts, then
    // they cannot all be distinct, therefore we have zero results.
    if (m < k) {
        return BigInteger.ZERO;
    }
    
    // If the sum of minimum element-values for k is less than the expected sum, then
    // we also have no results.
    final int v = ((k * ((int) k + 1)) / 2);
    if (sum < v) {
        return BigInteger.ZERO;
    }
    
    // We normalize the problem by lifting the distinction restriction.
    final Cache cache = new Cache();
    final int sumNorm = sum - v;
    final short mNorm = (short) (m - k);
    
    BigInteger result = calcNumRIPspecificKmaxM(sumNorm, k, mNorm, cache);

    System.out.println("Calculation (n=" + sum + ", k=" + k + ", m=" + m + ")");
    System.out.println("p = " + result);
    System.out.println("entries = " + cache.getNumEntries());
    System.out.println("c-rate = " + cache.getCacheRate());
    
    return result;
}

public BigInteger calcNumRIPspecificKmaxM(int sum, short k, short m, Cache cache) {
    
    // We can improve cache use by standing the k*m-rectangle upright (k being the 'bottom').
    if (k > m) {
        final short c = k;
        k = m;
        m = c;
    }
    
    // If the result is trivial, we just calculate it. This is true for k < 3
    if (k < 3) {
        if (k == 0) {
            return sum == 0 ? BigInteger.ONE : BigInteger.ZERO;
            
        } else if (k == 1) {
            return sum <= m ? BigInteger.ONE : BigInteger.ZERO;
            
        } else {
            final int upper = Math.min(sum, m);
            final int lower = sum - upper;
            
            if (upper < lower) {
                return BigInteger.ZERO;
            }
            
            final int difference = upper - lower;
            final int numSubParts = difference / 2 + 1;
            return BigInteger.valueOf(numSubParts);
        }
    }
    
    // If k * m / 2 < sum, we can 'invert' the sub problem to reduce the number of keys further.
    sum = Math.min(sum, k * m - sum);
    
    // If the sum is less than m and maybe even k, we can reduce the box. This improves the cache size even further.
    if (sum < m) {
        m = (short) sum;
        
        if (sum < k) {
            k = (short) sum;

            if (k < 3) {
                return calcNumRIPspecificKmaxM(sum, k, m, cache);
            }
        }
    }
    
    // If the result is non-trivial, we check the cache or delegate.
    final Triple<Short, Short, Integer> key = Triple.of(k, m, sum);
    final BigInteger cachedResult = cache.lookUp(key);
    if (cachedResult != null) {
        return cachedResult;
    }
    
    BigInteger current = BigInteger.ZERO;
    
    // i = m is reached in case the result is an ascending stair e.g. [1, 2, 3, 4]
    for (int i = 0; i <= m; ++i) {
        final int currentSum = sum - (i * k);
        if (currentSum < 0) {
            break;
        }
        
        short currentK = (short) (k - 1);
        short currentM = (short) (m - i);
        
        current = current.add(calcNumRIPspecificKmaxM(currentSum, currentK, currentM, cache));
    }
    
    // We cache this new result and return it.
    cache.enter(key, current);
    return current;
}

public static class Cache {
    private final HashMap<Triple<Short, Short, Integer>, BigInteger> map = new HashMap<>(1024);
    private long numLookUps = 0;
    private long numReuse = 0;
    
    public BigInteger lookUp(Triple<Short, Short, Integer> key) {
        ++numLookUps;
        
        BigInteger value = map.get(key);
        if (value != null) {
            ++numReuse;
        }
        
        return value;
    }
    
    public void enter(Triple<Short, Short, Integer> key, BigInteger value) {
        map.put(key, value);
    }
    
    public double getCacheRate() {
        return (double) numReuse / map.size();
    }
    
    public int getNumEntries() {
        return map.size();
    }
    
    public long numLookUps() {
        return numLookUps;
    }
    
    public long getNumReuse() {
        return numReuse;
    }
}

注意:这里我使用了 apache-common 的 Triple-class 作为 key,以节省显式 key-class 的实现,但这不是运行时的优化,它只是节省了代码。

编辑:除了修复@MBo 发现的问题(谢谢),我添加了一些快捷方式来达到相同的结果。该算法现在性能更好,缓存(重用)率更高。也许这会满足您的要求?

解释的优化(它们只适用于上述问题的转置之后):

  • 如果k &gt; m,我们可以将矩形“翻转”直立,对于合法分区的数量仍然可以得到相同的结果。这会将一些“躺”配置映射到“直立”配置,并减少不同键的总量。

  • 如果矩形中的正方形的数量大于“空格”的数量,我们可以将“空格”视为正方形,这会将另一组键映射在一起。

  • 如果sum &lt; k 和/或sum &lt; m,我们可以减少k 和/或m 的总和,仍然得到相同数量的分区。 (这是影响最大的优化,因为它经常跳过多个冗余的中间步骤并经常到达m = k = sum

【讨论】:

  • 非常好。由于更智能地缩小问题规模,运行速度比我的版本快 3-5 倍。
  • 谢谢,虽然你的版本很多更优雅!如果不首先转换整个问题,就不能这样做......
  • 哎呀...嗯..我想我需要把那个弄出来。
  • 找到了。毕竟循环需要上升到m。在“楼梯”场景中,解决方案实际上是在内部函数循环中的i = m。现在应该产生正确的结果。感谢您仔细检查!
  • 更聪明的人可以做哪些额外的优化?
【解决方案2】:

您的密钥包含 4 个部分,因此哈希空间可能会达到这些部分的最大值乘积的值。可以使用反向循环和零值作为自然限制将密钥减少到 3 个部分。

Python 示例使用内置功能 lru_cache 哈希表大小 = N*K*M

@functools.lru_cache(250000)
def diff_partition(N, K, M):
    '''Counts integer partitions of N with K distint parts <= M'''
    if K == 0:
        if N == 0:
            return 1
        return 0
    res = 0
    for i in range(min(N, M), -1, -1):
        res += diff_partition(N - i, K - 1, i - 1)
    return res

def diffparts(Sum, K, M):   #diminish problem size allowing zero part
    return diff_partition(Sum - K, K, M-1)

print(diffparts(500, 25, 200))

>>>147151784574

【讨论】:

  • @TreffnonX 这很有趣。是否可以使用一些参数依赖?
  • 其实我错了。我的回答是有缺陷的。它恰好适用于该示例,但最终需要三个键值。 (对不起)
【解决方案3】:

另一种方法是使用约束求解器并将其配置为显示所有解决方案。这是 MiniZinc 的解决方案:

include "globals.mzn";

int: sum = 15;
int: k = 4;
int: m = 10;

array[1..k] of var 1..m: numbers;

constraint sum(numbers) = sum;

constraint alldifferent(numbers);

constraint increasing(numbers);

solve satisfy;

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多