【问题标题】:Better Version for Backtracking更好的回溯版本
【发布时间】:2018-05-20 21:19:29
【问题描述】:

给定 n 个任务,每个任务可以在 1 个单位时间内执行,并且任务可以并行执行。每个任务只能在给定的时间范围内完成,例如在时间 t1 和 t2(包括两者)之间(t1
示例:对于 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 或不属于任何组。
最初,组范围足够大,因此可以将所有任务放入其中,但是当我们将任务添加到组时,时间范围会收敛。
我知道这个算法可以正常工作,但是对于某些输入,它的复杂性是指数级的。
因此,是否可以优化此算法,或者我应该采用其他策略?如果应用了其他策略,请告诉我优化或概念。

【问题讨论】:

    标签: c++ recursion backtracking recursive-backtracking


    【解决方案1】:

    我将解释一个(我认为)应该具有 N3 时间复杂度和 N2 内存复杂度(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*1018 次迭代。在 4Ghz CPU 上,每周期 1 次迭代(非常乐观),需要十年以上。

    因此,任何实际的解决方案都无法通过所有时间值对进行暴力破解。

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

    让我们举一个简单的案例,有 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。

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

    这些代表值从何而来?简单:

    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的区间执行

    从这个角度来看,情况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;
            }
        }
    }
    

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

    【讨论】:

      猜你喜欢
      • 2022-01-18
      • 1970-01-01
      • 2021-11-07
      • 1970-01-01
      • 1970-01-01
      • 2022-10-15
      • 2013-09-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多