Java中的作业调度算法

时间:2014-09-20 16:16:07

标签: java algorithm scheduling dynamic-programming

我需要为调度问题设计一个有效的算法,我真的没有线索。

有一台机器以一定的速度生产药丸。例如,如果允许连续工作一天,机器可能能够生产1丸,如果允许连续工作3天,则可以生产4丸,如果它可以工作5天,则可以生产16丸,等等。如果我停止机器并取出所有药丸,那么机器将从第1天开始。我从机器中取出的所有药丸都必须在同一天使用。

每天有一定数量的患者来吃药。患者必须在同一天进行治疗,未经治疗的患者将被忽略。目标是确定停止机器的天数,并在n天内尽可能多地治疗患者。

假设n = 5的天数,给出示例输入

int[] machineRate = {1,2,4,8,16};
int[] patients =    {1,2,3,0,2};

在这种情况下,如果我在第3天停止机器,我会吃4粒药。我可以治疗3名患者并丢弃1丸。然后我再次在第5天停止机器,因为它在第3天停止,它已经工作了2天,因此我有2个药片治疗2名患者。最后3 + 2 = 5,输出= 5名患者。

如果我在第2天,第3天,第5天停止机器。那么输出将是(第2天2名患者2粒)+(第3天3名患者1粒)+(2粒2粒)患者在第5天)。这相当于5名患者。

machineRate[]patients[]根据输入而有所不同。

找到最大治疗患者数的算法是什么?

3 个答案:

答案 0 :(得分:6)

这是一个很好的动态编程问题。

思考动态编程的方法是问自己两个问题:

  1. 如果我将一个(或多个)变量减少到零(或类似),是否存在这个问题的简单版本?
  2. 如果我知道大小n+1的所有问题的答案,是否有一种简单的方法来计算大小n问题的答案? (此处,"尺寸"是特定于问题的,您需要找到有助于解决问题的正确尺寸概念。)
  3. 对于这个问题,什么是一个微不足道的版本?好吧,假设天数为1.那么这很容易:我停下机器,尽可能多地对待病人。没有必要做其他任何事情。

    现在,如果我们考虑剩下的天数作为我们的大小概念,我们也会得到第二个问题的答案。假设我们知道剩下n天所有问题的所有答案。如果剩余maxTreat(days, running)天,并且机器最初的运行时间为days天,请让running写下我们可以处理的最大数量。

    现在还有n+1天;到目前为止,机器已运行k天。我们有两个选择:(1)停止机器; (2)不要阻止它。如果我们停止机器,我们今天可以治疗一些患者(我们可以根据k计算出这个数字),然后我们可以治疗maxTreat(n, 1)患者,因为那时n天离开了,明天机器将再运行一天。如果我们不停止机器,我们今天不能治疗任何人,但此后我们能够治疗maxTreat(n,k+1)患者,因为明天机器将会运行{ {1}}天。

    我将让您完成精确的细节,但为了有效地解决它,我们根据剩余的天数和机器到目前为止运行的天数创建一个多维数组。然后我们遍历数组,解决所有可能的问题,从平凡(左一天)开始,向后工作(两天,然后三天,等等)。在每个阶段,我们要解决的问题要么是微不足道的(所以我们可以写出答案),或者我们可以从上一步写入数组的条目中计算出来。

    关于动态编程的一个非常酷的事情是我们正在创建所有结果的缓存。因此,对于递归方法最终需要多次计算子问题的答案的问题,使用动态编程我们永远不会不止一次地解决子问题。

    现在我已经看到了您的实施的其他评论:

    首先,当你达到10,000左右时,我开始放慢速度并不会太惊讶。该算法为k+1,因为在每次迭代时,您必须使用最多O(n^2)条目填充数组,然后才能进入下一级别。我很确定n是你为这个谜题获得的最佳渐近复杂性。

    如果你想进一步加快速度,你可以看一下自上而下的方法。目前,您正在进行自下而上的动态编程:解决大小为0,然后是大小为1的情况,依此类推。但你也可以反过来做。本质上,这是一个相同的算法,就像你编写一个非常低效的递归解决方案一样,计算动态子问题的解决方案,除了你每次计算它时都缓存一个结果。所以它看起来像这样:

    1. 设置二维数组以保存子问题的解决方案。每种情况下预填充-1。值为-1表示您尚未解决该子问题。
    2. 编写一个例程,根据下一级子问题的答案解决O(n^2)。如果需要子问题的答案,请查看数组。如果那里有一个-1,你还没有解决那个,所以你递归地解决它,然后把答案放到数组中。如果除了-1以外的任何其他内容,您可以使用您在那里找到的值,因为您已经计算过它。 (您也可以使用maxTreat(days, running)代替多维数组。)
    3. 这在某种程度上更好,在另一种情况下更糟糕。它更糟糕,因为你有与递归相关的开销,并且因为你最终会用递归调用耗尽堆栈。您可能需要使用命令行参数将堆栈大小提升到JVM。

      但在一个关键方面它更好:你不能计算所有子问题的答案,但只能计算你需要知道答案的答案。对于某些问题,这是一个巨大的差异,而对于一些问题,它并非如此。很难获得正确的直觉,但我认为这可能会产生很大的不同。毕竟,每个答案仅取决于前一行中的两个子问题。

      最终的解决方案(不要尝试这个,直到你自上而下的递归递归!)是自上而下但没有递归。这样可以避免堆栈空间问题。要做到这一点,你需要创建一堆需要解决的子问题(使用HashMap),然后继续将它们从队列的前面取下,直到没有剩下。第一件事就是将需要解决方案的大问题推到堆栈上。现在,您迭代地从堆栈中弹出问题,直到它为空。弹出一个,然后将其命名为ArrayDeque。然后:

      1. 查看您的数组或P,看看HashMap是否已解决。如果是,请回答。
      2. 如果没有,请查看P的子问题是否已经解决。如果有,则可以解决P,然后缓存答案。如果堆栈现在为空,那么您已解决了最终问题,并输出了P的答案。
      3. 如果子问题都没有解决,那么将P推回堆栈。然后将任何尚未解决的P个子问题推送到堆栈中。
      4. 随着时间的推移,当您将主要问题及其子问题及其子问题推入堆栈时,您的堆栈将最初增长。然后,您将开始解决较小的实例并将结果放入缓存中,直到最终您拥有解决主要问题所需的一切。

        它不会使用比递归自上而下方法少得多的内存,但它确实使用堆空间而不是JVM堆栈空间,这意味着它可以更好地扩展,因为JVM堆栈比堆小得多

        但是,这很困难。至少,在开始编写更难的版本之前,请保留您的工作解决方案!

答案 1 :(得分:0)

另一种方法是预测第二天或几天。假设我们在最后几天看过1,2.patients,我们今天可以服用两片药,治愈两个病人或预测第二天三个或更多,并让机器运行。如果我们没有像1,1那样的加薪,我们预测明天会有一名病人,今天就服用一粒。如果第二天变得像1,4,8那样不同,我们只是将第二天的预测调整为1/2,即2。 这个解决方案的优点是你可以处理不确定性,即你不知道明天会带来什么。这允许我们流式传输数据。 不好的一面是,第一位患者将永远死亡。

答案 2 :(得分:0)

我已经实现了chiastic-security的设计,但是当n大于10000左右时,性能并不高。如果有人有任何其他想法,请告诉我,因为我认为这是一个非常有趣的问题。我一开始尝试使用递归但是内存不足,所以我不得不在循环中完成它。到目前为止,我正在存储一个带有所有结果的大二维数组,但后来我意识到我只需要访问之前的"行"结果所以我只使用了2个数组:" current"和"之前":

static int calculateMax() {
    int[] previous = new int[n];
    for (int daysMachineRunning=0; daysMachineRunning<n; daysMachineRunning++) {
        previous[daysMachineRunning] = treatPatients(0, daysMachineRunning);
    }

    int[] current = null;
    for (int daysRemaining=1; daysRemaining<n; daysRemaining++) {
        current = new int[n-daysRemaining];
        for (int daysMachineRunning=0; daysMachineRunning<n-daysRemaining; daysMachineRunning++) {
            current[daysMachineRunning] = Math.max(
                    treatPatients(daysRemaining, daysMachineRunning) + previous[0],
                    previous[daysMachineRunning+1]
                );
        }
        previous = current;
    }
    return current[0];
}

static int treatPatients(int daysRemaining, int daysMachineRunning) {
    return Math.min(patients[n-1-daysRemaining], machineRate[daysMachineRunning]);
}

编辑:我现在已经实施了第二种方法,但仍然遇到n> = 10000左右的问题:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space。如果有人有兴趣进一步追求我的代码:

static final int[][] results = new int[n][n];
static final SortedSet<Target> queue = new TreeSet<>(new Comparator<Target>() {
    @Override
    public int compare(Target o1, Target o2) {
        if (o1.daysRemaining < o2.daysRemaining)
            return -1;
        else if (o1.daysRemaining > o2.daysRemaining)
            return 1;
        else if (o1.daysMachineRunning < o2.daysMachineRunning)
            return 1;
        else if (o1.daysMachineRunning > o2.daysMachineRunning)
            return -1;
        else return 0;
    }
});

public static void main(String[] args) {
    for (int i=0; i<n; i++) {
        Arrays.fill(results[i], -1);
    }

    if (n <= 10) {
        System.out.println(Arrays.toString(machineRate));
        System.out.println(Arrays.toString(patients));
    } else
        System.out.println(n);

    System.out.println(calculateMax());
}

static class Target {
    int daysRemaining, daysMachineRunning;
    Target(int daysRemaining, int daysMachineRunning) {
        this.daysRemaining = daysRemaining;
        this.daysMachineRunning = daysMachineRunning;
    }
}

static int calculateMax() {
    addTarget(n-1, 0);
    while (results[n-1][0]==-1) {
        Target t = queue.first();
        queue.remove(t);
        calculateMax(t);
    }
    return results[n-1][0];
}

static void calculateMax(Target t) {
    int daysRemaining = t.daysRemaining;
    int daysMachineRunning = t.daysMachineRunning;
    int treatedPatients = Math.min(patients[n-1-daysRemaining], machineRate[daysMachineRunning]);
    if (daysRemaining==0)
        results[0][daysMachineRunning] = treatedPatients;
    else {
        int resultA = results[daysRemaining-1][0];
        int resultB = results[daysRemaining-1][daysMachineRunning+1];
        if (resultA>=0 && resultB>=0) {
            results[daysRemaining][daysMachineRunning] = Math.max(treatedPatients + resultA, resultB);
        }
        else {
            if (resultA==-1)
                addTarget(daysRemaining-1, 0);
            if (resultB==-1)
                addTarget(daysRemaining-1, daysMachineRunning+1);
            addTarget(daysRemaining, daysMachineRunning);
        }
    }
}

static void addTarget(int a, int b) {
    queue.add(new Target(a,b));
}