【问题标题】:Given list of integers, find the sets of numbers whose sum >= a target number, minimising the total amount that each set goes over the target给定整数列表,找到总和 >= 目标数的数集,最小化每个集超过目标数的总数
【发布时间】:2021-01-19 12:29:06
【问题描述】:

所以不知何故,我的一个亲戚得到了这些餐厅代金券,可以用来从每张收据中扣除 500 泰铢。可以要求餐厅开出多张收据,以便可以使用多张代金券。亲戚希望尽量少花钱(超过500的礼券就必须用现金支付)

更正式地说,问题是:

given a list of prices (being the prices of items to be ordered), what are the combinations of prices that would require the least amount of cash to be paid?

例如:

让价格 = [425, 105, 185, 185, 185, 98, 145, 155, 125, 125, 135, 295, 295, 155, 125]

如果我只是按给定顺序对价格求和,当总和超过 500 时停止:

[425 + 105], [185 + 185 + 185], [98 + 145 + 155 + 125], [125 + 135 + 295], [295 + 155 + 125]

每个组合的总和:

[530, 555, 523, 555, 575]

金额超过 500: [30, 55, 23, 55, 75]

要支付的总现金:238

需要最少现金的最佳价格组合是什么?

到目前为止,我已经尝试了一种蛮力方法,通过生成价格的所有排列并以与上述示例相同的方式计算每个排列所需的现金金额(从左到右求和,当 >= 500 时停止) .但是,当价格超过 10 个时,这种方法会导致 heap out of memory 错误:(

提前致谢。

【问题讨论】:

    标签: algorithm partition-problem


    【解决方案1】:

    我也得到了 238,带有回溯。

    下面的程序尝试所有可用的新状态组合:

    (index, sum_a, count_a, sum_b, count_b...)
    
      where index is the index in prices, and sum_x and count_x
      are the distinct sum x and the count of how many bins
      with that sum we currently have.
    

    如果该状态是先前看到的,它会返回从该状态获得的记录值,并避免搜索已知会导致大于或等于迄今为止最小记录结果的结果的分支。它使用单个对象进行递归,将状态存储在内存中并更新为backtracks。

    还有一个可调整的界限,允许一个 bin 有多大(行,if (new_sum >= 1.2 * 500))。

    function getPrefixSums(A){
      let pfxs = new Array(A.length);
      pfxs[-1] = 0;
      A.map((x, i) => pfxs[i] = A[i] + pfxs[i-1]);
      return pfxs;
    }
    
    function add(sum, new_sum, dp){
      if (dp.state[sum] == 1)
        delete dp.state[sum];
      else if (dp.state.hasOwnProperty(sum))
        dp.state[sum] -= 1;
      if (dp.state.hasOwnProperty(new_sum))
        dp.state[new_sum] += 1;
      else
        dp.state[new_sum] = 1;
      if (new_sum > 500)
        dp.total += new_sum - 500;
      else if (new_sum < 500);
        dp.remaining -= new_sum - sum;
    }
    
    function remove(sum, new_sum, dp){
      if (sum > 0 && !dp.state.hasOwnProperty(sum))
        dp.state[sum] = 1;
      else if (sum > 0)
        dp.state[sum] += 1;
      if (dp.state[new_sum] == 1)
        delete dp.state[new_sum];
      else if (dp.state.hasOwnProperty(new_sum))
        dp.state[new_sum] -= 1;
      if (new_sum > 500)
        dp.total -= new_sum - 500;
      else if (new_sum < 500);
        dp.remaining += new_sum - sum;
    }
    
    function g(prices, pfxs, i, dp, memo){
      const sorted = Object.entries(dp.state).sort(([sum1, count1], [sum2, count2]) =>
        Number(sum1) - Number(sum2));
      const key = String([i, sorted]);
      
      if (memo.hasOwnProperty(key))
        return memo[key];
    
      if (dp.total >= dp.best)
        return memo[key] = Infinity;
        
      if (i == prices.length){
        if (Object.keys(dp.state).some(x => Number(x) < 500))
          return memo[key] = Infinity;
        dp.best = Math.min(dp.best, dp.total);
        return memo[key] = dp.total;
      }
        
      let best = Infinity;
      let bestSum = -1;
        
      // Add bin
      if (pfxs[pfxs.length-1] - pfxs[i-1] >= 500 + dp.remaining){
        dp.remaining += 500;
        add(0, prices[i], dp);
        
        const candidate = g(prices, pfxs, i+1, dp, memo);
          
        best = candidate;
        bestSum = 0;
        
        dp.remaining -= 500;
        remove(0, prices[i], dp);
      }
    
      // Add to existing bin;
      for (let sum in dp.state){
        const new_sum = Number(sum) + prices[i];
        
        if (new_sum >= 1.2 * 500)
          continue;
          
        add(sum, new_sum, dp);
          
        const candidate = g(prices, pfxs, i+1, dp, memo);
    
        if (candidate < best){
          best = candidate;
          bestSum = sum;
        }
          
        remove(sum, new_sum, dp);
      }
      
      return memo[key] = best;
    }
    
    function backtrack(prices, total, memo){
      let m = [];
      
      for (let i in memo)
        if (memo[i] == total)
          m.push(i);
     
      m = m.map(x => x.split(',').map(Number));
      m.sort((a, b) => a[0] - b[0]);
      
      function validate(added, removed){
        return added.length == 1 &&
          removed.length < 2 &&
          !added.some(([sum, count]) => count > 1) &&
          !removed.some(([sum, count]) => count > 1);
      }
    
      function back(i, prev_idx, dp){
        if (i == m.length)
          return dp;
    
        const [idx, ...cts] = m[i];
        const _dp = cts.reduce(function(acc, x, i){
          if (!(i & 1))
            acc[x] = cts[i+1];
          return acc;
        }, {});
    
        if (idx == prev_idx)
          return back(i + 1, prev_idx, dp);
    
        let added = [];
        let removed = [];
    
        for (let sum in _dp){
          if (!dp.hasOwnProperty(sum))
            added.push([sum, _dp[sum]]);
          else if (dp[sum] > _dp[sum])
            removed.push([sum, dp[sum].count - _dp[sum]]);
        }
        for (let sum in dp){
          if (!_dp.hasOwnProperty(sum))
            removed.push([sum, dp[sum]]);
        }
    
        if (!validate(added, removed))
          return back(i + 1, prev_idx, dp);
    
        const [[new_sum, _]] = added;
        let old_bin = [];
    
        if (removed.length){
          const [[old_sum, _]] = removed;
          const len = dp[old_sum].bins.length;
          old_bin = dp[old_sum].bins[len - 1];
    
          if (dp[old_sum].count == 1){
            delete dp[old_sum];
    
          } else {
            dp[old_sum].count -= 1;
            dp[old_sum].bins.length = len - 1;
          }
        }
    
        if (dp[new_sum]){
          dp[new_sum].count += 1;
          dp[new_sum].bins.push(old_bin.concat(prices[idx-1]));
        } else {
          dp[new_sum] = {
            count: 1,
            bins: [old_bin.concat(prices[idx-1])]
          }
        }
    
        return back(i + 1, idx, dp);
      }
    
      function get_dp(row){
        const [idx, ...cts] = row;
        return cts.reduce(function(acc, x, i){
          if (!(i & 1)){
            acc[x] = {
              count: cts[i+1],
              bins: new Array(cts[i+1]).fill(null).map(_ => [x])
            };
          }
          return acc;
        }, {});
      }
      
      const dp = get_dp(m[1]);
      
      return back(2, 1, dp);
    }
    
    function f(prices){
      const pfxs = getPrefixSums(prices);
      const dp = {
        state: {'0': 1},
        total: 0,
        remaining: 0,
        best: Infinity
      };
      const memo = {};
      const result = g(prices, pfxs, 0, dp, memo);
    
      const _dp = backtrack(prices, result, memo);
      const bins = Object.values(_dp).flatMap(x => x.bins);
    
      return [result, bins];
    }
    
    var prices = [425, 105, 185, 185, 185, 98, 145, 155, 125, 125, 135, 295, 295, 155, 125];
    
    console.log(JSON.stringify(prices));
    console.log(JSON.stringify(f(prices)));

    【讨论】:

    • 谢谢。这很好用!虽然我觉得有点难以理解该程序的工作原理。
    • @patty 我在答案中添加了对该方法的一般描述。
    • @patty 我忘了提到(现在添加)对于允许的 bin 有多大的限制(行,if (new_sum &gt;= 1.2 * 500))。那和 500 应该被提取到变量中,以便能够使用不同的输入。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-04-12
    • 2013-11-03
    • 2021-01-08
    • 2014-09-10
    • 2016-04-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多