优化:具有最大值限制的整数划分

huangapple go评论69阅读模式
英文:

Optimize: Restricted integer partioning with max value

问题

以下是代码的翻译部分:

使用以下代码,我计算了受限整数分割(每个数字在每个分割中只能出现一次),每个分割中有k个数字,每个数字大于或等于1且不大于m。这段代码生成了大量的缓存值,因此很快耗尽了内存。

示例:

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

关于是否有避免/减少缓存的公式或如何计算具有km的受限整数分割的问题,这需要更深入的数学分析,但通常来说,对于这种类型的问题,使用动态规划或递归与记忆化搜索(缓存)是一种有效的方法,因为它们允许您以递归方式构建解决方案并避免冗余计算。如果存在一种更优雅或更高效的方法,通常需要深入研究特定问题的数学性质。

英文:

with the following code, I count the restricted integer partitions(each number can only occure once in each partition) with k numbers in each partition, each number is equal or greater than 1 and not greater than m. This code generate a lot of cache values so that it goes out memory quickly.

Example:

sum := 15, k := 4, m:= 10 expected result is 6

Has following restricted integer partitions:

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&lt;&gt;());
}
private BigInteger calcRestrictedIntegerPartitionsHelper(int sum, short k1, short k, short start, short end, Map&lt;Key,BigInteger&gt; cache){
if(sum &lt; 0){
return BigInteger.ZERO;
}
if(k1 == k){
if(sum ==0){
return BigInteger.ONE;
}
return BigInteger.ZERO;
}
if(end*(k-k1) &lt; 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 &lt;= 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;
}

Is there formula to avoid/reduce caching? Or how Can I count restricted integer partions with k and m?

答案1

得分: 3

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

Python示例中使用内置功能lru_cache,散列表大小为N*K*M

import functools

@functools.lru_cache(250000)
def diff_partition(N, K, M):
    '''Counts integer partitions of N with K distinct 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
英文:

Your key contains 4 parts, so hash space might reach value of product of max values for these parts. It is possible to diminish key to 3 parts using backward loops and zero value as natural limit.

Python example uses in-built functionality lru_cache with hashtable size = N*K*M

@functools.lru_cache(250000)
def diff_partition(N, K, M):
&#39;&#39;&#39;Counts integer partitions of N with K distint parts &lt;= M&#39;&#39;&#39;
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))
&gt;&gt;&gt;147151784574

答案2

得分: 3

您的问题可以进行转置,因此您的缓存中只需要 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):

// 从 sum 中减去 [1, 2, 3, ... k] 的和,即 (k * (k + 1) / 2)
sum_2 = sum - (k * (k + 1) / 2)

// 从 m 中减去最大位置(即 k)
m_2 = m - k

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

英文:

Your problem can be transposed, so you only need 3 keys in your cache and a lot less runtime to boot. Less distinct keys means better caching (A smarter person than me may still find a cheaper solution).

Let's view the partitions as sets. The elements of each set shall be ordered (ascending).
You have already done this implicitly, when you stated the expected results for sum := 15, k := 4, m:= 10 as [1, 2, 3, 9]; [1, 2, 4, 8] ....

The restrictions you defined for the partitions are:

  • exactly k elements per set
  • max m as element
  • distinct values
  • non-zero positive integers

The restriction of distinction is actually a bit bothersome, so we will lift it.
For that, we need to transform the problem a bit. Because the elements of your set are ascending (and distinct), we know, that the minimum value of each element is an ascending sequence (if we ignore that the sum must be sum), so the minia are: [1, 2, 3, ...].
If m were for example less than k, then the number of possible partitions would always be zero. Likewise, if the sum of [1, 2, 3, ... k] is more than sum, then you also have zero results. We exclude these edge cases at the beginning, to make sure the transformation is legal.

Let us look at a geometric representation of a 'legal partition' and how we want to transform it. We have k columns, m rows and sum squares are filled blue (either light or dark blue).

优化:具有最大值限制的整数划分

The red and dark blue squares are irrelevant, as we already know, the dark blue squares must always be filled, and the red ones must always be empty. Therefore we can exclude them from our calculation and assume their respective states as we go along. The resulting box is represented on the right side. Every column was 'shifted down' by it's position, and the red and dark blue areas are cut off.
We now have a smaller overall box and a column can now be empty (and we may have the same number of blue boxes among neighboring columns).

Algorithmically the transformation now works like this:
For every element in a legal partition, we subtract it's position (starting at 1). So for
[1, 2, 4, 8] we get [0, 0, 1, 4]. Furthermore, we have to adapt our bounds (sum and m) accordingly:

// 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

Now we have transposed our partitioning problem into another partitioning problem, one that does not have the restriction of distinct elements! Also, this partition can contain element 0, which our original could not. (We keep the internal ascending order).

Now we need to refine the recursion a bit. If we know the elements are ascending, not necessariely distinct and always less-equal to m_2, then we have bound the possible elements to a range. Example:

[0, 1, 3, n1, n2]
=&gt; 3 &lt;= n1 &lt;= m_2
=&gt; 3 &lt;= n2 &lt;= m_2

优化:具有最大值限制的整数划分

Because we know that n1 and n2 in the example are 3 or greater, when calling the recursion, we can also instead reduce them both by 3 and reduce sum_2 by 2 * 3 (one is the number of 'open' elements, one is the value of the last 'fixed' element). This way, what we pass in the recursion does not have an upper and a lower bound, but only an upper bound, which is what we had before (m).

Because of this, we can toss 1 value of your cache key: start. Instead we now only have 3: sum, m and k, when solving this reduced problem.

The following implementation works to this effect:

@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 &lt; 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 &lt; 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(&quot;Calculation (n=&quot; + sum + &quot;, k=&quot; + k + &quot;, m=&quot; + m + &quot;)&quot;);
	System.out.println(&quot;p = &quot; + result);
	System.out.println(&quot;entries = &quot; + cache.getNumEntries());
	System.out.println(&quot;c-rate = &quot; + 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 &#39;bottom&#39;).
	if (k &gt; m) {
		final short c = k;
		k = m;
		m = c;
	}
	
	// If the result is trivial, we just calculate it. This is true for k &lt; 3
	if (k &lt; 3) {
		if (k == 0) {
			return sum == 0 ? BigInteger.ONE : BigInteger.ZERO;
			
		} else if (k == 1) {
			return sum &lt;= m ? BigInteger.ONE : BigInteger.ZERO;
			
		} else {
			final int upper = Math.min(sum, m);
			final int lower = sum - upper;
			
			if (upper &lt; lower) {
				return BigInteger.ZERO;
			}
			
			final int difference = upper - lower;
			final int numSubParts = difference / 2 + 1;
			return BigInteger.valueOf(numSubParts);
		}
	}
	
	// If k * m / 2 &lt; sum, we can &#39;invert&#39; 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 &lt; m) {
		m = (short) sum;
		
		if (sum &lt; k) {
			k = (short) sum;

			if (k &lt; 3) {
				return calcNumRIPspecificKmaxM(sum, k, m, cache);
			}
		}
	}
	
	// If the result is non-trivial, we check the cache or delegate.
	final Triple&lt;Short, Short, Integer&gt; 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 &lt;= m; ++i) {
		final int currentSum = sum - (i * k);
		if (currentSum &lt; 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&lt;Triple&lt;Short, Short, Integer&gt;, BigInteger&gt; map = new HashMap&lt;&gt;(1024);
	private long numLookUps = 0;
	private long numReuse = 0;
	
	public BigInteger lookUp(Triple&lt;Short, Short, Integer&gt; key) {
		++numLookUps;
		
		BigInteger value = map.get(key);
		if (value != null) {
			++numReuse;
		}
		
		return value;
	}
	
	public void enter(Triple&lt;Short, Short, Integer&gt; 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;
	}
}

Note: I used apache-common's Triple-class as key here, to spare the implementation of an explicit key-class, but this is not an optimization in runtime, it just saves code.

Edit: Beside a fix to a problem found by @MBo (thank you), I added a few shortcuts to reach the same result. The algorithm now performs even better, and the cache (reuse) rate is better. Maybe this will satisfy your requirements?

The optimizations explained (they are only applicable after the above mentioned transposition of the problem):

  • If k &gt; m, we can 'flip' the rectangle upright, and still get the same result for the number of legal partitions. This will map some 'lying' configurations into 'upright' configurations and reduce the overall amount of different keys.

优化:具有最大值限制的整数划分

  • If the number of squares in the rectangle is larger than the number of 'empty spaces', we can consider the 'empty spaces' as squares instead, which will map another bunch of keys together.

优化:具有最大值限制的整数划分

  • If sum &lt; k and/or sum &lt; m, we can reduce k and/or m to sum, and still get the same number of partitions. (this is the most impacting optimization, as it often skips multiple redundant interim steps and frequently reaches m = k = sum)

优化:具有最大值限制的整数划分

答案3

得分: 1

另一种方法是使用约束求解器并配置它以显示所有解决方案。以下是使用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;
英文:

An alternative would be to use a constraint solver and configure it to show all solutions. Here a solution with MiniZinc:

include &quot;globals.mzn&quot;;
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;

huangapple
  • 本文由 发表于 2020年10月12日 11:58:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/64311503.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定