更好的回溯版本

时间:2018-05-20 21:19:30

标签: c++ recursion backtracking recursive-backtracking

给定n个任务,每个任务可以在1个单位时间内执行,任务可以并行执行。每个任务只能在给定的时间范围内完成,比如在时间t1和t2之间(包括两者)(t1 <= t2)。目标是找到可以在两个时刻执行的最大任务。

示例:对于5个任务(n = 5),
任务1:{1,5}
任务2:{3,4}
任务3:{5,6}
任务4:{7,12}
任务5:{8,100}

在这里,我们最多可以完成4项任务 任务1和2可以在[3,4]之间的时间点执行,任务4和5可以在[8,12]之间的时间点执行。

  要么


任务1和3可以在时刻5执行,任务4和5可以在[8,12]之间的时刻执行。

现在这里是回溯算法的C ++版本:

int result = 0, n;
int task[1000][2];

//Checks if task overlaps with the time range [t1, t2]
bool taskFit(int &taskno, int &t1, int &t2){
    if(task[taskno][1] &lt t1)return false;
    else if(task[taskno][0] > t2)return false;
    else return true;
}

void backtrack(int t1, int t2, int t3, int t4, int taskno, int grp1, int grp2){
    //t1, t2 represultent time bounds for group1
    //t3, t4 represultent time bounds for group2
    //grp1: number of tasks that can be performed in time range of group1
    //grp2: number of tasks that can be performed in second time stamp

    result = max(grp1 + grp2, result);
    if(result==n || taskno==(n+1))return;

    //putting task in first group if it fits in the range of group1
    if(taskFit(taskno, t1, t2))
        backtrack(max(t1, task[taskno][0]), min(t2, task[taskno][1]), t3, t4, taskno+1, grp1+1, grp2);

    //putting task in second group if it fits in its range
    if(taskFit(taskno, t3, t4))
        backtrack(t1, t2, max(t3, task[taskno][0]), min(t4, task[taskno][1]), taskno+1, grp1, grp2+1);

    //simply ignoring the task
    backtrack(t1, t2, t3, t4, taskno+1, grp1, grp2);
}

main(){
    //...we have the value of n and time range of all n tasks
    // for ith task time range in obtained as [task[i][0], task[i][1]]

    //initially both groups are set in time range = [0, 2000000000]

    //here we put the task1 in first group
    //thus setting the new range for group 1
    backtrack(task[0][0], task[0][1], 0, 2000000000, 2, 1, 0);

    //ignoring the first task, the group ranges remain as it is
    backtrack(0, 2000000000, 0, 2000000000, 2, 0, 0);
}


上述回溯算法考虑任务的3个案例,它位于group1或group2中或不属于任何组。
最初,组范围足够大,以便可以将所有任务放入其中,但是当我们向组添加任务时,时间范围会收敛。 我知道这个算法可以正常工作,但对于某些输入,它的复杂性是指数级的。
因此可以优化这种算法,或者我应该采用另一种策略吗?如果应用其他策略,请告诉我优化或概念。

1 个答案:

答案 0 :(得分:0)

我将解释一种算法(我认为)应该具有N 3 时间复杂度,并且N 2 存储器复杂度(N是任务数)。我将尝试描述它的来源和来源。它是如何工作的,但如果你只想要伪代码,你可以跳到最后。

第1步:重新索引输入数据

首先,我想以矩阵(或“涂鸦”)的方式表示您的输入数据。如果我从你的例子中取一个子集:

+--------+---+---+---+---+---+---+---+---+---+----+----+----+
|        | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
+--------+---+---+---+---+---+---+---+---+---+----+----+----+
| Task 1 | X | X | X | X | X |   |   |   |   |    |    |    |
| Task 2 |   |   | X | X |   |   |   |   |   |    |    |    |
| Task 3 |   |   |   |   | X | X |   |   |   |    |    |    |
| Task 4 |   |   |   |   |   |   | X | X | X | X  | X  | X  |
+--------+---+---+---+---+---+---+---+---+---+----+----+----+

目前,您的数据按行编制:task[taskIndex] // --> time interval of this task。如果我们按列重新编制索引会发生什么:availableTasks[timeIndex] // --> set of tasks that can be executed at this time

通过这种数据结构的简单实现,我们可以编写以下天真算法(在伪C ++中):

int MAX_TIME = 2000000000; // 2,000,000,000
// reindexed input
std::unordered_set<int>; availableTasks[MAX_TIME];
// main algorithm
int bestTotal = -1; // result
int r1, r2; // time values to get bestTotal
// brute force: try all pair of time values:
for (int t1 = 0; t1 <= MAX_TIME-1; t1++) {
    for (int t2 = t1; t2 <= MAX_TIME; t2++) {
        int count = union(availableTasks[t1], availableTasks[t2]).size();
        if (count > bestCount) {
            r1 = t1; r2 = t2;
            bestCount = count;
        }
    }
}

好消息:时间复杂性不再是指数级的!坏消息:

  1. 内存使用量巨大:由于MAX_TIME的价值,即使只有很少的任务,availableTasks也会有几GB。
  2. 荒谬的执行时间:双循环具有大约2 * 10 18 次迭代。在4Ghz CPU上进行1次迭代/循环(非常乐观),这已经超过十年了。
  3. 因此,任何实际的解决方案都不能通过所有时间值来蛮力。

    第2步:限制候选时间值。

    让我们来看两个任务的简单案例:

    • 任务0:[1; 1 499 999]
    • 任务1:[500 000; 1 999 999]

    直观地说,您可以说没有必要测试2,000,000个时间值。您会认为有4个时间间隔值(上限除外):

    • 间隔A:[1; 500 000 [
    • 间隔B:[500 000; 1 500 000 [
    • 间隔期C:[1 500 000; 2 000 000 [
    • 间隔期D:[2 000 000; MAX_TIME [

    如果在同一时间间隔内选择TaTb,我们会availableTasks[Ta] == availableTasks[Tb]。所以基本上,我们只需要测试每个时间间隔内的单个时间值。我将选择下限来表示每个区间:1 | 500,000 | 1,500,000 | 2,000,000。

    注意:在数学术语中,我们通过定义partitioningt1~t2 <=> availableTasks[t1] == availableTasks[t2],然后从每个选择一个代表性元素,equivalence relation时间值集合等价类。

    这些代表性价值来自哪里?易:

    tasks[0][0] // 1
    tasks[1][0] // 500,000
    tasks[0][1] + 1 // 1,500,000
    tasks[1][1] + 1 // 2,000,000
    

    第一个候选限制规则:如果我们测试集S={tasks[*][0], tasks[*][1]+1}中的所有对,我们一定会找到最佳解决方案。

    这应该已经限制了足够的值以使算法在合理的时间内工作,但我们可以做得更简单,更快。上一组中的值可以解释如下:

    1. t=tasks[k][0]:任务k无法在上一个时间间隔内执行(以t-1结尾),但可以在t开始的时间间隔内执行
    2. t=tasks[k][1]:任务k可以在前一个时间间隔内执行(以t-1结尾),但不能在t
    3. 的时间间隔内执行

      从这个角度来看,案例2中从t开始的间隔是无用的候选者:除非有l验证tasks[l][0] == tasks[k][1]+1(意思是:在时间t,有另一个变为可执行的任务,所以t也属于1),前一个区间中的任何时间值都是必要的更好的候选者(因为availableTasks[t-1]严格包含availableTasks[t])。因此:

      更好的候选限制规则:如果我们测试集S={tasks[*][0]}中的所有对,我们肯定会找到最佳解决方案。

      算法(未经测试,伪C ++)

      生成重建索引的输入数据:

      int n = 1000; //number of tasks
      // availableTasks[timeIndex] : set of tasks executable at timeIndex.
      // Only candidate values are stored in the map.
      std::map<int, std::unordered_set<int>> availableTasks;
      for ( timeCandidate : task[*][0] ) {
          auto& set = availableTasks[timeCandidate];
          for ( int taskIndex = 0; taskIndex < n; taskIndex++) {
              if (timeCandidate in task[taskIndex]) {
                  set.insert(task);
              }
          }
      }
      

      主要算法:

      int bestTotal = -1; // result
      int r1, r2; // time values to get bestTotal
      auto& map = availableTasks; // alias
      for (auto it1=map.begin(); it1 != map.end(): it1++) {
          int t1 = it1->first;
          auto& set1 = it1->second;
          auto it2 = it1; // copy the iterator
          it2++;
          for (; it2 != map.end(); it2++) {
              int t2 = it2->first;
              auto& set2 = it2->second;
              // assuming here that union().size() can be computed in O(n) time.
              int count = union(set1, set2).size();
              if (count > bestCount) {
                  r1 = t1; r2 = t2;
                  bestCount = count;
              }
          }
      }
      

      如果这个算法有效,我认为有可能在空间和空间方面进一步完善它。时间复杂性。但是这个帖子已经很长了^^。首先,我建议您检查一下这是否正确,是否符合您的性能要求。