【问题标题】:Optimally cutting a stick at specified locations在指定位置以最佳方式切割木棍
【发布时间】:2014-01-14 05:23:26
【问题描述】:

你必须将一根长度为l 的棍子切成几块。必须在位置c1, c2, c3, ..., cn 进行切割,其中ci1n-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),它返回在c_1, c_2, ..., c_n处切割一根长度为stick_length的棍子的最小成本,那么递归关系看起来像这样

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 是要剪切的剩余位置的排列。我们必须将所有可能的排列传递给递归函数,而不仅仅是我写的一个。

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

【问题讨论】:

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

标签: algorithm


【解决方案1】:

另一种 DP 解决方案:

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

Delphi 代码(输出 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]]);

【讨论】:

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

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

在这个问题中 L

正如我在 cmets 中提到的,如果您注意到每次切割时,可以进行切割的棍子的可能长度 = 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.

【讨论】:

  • 能否为问题中的示例构建一个DP矩阵来说明?
  • @Gerard 示例添加用于说明。请注意,对于单个剩余长度,可以保留多组切割。在此示例中,每个剩余长度只有一组(如果我没有遗漏)
  • 你能指出如何表示剩余切割的集合吗?这是这个解决方案的关键。
  • @PhamTrung 该实现取决于编程语言。在 C++ 中,我会为每个剩余长度使用一个集合向量。
【解决方案3】:

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

首先,我们有长度从0到n的棍子,所以我们调用函数

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

开始 = 0,结束 = n。

对于每个大于 start 且小于 end 的切割点,我们都有公式

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]

【讨论】:

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

抱歉,我可以随时像冰箱一样嗡嗡作响,但用数学说话更成问题。我可能可以为我的一生制定算法,但只要业力警察让我一个人呆着,我就不会。

这是我的解决方案(在 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

【讨论】:

    【解决方案5】:

    我尊重上述所有解决方案,这是我在 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);
        }
    }
    

    【讨论】:

    • 这是一个解决方案吗?你能用输入和输出解释一下吗?
    猜你喜欢
    • 2011-11-12
    • 2020-03-18
    • 2014-03-25
    • 1970-01-01
    • 2015-12-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多