Удовлетворяющие тройки в графе - PullRequest
0 голосов
/ 22 февраля 2020

Я решаю проблему, когда у вас есть N событий (1 <= N <= 100000) в течение M дней (2 <= M <= 10 ^ 9). Вы пытаетесь найти минимальное время возникновения для каждого события. </p>

Для каждого события вы знаете, что оно не могло произойти до дня Si. У вас также есть C тройки (1 <= C <= 10 ^ 5), описываемые (a, b, x). Событие b должно было произойти не менее чем через x дней после a. </p>

Пример:

Существует 4 события, которые распространяются на 10 дней. Событие 1 должно было произойти в первый день или позже. Событие 2 должно было произойти в день 2 или после. Событие 3 должно было произойти в день 3 или после. Событие 4 должно было произойти в День 4 или позже.

Тройки: (1, 2, 5); (2, 4, 2); (3, 4, 4). Это означает, что событие 2 должно было произойти не менее 5 дней после события 1; Событие 4 должно было произойти как минимум через 2 дня после события 2; и событие 4 должно было произойти не менее 4 дней после события 3.

Решение состоит в том, что событие 1 произошло в день 1; Событие 2 произошло в день 6; Событие 3 произошло в день 3; и Событие 4 произошло в День 4. Причина этого заключается в том, что Событие 2 произошло как минимум через пять дней после События 1, поэтому оно не могло произойти до Дня 1 + 5 = 6. Событие 4 произошло как минимум через два дня после события 2, поэтому оно не могло произойти до дня 6 + 2 = 8.

Мое решение:

У меня была идея использовать тройки для создания Направленный граф. Таким образом, в приведенном выше примере график будет выглядеть следующим образом:

1 --5-> 2 --2-> 4

3 --4-> 4

По сути, вы создаете направленное преимущество от события, которое произошло первым, до события, которое должно было произойти после. Вес ребра - это количество дней, которое должно было произойти, по крайней мере, после.

Я думал, что мы могли бы сначала использовать входные данные для создания графика. Затем вам нужно просто выполнить бинарный поиск по всем возможным датам начала первого события (от 1 до 10 ^ 9, что составляет около 30). В этом случае первое событие - это Событие 1. Затем вы бы go просмотрели график и увидели, возможна ли эта начальная дата. Если вы когда-либо сталкивались с событием, в котором дата его появления была раньше даты Si, то вы бы прекратили этот поиск и продолжили бинарный поиск. Это решение сработало бы легко, если бы не «событие b должно было произойти, по крайней мере, x дней после a».

У кого-нибудь есть какие-то другие решения для решения этой проблемы, или как изменить мою так, чтобы она работала? Спасибо! Если у вас есть какие-либо вопросы, пожалуйста, дайте мне знать:))

Ответы [ 3 ]

2 голосов
/ 22 февраля 2020

Рассмотрим топологическую сортировку вашего DAG.

Для списка L, соответствующего топосорту вашего графа, у вас есть в конце листья. Тогда для вершины непосредственно перед

L = [..., v, leaves]

вы знаете, что ребра, выходящие из v, могут только go к вершинам после (здесь leaves). Это позволяет вам вычислить минимальный вес, связанный с v, применяя максимум Дамьена

Делайте это до головы L.

Топологическая сортировка O (V + E)


Вот иллюстрация с более интересным графиком (прочитайте его сверху вниз)

    5
   /  \
  4    7
       1 2
       0
       6

Порядок топологии: (4601275)

Таким образом, мы посетим в порядке 4,6,0,1,2,7, затем 5, и у каждой вершины, которую мы посетим, все ее зависимости уже вычислены.

Предположим, что у каждой вершины k есть событие, происходящее после 2^k дней. Следующая дата называется весом.

например, вершина 4 взвешена 2^4

Предположим, что каждое ребро (i,j) взвешено 5*i + j

  • 6 взвешивается 2^6 = 64
  • 0 взвешивается max(2^0, 64 + (0*5+6)) = 70
  • 1 занимает max(2^1, 70 + 5) = 75
  • 7 принимает max(2^7, 75 + 5*7+1, 2^2) = 2^7

Балл Следует отметить (здесь для 7), что минимальная дата, вызванная зависимостями узла, может произойти до даты, прикрепленной к этому узлу. (и мы должны сохранить самый большой)

function topologicalSort({ V, E }) {
  const visited = new Set ()
  const stack = []
  function dfs (v) {
    if (visited.has(v)) { return }
    E.has(v) && E.get(v).forEach(({ to, w }) => dfs(to))
    visited.add(v)
    stack.push(v)
  }
  // process nodes without incoming edges first
  const heads = new Set ([...V])
  for (const v of V) {
    const edges = E.get(v)
    edges && edges.forEach(({ to }) => heads.delete(to))
  }
  for (const v of heads) {
    dfs(v)
  }
  for (const v of V) {
    dfs(v)
  }
  return stack
}
class G {
  constructor () {
    this.V = new Set()
    this.E = new Map()
  }

  setEdges (from, tos) {
    this.V.add(from)
    tos.forEach(({ to, w }) => this.V.add(to))
    this.E.set(from, tos)
  }
}
function solve ({ g, vToWeight }) {
  const stack = topologicalSort(g)
  console.log('ordering', stack.join(''))
  stack.forEach(v => {
    const edges = g.E.get(v)
    if (!edges) { return }
    const newval = Math.max(
      vToWeight.get(v),
      ...edges.map(({ to, w }) => vToWeight.get(to) + w)
    )
    console.log('setting best for', v, edges.map(({ to, w }) =>  [vToWeight.get(to), w].join('+') ))
    vToWeight.set(v, newval)
  })
  return vToWeight
}
function demo () {
  const g = new G ()
  g.setEdges(2, [{ to: 1, w: 5 }])
  g.setEdges(4, [{ to: 2, w: 2 }, { to: 3, w: 4 }])
  const vToWeight = new Map ([
    [1, 1],
    [2, 6],
    [3, 3],
    [4, 4]
  ]) 
  return { g, vToWeight }
}
function demo2 () {
  const g = new G ()
  const addEdges = (i, ...tos) => {
    g.setEdges(i, tos.map(to => ({ to, w: 5 * i + to })))
  }
  addEdges(5,4,7)
  addEdges(7,1,2)
  addEdges(1,0)
  addEdges(0,6)
  const vToWeight = new Map ([...g.V].map(v => [v, 2**v])) 
  return { g, vToWeight }
}
function dump (map) {
  return [...map].map(([k, v])=> k+'->'+v)
}
console.log('----op\s sol----\n',dump(solve(demo())))
console.log('----that case---\n',dump(solve(demo2())))
2 голосов
/ 23 февраля 2020

Матрица расстояний (между всеми парами событий = узлами) может быть получена итеративным способом, аналогичным алгоритму Флойда. По существу, итеративно:

T(x, y) = max (T(x,y), T(x, z) +T (z, y))

Однако, как указано в комментарии в комментарии OP, алгоритм Флойд имеет значение O (n ^ 3), что слишком много для значения от n до 10 ^ 5 .

Ключевым моментом является то, что l oop не существует, и поэтому должен существовать более эффективный алгоритм.

В своем предложении Гродзи сделал хорошее предложение: используйте топологию c вид Направленной ациклической диаграммы c (DAG).

Я сделал реализацию в C ++ в соответствии с этой идеей, с основным отличием:

  • Я использовал простую сортировку (из библиотеки C ++) для построения топологической сортировки. Делать это просто и имеет сложность O (n logn). Предложенный Гродзи специальный метод может быть более эффективным (кажется, O (n)). Однако его очень легко реализовать, и такая сложность остается низкой.

После топологической сортировки мы знаем, что данное событие зависит только от событий перед ним. Для этой части это обеспечивает сложность O (C), где C - это число троек, то есть количество ребер.

    #include    <iostream>
    #include    <vector>
    #include    <set>
    #include    <unordered_set> 
    #include    <algorithm>
    #include    <tuple>
    #include    <numeric>

    struct Triple {
        int event1;
        int event2;
        int days;
    };

    struct Pred {
        int pred;
        int days;
    };

    void print_result (const std::vector<int> &index, const std::vector<int> &times) {
        int n = times.size();
        for (int i = 0; i < n; i++) {
            std::cout << index[i]+1 << " " << times[index[i]] << "\n";
        }
    }

    std::tuple<std::vector<int>, std::vector<int>> ordering (int n, const std::vector<Triple> &triples) {
        std::vector<int> index(n);
        std::vector<int> times(n, 0);
        std::iota(index.begin(), index.end(), 0);

    //  Build predecessors matrix and sets
        std::vector<std::vector<Pred>> pred (n);
        std::vector<std::unordered_set<int>> set_pred (n);
        for (auto &triple: triples) {
                pred[triple.event2 - 1].emplace_back(Pred{triple.event1 - 1, triple.days});

                set_pred[triple.event2 - 1].insert(triple.event1 - 1);
        }

    //  Topological sort
        std::sort (index.begin(), index.end(), [&set_pred] (int &i, int &j) {return set_pred[j].find(i) != set_pred[j].end();});

    //  Iterative calculation of times of arrival
        for (int i = 1; i < n; ++i) {
            int ip = index[i];
            for (auto &p: pred[ip]) {
                    times[ip] = std::max(times[ip], times[p.pred] + p.days);
            }
        }

    //  Final sort, according to times of arrival
        std::sort (index.begin(), index.end(), [&times] (int &i, int &j) {return times[i] < times[j];});

        return {index, times};
    }

    int main() {
        int n_events = 4;
        std::vector<Triple> triples = {
            {1, 2, 5},
            {1, 3, 1},
            {3, 2, 6},
            {3, 4, 1}
        };

        std::vector<int> index(n_events);
        std::vector<int> times(n_events);
        std::tie (index, times) = ordering (n_events, triples);

        print_result (index, times);
    }

Результат:

1 0
3 1
4 2
2 7
2 голосов
/ 22 февраля 2020

Это может быть сопоставлено с Простая временная сеть , где литература богата, например:

  • Dechter, Rina, Itay Meiri, and Judea Pearl. "Temporal constraint networks." Artificial intelligence 49.1-3 (1991): 61-95..
  • Planken, Léon Robert. "Algorithms for simple temporal reasoning." (2013). полная диссертация

Как указано в комментариях, кратчайшие пути всех пар могут вычислить минимальную сеть (которая также генерирует новые дуги / ограничения между всеми этими событиями ). Если ваш график разреженный , Алгоритм Джонсона лучше, чем Флойд-Варшалл.

Если вас не волнует полная минимальная сеть, а только границ ваших событий, вас интересует только первый столбец и первый ряд матрицы расстояний кратчайших путей для всех пар. Вы можете рассчитать эти значения, применяя Bellman-Ford * 2 * n * раз. Эти значения являются расстояниями root -> i и i -> root, где root - это время 0.

Просто некоторые замечания о вещах, которые указал Дэмиен (рассуждая с нуля, кажется: впечатляет):

  • мы используем отрицательные веса в общей задаче, так что чистый Дейкстра не будет
  • существование отрицательного цикла <-> неосуществимость / нет решения / противоречиво
  • потребуется некоторая root вершина, которая является источником времени

Редактировать: выше несколько целей сильный вывод / распространение как предоставление жестких границ в отношении их доменов значений .

Если вы заинтересованы только в некотором согласованном решении , возможно, есть еще одна идея - просто опубликовать эти ограничения как linear-program и использовать одну из высокооптимизированных реализаций для ее решения (с открытым исходным кодом) мир: CoinOR clp; возможно гугл глоп). Симплексные решения должны дать вам целостное решение (я думаю, что проблема абсолютно унимодулярна ). Решатели, основанные на внутренних точках, должны быть быстрее, но я не уверен, что ваш результат будет целым без какой-либо дополнительной необходимости для кроссовера . (может быть хорошей идеей добавить фиктивную цель, например min(max(x)) (по типу makepan))

...