Блог пользователя Yukuk

Автор Yukuk, 3 года назад, По-русски

Введение

Недавно обнаружил, что на Codeforces нет блога с объяснением такого интересного алгоритма, как 1-K BFS. Поэтому я решил написать этот блог для тех, кто, так же как и я, любит читать туториалы именно здесь (как минимум тут есть комментарии, где можно предложить задачи или задать вопрос). Я постараюсь писать максимально понятно, но это мой первый опыт написания текста такого плана, так что не судите строго.

Алгоритм

Для тех, кто знаком с BFS (если не знакомы, рекомендую перед продолжением чтения познакомиться), я прикрепляю картинку. 1-K BFS тоже ищет кратчайшие пути и отличается от BFS лишь тем, что мы заводим массив очередей, а не одну. Для разъяснения рассмотрим следующую задачу: Дан граф на N вершинах и M рёбрах, на каждом ребре записана ненулевая цифра. Каждому пути сопоставим число, образованное цифрами на рёбрах. Необходимо для каждой вершины, кроме первой, найти минимальную сумму цифр по числам, соответствующим путям от первой до неё. N <= 10^7, M <= 5 * 10^7.

Перефразируем условие — нужно просто найти кратчайший путь от первой вершины до любой другой. Но хитрый автор задачи подобрал такие ограничения, что алгоритм Дейкстры тут не поможет. Можно заметить очень маленькое ограничение на веса рёбер — они лежат на отрезке от 1 до 9. "1-K" в названии нашего алгоритма как раз означает то, что веса рёбер лежат на отрезке от 1 до K, в нашем случае K = 9.

Сначала заведём массив из X очередей, где X — магическая величина, о которой я скажу чуть ниже, и положим стартовую вершину в нулевую очередь. Кроме того, заведем массив d[v], где мы будем хранить ответ (для начальной вершины ответ — ноль), а также массив used[v] — анализировали ли мы вершину v в алгоритме. По сути, алгоритм работает так: пусть мы смотрим на вершину u и, как и в BFS, уже знаем корректное расстояние до неё. Теперь каждую вершину w, в которую есть ребро из u, добавим в очередь под номером d[u] + <вес ребра>. Так как ребра неотрицательные, после этого расстояние до u никогда не улучшиться. Необходимо поддерживать номер очереди, на которую мы смотрим, пусть этот номер лежит в переменной pos. Для следующей итерации увеличим pos до первой непустой очереди.

Может показаться, что так как ответы могут принимать значения порядка (N — 1) * K, то и X надо взять большим. На самом же деле, достаточно взять X равным всего лишь K + 1. Почему можно так сделать? Заметим, что в каждый момент времени нам нужна только K + 1 очередь, так как мы не можем класть вершины в очереди с номерами, большими pos более чем на K. Поэтому остается дописать % (K + 1) в обращении к индексу очереди, и алгоритм готов!

Естественно, код для лучшего понимания:

        bfs[0].push(start);
	d[start] = 0;
	int pos = 0, kol = 1; // удобно ввести переменную kol - количество вершин, которые лежат в очередях
	while (kol > 0)
	{
		while (bfs[pos % (k + 1)].empty())
		{
			++pos;
		}
		int u = bfs[pos % (k + 1)].front(); 
		bfs[pos % (k + 1)].pop();
		--kol;
		if (used[u]) // уже использованная вершина может лежать в других очередях
		{
			continue;
		}
		used[u] = true;
		for (pair<int, int> edge : g[u])
		{
			int cost = edge.first, w = edge.second;
			if (d[u] + cost < d[w])
			{
				d[w] = d[u] + cost;
				bfs[d[w] % (k + 1)].push(w);
				++kol;
			}
		}
	}

Нетрудно понять, что асимптотика такого алгоритма O(N*K + M), так как после каждой из N вершин мы можем увеличить pos максимум K раз, а также мы смотрим на каждое из M ребер. Понятно также, как делать восстановление ответа — запоминаем предков при обновлении ответа.

На этом у меня всё, надеюсь, вам понравилось!

  • Проголосовать: нравится
  • +229
  • Проголосовать: не нравится

»
3 года назад, # |
  Проголосовать: нравится +82 Проголосовать: не нравится

I believe this is also known as Dial's algorithm. There is a mention of it in the cp-algorithms article on 0-1 bfs, however I don't think there was a good tutorial on it anywhere until this blog.

»
3 года назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

The author says "but restrictions do not allow us to use Dijkstras". Why can't we use Dijkstra to solve this ? Can someone explain ? What restrictions are making Dijkstras inapplicable here ?

»
3 года назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Sorry, if the question might be too obvious but why aren't we initializing pos to 0 after the while loop. Isn't it possible that for some vertex d[w]=d[u]+cost(w's value) will lead to some weight whose modulo (k+1) is less than it's current value(u's value)?

  • »
    »
    3 года назад, # ^ |
    Rev. 3   Проголосовать: нравится 0 Проголосовать: не нравится

    You don't need to write modulo when you count d[i] value for any i. It is needed only not to hold N*K queues in the vector. And yes, you don't need to set pos = 0 after the while loop.

»
3 года назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Cool algorithm. Another nice trick for graphs with small non-negative integer edge weights is given here. We can generalize the weights to $$$K$$$ instead of $$$2$$$.

Edges with weights $$$0$$$, we can just join the two vertices into one. Then we perform normal BFS. Time complexity is the same as the above algorithm but it's probably more implementation.

»
3 года назад, # |
Rev. 2   Проголосовать: нравится +6 Проголосовать: не нравится

Can someone explain the last part as to why array size of k + 1 will suffice. As to what I understood is that the algorithm is just like dijkstra, but here finding the vertex with lowest distance takes O(k) time, which makes it faster and also possible for low values of k.

  • »
    »
    3 года назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Imagine that you haven't analyzed vertexes u, v such that d[u] is minimal and d[v] is maximal from possible. You can notice that d[v] — d[u] <= k. If not, we had to push v to queue when we were analyzing some vertex w such that d[w] > d[u] — contradiction (because in this case we've already deleted u from queue). Because of that, we need only k + 1 queues.

»
3 года назад, # |
  Проголосовать: нравится +16 Проголосовать: не нравится

If you want to check an alternative implementation that also works for $$$0$$$-weighted edges (edge costs in range $$$[0..lim)$$$) and is quite compact, this is what I've written for our ICPC notebook. Complexity is $$$O(V + E + maxd)$$$ (bounded by $$$O(V \cdot lim + E)$$$).

template<typename Graph>
vector<int> SmallDijkstra(Graph& graph, int src, int lim) {
  vector<vector<int>> qs(lim);
  vector<int> dist(graph.size(), -1);

  dist[src] = 0; qs[0].push_back(src);
  for (int d = 0, maxd = 0; d <= maxd; ++d) {
    for (auto& q = qs[d % lim]; q.size(); ) {
      int node = q.back(); q.pop_back();
      if (dist[node] != d) continue;
      for (auto [vec, cost] : graph[node]) {
        if (dist[vec] != -1 && dist[vec] <= d + cost) continue;
        dist[vec] = d + cost;
        qs[(d + cost) % lim].push_back(vec);
        maxd = max(maxd, d + cost);
      }
    }
  }
  return dist;
}
»
3 года назад, # |
  Проголосовать: нравится +11 Проголосовать: не нравится

Can someone provide links to questions based on this algorithm?

Thanks

»
2 года назад, # |
Rev. 3   Проголосовать: нравится +1 Проголосовать: не нравится

Thank you for such a great blog. I have two doubts.
Doubt-1:
Can anyone explain why time complexity N*K + M and not N*(K + M)? As the last for loop is in while loop hence I think it should be N*(K + M) not N*K + M.
Doubt-2:
Also, I think time complexity should be something like N*K + H, where H denotes total relaxations.
Please correct if I am wrong.

  • »
    »
    2 года назад, # ^ |
    Rev. 3   Проголосовать: нравится 0 Проголосовать: не нравится

    The inner relaxation loop will run for a vertex only if $$$used[vertex] = false$$$.

    This means that in the worst case, the relaxation loop will run for all the vertices.

    This has a time complexity of $$$\mathcal{O}(E)$$$. Since, it's equivalent to

    $$$\sum_{v\in V} {deg[v]} = 2 * E$$$

    Another way to think it is that each edge will be used at most twice in relaxation (Let $E = (u, v)$, then once while relaxing neighbors of $$$u$$$ and once again for relaxing neighbors of $$$v$$$).

»
6 месяцев назад, # |
Rev. 3   Проголосовать: нравится -11 Проголосовать: не нравится
if (used[u]) // used vertex can be in other queues
{
	continue;
}
used[u] = true;

This part seems to be wrong, even if the vertex u is used in some other queue, its distance from the other vertex can be minimised just like we consider it in normal dijkstra algorithm.

Counter Case : now if we find the distance from node 1 to 4 using the given algorithm then distance from node 1 to 4 will be 4 (1-2-4) but the optimal answer is 3 (1-3-4)

graph

»
3 месяца назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Can you use the algorithm with edges weights zero?(sorry for my bad english)