Я столкнулся с этой проблемой.
Учитывая взвешенное дерево T, найдите минимальное количество ребер, чтобы сформировать простой путь (без повторяющихся вершин или ребер) веса (суммы весов ребер) ровно L.
Более подробная информация:
L задается в качестве входных данных и может быть разной для каждого случая. В дереве N вершин, пронумерованных от 0 до N-1.
Моей первой мыслью было то, что лучшее, что я могу сделать, — это просмотреть все пути N^2 в T. Вот работоспособный код с примером входных данных.
import java.util.*;
class Edge {
int toVertex, weight;
Edge(int v, int w) {
toVertex = v; weight = w;
}
}
class Solver {
// method called with the tree T given as adjacency list and the path length L to achieve
// method to return minimum edges to create path of length L or -1 if impossible
public static int solve(List<List<Edge>> T, long L) {
int min = (int) 1e9;
for (int i = 0; i < T.size(); i++) {
min = Math.min(min, test(T, L, i, -1, 0, 0));
}
if (min == (int) 1e9) {
return -1;
} else {
return min;
}
}
static int test(List<List<Edge>> T, long L, int vertex, int parent, long length, int edges) {
if (length == L) {
return edges;
} else if (length < L) {
int min = (int) 1e9;
for (Edge edge : T.get(vertex)) {
if (edge.toVertex != parent) {
min = Math.min(min, test(T, L, edge.toVertex, vertex, length + edge.weight, edges + 1));
}
}
return min;
} else {
return (int) 1e9; // overshoot
}
}
}
// provided code
public class Main {
static void putEdge(List<List<Edge>> T, int vertex1, int vertex2, int weight) {
T.get(vertex1).add(new Edge(vertex2, weight));
T.get(vertex2).add(new Edge(vertex1, weight));
}
public static void main(String[] args) {
// example input
List<List<Edge>> T = new ArrayList<List<Edge>>();
int N = 8;
for (int i = 0; i < N; i++) T.add(new ArrayList<Edge>());
putEdge(T, 0, 1, 2);
putEdge(T, 1, 2, 1);
putEdge(T, 1, 3, 2);
putEdge(T, 2, 6, 1);
putEdge(T, 6, 7, 1);
putEdge(T, 3, 4, 1);
putEdge(T, 3, 5, 4);
System.out.println(Solver.solve(T, 5L)); // path from 4 to 5 have 2 edges and length 5
}
}
Но это превышает лимит времени, когда N достигает около 10 000. Я также рассматривал бинарный поиск ответа, но проверка возможности конкретного ответа выглядит так же сложно, как и решение исходной задачи.
Есть ли более эффективный способ решить эту проблему, чтобы каким-то образом избежать проверки всех путей?
@btilly Я написал длину как сумму весов, поэтому отредактировал ее. Это точно Л.
Вам нужно только вычислить суммы и длины восходящих путей. Путь, который вы будете искать, на некоторое расстояние поднимается в одну сторону, а затем падает в другую, и его можно сопоставить с помощью поиска по хешу. Если глубина дерева d
, это будет O(d n)
. В случайном дереве d = O(log(n))
, так что это улучшение.
Можете ли вы также добавить образец тестового примера?
@RohanSharma В основном методе есть пример ввода (прокрутите код вниз).
@btilly это интересно. не уверен, что тестовые случаи являются случайными, хотя
Что это за дерево? Это N-арное или двоичное дерево?
@RohanSharma не двоичный. из каждой вершины может выходить любое количество ребер. никаких ограничений на это
Рассмотрим следующий алгоритм:
# Walk the tree in every node from Root node *Recursively (parent-weight=0):
1 Sort the edges by Weight Descending
2 Iterate the sorted edges in the node.
2.1 Calculate the branch-weight: node parent-weight + edge weight
2.2 IF branch-weight = L THEN Bingo! :)
ELSE if branch-weight > L THEN Skip edge
ELSE Walk the egde's end node *Recursively (parent-weight = branch-weight).
Я считаю, что это вполне оптимальный алгоритм.
Я не понимаю, как это позволит найти пути, которые не включают корень или которые включают корень, но идут вниз по двум ветвям.
Ах, хорошо, я использовал другое распространенное определение «пути» как путь от корня...
путь — это путь в любом месте дерева. также у дерева нет корня, это просто N вершин и N-1 ребер.
Да, по математике.
Есть два основных способа решения этой проблемы; Я опишу более простой метод.
Для начала произвольным образом укорените дерево (вершина 0, как правило, является хорошим выбором, поскольку она всегда существует). Обозначим через dist[x]
сумму весов ребер на пути от корня до x
, а через depth[x]
обозначим количество ребер на этом пути.
Для любых двух различных узлов u
и v
между ними существует один уникальный простой путь, который идет от u
к наименьшему общему предку (LCA) двух узлов, а затем к v
. Мы можем выразить общий вес на этом пути как dist[u] + dist[v] - 2 * dist[LCA(u, v)]
, поскольку ребра от корня до LCA учитываются как в dist[u]
, так и в dist[v]
. Аналогично, количество ребер на пути равно depth[u] + depth[v] - 2 * depth[LCA(u, v)]
.
Далее давайте рассмотрим каждый узел n
как возможный LCA на пути между двумя узлами веса L
. В этом случае оба этих узла должны находиться в поддереве n
(включая его самого). Чтобы вычислить оптимальный ответ в каждом узле, мы будем хранить карту для каждого узла, которая сопоставляет каждому возможному dist[x]
минимальное depth[x]
, которое может достичь этого расстояния для любого x
в поддереве n
.
Чтобы обработать узел n
, мы перебираем каждый дочерний узел c
и сначала вычисляем для него эту карту, а затем объединяем этот результат в карту текущего узла в два этапа. Для каждой пары ключ-значение (d, e) в дочернем поддереве мы проверяем карту текущего узла n
на наличие ключа k
, который удовлетворяет k + d - 2 * dist[n] = L
. Если он существует, мы нашли путь веса L
. Теперь мы можем обновить наш ответ минимумом текущего ответа и суммой количества ребер для двух частей этого пути. После выполнения всех необходимых обновлений ответа с поддеревом c
мы обновляем карту для узла n
картой для c
, сохраняя минимальное количество ребер для всех замеченных на данный момент расстояний (чтобы убедиться, что мы можем найти оптимальные пути из карты при рассмотрении последующих поддеревьев).
Чтобы эффективно обновлять эти карты, мы будем выбирать всегда обновлять карту с большим количеством ключей, используя карту с меньшим количеством ключей. В худшем случае все узлы имеют разные расстояния от корня. Всякий раз, когда расстояние до определенного узла добавляется от одной карты к другой, результирующая карта должна быть как минимум в два раза больше размера старой карты. Размер любой карты не может превышать N
ключей, поэтому каждый элемент можно добавить не более чем на log N
карты. Каждый узел вносит свой вклад в обновления log N
, поэтому временная и пространственная сложность O(N log N)
являются одновременно dist[x]
.
public static int solve(List<List<Edge>> T, long L) {
var minEdgesForDist = Stream.<Map<Long, Integer>>generate(HashMap::new).limit(T.size()).collect(Collectors.toCollection(ArrayList::new));
return new Object() {
// creating new object to define methods inside the context of the solve method
int dfs(int node, int par, int depth, long dist) {
minEdgesForDist.get(node).put(dist, depth); // for node itself
int ret = Integer.MAX_VALUE;
for (var edge : T.get(node))
if (edge.toVertex != par) {
ret = Math.min(ret, dfs(edge.toVertex, node, depth + 1, dist + edge.weight));
if (minEdgesForDist.get(edge.toVertex).size() > minEdgesForDist.get(node).size())
Collections.swap(minEdgesForDist, edge.toVertex, node); // important!
for (var entry : minEdgesForDist.get(edge.toVertex).entrySet()) {
var other = minEdgesForDist.get(node).get(L + 2 * dist - entry.getKey());
if (other != null)
ret = Math.min(ret, entry.getValue() + other - 2 * depth);
}
for (var entry : minEdgesForDist.get(edge.toVertex).entrySet())
minEdgesForDist.get(node).merge(entry.getKey(), entry.getValue(), Math::min);
}
return ret;
}
int getAnswer() {
int ret = dfs(0, 0, 0, 0);
return ret != Integer.MAX_VALUE ? ret : -1;
}
}.getAnswer();
}
очень крутое решение!!
Вы имеете в виду вес
L
? Кратчайший путьL
должен бытьL
длинным, не так ли? И еще, нужно ли точно приземлиться на весL
или хотя быL
? (Второе гораздо проще.)