Итак, на конкурсе программирования на Hackerrank была задача под названием «Ациклический граф», которая в основном сводится к подсчету количества узлов, достижимых из каждого узла в «направленном ациклическом графе». Например, скажем, у вас есть такой график:
[ 1 ] ---->[ 2 ]--->[ 4 ]--->[ 5 ]
[ 3 ] ------/
Счетчик достижимости (включая исходный узел):
Node 1: 4
Node 2: 3
Node 3: 4
Node 4: 2
Node 5: 1
Мой подход был обходом «сначала в глубину» с мемоизацией. Немного осмотрелся, но кажется, что время выполнения нельзя улучшить еще больше из-за пересчета, который происходит в таких случаях:
[ 1 ] ---->[ 2 ]--->[ 4 ]--->[ 5 ]
[ 3 ] ------/--------/
Третий узел будет считать четвертый узел, хотя второй узел уже считал четвертый узел. Что еще хуже, я решаю эти задачи только в JavaScript. Это мой основной язык, и я получаю удовольствие от раздвигания его границ. Никто в таблице лидеров еще не решил ее на JavaScript, но я предполагаю, что это возможно. После конкурса мне удалось пройти 13 тестов из 24 со следующим кодом:
function Solution( graph, nodes ) {
var memory = new Array( nodes + 1 )
, result = 0;
graph.forEach( ( a, v ) => DepthFirstSearch( graph, v, memory ) );
// challenge asks for an output variation, but the accurate
// reachability count of every node will be contained in "d.length".
memory.forEach( ( d, i ) => { if ( i && ( 2 * d.length ) >= nodes ) result++; } );
return result;
}
function DepthFirstSearch( graph, v, memory ) {
if ( memory[ v ] ) return memory[ v ];
var descendants = new Uint16Array( [ v ] );
graph[ v ].forEach( u => {
descendants = MergeTypedArrays(
DepthFirstSearch( graph, u, memory ),
descendants
);
} );
// make elements unique
// to avoid over counting
return memory[ v ] = Uint16Array.from( new Set( descendants ) );
}
function MergeTypedArrays(a, b) {
var c = new a.constructor( a.length + b.length );
c.set( a );
c.set( b, a.length );
return c;
}
// adjacency list
var graph = [
[], // 0
[ 2 ], // 1
[ 4 ], // 2
[ 2 ], // 3
[ 5 ], // 4
[] // 5
];
var nodes = 5;
Solution( graph, nodes );
Он не работает для всех входных данных размером более 50 КБ, предположительно входных данных с большим набором узлов и ребер (т.е. 50 000 узлов и 40 000 ребер). Не сумев определить или придумать более быстрый и эффективный алгоритм памяти, я совершенно не понимаю, что делать дальше. Думал о том, чтобы сделать DFS итеративной, но я думаю, что потребление памяти для запоминания тысяч массивов затмит это, что, по-видимому, является основной проблемой. Я получаю «Abort Called» и «Runtime Error» на Hackerrank для 11 неудачных тестов (в отличие от «Timeout»). Также попробовал «bitSets» с «union», но потребление памяти оказалось хуже, поскольку массивы bitSets должны быть достаточно большими, чтобы хранить числа до 50 000.
Ограничения:
1 ≤ n,m ≤ 5×10^4
1 ≤ a(i),b(i) ≤ n and a(i) ≠ b(i)
It is guaranteed that graph G does not contain cycles.
Просто хочу уточнить, что я не получу очков за прохождение всех тестов, так как этот вызов заблокирован, это в образовательных целях, в основном по оптимизации. Я знаю о связанных сообщениях SO, которые указывают на топологическую сортировку, но, насколько я понимаю, топологическая сортировка по-прежнему будет пересчитываться в случаях, подобных описанному выше, поэтому не является жизнеспособным решением. Если я неправильно понял, пожалуйста, просветите меня. Заранее благодарим вас за ваше время.
Вопрос: Как я могу оптимизировать это дальше? Есть ли более эффективный подход?
count++;
после проверки того, посещали ли вы уже узел - извините за это. 07.03.2016visited[node] = false;
наvisited[node] = true;
. Без этого изменения он все равно пройдет тест, который я представил, но не пройдет циклические случаи. Ознакомьтесь с этим тестовым примером, и вы увидите, что я m говоря (самая первая цифра — это количество узлов, цифра рядом с ней — это количество ребер, строки под этими двумя цифрами — это фактические ребра). Он должен вывестиNode 1: 3, Node 2: 4, Node 3: 2, Node 4: 7, Node 5: 6, Node 6: 8, Node 7: 10, Node 8: 5, Node 9: 1, Node 10: 9
07.03.2016>= |V| / 2
, мне удалось уменьшить input11.txt с ~13м до ~300с. После оптимизации>= |V| / 2
я снизил его до ~99 с. С файлом input02.txt никогда не возникало проблем, он постоянно работал 9 мс на любой версии. Перевернутая итерация ничего не сделала для меня, и я подозреваю, что она не зависит от тестового примера. Получил доступ к редакции (и автору code), не похоже, что они заботятся о>= |V| / 2
. 08.03.2016visited[neighbor] == false && ignored[node] == false
наvisited[neighbor] == false && ignored[neighbor] == false
немного отрубила мне время. 09.03.2016