问题 切割棒使成本最小化


你必须切一根长棍 l 分成几块。必须在地点进行削减 c1, c2, c3, ..., cn,哪里 ci 是一个整数 1 和 n-1 (包括的)。切割的成本等于制作它的棒的长度。削减的顺序应该是什么,以最大限度地降低运营的总成本?

例如,考虑一个长度的棍子 10 并且必须在地点进行切割 2, 4, 7。你可以按照给定的顺序切割木棒。第一次削减将花费 10因为棍子很长 10。第二次削减将花费 8因为制作切口的剩余棍子是长度的 10 - 2 = 8。最后一次削减会花费 6,因为剩下的棍子的长度是 10 - 4 = 6。总费用是 10 + 8 + 6 = 24 

但是如果我们按顺序切断棍子: 4, 2, 7,我们得到的成本 10 + 4 + 6 = 20 这对我们来说更好。

设计一种算法来解决问题。

我很确定这是一个DP问题。我能看到的一种诱人的复发关系是,如果我们切一根棍子,我们会得到两根小棍子。如果我们知道这两种棒的最佳解决方案,我们可以很容易地找出更大棒的最佳解决方案。但这将是非常低效的。

如果你有一个递归函数 min_cost(stick_length, c_1, c_2, ..., c_n) 它返回了切割长度的最低成本 stick_length 在 c_1, c_2, ..., c_n,递归关系看起来像这样

min_cost(stick_length, c_1, c_2, ..., c_n) =
    stick_length 
    + minimum(min_cost(c_1, a_1, a_2, ..., a_i) 
    + min_cost (stick_length - c_1, 
                a_(i+1), ..., a_(n-1)),
                min_cost(c_2, a_1, a_2, ..., a_i) 
    + min_cost(stick_length - c_2, 
               a_(i+1), ..., a_(n-1)), ... , 
               min_cost(c_n, a_1, a_2, ..., a_i)
    + min_cost(stick_length - c_n,
                a_(i+1), ..., a_(n-1)))`,

哪里 a_1, a_2, ..., a_n 是剩余的待切割地点的排列。我们必须将所有可能的排列传递给重复函数,而不仅仅是我写过的一个。

这显然是不切实际的。我该如何解决这个问题?


4044
2018-01-14 05:23


起源

^这正是我想知道的。 - Gerard
我不认为完整的解决方案会太复杂。这个问题来自信息学奥林匹克运动会。 - Gerard
@Gerard每次都进行了切割,你将整个棒分成另外两根棍子,所以你只需要为那两根棍子调用另一个递归功能,这有助于减少公式。只需要对c1,c2 ......进行排序并处理一些计算。 - Pham Trung
@PhamTrung:我不知道在哪根棍子上做出哪些削减。如何按升序排序有帮助? - Gerard
我如何联系Andrew Barber?我不知道为什么他把这个搁置了。他甚至没有评论过。 - Gerard


答案:


还有一个DP解决方案:

让COST(a,b)成为切割第a和第b个切点之间的最佳成本。很明显,COST(a,a)和COST(a,a + 1)为零。我们可以计算COST(a,b)的最佳值,作为通过所有中间点a + 1 ... b-1加上自己的段长度的最小值。因此我们可以通过对角线对角填充三角形表,并找到最终结果作为COST(开始,结束),具有O(N ^ 3)时间复杂度和O(N ^ 2)空间

德尔福代码(输出 Cost 20 Sequence 4 2 7

var
  Cuts: TArray<Integer>;
  Cost: array of array of Integer;
  CutSequence: array of array of String;
  N, row, col, leftpos, rightpos, cutpos, Sum: Integer;
begin
  Cuts := TArray<Integer>.Create(0, 2, 4, 7, 10); // start, cuts, end points
  N := Length(Cuts);
  SetLength(Cost, N, N);  //zero-initialized 2D array
  SetLength(CutSequence, N, N);  //zero-initialized 2D array

  for rightpos := 2 to N - 1 do
    for leftpos := rightpos - 2 downto 0 do begin //walk along the diagonals
                                                  //using previously computed results
      //find the best (mincost) cut
      Cost[leftpos, rightpos] := MaxInt; //big value
      for cutpos := leftpos + 1 to rightpos - 1 do begin
        Sum := Cost[leftpos, cutpos] + Cost[cutpos, rightpos];
        if Sum < Cost[leftpos, rightpos] then begin
          Cost[leftpos, rightpos] := Sum;
          //write down best sequence
          CutSequence[leftpos, rightpos] := Format('%d %s %s', [Cuts[CutPos],
            CutSequence[leftpos, cutpos], CutSequence[cutpos, rightpos]]);
        end;
      end;

      //add own length
      Cost[leftpos, rightpos] :=
        Cost[leftpos, rightpos] + Cuts[rightpos] - Cuts[leftpos];
    end;

  //show the best result
  Caption := Format('Cost %d  Sequence %s',[Cost[0, N-1], CutSequence[0, N-1]]);

4
2018-01-14 10:51



这是我在重新思考之后提出的完全相同的解决方案。 - Gerard
@MBo为什么在Cuts中添加0(第一个元素)和10个(最后一个元素)? - user248884
@ user248884 0和10是获取第一个和最后一个段的长度所需的结束坐标 - MBo
是。但我的问题是为什么我们需要在Cuts数组中使用它们? - user248884
也许我不明白你的疑虑,但这似乎是解释表达长度的最简单方法 Cost[leftpos, rightpos] + Cuts[rightpos] - Cuts[leftpos]; - MBo


这实际上是UVa Online Judge的一个问题。 http://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=944

在这个问题中,L <1000且n <50。

正如我在评论中提到的那样,如果你注意到每次切割,可以进行切割的可能长度= n。

总可能 remaining lengths 应该是有限的,并且对于每个剩余长度的数量 sets of cuts remaining 也应该是有限的。因此,您可以在剩余长度上构建DP。

从最小的,从每个“剩余长度”开始,您可以计算进一步削减它的最低成本。

就像是:

DP[k][SetOfCutsRemaining] = k + Min( DP[m1][SetOfCutsRemaining till c1] 
                                 + DP[k-m1][SetOfCutsremaining from c1], 
                                 DP[m2][SetOfCutsRemaining till c2] 
                                 + DP[k-m2][SetOfCutsremaining from c2],... )
                           where mi are the lengths remaining if we make a cut at ci

然后你需要做到这一点 DP[L][InitialSetOfCuts]

在示例问题中, L = 10, ci = 2, 4, 7

其余长度及其相应的切割剩余如下。 注意,在这种情况下,组合的数量应为C(n + 2,2)=(n + 2)(n + 1)/ 2 = 10

2 {} (2 times, 0-2 and 2-4)
3 {} (2 times, 4-7 and 7-10)
4 {c1}
5 {c2}
6 {c3}
7 {c1, c2}
8 {c2, c3}
10 {c1, c2, c3}

DP[2][{}] = 0 (No cut remaining)
DP[3][{}] = 0 (No cut remaining)
DP[4][{c1}] = 4 (1 cut remaining)
DP[5][{c2}] = 5 (1 cut remaining)
DP[6][{c3}] = 6 (1 cut remaining)
DP[7][{c1,c2}] = 7 + Min( DP[2]{} + DP[5][{c2}], DP[3]{} + DP[4][{c1}] )
               = 7 + Min( 5, 4 ) = 11.
DP[8][{c2,c3}] = 8 + Min( DP[2]{} + DP[6][{c3}], DP[3]{} + DP[5][{c2}] )
               = 8 + Min( 6, 5 ) = 13.
DP[10][{c1,c2,c3}] = 10 + Min( DP[2]{} + DP[8][{c2,c3}], DP[4]{c1} + DP[6][{c3},
                                DP[7][{c1,c2}] + DP[3]{} )
               = 10 + Min( 13, 10, 11 ) = 20.

3
2018-01-14 06:42



您能否为问题中的示例构建一个DP矩阵来说明? - Gerard
@Gerard示例添加了插图。请注意,对于单个剩余长度,可以保留多组切割。在这个例子中,每个剩余长度只有一个集合(如果我没有遗漏任何长度) - Abhishek Bansal
你能指出如何代表剩余的削减量吗?这是此解决方案的关键点。 - Pham Trung
@PhamTrung该实现依赖于编程语言。在C ++中,我将为每个剩余长度使用集合向量。 - Abhishek Bansal
你是完全正确的;抱歉,我错过了! +1 :) - Alex Reinking


首先,假设我们有一个切割位置的升序数组,所以在OP例子中,它将是 {2,4,7}

首先,我们有从0到n的长度,所以我们称之为函数

int cal(int start, int end , int [] cuts)

start = 0和end = n。

对于大于开始且小于结束的每个切割点,我们都有公式

int result = 1000000;
for(int i = 0; i < cuts.length; i++){
   if(cuts[i]> start && cuts[i]<end){
         int val = (end - start) + cal(start, cuts[i], cuts) + cal(cuts[i],end , cuts); 
         result = min(val, result);
   } 
}

DP表 可以简单

dp[start][end]

所以,整个解决方案将是:

int cal(int start, int end, int[]cuts){
    if(dp[start][end]!= -1){//Some initializations need to be done
        return dp[start][end];
    }
    int result = 1000000;
    for(int i = 0; i < cuts.length; i++){
       if(cuts[i]> start && cuts[i]<end){
         int val = (end - start + 1) + cal(start, cuts[i], cuts) + cal(cuts[i],end , cuts); 
         result = min(val, result);
       } 
    }
    return dp[start][end] = result;
}

为了进一步增强使用空间,我们可以参考每个切割位置 作为其索引 在数组中 cuts

添加了切割数组的起点和终点,我们有以下数组

{0,2,4,7,10}

通过将起始位置称为索引0,以索引4结束,我们可以将数组dp的空间从dp [10] [10]减小到dp [5] [5]


2
2018-01-14 07:46



如果我没有错,那么这不是OP已经试过的确切解决方案吗? - Abhishek Bansal
@AbhishekBansal正如OP提到的,他的解决方案是不切实际的,所以我只是向他展示另一种方法:) - Pham Trung
IMO这不够有效,因为长度2到7应分别按顺序(2,7)和(7,2)在2和7处进行切割,因此应重复计算。 - Abhishek Bansal
@AbhishekBansal你能说明一点吗?对于2和7的削减,从(0,10)开始的状态将变为(0,2)和(2,10),然后从开始(2,10)变为(2,7)和(7, 10)。当然使用DP将避免重复计算 - Pham Trung
我可能误解了你的解决方案。你在使用DP还是递归?从你的代码看来它似乎是递归。如果是这样,则必须计算(2,10)和(0,7)的状态(2,7)。 - Abhishek Bansal


对不起,我随时都可以像冰箱一样嗡嗡作响,但用数学说话更是个问题。我可能会为我的生活规范化算法,但只要业力警察让我独自一人,我就不会。

这是我的解决方案(在JavaScript中)。

这是一种纯粹的蛮力方法。
没有一个切口(如果我可以这样说),所有分支都被采用。

似乎n次切割的探索切割数量等于3 ^^ n(我测量了它)。我怀疑这有一个微不足道的解释,但试图找到它让我头疼,所以......

我使用另一个注释中建议的格式,即数组的最左边和最右边的元素表示当前棒的末端。
例如, [0,2,4,7,10] 意思是“在0到10的范围内切割位置2,4和7”。

function try_cut_raw (list)
{
    // terminal ends
    if (list.length == 2) return 0;
    if (list.length == 3) return list[2]-list[0];

    // left and right split
    var cost_min = 1e6;
    for (var i = 1 ; i != list.length-1 ; i++)
    {
        var cost = try_cut_raw (list.slice (0, i+1))
                 + try_cut_raw (list.slice (i, list.length));
        if (cost < cost_min) cost_min = cost;
    }
    return cost_min+list[list.length-1]-list[0];
}

更精确的一个,返回要应用的切割的半有序序列以实现结果。

function try_cut (list)
{
    // terminal ends
    if (list.length == 2) return { cost: 0, seq:[] };
    if (list.length == 3) return { cost: list[2]-list[0], seq:[list[1]] };

    // left and right split, retaining best value
    var i_min;
    var cost_min = 1e6;
    var seq_min;
    for (var i = 1 ; i != list.length-1 ; i++)
    {
        var cl = try_cut (list.slice (0, i+1));
        var cr = try_cut (list.slice (i, list.length));

        var cost = cl.cost+cr.cost;
        if (cost < cost_min)
        {
            cost_min = cost;
            // store cut order associated with best result
            seq_min  = [list[i]].concat (cl.seq).concat(cr.seq);
        }
    }
    return { cost: cost_min+list[list.length-1]-list[0], seq: seq_min }
}

带有OP输入的测试用例以及初始质询页面中的两个示例

function cut (list)
{
var cut = try_cut (list);
var cut_raw = try_cut_raw (list);
console.log ("["+list+"] -> "+cut.seq+" cost "+cut.cost+"/"+cut_raw);
}

cut ([0,2,4,7,10]);
cut ([0,25,50,75,100]);
cut ([0,4,5,7,8,10]);

产量

[0,2,4,7,10] -> 4,2,7 cost 20/20
[0,25,50,75,100] -> 50,25,75 cost 200/200
[0,4,5,7,8,10] -> 4,7,5,8 cost 22/22

0
2018-01-14 09:33





我尊重所有上述解决方案,这是我在Java中解决这个问题的方法。

这可能对某人有帮助。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;

public class CutTheSticks2 {
    public static void main(String s[]) throws NumberFormatException, IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        short N = Short.parseShort(br.readLine());
        short[] A = new short[N];
        N = 0;
        for (String str : br.readLine().split(" ")) {
            A[N++] = Short.parseShort(str);
        }

        Arrays.sort(A);

        StringBuffer sb = new StringBuffer();
        System.out.println(N);
        for (int i = 1; i < N; i++) {
            if (A[i - 1] != A[i]) {
                sb.append((N - i) + "\n");
            }
        }

        // OUTPUT
        System.out.print(sb);
    }
}

0
2018-03-10 18:57



这是一个解决方案吗?你能用输入和输出解释一下吗? - user3198603