【问题标题】:Job Scheduling Algorithm in JavaJava中的作业调度算法
【发布时间】:2014-11-15 01:03:16
【问题描述】:

我需要为调度问题设计一个有效的算法,但我真的不知道。

有一台机器可以以一定的速度生产药丸。例如,机器连续工作 1 天可以生产 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[] 因输入而异。

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

【问题讨论】:

  • 您可以计算所有可能的结果,并确定哪一个结果的治疗人数最多。
  • 那些病人呢,如果他们在入住那天没有得到治疗,他们会死吗?如果是这样,则意味着他们需要在入住当天接受治疗,否则您必须跟踪未治疗患者的数量。
  • 是否有不能再治疗患者的限制/截止日期?
  • 考虑动态规划,从最后一天开始。
  • 患者必须在同一天治疗,未治疗的患者被忽略。

标签: java algorithm scheduling dynamic-programming


【解决方案1】:

这是一个很好的动态规划问题。

思考动态规划的方法是问自己两个问题:

  1. 如果我将一个(或多个)变量减少到零(或类似),是否存在此问题的简单版本?
  2. 如果我知道所有n 大小问题的答案,是否有一种简单的方法来计算n+1 大小问题的答案? (这里,“大小”是针对特定问题的,您需要找到有助于解决手头问题的正确大小概念。)

对于这个问题,什么是简单的版本?好吧,假设天数是 1。那么很容易:我停止机器,尽可能多地治疗患者。做其他事情没有意义。

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

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

我会让你计算出精确的细节,但为了有效地解决它,我们根据剩余的天数和机器到目前为止运行的天数创建一个多维数组。然后我们遍历数组,解决所有可能的问题,从琐碎的(还剩一天)开始,向后工作(两天,然后是三天,依此类推)。在每个阶段,我们要解决的问题要么是微不足道的(因此我们可以将答案写进去),要么可以根据我们在上一步写入数组的条目进行计算。

动态编程真正酷的地方在于,我们会在执行过程中创建所有结果的缓存。因此,对于递归方法最终需要多次计算子问题的答案的问题,使用动态规划,我们永远不会不止一次地解决子问题。

现在我已经看到了您的实现,还有其他 cmets:

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

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

  1. 设置二维数组以保存子问题的解决方案。为每种情况预先填充 -1。值 -1 表示您尚未解决该子问题。
  2. 编写一个解决maxTreat(days, running) 的例程,以解决下一级子问题的答案。当您想要子问题的答案时,请查看数组。如果那里有一个-1,那么你还没有解决那个问题,所以你递归地解决它,然后将答案放入数组中。如果有除 -1 以外的任何值,您可以使用在那里找到的值,因为您已经计算过了。 (您也可以使用HashMap 代替多维数组。)

这一方面更好,另一方面更糟。更糟糕的是,您有与递归相关的开销,并且因为递归调用最终会耗尽堆栈。您可能需要使用 JVM 的命令行参数来增加堆栈大小。

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

最终的解决方案(在您首先获得自顶向下递归之前不要尝试此方法!)是自顶向下但不使用递归。这将避免堆栈空间问题。为此,您创建了一堆需要解决的子问题(使用ArrayDeque),并不断将它们从队列的前面移走,直到没有剩余。第一件事是将需要解决的大问题推入堆栈。现在,您迭代地从堆栈中弹出问题,直到它为空。弹出一个,并称之为P。那么:

  1. 查看您的数组或HashMap 以查看P 是否已解决。如果是,请返回答案。
  2. 如果没有,请查看P 的子问题是否已经解决。如果他们有,那么您可以解决P,并缓存答案。如果堆栈现在是空的,那么您已经解决了最后一个问题,并且您输出了P 的答案。
  3. 如果子问题还没有全部解决,则将P 推回堆栈。然后将P 的任何尚未解决的子问题也推入堆栈。

随着您的进展,您的堆栈最初会随着您将主要问题及其子问题以及子问题推入堆栈而增长。然后您将开始解决较小的实例并将结果放入缓存中,直到最终您拥有解决主要问题所需的一切。

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

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

【讨论】:

    【解决方案2】:

    另一种方法是预测接下来的一天或几天。假设我们在过去的几天里看到了 1,2 名患者,我们可以要么今天服用两粒药并治愈两名患者,要么预测第二天的三个或更多患者并让机器运行。如果我们没有像 1,1 这样的加薪,我们会预测明天有一名患者,今天服用一粒药。如果第二天结果与 1、4、0 不同,我们只需将第二天的预测调整为 1/2,即 2。 这个解决方案的好处是你可以在不确定的情况下工作,即你不知道明天会发生什么。这使我们能够流式传输数据。 缺点是第一个病人总是会死。

    【讨论】:

    • 这样做的主要缺点是它不一定能解决问题,它要求找到最大数量的治疗患者的算法。这可能会或可能不会找到最大值。
    • 绝对正确。这会给我们一个平均的结果。并且会因不可预测的输入而完全失败。但它对流行病很有用。
    【解决方案3】:

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

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

    【讨论】:

    • 我在回答中添加了大量细节,说明哪些地方可以改进,哪些地方不能改进。
    • 谢谢,我记得来自 uni 的这些东西,但有一段时间没有尝试过...如果您有兴趣,请查看我上面的更新代码,如果我有请告诉我做错了什么,或者这只是 java / 内存空间的限制。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-04-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-01-19
    相关资源
    最近更新 更多