Skip to content

Commit b7b71d2

Browse files
committed
askrene: add a MCF refinement
Add a new function to compute a MCF using a more general description of the problem. I call it mcf_refinement because it can start with a feasible flow (though this is not necessary) and adapt it to achieve optimality. Changelog-None: askrene: add a MCF refinement Signed-off-by: Lagrang3 <[email protected]>
1 parent 3b5fe63 commit b7b71d2

File tree

1 file changed

+302
-41
lines changed

1 file changed

+302
-41
lines changed

plugins/askrene/algorithm.c

Lines changed: 302 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -341,68 +341,329 @@ s64 node_balance(const struct graph *graph,
341341
return balance;
342342
}
343343

344+
/* Helper.
345+
* Compute the reduced cost of an arc. */
346+
static inline s64 reduced_cost(const struct graph *graph, const struct arc arc,
347+
const s64 *cost, const s64 *potential)
348+
{
349+
struct node src = arc_tail(graph, arc);
350+
struct node dst = arc_head(graph, arc);
351+
return cost[arc.idx] - potential[src.idx] + potential[dst.idx];
352+
}
353+
354+
/* Finds an optimal path from the source to the nearest sink node, by definition
355+
* a node i is a sink if node_balance[i]<0. It uses a reduced cost:
356+
* reduced_cost[i,j] = cost[i,j] - potential[i] + potential[j]
357+
*
358+
* */
359+
static struct node dijkstra_nearest_sink(const tal_t *ctx,
360+
const struct graph *graph,
361+
const struct node source,
362+
const s64 *node_balance,
363+
const s64 *capacity,
364+
const s64 cap_threshold,
365+
const s64 *cost,
366+
const s64 *potential,
367+
struct arc *prev,
368+
s64 *distance)
369+
{
370+
struct node target = {.idx = INVALID_INDEX};
371+
tal_t *this_ctx = tal(ctx, tal_t);
372+
373+
if (!this_ctx)
374+
/* bad allocation */
375+
goto finish;
376+
377+
/* check preconditions */
378+
assert(graph);
379+
assert(node_balance);
380+
assert(capacity);
381+
assert(cost);
382+
assert(potential);
383+
assert(prev);
384+
assert(distance);
385+
386+
const size_t max_num_arcs = graph_max_num_arcs(graph);
387+
const size_t max_num_nodes = graph_max_num_nodes(graph);
388+
389+
assert(source.idx < max_num_nodes);
390+
assert(tal_count(node_balance) == max_num_nodes);
391+
assert(tal_count(capacity) == max_num_arcs);
392+
assert(tal_count(cost) == max_num_arcs);
393+
assert(tal_count(potential) == max_num_nodes);
394+
assert(tal_count(prev) == max_num_nodes);
395+
assert(tal_count(distance) == max_num_nodes);
396+
397+
for (size_t i = 0; i < max_num_arcs; i++) {
398+
/* is this arc saturated? */
399+
if (capacity[i] < cap_threshold)
400+
continue;
401+
402+
struct arc arc = {.idx = i};
403+
struct node tail = arc_tail(graph, arc);
404+
struct node head = arc_head(graph, arc);
405+
s64 red_cost =
406+
cost[i] - potential[tail.idx] + potential[head.idx];
407+
408+
/* reducted cost cannot be negative for non saturated arcs,
409+
* otherwise Dijkstra does not work. */
410+
if (red_cost < 0)
411+
goto finish;
412+
}
413+
414+
for (size_t i = 0; i < max_num_nodes; ++i)
415+
prev[i].idx = INVALID_INDEX;
416+
417+
/* Only in debug mode we keep track of visited nodes. */
418+
#ifndef NDEBUG
419+
bitmap *visited =
420+
tal_arrz(this_ctx, bitmap, BITMAP_NWORDS(max_num_nodes));
421+
assert(visited);
422+
#endif
423+
424+
struct priorityqueue *q;
425+
q = priorityqueue_new(this_ctx, max_num_nodes);
426+
const s64 *const dijkstra_distance = priorityqueue_value(q);
427+
428+
priorityqueue_init(q);
429+
priorityqueue_update(q, source.idx, 0);
430+
431+
while (!priorityqueue_empty(q)) {
432+
const u32 idx = priorityqueue_top(q);
433+
const struct node cur = {.idx = idx};
434+
priorityqueue_pop(q);
435+
436+
/* Only in debug mode we keep track of visited nodes. */
437+
#ifndef NDEBUG
438+
assert(!bitmap_test_bit(visited, cur.idx));
439+
bitmap_set_bit(visited, cur.idx);
440+
#endif
441+
442+
if (node_balance[cur.idx] < 0) {
443+
target = cur;
444+
break;
445+
}
446+
447+
for (struct arc arc = node_adjacency_begin(graph, cur);
448+
!node_adjacency_end(arc);
449+
arc = node_adjacency_next(graph, arc)) {
450+
/* check if this arc is traversable */
451+
if (capacity[arc.idx] < cap_threshold)
452+
continue;
453+
454+
const struct node next = arc_head(graph, arc);
455+
456+
const s64 cij = cost[arc.idx] - potential[cur.idx] +
457+
potential[next.idx];
458+
459+
/* Dijkstra only works with non-negative weights */
460+
assert(cij >= 0);
461+
462+
if (dijkstra_distance[next.idx] <=
463+
dijkstra_distance[cur.idx] + cij)
464+
continue;
465+
466+
priorityqueue_update(q, next.idx,
467+
dijkstra_distance[cur.idx] + cij);
468+
prev[next.idx] = arc;
469+
}
470+
}
471+
for (size_t i = 0; i < max_num_nodes; i++)
472+
distance[i] = dijkstra_distance[i];
473+
474+
finish:
475+
tal_free(this_ctx);
476+
return target;
477+
}
478+
479+
/* Problem: find a potential and capacity redistribution such that:
480+
* excess[all nodes] = 0
481+
* capacity[all arcs] >= 0
482+
* cost/potential [i,j] < 0 implies capacity[i,j] = 0
483+
*
484+
* Q. Is this a feasible solution?
485+
*
486+
* A. If we use flow conserving function sendflow, then
487+
* if for all nodes excess[i] = 0 and capacity[i,j] >= 0 for all arcs
488+
* then we have reached a feasible flow.
489+
*
490+
* Q. Is this flow optimal?
491+
*
492+
* A. According to Theorem 9.4 (Ahuja page 309) we have reached an optimal
493+
* solution if we are able to find a potential and flow that satisfy the
494+
* slackness optimality conditions:
495+
*
496+
* if cost_reduced[i,j] > 0 then x[i,j] = 0
497+
* if 0 < x[i,j] < u[i,j] then cost_reduced[i,j] = 0
498+
* if cost_reduced[i,j] < 0 then x[i,j] = u[i,j]
499+
*
500+
* In our representation the slackness optimality conditions are equivalent
501+
* to the following condition in the residual network:
502+
*
503+
* cost_reduced[i,j] < 0 then capacity[i,j] = 0
504+
*
505+
* Therefore yes, the solution is optimal.
506+
*
507+
* Q. Why is this useful?
508+
*
509+
* A. It can be used to compute a MCF from scratch or build an optimal
510+
* solution starting from a non-optimal one, eg. if we first test the
511+
* solution feasibility we already have a solution canditate, we use that
512+
* flow as input to this function, in another example we might have an
513+
* algorithm that changes the cost function at every iteration and we need
514+
* to find the MCF every time.
515+
* */
516+
static bool mcf_refinement(const tal_t *ctx,
517+
const struct graph *graph,
518+
s64 *excess,
519+
s64 *capacity,
520+
const s64 *cost,
521+
s64 *potential)
522+
{
523+
bool solved = false;
524+
tal_t *this_ctx = tal(ctx, tal_t);
525+
526+
if (!this_ctx)
527+
/* bad allocation */
528+
goto finish;
529+
530+
assert(graph);
531+
assert(excess);
532+
assert(capacity);
533+
assert(cost);
534+
assert(potential);
535+
536+
const size_t max_num_arcs = graph_max_num_arcs(graph);
537+
const size_t max_num_nodes = graph_max_num_nodes(graph);
538+
539+
assert(tal_count(excess) == max_num_nodes);
540+
assert(tal_count(capacity) == max_num_arcs);
541+
assert(tal_count(cost) == max_num_arcs);
542+
assert(tal_count(potential) == max_num_nodes);
543+
544+
s64 total_excess = 0;
545+
for (u32 i = 0; i < max_num_nodes; i++)
546+
total_excess += excess[i];
547+
548+
if (total_excess)
549+
/* there is no way to satisfy the constraints if supply does not
550+
* match demand */
551+
goto finish;
552+
553+
/* Enforce the complementary slackness condition, rolls back
554+
* constraints. */
555+
for (u32 arc_id = 0; arc_id < max_num_arcs; arc_id++) {
556+
struct arc arc = {.idx = arc_id};
557+
const s64 r = capacity[arc.idx];
558+
559+
if (reduced_cost(graph, arc, cost, potential) < 0 && r > 0) {
560+
/* This arc's reduced cost is negative and non
561+
* saturated. */
562+
sendflow(graph, arc, r, capacity, excess);
563+
}
564+
}
565+
566+
struct arc *prev = tal_arr(this_ctx, struct arc, max_num_nodes);
567+
s64 *distance = tal_arrz(this_ctx, s64, max_num_nodes);
568+
if (!prev || !distance)
569+
goto finish;
570+
571+
/* Now build back constraints again keeping the complementary slackness
572+
* condition. */
573+
for (u32 node_id = 0; node_id < max_num_nodes; node_id++) {
574+
struct node src = {.idx = node_id};
575+
576+
/* is this node a source */
577+
while (excess[src.idx] > 0) {
578+
579+
/* where is the nearest sink */
580+
struct node dst = dijkstra_nearest_sink(
581+
this_ctx, graph, src, excess, capacity, 1, cost,
582+
potential, prev, distance);
583+
584+
if (dst.idx >= max_num_nodes)
585+
/* we failed to find a reacheable sink */
586+
goto finish;
587+
588+
/* traverse the path and see how much flow we can send
589+
*/
590+
s64 delta = get_augmenting_flow(graph, src, dst,
591+
capacity, prev);
592+
593+
delta = MIN(excess[src.idx], delta);
594+
delta = MIN(-excess[dst.idx], delta);
595+
assert(delta > 0);
596+
597+
/* commit that flow to the path */
598+
augment_flow(graph, src, dst, prev, excess, capacity,
599+
delta);
600+
601+
/* update potentials */
602+
for (u32 n = 0; n < max_num_nodes; n++) {
603+
/* see page 323 of Ahuja-Magnanti-Orlin.
604+
* Whether we prune or not the Dijkstra search,
605+
* the following potentials will keep reduced
606+
* costs non-negative. */
607+
potential[n] -=
608+
MIN(distance[dst.idx], distance[n]);
609+
}
610+
}
611+
}
612+
613+
#ifndef NDEBUG
614+
/* verify that we have satisfied all constraints */
615+
for (u32 i = 0; i < max_num_nodes; i++) {
616+
assert(excess[i] == 0);
617+
}
618+
#endif
619+
620+
finish:
621+
tal_free(this_ctx);
622+
return solved;
623+
}
344624

345625
bool simple_mcf(const tal_t *ctx, const struct graph *graph,
346626
const struct node source, const struct node destination,
347627
s64 *capacity, s64 amount, const s64 *cost)
348628
{
349629
tal_t *this_ctx = tal(ctx, tal_t);
630+
if (!this_ctx)
631+
/* bad allocation */
632+
goto fail;
633+
634+
if (!graph)
635+
goto fail;
636+
350637
const size_t max_num_arcs = graph_max_num_arcs(graph);
351638
const size_t max_num_nodes = graph_max_num_nodes(graph);
352-
s64 remaining_amount = amount;
353-
354-
if (amount < 0)
355-
goto finish;
356639

357-
if (!graph || source.idx >= max_num_nodes ||
640+
if (amount < 0 || source.idx >= max_num_nodes ||
358641
destination.idx >= max_num_nodes || !capacity || !cost)
359-
goto finish;
642+
goto fail;
360643

361644
if (tal_count(capacity) != max_num_arcs ||
362645
tal_count(cost) != max_num_arcs)
363-
goto finish;
646+
goto fail;
364647

365-
struct arc *prev = tal_arr(this_ctx, struct arc, max_num_nodes);
366-
s64 *distance = tal_arrz(this_ctx, s64, max_num_nodes);
367648
s64 *potential = tal_arrz(this_ctx, s64, max_num_nodes);
649+
s64 *excess = tal_arrz(this_ctx, s64, max_num_nodes);
368650

369-
if (!prev || !distance || !potential)
370-
goto finish;
651+
if (!potential || !excess)
652+
/* bad allocation */
653+
goto fail;
371654

372-
/* FIXME: implement this algorithm as a search for matching negative and
373-
* positive balance nodes, so that we can use it to adapt a flow
374-
* structure for changes in the cost function. */
375-
while (remaining_amount > 0) {
376-
if (!dijkstra_path(this_ctx, graph, source, destination,
377-
/* prune = */ true, capacity, 1, cost,
378-
potential, prev, distance))
379-
goto finish;
655+
excess[source.idx] = amount;
656+
excess[destination.idx] = -amount;
380657

381-
/* traverse the path and see how much flow we can send */
382-
s64 delta = get_augmenting_flow(graph, source, destination,
383-
capacity, prev);
658+
if (!mcf_refinement(this_ctx, graph, excess, capacity, cost, potential))
659+
goto fail;
384660

385-
/* commit that flow to the path */
386-
delta = MIN(remaining_amount, delta);
387-
assert(delta > 0 && delta <= remaining_amount);
661+
tal_free(this_ctx);
662+
return true;
388663

389-
augment_flow(graph, source, destination, prev, NULL, capacity,
390-
delta);
391-
remaining_amount -= delta;
392-
393-
/* update potentials */
394-
for (u32 n = 0; n < max_num_nodes; n++) {
395-
/* see page 323 of Ahuja-Magnanti-Orlin.
396-
* Whether we prune or not the Dijkstra search, the
397-
* following potentials will keep reduced costs
398-
* non-negative. */
399-
potential[n] -=
400-
MIN(distance[destination.idx], distance[n]);
401-
}
402-
}
403-
finish:
664+
fail:
404665
tal_free(this_ctx);
405-
return remaining_amount == 0;
666+
return false;
406667
}
407668

408669
s64 flow_cost(const struct graph *graph, const s64 *capacity, const s64 *cost)

0 commit comments

Comments
 (0)