BMINAIEV_BLOG Telegram 83
XOR Linked Tree

Вернемся к постам про низкоуровневые оптимизации в алгоритмах. Я когда-то прочитал про трюк, который используется очень редко на практике, но он все равно прикольный, так что решил про него рассказать.

Допустим вам дали дерево (связный неориентированный граф без циклов) в виде списка ребер, и вам нужно для каждой вершины посчитать расстояние от нее до корня. Как это сделать наиболее эффективно?

Понятно, что нужно просто запустить bfs/dfs из корня и сохранить расстояние до каждой вершины. Во время bfs нужно уметь обходить всех соседей текущей вершины. Для этого придется исходный список ребер превратить в N списков (для каждой вершины список ее соседей). Обычно N списков хранятся как Vec<Vec<usize>>, а это значит N отдельных аллокаций в heap, каждая из которых в среднем маленького размера, а такие обращения к памяти работают долго. На моем компьютере на каком-то дереве из миллиона вершин такой код работает 154мс.

Простой способ это ускорить — не делать N отдельных аллокаций, а сложить все ребра в один массив, а для каждой вершины запомнить какой отрезок массива соотвествует ребрам из этой вершины. Это работает значительно быстрее, условные 49мс на моем компьютере.

А теперь расскажу алгоритм, который работает 34мс. Для каждой вершины сохраним два числа — количество ее соседей, а также XOR всех ее соседей. Вместо того, чтобы запускать bfs, вначале найдем топологическую сортировку дерева. Будем постепенно удалять листья из дерева, пока не останется только корень. Чтобы узнать, что вершина лист — нужно проверить, что у нее ровно один сосед (и мы знаем какой, он записан в XOR). Когда удаляем лист, нужно обновить XOR и количество соседей у его единственного соседа. После того как все вершины удалены, можно пройтись по ним в обратном порядке и посчитать расстояния до корня.

Код получается примерно такой:
fn get_heights_xor_tree(edges: &[(usize, usize)]) -> Vec<usize> {
let n = edges.len() + 1;
let mut cnt_neighbours = vec![0; n];
let mut xor_neighbours = vec![0; n];
for &(u, v) in edges {
cnt_neighbours[u] += 1;
cnt_neighbours[v] += 1;
xor_neighbours[u] ^= v;
xor_neighbours[v] ^= u;
}
let mut queue = Vec::with_capacity(n);
let mut parent = vec![n; n];
parent[0] = 0;
for mut v in 0..n {
while cnt_neighbours[v] == 1 && parent[v] == n {
queue.push(v);
let p = xor_neighbours[v];
parent[v] = p;
cnt_neighbours[p] -= 1;
xor_neighbours[p] ^= v;
v = p;
}
}
assert_eq!(queue.len(), n - 1);
let mut h = vec![0; n];
for &v in queue.iter().rev() {
h[v] = h[parent[v]] + 1;
}
h
}



tgoop.com/bminaiev_blog/83
Create:
Last Update:

XOR Linked Tree

Вернемся к постам про низкоуровневые оптимизации в алгоритмах. Я когда-то прочитал про трюк, который используется очень редко на практике, но он все равно прикольный, так что решил про него рассказать.

Допустим вам дали дерево (связный неориентированный граф без циклов) в виде списка ребер, и вам нужно для каждой вершины посчитать расстояние от нее до корня. Как это сделать наиболее эффективно?

Понятно, что нужно просто запустить bfs/dfs из корня и сохранить расстояние до каждой вершины. Во время bfs нужно уметь обходить всех соседей текущей вершины. Для этого придется исходный список ребер превратить в N списков (для каждой вершины список ее соседей). Обычно N списков хранятся как Vec<Vec<usize>>, а это значит N отдельных аллокаций в heap, каждая из которых в среднем маленького размера, а такие обращения к памяти работают долго. На моем компьютере на каком-то дереве из миллиона вершин такой код работает 154мс.

Простой способ это ускорить — не делать N отдельных аллокаций, а сложить все ребра в один массив, а для каждой вершины запомнить какой отрезок массива соотвествует ребрам из этой вершины. Это работает значительно быстрее, условные 49мс на моем компьютере.

А теперь расскажу алгоритм, который работает 34мс. Для каждой вершины сохраним два числа — количество ее соседей, а также XOR всех ее соседей. Вместо того, чтобы запускать bfs, вначале найдем топологическую сортировку дерева. Будем постепенно удалять листья из дерева, пока не останется только корень. Чтобы узнать, что вершина лист — нужно проверить, что у нее ровно один сосед (и мы знаем какой, он записан в XOR). Когда удаляем лист, нужно обновить XOR и количество соседей у его единственного соседа. После того как все вершины удалены, можно пройтись по ним в обратном порядке и посчитать расстояния до корня.

Код получается примерно такой:

fn get_heights_xor_tree(edges: &[(usize, usize)]) -> Vec<usize> {
let n = edges.len() + 1;
let mut cnt_neighbours = vec![0; n];
let mut xor_neighbours = vec![0; n];
for &(u, v) in edges {
cnt_neighbours[u] += 1;
cnt_neighbours[v] += 1;
xor_neighbours[u] ^= v;
xor_neighbours[v] ^= u;
}
let mut queue = Vec::with_capacity(n);
let mut parent = vec![n; n];
parent[0] = 0;
for mut v in 0..n {
while cnt_neighbours[v] == 1 && parent[v] == n {
queue.push(v);
let p = xor_neighbours[v];
parent[v] = p;
cnt_neighbours[p] -= 1;
xor_neighbours[p] ^= v;
v = p;
}
}
assert_eq!(queue.len(), n - 1);
let mut h = vec![0; n];
for &v in queue.iter().rev() {
h[v] = h[parent[v]] + 1;
}
h
}

BY Боря программирует


Share with your friend now:
tgoop.com/bminaiev_blog/83

View MORE
Open in Telegram


Telegram News

Date: |

Deputy District Judge Peter Hui sentenced computer technician Ng Man-ho on Thursday, a month after the 27-year-old, who ran a Telegram group called SUCK Channel, was found guilty of seven charges of conspiring to incite others to commit illegal acts during the 2019 extradition bill protests and subsequent months. Today, we will address Telegram channels and how to use them for maximum benefit. Ng Man-ho, a 27-year-old computer technician, was convicted last month of seven counts of incitement charges after he made use of the 100,000-member Chinese-language channel that he runs and manages to post "seditious messages," which had been shut down since August 2020. In the “Bear Market Screaming Therapy Group” on Telegram, members are only allowed to post voice notes of themselves screaming. Anything else will result in an instant ban from the group, which currently has about 75 members. Private channels are only accessible to subscribers and don’t appear in public searches. To join a private channel, you need to receive a link from the owner (administrator). A private channel is an excellent solution for companies and teams. You can also use this type of channel to write down personal notes, reflections, etc. By the way, you can make your private channel public at any moment.
from us


Telegram Боря программирует
FROM American