Лучшая версия для возврата - PullRequest
0 голосов
/ 21 мая 2018

Учитывая n задач, каждая из них может быть выполнена за 1 единицу времени, а задачи могут выполняться параллельно.Каждое задание может быть выполнено только в течение определенного периода времени, скажем, между временем t1 и t2 (оба включительно) (t1 <= t2).Цель состоит в том, чтобы найти максимальное количество задач, которые можно выполнить в 2 момента времени. <br>
Пример: для 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 случая для задачи: он принадлежит группе 1 или группе 2 или не принадлежит ни к одной группе.
Первоначально диапазоны групп достаточно велики, так что в них можно помещать все задачи, но когда мы добавляем задачу в группу, диапазон времени сходится.
Я знаю, что этот алгоритм работает правильно, но для некоторых входов его сложность оказывается экспоненциальной.
Таким образом, возможно ли оптимизировать этот алгоритм или, может быть, я должен пойти с какой-то другой стратегией?Пожалуйста, дайте мне знать об оптимизации или концепции, если применяются другие стратегии.

1 Ответ

0 голосов
/ 21 мая 2018

Я объясню алгоритм, который (я думаю) должен иметь 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 будет несколько ГБ даже при небольшом количестве задач.
  2. Смешное время выполнения: двойной циклиметь примерно 2 * 10 18 итераций.На процессоре 4 ГГц с 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 [

Если Ta и Tb выбраны в одном интервале, мы имеем availableTasks[Ta] == availableTasks[Tb].Таким образом, в основном, мы просто должны проверить одно временное значение в каждом интервале.Я выберу нижнюю границу для представления каждого интервала: 1 |500 000 |1 500 000 |2 000 000.

Примечание. В математических терминах мы разбиваем набор значений времени, определяя отношение эквивалентности : t1~t2 <=> 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

С этой точки зрения, интервал, начинающийся с t в случае 2, является бесполезным кандидатом: если только 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;
        }
    }
}

Если этот алгоритм работает, я думаю, что можно было бы уточнить его в дальнейшемУсловия пространственной и временной сложности.Но этот пост уже довольно длинный ^^.Сначала я предлагаю проверить, правильно ли это, и соответствует ли оно вашим требованиям к производительности.

...