-1

My question has a lot in common with this one: Split a list of numbers into n chunks such that the chunks have (close to) equal sums and keep the original order

The main difference is that I have a slightly different metric to figure out which split is "best", and I have an arbitrary condition to respect while doing so.

Every item in my list has two components. Weight and Volume. I have to split them into n different subgroups, while having the total weights of every subgroup as close as possible. The way to test that is simply to get the difference between the heaviest and the lightest subgroup. The smaller this difference is, the better. This means that subgroups [15][15][15][10] are worth the same in final score as subgroups [15][13][11][10].

Then, this is the part I can't figure out how to add into the algorithms proposed as answers to the linked question, I have a hard condition that has to be respected. There is a maximum volume [v] for each subgroup, and none of them can go above it. Going above does not reduce score, it invalidates the entire answer.

How could the algorithms (and code snippets) used as answers to the previous be adapated to take into account the volume condition and the slightly different scoring method?

I am looking for code, pseudo-code or written (detailed) explanation of how this could be done. The question is taggued C# because that's what I'm using, but I am confident that I can translate from any non-esoteric language so feel free to go with whatever you like if you answer with code.

As mentioned in the other question, this problem is very complex and finding the best solution might not be feasible in reasonable computing time, therefore I am looking for an answer that gives a "good enough" solution, even if it might not be the best.

Kaito Kid
  • 983
  • 4
  • 15
  • 34
  • Do also want to retain the original order of the elements in the list? Else, I can propose a solution based on dynamic-programming if sorting the list by weights is allowed. – Nilesh Oct 03 '18 at 16:57
  • I don't care about the order for this question. I can always get it back at the end (in the real code, the objects have more properties than just weight and volume, including a unique ID which is the original order) – Kaito Kid Oct 03 '18 at 17:00
  • Being able to optimize the smallest number of trucks that can move the greatest number of packages of given weights and volumes is a problem that if you had an efficient solution, you could literally make billions of dollars on it. You know who has an efficient solution? Amazon. But I'd be willing to bet that they're not sharing that code. :-) – Eric Lippert Oct 03 '18 at 18:24

1 Answers1

2

I've formulated a deterministic solution for the given problem using dynamic-programming, sharing the code for the same https://ideone.com/pkfyxg

#include<iostream>
#include<vector>
#include<climits>
#include<cstring>
#include<algorithm>
using namespace std;

// Basic structure for the given problem
struct Item {
    float weight;
    float volume;

    Item(float weight, float volume) {
        this->weight = weight;
        this->volume = volume;
    }

    bool operator<(const Item &other) const {
        if(weight == other.weight) {
            return volume < other.volume;
        }
        return weight < other.weight;
    }
};

// Some constant values
const static int INF = INT_MAX / 100;
const static int MAX_NUM_OF_ITEMS = 1000;
const static int MAX_N = 1000;

// Parameters that we define in main()
float MAX_VOLUME;
vector<Item> items;

// DP lookup tables
int till[MAX_NUM_OF_ITEMS];
float dp[MAX_NUM_OF_ITEMS][MAX_N];

/**
 * curIndex: the starting index from where we aim to formulate a new group
 * left: number of groups still left to be formed
 */ 
float solve(int curIndex, int left) {
    // Baseline condition
    if(curIndex >= items.size() && left == 0) {
        return 0;
    }
    if(curIndex >= items.size() && left != 0) {
        return INF;
    }
    // If we have no more groups to be found, but there still are items left
    // then invalidate the solution by returning INF
    if(left <= 0 && curIndex < items.size()) {
        return INF;
    }

    // Our lookup dp table
    if(dp[curIndex][left] >= 0) {
        return dp[curIndex][left];
    }

    // minVal is the metric to optimize which is the `sum of the differences
    // for each group` we intialize it as INF
    float minVal = INF;

    // The volume of the items we're going to pick for this group
    float curVolume = 0;

    // Let's try to see how large can this group be by trying to expand it 
    // one item at a time
    for(int i = curIndex; i < items.size(); i++) {
        // Verfiy we can put the item i in this group or not
        if(curVolume + items[i].volume > MAX_VOLUME) {
            break;
        }
        curVolume += items[i].volume;
        // Okay, let's see if it's possible for this group to exist
        float val = (items[i].weight - items[curIndex].weight) + solve(i + 1, left - 1);
        if(minVal >= val) {
            minVal = val;
            // The lookup table till tells that the group starting at index
            // curIndex, expands till i, i.e. [curIndex, i] is our valid group
            till[curIndex] = i + 1;
        }
    }
    // Store the result in dp for memoization and return the value
    return dp[curIndex][left] = minVal;
}

int main() {
    // The maximum value for Volume
    MAX_VOLUME = 6;
    // The number of groups we need
    int NUM_OF_GROUPS = 5;

    items = vector<Item>({
    // Item(weight, volume)
        Item(5, 2),
        Item(2, 1),
        Item(10, 3),
        Item(7, 2),
        Item(3, 1),
        Item(5, 3),
        Item(4, 3),
        Item(3, 2),
        Item(10, 1),
        Item(11, 3),
        Item(19, 1),
        Item(21, 2)
    });

    // Initialize the dp with -1 as default value for unexplored states
    memset(dp, -1, sizeof dp);

    // Sort the items based on weights first
    sort(items.begin(), items.end());

    // Solve for the given problem
    int val = solve(0, NUM_OF_GROUPS);

    // If return value is INF, it means we couldn't distribute it in n
    // groups due to the contraint on volume or maybe the number of groups
    // was greater than the number of items we had ^_^
    if(val >= INF) {
        cout << "Not possible to distribute in " << NUM_OF_GROUPS;
        return 0;
    }

    // If a solution exists, use the lookup till array to find which items
    // belong to which set  
    int curIndex = 0, group = 1;
    while(curIndex < items.size()) {
        cout << "Group #" << group << ": ";
        for(int i = curIndex; i < till[curIndex]; i++)
            cout << "(" << items[i].weight << ", " << items[i].volume << ") ";
        cout << '\n';
        group++;    
        curIndex = till[curIndex];
    }
}

I've added comments to the code to help you understand it's working better. The overall runtime complexity for the same is O(num_of_groups * (num_of_items)2) Let me know if you need more explanation around the same ^^;

Nilesh
  • 1,388
  • 6
  • 17
  • I've tried it, but your solution tries to make more groups than what I set in NUM_OF_GROUPS. Specifically, I put 100 max volume and 100 groups, and it tries to put items in group #104 for some reason. I had to change lots of types because my weights and volumes are actually floats, but it doesn't seem to be the cause unless I am mistaken. – Kaito Kid Oct 03 '18 at 19:20
  • @KaitoKid that's unusual unless I've overlooked some bug in the code. Would it be possible for you to share your code with me so that I can take a look and debug it at my end? – Nilesh Oct 03 '18 at 19:32
  • https://pastebin.com/uparnTfK Here it is. It is also converted to C# so I hope you can read it. If the mistake is in my translation, sorry. – Kaito Kid Oct 03 '18 at 19:55
  • @KaitoKid I realized that I had incorrectly code the termination conditions for the recursive solve function, I've updated my answer with the correct solution now. I also did that for your C# code which is available here: https://pastebin.com/RKvzHyft – Nilesh Oct 03 '18 at 20:04
  • @KaitoKid the changes in the C# code as visible on https://pastebin.com/RKvzHyft is from line 30-34 for the baseline conditions and on line 64 where I've also included the `>=` clause instead of just `>` – Nilesh Oct 03 '18 at 20:06
  • @KaitoKid Erm, I just noticed your code and apparently we need to first sort the boxes based on weight, and if two boxes have the same weight then we need to sort by volume among them. Check my overloaded < operator in my Item structure. This is required for the solution to be optimal – Nilesh Oct 03 '18 at 20:22
  • Thanks a lot, I'll try that right away. As for the operator, my bad – Kaito Kid Oct 04 '18 at 11:44
  • I originally got IndexOutOfRangeExceptions after lots of recursive calls on the if(dp[curIndex, left] != -1) line. I added a small validation segment (lines 38-41), and I still get boxes assigned to invalid groups (110). https://pastebin.com/NeRLwPK3 – Kaito Kid Oct 04 '18 at 13:40
  • Does your number of items and groups exceed the size `const static int MAX_NUM_OF_ITEMS = 1000; const static int MAX_N = 1000;`? Can you let me know about the number of items you are working with? – Nilesh Oct 04 '18 at 13:45
  • In my version, I changed MAX_NUM_OF_ITEMS to 5000 because it is possible I get more than 1000, but never more than 5000. I am working with 500-4500 items, in 100 groups with a max volume per group of 100. – Kaito Kid Oct 04 '18 at 13:48
  • @KaitoKid I've corrected your code and have set MAX_N to 200 since you aren't expecting more than 100 groups. The changes are in line 38-44. Let me know if it works out now. For the constraints that you've imposed, the program might take a while to run but it should give the optimal result. If it still throws an IndexOutOfRange exception then can you tell me for which index? Apologies for making you work so much. https://pastebin.com/860td62N – Nilesh Oct 04 '18 at 14:11
  • It seems to be working fine now. On the other hand, it takes wayy too long for my needs. Since this is what I had asked for, I have marked the answer as accepted. I'll try on my side to change your code from "perfect but long" to "close enough and faster". Thanks a lot! – Kaito Kid Oct 04 '18 at 14:28
  • Thanks for accepting my answer :) If you want to make it more faster with a bit sub-optimal results then you can consider adding a `break;` in the `if(minVal >= val)` condition. – Nilesh Oct 04 '18 at 14:43