2

I have a list of objects with each Item having a cost and a set of resources associated with it (see below). I'm looking for a way to select a subset from this list based on the combined cost and each resource must be contained at most once (not every resource has to be included though). The way the subset's combined cost is calculated should be exchangeable (e.g. max, min, avg). If two subsets have the same combined cost the subset with more items is selected.

 Item  |  cost   resources [1..3]
 ================================
  P1   |  0.5          B
  P2   |   4         A B C
  P3   |  1.5        A B
  P4   |   2             C
  P5   |   2         A

This would allow for these combinations:

 Variant  |   Items    sum
 ==========================
    V1    |  P1 P4 P5  4.5
    V2    |     P2      4
    V3    |   P3 P4    3.5

For a maximum selection V1 would be selected. The number of items can span from anywhere between 1 and a few dozen, the same is true for the number of resources.

My brute force approach would just sum up the cost of all possible permutations and select the max/min one, but I assume there is a much more efficient way to do this. I'm coding in Java 8 but I'm fine with pseudocode or Matlab.

I found some questions which appeared to be similar (i.e. (1), (2), (3)) but I couldn't quite transfer them to my problem, so forgive me if you think this is a duplicate :/

Thanks in advance! ~

Clarification

A friend of mine was confused about what kinds of sets I want. No matter how I select my subset in the end, I always want to generate subsets with as many items in them as possible. If I have added P3 to my subset and can add P4 without creating a conflict (that is, a resource is used twice within the subset) then I want P3+P4, not just P3.

Clarification2

"Variants don't have to contain all resources" means that if it's impossible to add an item to fill in a missing resource slot without creating a conflict (because all items with the missing resource also have another resource already present) then the subset is complete.

Community
  • 1
  • 1
Managarm
  • 1,070
  • 3
  • 12
  • 25
  • could you be exact about few dozen , perhaps an upper bound ? – advocateofnone Mar 07 '15 at 16:14
  • I don't expect there to be more than 100. – Managarm Mar 07 '15 at 16:38
  • This problem is NP-Hard, even without the "Resources" factor, you are dealing with the [knapsack-problem](http://en.wikipedia.org/wiki/Knapsack_problem). – amit Mar 07 '15 at 17:06
  • I thought so, but I'm fine with a "good guesstimate" as long as it's more efficient than brute force :) I was hoping that the resources would make this a bit easier due to the additional combinatory restrictions. – Managarm Mar 07 '15 at 17:10

2 Answers2

0

This problem is NP-Hard, even without the "Resources" factor, you are dealing with the knapsack-problem.

If you can transform your costs to relatively small integers, you may be able to modify the Dynamic Programming solution of Knapsack by adding one more dimension per resource allocated, and have a formula similar to (showing concept, make sure all edge cases work or modify if needed):

D(_,_,2,_,_) = D(_,_,_,2,_) = D(_,_,_,_,2) = -Infinity
D(x,_,_,_,_) = -Infinity   x < 0
D(x,0,_,_,_) = 0 //this stop clause is "weaker" than above stop clauses - it can applies only if above don't.
D(x,i,r1,r2,r3) =  max{1+ D(x-cost[i],i-1,r1+res1[i],r2+res2[i],r3+res3[i]) , D(x,i-1,r1,r2,r3)} 

Where cost is array of costs, and res1,res2,res3,... are binary arrays of resources needed by eahc item.

Complexity will be O(W*n*2^#resources)

amit
  • 175,853
  • 27
  • 231
  • 333
0

After giving my problem some more thoughts I came up with a solution I am quite proud of. This solution:

  • will find all possible complete variants, that is, variants where no additional item can be added without causing a conflict
  • will also find a few non-complete variants. I can live with that.
  • can select the final variant by any means you want.
  • works with non-integer item-values.

I realized that this is indeed not a variant of the knapsack problem, as the items have a value but no weight associated with them (or, you could interpret it as a variant of the multi-dimensional knapsack problem variant but with all weights equal). The code uses some lambda expressions, if you don't use Java 8 you'll have to replace those.

public class BenefitSelector<T extends IConflicting>
{
public ArrayList<T> select(ArrayList<T> proposals, Function<T, Double> valueFunction)
{
    if (proposals.isEmpty())
        return null;


    ArrayList<ArrayList<T>> variants = findVariants(proposals);

    double value = 0;
    ArrayList<T> selected = null;

    for (ArrayList<T> v : variants)
    {
        double x = 0;

        for (T p : v)
            x += valueFunction.apply(p);

        if (x > value)
        {
            value = x;
            selected = v;
        }
    }

    return selected;
}

private ArrayList<ArrayList<T>> findVariants(ArrayList<T> list)
{
    ArrayList<ArrayList<T>> ret = new ArrayList<>();
    Conflict c = findConflicts(list);

    if (c == null)
        ret.add(list);

    else
    {
        ret.addAll(findVariants(c.v1));
        ret.addAll(findVariants(c.v2));
    }

    return ret;
}

private Conflict findConflicts(ArrayList<T> list)
{
    // Sort conflicts by the number of items remaining in the first list
    TreeSet<Conflict> ret = new TreeSet<>((c1, c2) -> Integer.compare(c1.v1.size(), c2.v1.size()));

    for (T p : list)
    {
        ArrayList<T> conflicting = new ArrayList<>();

        for (T p2 : list)
            if (p != p2 && p.isConflicting(p2))
                conflicting.add(p2);

        // If conflicts are found create subsets by
        // - v1: removing p 
        // - v2: removing all objects offended by p
        if (!conflicting.isEmpty())
        {
            Conflict c = new Conflict(p);

            c.v1.addAll(list);
            c.v1.remove(p);

            c.v2.addAll(list);
            c.v2.removeAll(conflicting);

            ret.add(c);
        }
    }

    // Return only the conflict with the highest number of elements in v1 remaining.
    // The algorithm seems to behave in such a way that it is sufficient to only  
    // descend into this one conflict. As the root list contains all items and we use
    // the remainder of objects there should be no way to miss an item.
    return ret.isEmpty() ? null 
                         : ret.last();
}



private class Conflict
{
    /** contains all items from the superset minus the offending object */
    private final ArrayList<T>  v1  = new ArrayList<>();

    /** contains all items from the superset minus all offended objects */
    private final ArrayList<T>  v2  = new ArrayList<>();

    // Not used right now but useful for debugging
    private final T             offender;


    private Conflict(T offender)
    {
        this.offender = offender;
    }
}
}

Tested with variants of the following setup:

public static void main(String[] args)
{
    BenefitSelector<Scavenger> sel = new BenefitSelector<>();

    ArrayList<Scavenger> proposals = new ArrayList<>();
    proposals.add(new Scavenger("P1",   new Resource[] {Resource.B},                            0.5));
    proposals.add(new Scavenger("P2",   new Resource[] {Resource.A, Resource.B, Resource.C},    4));
    proposals.add(new Scavenger("P3",   new Resource[] {Resource.C},                            2));
    proposals.add(new Scavenger("P4",   new Resource[] {Resource.A, Resource.B},                1.5));
    proposals.add(new Scavenger("P5",   new Resource[] {Resource.A},                            2));
    proposals.add(new Scavenger("P6",   new Resource[] {Resource.C, Resource.D},                3));
    proposals.add(new Scavenger("P7",   new Resource[] {Resource.D},                            1));

    ArrayList<Scavenger> result = sel.select(proposals, (p) -> p.value);
    System.out.println(result);
}

private static class Scavenger implements IConflicting
{
    private final String        name;
    private final Resource[]    resources;
    private final double        value;


    private Scavenger(String name, Resource[] resources, double value)
    {
        this.name = name;
        this.resources = resources;
        this.value = value;
    }



    @Override
    public boolean isConflicting(IConflicting other)
    {
        return !Collections.disjoint(Arrays.asList(resources), Arrays.asList(((Scavenger) other).resources));
    }

    @Override
    public String toString()
    {
        return name;
    }
}

This results in [P1(B), P5(A), P6(CD)] with a combined value of 5.5, which is higher than any other combination (e.g. [P2(ABC), P7(D)]=5). As variants aren't lost until they are selected dealing with equal variants is easy as well.

Managarm
  • 1,070
  • 3
  • 12
  • 25
  • Just noticed, it seems like the algorithm does not differ depending on the conflict set chosen. The findConflicts method could thus be improved by returning the first conflict found. – Managarm Mar 08 '15 at 19:13