posted 10 months ago
I was unable to come up with a clever solution, even after very long thinking, so I decided to use brute force. That sounds horrible with runtimes of O(2^N), but I think I found a way to ease the pain a little.
My idea was as I described above, albeit that the part of determining the most frequent node in all the generated paths is flawed, since there is no guarantee that the reduction achieved will the smallest one.
To represent a Path in an as cheaply way as possible, I used a List as I learned it in a Scala-course. I called it MuisList, and it has a head and a MuisList as tail. The advantage is that if I have a Path, and there are three more nodes to go to, I then get three new MuisLists that all share the existing MuisList as tail. Each node will be stored only once this way.
Next step is to determine all paths from startnode to endnode. I used plain old DFS for this, and to prevent infinite paths if there are cycles in the Graph, I made sure that a node can only appear once in a path. The method is called pathsFromTo.
Having this List of paths, I first determine all the unique nodes in these paths. Then comes the brute-force approach: first I delete all paths that contain the first node. If there are no remaining paths, then removing that node makes sure that there is no path from start to finish, and I store that first node, together with the cost of it, in a resultlist. If there are still paths, then I delete from them all paths that contain the second node, et cetera, until either we run out of nodes, or there are no remaining paths, and in that case I calculate the cost of the removed nodes.
The result is a List of costs and the list of removed nodes that go with that cost. And with that you can determine whatever you like.
That second method is called getCostsOfPaths. It uses recursion and a helper-method for that.
For Pauls example I get:
****************
Paths found:
[1, 5, 4, 3, 6]
[1, 5, 4, 3, 2, 6]
[1, 4, 3, 6]
[1, 4, 3, 2, 6]
****************
NodeRemoval[cost=100, removedNodes=[1]]
NodeRemoval[cost=50, removedNodes=[2, 3]]
NodeRemoval[cost=100, removedNodes=[2, 4]]
NodeRemoval[cost=240, removedNodes=[2, 5, 6]]
NodeRemoval[cost=230, removedNodes=[2, 6]]
NodeRemoval[cost=20, removedNodes=[3]]
NodeRemoval[cost=70, removedNodes=[4]]
NodeRemoval[cost=210, removedNodes=[5, 6]]
NodeRemoval[cost=200, removedNodes=[6]]
BUILD SUCCESSFUL (total time: 0 seconds)
The normal list of subsets from the list (1, 2, 3, 4, 5, 6) would involve 2^6 subsets, and here I only needed to check 9 of them. So, although it is O(2^N), I hope in practice it will be a lot less bad. One optimization would be that if we are creating a list of removals, and the current cost of it is higher that the minimum cost found sofar, then you can skip that path. Here is the full code:
There are three kinds of actuaries: those who can count, and those who can't.