Algorithm for distributing beads puzzle (2)?

2019-03-31 03:23发布

问题:

Let's say you have a circle (shown below) with N slots. Your goal is to end up with a specified number of beads in each slot, and you have an array of size N containing the amount of beads you need in each slot. For example, if the array was {1, 5, 3}, then you would need to end up with 1 bead in slot 1, 5 beads in slot 2, and 3 beads in slot 3. You have an infinite amount of beads.

You can "unlock" X slots. Once you unlock a slot, you can start putting beads in that slot. You can move beads that are already in slots, but you can only move clockwise.

What is the minimum distance the beads have to move in order to solve the problem?

Here's an example:

N = 6, X = 2. Array: {2, 5, 4, 2, 6, 2}

Unlock slots 2 and 5. Put 11 bead into slot 2 and travel a total distance of 8 to get to slots 2, 3, and 4. Put 10 beads into slot 5 and travel a total distance of 6 to get to slots 5, 6 and 1. 8 + 6 = 14, so the answer is 14.

回答1:

The problem has some things to note:

  • there is no benefit in moving beads to (or beyond) another unlocked slot, as the number of moves would be smaller if those beads started in that other unlocked slot;
  • as a consequence, once the slots to unlock are chosen, the amounts of beads to put in those, and the number of moves is determined;
  • if the cost (number of moves) has been calculated for a particular set of unlocked slots, then the cost for a neighbouring configuration (where a previously unlocking slot remains locked, but the next slot gets unlocked) can be easily derived, without having to calculate from scratch;
  • it is not true that the slot that must receive the most beans is always unlocked in the optimal solution;
  • it is not true that if one slot choice is kept variable that the cost will either increase or decrease in the direction that the next slot is chosen; it can go up and down, up again and down again.

The algorithm suggested here will go through all combinations of possible slot selections and select the combination with the lowest moves. The time complexity is thus O(n!/[(n-x)!x!]).

I think that there should be a more efficient algorithm, which does not need to visit all combinations, but I didn't find any mathematical pattern that would allow this.

Here is the algorithm in a JavaScript snippet:

function optimalCollectors(beadCounts, collectorCount) {
    // Initialisation
    var n = beadCounts.length;
    if (n < collectorCount) return {error: "not enough slots"};
    var beads = beadCounts.reduce(function (beads, beadCount) {
        return beads + beadCount;
    });
    var cost = beadCounts.reduce(function (cost, beadCount, idx) {
        return cost + beadCount * (idx+1);
    });
    var solution = {
        cost: cost, // too large, to make sure it gets improved
        slots: [] // numbers of the slots chosen for the solution
    };
    var collectorSlots = Array(collectorCount);

    function findNextCollector(collectorNo, startSlot, cost, beads) {
        var addBeads = 0;
        for (var slot=startSlot; slot<=n-collectorCount+collectorNo; slot++) {
            collectorSlots[collectorNo] = slot;
            // progressively calculate total cost, and number of beads
            // "in front" of the currently tried slot.
            cost -= beads;
            beads += addBeads - beadCounts[slot];
            if (collectorNo == collectorCount - 1) { // all slots chosen
                if (cost < solution.cost) { // found a current best
                    solution.cost = cost;
                    // copy currently selected slot numbers:
                    solution.slots = collectorSlots.slice(0);
                }
            } else {
                findNextCollector(collectorNo+1, slot+1, cost, beads);
            }
            if (collectorNo) {
                cost += beadCounts[slot] * (slot + 1 - startSlot);
            } else {
                cost += beadCounts[slot] * (n - 1);
                addBeads = beadCounts[slot];
            }
        }
    }
    
    findNextCollector(0, 0, cost, beads);
    return solution;
}

function randomInput(n) {
    // The random values are in the range 0..n-1. This is just 
    // a convenient choice, in reality there has not to be a limit.
    return Array.from({length: n}, x => Math.floor(Math.random() * n));
}

// Link with I/O
var beads = document.getElementById('beads');
var collectors = document.getElementById('collectors');
var randomize = document.getElementById('randomize');
var calculate = document.getElementById('calculate');
var output = document.getElementById('output');

// Capture events
randomize.onclick = function() {
    var n = 5 + Math.floor(Math.random() * 7);
    beads.value = randomInput(n).join(',');
    collectors.value = 2 + Math.floor(Math.random() * (n/2-2));
    calculate.onclick();
};

calculate.onclick = function() {
    var beadCounts = beads.value.split(',').map(Number);
    var collectorCount = Number(collectors.value);
    var solution = optimalCollectors(beadCounts, collectorCount);
    if (solution.error) {
        output.textContent = 'Error: ' + solution.error;
        return;
    }
    output.textContent = 
        '\nInput: ' + JSON.stringify(beadCounts) +
        '\nNumber of moves: ' + solution.cost +
        '\nChosen slots (0-based): ' + JSON.stringify(solution.slots);
};
Comma-separated list of number of beads per slot:<br/>
<input id="beads" size="30" value="2, 5, 4, 2, 6, 2">
<button id="randomize">Randomize</button><br/>
Number of bead-collecting slots:<br/>
<input id="collectors" size="3" value="2"></br>
<button id="calculate">Find collector slots minimising cost</button></br>
<pre id="output"></pre>