Algorithm or solution for selecting possible combinations of menu items within a budget

Solution 1:

Here is a solution that uses recursion.

To make things easier, I have defined an Item class that stores the name and the price of an item. The price is expressed in cents to avoid rounding issues. The menu is a list of items.

import java.util.*;

public class Solver
{
    private ArrayList<Item> menu;
    private ArrayList<String[]> solutions;

    public static class Item
    {
        public String name;
        public int price;

        public Item(String name, int price)
        {
            this.name = name;
            this.price = price;
        }
    }

    public Solver(ArrayList<Item> menu)
    {
        this.menu = menu;
    }

    public ArrayList<String[]> solve(int budget)
    {
        solutions = new ArrayList<String[]>();
        solve(new ArrayList<Item>(), 0, budget);
        return solutions;
    }

    private void solve(ArrayList<Item> items, int first, int budget)
    {
        if(budget==0)
        {
            // We have found a solution, store it
            solutions.add(items.stream().map(e -> e.name).toArray(String[]::new));
        }
        else
        {
            // Search for an item that fits in the budget
            for(int i=first;i<menu.size();i++)
            {
                Item item = menu.get(i);
                if(item.price<=budget)
                {
                    items.add(item);
                    solve(items, i, budget-item.price);
                    items.remove(items.size()-1);
                }
            }
        }
    }

    public static void main(String[] args)
    {
        ArrayList<Item> menu = new ArrayList<Item>();
        menu.add(new Item("Fruit", 215));
        menu.add(new Item("Fries", 275));
        menu.add(new Item("Salad", 335));
        menu.add(new Item("Wings", 355));
        menu.add(new Item("Mozzarella", 420));
        menu.add(new Item("Plate", 580));

        Solver solver = new Solver(menu);
        ArrayList<String[]> solutions = solver.solve(550);
        for(int i=0;i<solutions.size();i++)
            System.out.println("Solution "+(i+1)+": "+Arrays.toString(solutions.get(i)));
    }
}

Output:

Solution 1: [Fruit, Salad]
Solution 2: [Fries, Fries]

Solution 2:

This problem is a direct application of the Coin Change problem.

A dynamic programming solution can be constructed recursively as follows:

For each item, the solution is the combination of two cases:

  1. The item is included in the solution
  2. The item is excluded

For the first case, the solution is the results of getBuyableItems(Menu, budget - item.value) While for the second case the solution is getBuyableItems(Menu after removing {item}, budget).

public class Menu {
  List<Pair<String, Integer>> menu = new ArrayList<>();

  public Menu() {
    menu.add(Pair.of("Fruit", 215));
    menu.add(Pair.of("Fries", 275));
    menu.add(Pair.of("Salad", 335));
    menu.add(Pair.of("Wings", 355));
    menu.add(Pair.of("Mozzarella", 420));
    menu.add(Pair.of("Plate", 580));
  }

  public List<List<String>> getListOfBuyableItemsRec(int m, int budget) {
    if (budget == 0) {
      List<List<String>> list = new ArrayList<>();
      list.add(new ArrayList<>());
      return list;
    }
    if (budget < 0)
      return null;
    if (m <= 0 && budget > 0)
      return null;

    List<List<String>> exclude_m = getListOfBuyableItemsRec(m - 1, budget);

    List<List<String>> include_m = getListOfBuyableItemsRec(m, budget - menu.get(m - 1).getValue());

    if (include_m != null) {
      include_m = include_m.stream().map(l -> {
        l.add(menu.get(m - 1).getKey());
        return l;
      }).collect(Collectors.toList());
    } else
      include_m = new ArrayList<>();

    if (exclude_m != null)
      include_m.addAll(exclude_m);

    return include_m;
  }

  public static void main(String[] str) {
    Menu menu1 = new Menu();
    var res = menu1.getListOfBuyableItemsRec(6, 550);
    if (res != null && !res.isEmpty())
      res.stream().forEach(l -> System.out.println(l));
    else
      System.out.println("no solution has been found");
  }
}
Solutions 
[Fruit, Salad]
[Fries, Fries]

However, this solution is not efficient and it may cause a memory issue for large cases. There is another solution that uses a technique called memoization.

For this problem, we can define a table of all possible sub-problems and construct that table gradually until we reach the solution at the final position. Each cell in the table represents a case starting from 0 to the requested budget. Eventually, each cell will hold the solutions for the corresponding sub-problem. For example, table[215] will have one list {"Fruit"}. The advantage of this solution is that we don't need to compute the same sub-problem every time we need it.

A solution for table[j] can be constructed through item i (given j >= i) by grabbing all the solutions in table[j-i] and adding the item i key to those solutions.

public class Menu {
  //initialization code
  public List<List<String>> getListOfBuyableItemsIter(int m, int budget) {
    // table[i] will be storing the solutions for
    // value i. We need budget+1 rows as the table is constructed
    // in bottom up manner using the base case (budget = 0)
    List<List<String>>[] table = new List[budget + 1];

    for (int i = 0; i <= budget; i++)
      table[i] = new ArrayList<>();

    table[0].add(new ArrayList<>());

    // Pick all items one by one and update the table[] values after
    // the index greater than or equal to the value of the picked item
    IntStream.range(0, m).forEach(i -> {
      IntStream.rangeClosed(menu.get(i).getValue(), budget).forEach(j -> {
        List<List<String>> lists = table[j - menu.get(i).getValue()];
        List<List<String>> cloneLists = new ArrayList<>();
        for (var l : lists) {
          List<String> newList = new ArrayList<>(l);
          newList.add(menu.get(i).getKey());
          cloneLists.add(newList);
        }
        table[j].addAll(cloneLists);
      });
    });
    return table[budget];
  }

  public static void main(String[] str) {
    Menu menu1 = new Menu();
    //var res1 = menu1.getListOfBuyableItemsRec(6, 550);
    var res2 = menu1.getListOfBuyableItemsIter(6, 550);
    if (res2 != null && !res2.isEmpty())
      res2.stream().forEach(l -> System.out.println(l));
    else
      System.out.println("no solution has been found");
  }
}
Solutions:
[Fries, Fries]
[Fruit, Salad]