在具有M容量的K室中分配N吨食物

时间:2016-02-15 16:38:00

标签: java algorithm performance recursion

我在网上发现了这个问题:

  

你有N吨食物和K室存放。每个房间都有M容量。您可以在房间内分配食物的方式,以便每个房间至少有1吨食物。

我的方法是递归地找到满足问题条件的所有可能的变化。我从一个大小为K的数组开始,初始化为1.然后我继续向数组的每个元素添加1并递归检查新数组是否满足条件。但是,递归树太快太快,程序需要太长时间才能获得稍高的N,K和M值。

实现此任务的更有效算法是什么?是否要对现有的算法实现进行任何优化?

这是我的实施:

import java.util.Arrays;
import java.util.HashSet;
import java.util.Scanner;

public class Main {
    // keeping track of valid variations, disregarding duplicates
    public static HashSet<String> solutions = new HashSet<>();

    // calculating sum of each variation
    public static int sum(int[] array) {
        int sum = 0;
        for (int i : array) {
            sum += i;
        }

        return sum;
    }

    public static void distributionsRecursive(int food, int rooms, int roomCapacity, int[] variation, int sum) {
        // if all food has been allocated
        if (sum == food) {
            // add solution to solutions
            solutions.add(Arrays.toString(variation));
            return;
        }

        // keep adding 1 to every index in current variation
        for (int i = 0; i < rooms; i++) {
            // create new array for every recursive call
            int[] tempVariation = Arrays.copyOf(variation, variation.length);
            // if element is equal to room capacity, can't add any more in it
            if (tempVariation[i] == roomCapacity) {
                continue;
            } else {
                tempVariation[i]++;
                sum = sum(tempVariation);
                // recursively call function on new variation
                distributionsRecursive(food, rooms, roomCapacity, tempVariation, sum);
            }
        }
        return;
    }

    public static int possibleDistributions(int food, int rooms, int roomCapacity) {
        int[] variation = new int[rooms];
        // start from all 1, keep going till all food is allocated
        Arrays.fill(variation, 1);
        distributionsRecursive(food, rooms, roomCapacity, variation, rooms);
        return solutions.size();
    }

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int food = in.nextInt();
        int rooms = in.nextInt();
        int roomCapacity = in.nextInt();

        int total = possibleDistributions(food, rooms, roomCapacity);
        System.out.println(total);
        in.close();
    }
}

3 个答案:

答案 0 :(得分:3)

是的,如果您以天真的方式执行此操作,您的递归树将变大。假设你有10吨和3个房间,M = 2。一个有效的安排是[2,3,5]。但你也有[2,5,3],[3,2,5],[3,5,2],[5,2,3]和[5,3,2]。因此,对于每个有效的数字分组,实际上都是K!排列。

解决此问题的一种可能更好的方法是确定可以使多少种方法使K个数字(最小M和最大N)加起来为N.首先使第一个数字尽可能大,这将是{ {1}}。在我的例子中,那将是:

N-(M*(K-1))

给出答案[6,2,2]。

然后,您可以通过从左到右“移动”值来构建算法来调整数字以得出有效组合。在我的例子中,你有:

10 - 2*(3-1) = 6

通过确保值从左向右递减,可以避免看似无限的递归。例如,在上面你永远不会有[3,4,3]。

如果您确实需要所有有效排列,则可以为上述每种组合生成排列。但我怀疑这不是必要的。

我认为这应该足以让你开始寻求一个好的解决方案。

答案 1 :(得分:2)

一种解决方案是从k-1个房间的结果中计算k个房间的结果。

我已经简化了一个允许在一个房间存储0吨的问题。如果我们必须存储至少1个,我们可以提前减去它并将房间容量减少1个。

所以我们定义一个函数calc:(Int,Int)=&gt; List [Int]计算多个房间,容量计算组合数量列表。第一个条目包含我们用于存储0的组合数,存储1时的下一个条目,依此类推。

我们可以轻松地为一个房间计算此功能。因此calc(1,m)给出了一个直到第m个元素的列表,然后它只包含零。

对于更大的k,我们可以递归地定义这个函数。我们只计算calc(k - 1,m),然后通过总结旧列表的前缀来构建新列表。例如。如果我们要存储5吨,我们可以在第一个房间存储所有5个,在以下房间存储0个,或者在第一个房间存储4个,在下面的1个存储,依此类推。所以我们必须总结其余房间的0到5的组合。

由于我们有最大容量,我们可能不得不省略一些组合,即如果房间只有容量3,我们不能计算在其余房间存储0和1吨的组合。

我在Scala中实现了这种方法。我已经使用了流(即无限列表)但是你知道你需要的最大元素数量,这是没有必要的。

方法的时间复杂度应为O(k * n ^ 2)

def calc(rooms: Int, capacity: Int): Stream[Long] =
  if(rooms == 1) {
    Stream.from(0).map(x => if(x <= capacity) 1L else 0L)
  } else {
    val rest = calc(rooms - 1, capacity)
    Stream.from(0).map(x => rest.take(x+1).drop(Math.max(0,x - capacity)).sum)
  }

你可以在这里试试:

http://goo.gl/tVgflI

(我已经将BigInt替换为Long,以使其适用于更大的数字)

答案 2 :(得分:1)

首先提示,删除distributionsRecursive,不要建立解决方案列表。所有解决方案的列表都是一个庞大的数据集。只需计算一下。

这将允许您将possibleDistributions转换为根据自身定义的递归函数。递归步骤为possibleDistributions(food, rooms, roomCapacity) = sum from i = 1 to roomCapacity of possibleDistributions(food - i, rooms - 1, roomCapacity)

您将节省大量内存,但仍存在潜在的性能问题。但是,使用纯递归函数,您现在可以使用https://en.wikipedia.org/wiki/Memoization修复此问题。