0

I am wondering about an issue that I found here: Finding cheapest combination of items with conditions on the selection I wanted to continue discussing this because I find this very interesting and the solution really cool, but I have issues understanding it completely. So I thought asking this in the comments on the other question might be out of scope.

The question was on how to find the cheapest way to buy an amount n from sellers S that have different prices on item i. For completeness, the table:

Name            Price      Units in storage
Supplier #1     17$        1 Unit
Supplier #2     18$        3 Units
Supplier #3     23$        5 Units

The suggested solution (and correct answer) was to use Dijkstra's algorithm. I would like to discuss the implications of this and the time complexity. First of, I have created a very simple java-loop to determine different prices on different combinations:

public class Graph2 {

    public static class Vendor {
        int itemCount = 0;
        double price = 0.0;

        public Vendor(int itemCount, double price ) {
            this.price = price;
            this.itemCount = itemCount;
        }
    }

    public static class Combo {

        public Combo(int aCount, int bCount, int cCount, double total) {
            this.aCount = aCount;
            this.bCount = bCount;
            this.cCount = cCount;
            this.total = total;
        }

        public int aCount;
        public int bCount;
        public int cCount;

        public double total;

    }

    public static int counter = 0;

    public static void main(String[] args) {

        Vendor a = new Vendor(1, 17.0);
        Vendor b = new Vendor(3, 18.0);
        Vendor c = new Vendor(5, 23.0);

        Map<Integer, List<Combo>> comboMap = new HashMap<>();

        for(int i_a = 0; i_a <= a.itemCount; i_a++) {

            for(int i_b = 0; i_b <= b.itemCount; i_b++) {

                for(int i_c = 0; i_c <= c.itemCount; i_c++) {

                    StringBuffer buf = new StringBuffer();
                    buf.append("A: ").append(i_a).append(" B: ").append(i_b).append(" C: ").append(i_c);

                    int totalCount = i_a + i_b + i_c;

                    List<Combo> combos = comboMap.computeIfAbsent(totalCount, k -> new ArrayList<>());

                    combos.add(new Combo(i_a, i_b, i_c, i_a * a.price + i_b * b.price + i_c * c.price));

                }
            }
        }

        comboMap.entrySet().stream().forEach(e -> {

            Integer totalCount = e.getKey();
            List<Combo> combos = e.getValue();

            combos.forEach( combo -> {
                counter++;
                StringBuffer buffer = new StringBuffer();
                buffer.append("Buying ").append(totalCount).append(" items. ").append("A: ").append(combo.aCount)
                      .append(" B: ").append(combo.bCount).append(" C: ").append(combo.cCount).append(" Costs: ").append(combo.total);

                System.out.println(buffer.toString());
            });

        });


        System.out.println("Combinations: " + counter);
    }
}

This yields something like this:

// omitting the first X lines for they are not that important
Buying 8 items. A: 1 B: 3 C: 4 Costs: 163.0
Buying 9 items. A: 1 B: 3 C: 5 Costs: 186.0
Combinations: 48

So, for this specific problem, iterating through all sellers on their amount of items and calculating what any combination of items costs would take 48 iterations. Or: a * b * c, where a,b and c are the respective item counts of the sellers (48 because I count the 0-results as well)

Now, moving to a graph. I understand that this is a shortest-path problem (or can be modelled as such). This means we can have a graph that represents buy-operations, one at a time, then we can use Dijkstra and determine the shortest path by adding the price on each edge to the total weight.

I struggeled to even construct the graph (apart from manually adding the vertexes and creating the edges which was so tedious that I stopped). But this brought me to this point: Isn't the graph going to have a count of Vertices/Edges that is bigger than a * b * c ?

For example, for only 1 subgraph, I would do this: (I am not doing the complete one, just to demonstrate my thoughts). A, B, C are the sellers. S is the source (no items):

S -> A  // connect to A
A -> B|C // connect A to B or C  
B -> B|C // connect B to B or C 
C -> B|C // connect C to B or C 
... 

That to mean means roughly:

from S -> Amount of sellers (3) 
from each of the sellers node on level 1 -> Amount of sellers (3 * 3)
This continues until the items count goes to zero I believe, at which point it will be *2 

This to me means that the graph will have every single combination of ways to calculate things (which will in turn cause duplication and higher complexity). This is because a -> b -> c == c -> b -> a and so on. But how do I determine this when modelling a graph and/or how do I even approach this? I suppose in my algorithm training I never really had to build a graph, but rather got one as an input that I operated upon.

So, summing up, I wonder:

  • How do I decide to use Dijkstra's algortihm here?
  • How do I translate a table such as above into a graph?
  • Isn't iterating over the table and calculating these combinations easier and more efficient than modelling a graph?
  • Assuming we use Dijkstra, do we have do construct the graph every single time we want to execute or would that require us to store the information in a graph right away?

For completeness, in Java, this is how I tried to model my graph:

    public static void main(String[] args) {
        //      A     17$        1 Unit
        //      B     18$        3 Units
        //      C     23$        5 Units
        Graph g = new Graph();

        // Nodes
        List<Vertex> vertexes = new ArrayList<>();
        vertexes.add(v("source", 0, 0.0));
        vertexes.addAll(IntStream.range(1, 2).mapToObj(i -> v("A_"+ i, i, 17.0)).collect(Collectors.toList()));
        vertexes.addAll(IntStream.range(1, 4).mapToObj(i -> v("B_"+ i, i, 18.0)).collect(Collectors.toList()));
        vertexes.addAll(IntStream.range(1, 6).mapToObj(i -> v("C_"+ i, i, 23.0)).collect(Collectors.toList()));

        // Sort the nodes by level (e.g. level 1 means buying 1 item from the seller) 
        Collections.sort(vertexes, new Comparator<Vertex>() {

            @Override
            public int compare(Vertex o1, Vertex o2) {
                return Integer.compare(o1.level, o2.level);
            }
        });

        // connect the vertexes: 
        for(int i = 0; i< vertexes.size(); i++) {
            Vertex s = vertexes.get(i);
            for(int j = i+1; j < vertexes.size(); j++) {
                Vertex d = vertexes.get(j);

                if(d.level == s.level) continue; // same level can't connect as we can not buy 1 and end up with the same count 

                if(d.level -1 != s.level) break; // level difference > 1 means we would buy 1 and end up with 2 items 

                Edge e = e(s, d, d.price); // Create the edge from n to n+1 so that we can buy this 

                g.edges.add(e);
            }
        }

        g.edges.forEach(System.out::println);
        g.vertexes.addAll(vertexes);

    }

This prints the following graph for me:

<source> to <A_1> Cost 17.0
<source> to <B_1> Cost 18.0
<source> to <C_1> Cost 23.0
<A_1> to <B_2> Cost 18.0
<A_1> to <C_2> Cost 23.0
<B_1> to <B_2> Cost 18.0
<B_1> to <C_2> Cost 23.0
<C_1> to <B_2> Cost 18.0
<C_1> to <C_2> Cost 23.0
<B_2> to <B_3> Cost 18.0
<B_2> to <C_3> Cost 23.0
<C_2> to <B_3> Cost 18.0
<C_2> to <C_3> Cost 23.0
<B_3> to <C_4> Cost 23.0
<C_3> to <C_4> Cost 23.0
<C_4> to <C_5> Cost 23.0

This however is incomplete, it only allows to buy items up to the max amount of items of all sellers. That is why I thought I need a graph that has all combinations of all sellers in any order, so that I can determine any order and their paths.

I hope I managed to write my thoughts/questions down so they make logical sense :) if you find that it is unclear please let me know and I will try and make it a bit clearer.

Community
  • 1
  • 1
pandaadb
  • 6,306
  • 2
  • 22
  • 41
  • IMHO the answer given there doesn't provide any benefit over iterating over every node. – Tymur Gubayev Mar 08 '17 at 19:13
  • This was my thought as well. It was just really confusing as to how the graph would work. And why one would use it in the first place – pandaadb Mar 09 '17 at 13:11

1 Answers1

1

You don't need to create a graph explicitly, you just need to use the greedy principle similar to Dijkstra. In other words, at each buying step, you should buy from the cheapest provider.

You can also formulate this problem as linear programming, although it's not necessary in this case.

THN
  • 3,351
  • 3
  • 26
  • 40
  • This would work for the very basic example, but if I added restrictions such as "buying 3 items gives you a 15% discount" then this would be a bit different. I was interested in if/how/why a graph would be better. I have been thinking about this for days now and the more i think about it, the more I am convinced that the graph would rather be a massive tree of all possible buying combinations, so almost a brute-force approach (if i would model the graph in that way) – pandaadb Mar 09 '17 at 13:14
  • This greedy approach only works for simple cases. With more complex constraints you need more powerful and appropriate approaches. You also need to restrict the complexity of the constraints, as if it were not restricted, the problem's complexity would be unbounded and there might be no reasonable approach to solve it. – THN Mar 09 '17 at 15:18
  • Thanks! With regards to `You don't need to create a graph explicitly, you just need to use the greedy principle similar to Dijkstra.` Would that be something among the lines of having an ordered list of buyers that just orders them by their respective prices so that one could simply step through the list, buy as much as possible and then break once it hits the target amount? – pandaadb Mar 09 '17 at 15:37
  • For the simple example, yes. – THN Mar 09 '17 at 15:54