Search code examples
c++algorithmgraphgreedygraph-traversal

Acyclic undirected graph allocation problem


We have an allocation problem: each node is a fuel source, and every edge contains a number of evenly spaced out lamps (think of them as being 1 m apart from each other). Each fuel source can power lamps that are on the immediate edges around it (it cannot fuel lamps through other nodes). A fuel source also has a radius in which it fuels lamps around it - depending on the radius (in meters) we can calculate the amount of fuel a give fuel source provides - example: a fuel source with a radius of 2 can fuel all the lamps within its radius and, in total, uses up 2 liters of fuel (fuel usage is a function of the radius).

Note that two nodes have only one path between each other (meaning that that the number of edges is equal to the number of nodes - 1).

The goal is to calculate the optimal radii of the fuel sources so that we minimize the fuel usage while fueling all the lamps.

Here is an example graph

The solution to this graph looks like this. The red ellipsoids are supposed to visualize the radii of the fuel sources, and below is a table of the relevant values:

Node, fuel source Radius, fuel consumption
0 1
1 2
2 0
3 2
4 0
5 0

After we add up all the fuel amounts, we get the result: 5.

The input for the above task looks like this:

6     // 6 nodes (numVertices)
0 1 3 // Node 0 with an edge containing 3 nodes going to node 3 (src dest lights)
1 2 1 // ...
2 3 2
1 4 2
1 5 2

So far I've tried loading up my edges like this (note that this solution is quite cursed, especially with my use of pointers):

struct light {
    int count;
};

struct node {
    int d;
    light* l;
};

std::vector<node*>* tree;
int numVertices;

// Do this for all values in the input
void AddEdge(int src, int dest, int lights) {
        light* l = new light{ lights };
        tree[src].push_back(new node{ dest, l });
        tree[dest].push_back(new node{ src, l });
}

And then I solved the graph by using a greedy algorithm to 'remove' as many lamps per step as possible:

void Solve() {
    int fuel = 0;

    while (true) {
        int maxNode = 0;
        int maxNodeLights = 0;

        for (int A = 0; A < numVertices; A++)
        {
            int lightsOnNode = 0;

            for (node* B : tree[A])
            {
                lightsOnNode += B->l->count;
            }

            if (lightsOnNode > maxNodeLights) {
                maxNodeLights = lightsOnNode;
                maxNode = A;
            }
        }

        if (maxNodeLights > 0) {
            bool addedRange = false;

            for (node* B : tree[maxNode])
            {
                if (B->l->count > 0) {
                    B->l->count--;
                    addedRange = true;
                }
            }

            if (addedRange) {
                fuel++;
            }
        }
        else {
            break;
        }
    }

    std::cout << fuel << '\n';
}

If we were to parse the input string from the example case it would look like this:

numVertices = 6;

AddEdge(0, 1, 3);
AddEdge(1, 2, 1);
AddEdge(2, 3, 2);
AddEdge(1, 4, 2);
AddEdge(1, 5, 2);

Solve();

This works for simple graphs like the one above, but it fails by a small margin once a more complex graph is introduced, since the greedy algorithm doesn't look ahead if there is a better option a couple steps in the future.

A long test case can be found here. The fuel amount consumed by this graph is 77481.

New, failing test cases:

1 0 4
2 1 1
3 2 4
4 3 1
5 1 2
6 1 1
7 2 2
8 3 2
9 1 1
10 1 3
11 5 1
12 0 2
13 10 4
14 3 3
15 5 4

(Output: 16. Correct output: 17)

1 0 2
2 1 3
3 2 2
4 1 4
5 4 3
6 3 2
7 5 3
8 3 4

(Output: 10. Correct output: 11)

1 0 4
2 0 3
3 0 4
4 3 3
5 2 2
6 3 1
7 2 1
8 3 2
9 3 2
10 2 1
11 9 1
12 4 2
13 5 2
14 8 2
15 9 1
16 14 2
17 3 3
18 3 4

(Output: 15. Correct output: 16)


Solution

  • Algorithm pseudocode:

    • WHILE lamps remain unfueled
      • LOOP over sources
        • IF source has exactly one edge with unfueled lamps
          • SET fuel source to source at other end of unfueled edge
          • INCREMENT radius of fuel source with unfueled edge lamp count
          • LOOP over edges on fuel source
            • IF edge fueled lamp count from fuel sourve < fuel source radius
              • SET edge fueled lamp count from fuel source to fuel source radius

    C++ code for this is at https://github.com/JamesBremner/LampLighter.

    Sample output

    Input
    0 1 1
    1 2 1
    2 3 1
    3 4 1
    0 5 1
    5 6 1
    6 7 1
    7 8 1
    0 9 1
    9 10 1
    10 11 1
    11 12 1
    
    Source radii
    0 r=0
    1 r=1
    2 r=0
    3 r=1
    4 r=0
    5 r=1
    6 r=0
    7 r=1
    8 r=0
    9 r=1
    10 r=0
    11 r=1
    12 r=0
    
    Total fuel 6
    

    Handling different numbers of lamps on each edge

    0 1 3
    1 2 1
    0 5 3
    5 6 1
    
    Source radius
    0 r=2
    1 r=1
    2 r=0
    5 r=1
    6 r=0
    
    Total fuel 4
    

    Another test

    lamp ..\test\data11_19_2.txt
    

    Output:

    1 0 2
    2 1 3
    3 2 2
    4 1 4
    5 4 3
    6 3 2
    7 5 3
    8 3 4
    
    Source radius
    1 r=3
    0 r=0
    2 r=0
    3 r=4
    4 r=0
    5 r=3
    6 r=0
    7 r=0
    8 r=0
    
    Total fuel 10
    

    Acknowledgement: This algorithm is based on an insight from MarkB who suggested that fueling should begin at leaf nodes and work "downwards"