给定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] < 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中或不属于任何组。
最初,组范围足够大,以便可以将所有任务放入其中,但是当我们向组添加任务时,时间范围会收敛。
我知道这个算法可以正常工作,但对于某些输入,它的复杂性是指数级的。
因此可以优化这种算法,或者我应该采用另一种策略吗?如果应用其他策略,请告诉我优化或概念。
答案 0 :(得分:0)
我将解释一种算法(我认为)应该具有N 3 时间复杂度,并且N 2 存储器复杂度(N是任务数)。我将尝试描述它的来源和来源。它是如何工作的,但如果你只想要伪代码,你可以跳到最后。
首先,我想以矩阵(或“涂鸦”)的方式表示您的输入数据。如果我从你的例子中取一个子集:
+--------+---+---+---+---+---+---+---+---+---+----+----+----+
| | 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;
}
}
}
好消息:时间复杂性不再是指数级的!坏消息:
MAX_TIME
的价值,即使只有很少的任务,availableTasks
也会有几GB。因此,任何实际的解决方案都不能通过所有时间值来蛮力。
让我们来看两个任务的简单案例:
直观地说,您可以说没有必要测试2,000,000个时间值。您会认为有4个时间间隔值(上限除外):
如果在同一时间间隔内选择Ta
和Tb
,我们会availableTasks[Ta] == availableTasks[Tb]
。所以基本上,我们只需要测试每个时间间隔内的单个时间值。我将选择下限来表示每个区间:1 | 500,000 | 1,500,000 | 2,000,000。
注意:在数学术语中,我们通过定义partitioning:t1~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}
中的所有对,我们一定会找到最佳解决方案。
这应该已经限制了足够的值以使算法在合理的时间内工作,但我们可以做得更简单,更快。上一组中的值可以解释如下:
t=tasks[k][0]
:任务k无法在上一个时间间隔内执行(以t-1
结尾),但可以在t
开始的时间间隔内执行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]}
中的所有对,我们肯定会找到最佳解决方案。
生成重建索引的输入数据:
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;
}
}
}
如果这个算法有效,我认为有可能在空间和空间方面进一步完善它。时间复杂性。但是这个帖子已经很长了^^。首先,我建议您检查一下这是否正确,是否符合您的性能要求。