找到数组和给定天数的最长总和

4
一个徒步旅行者计划穿越山脉进行多天的远足。有n条小径,每条小径都有一定的长度,以列表形式给出,需要按照列表中给出的顺序依次穿越。该徒步旅行者希望通过在最少的天数内完成这次远足,创造新的世界纪录。他还希望确保每天至少穿越一条小径,并确保每天穿越的最长小径长度之和尽可能地小。请找出这个最小的长度之和。
例如,假设有n = 6条小径,长度分别为trails = [10, 5, 9, 3, 8, 15],记录record = 2。那么,徒步旅行者在第一天穿越第一条小径,因此最长的小径长度为10,在第二天穿越其余的小径,所以第二天的最长小径长度为max(5, 9, 3, 8, 15) = 15。所有天数中最长小径的长度之和为10 + 15 = 25。返回25。
其他例子:
array = [150, 200, 400, 350, 250] and record = 3

Ans:
Cover the first trail on day 1, the second on day 2, and the remaining on day 3.
The total is 150 + 200 + max(400, 350, 250) = 750

例子:

array = [78, 45, 12, 56, 85, 45], record=1

Ans:

Covers all the trails in one day.

Output = 85

**限制条件:**
n的大小为1到300,记录的大小为1到300,数组元素的值范围为1到10^5。 程序的结果是一个整数,所以不需要使用长整型。
这是我的方法:
从数组中选择前(record-1)个项目并将它们加入结果,然后从(record到数组长度)中获取最大的项目并将其加入结果。
根据指示我编写了代码。这是在一个月前的hackerrank面试中被问到的问题,这段代码只解决了15个测试用例中的10个。剩下的测试用例失败,显示输出错误,并且它们是隐藏的测试用例。
我是否漏掉了什么导致测试用例失败?我尝试使用长整型变量,但得分没有改善。
**更新:**
使用动态规划:
public static void main(String[] args) {
        solve(new int[]{1000, 500, 2000, 8000, 1500}, 3); // correct result : 9500
        solve(new int[]{3000, 1000, 4000}, 2); // correct result : 7000
        solve(new int[]{190, 200, 450, 499, 358, 160}, 3); // correct result : 849
    }
    
    public static void solve(int[] x, int D) {
        int T = x.length; // Number of trails
        
        int[][][] v = new int[T + 1][D + 1][T + 1];
        int[][][] z = new int[T + 1][D + 1][T + 1];
        
        // Dynamic programming
        for (int t = 1; t <= T; t++) {
            for (int d = 1; d <= Math.min(D, t); d++) {
                for (int n = 1; n <= t; n++) {
                    if (t == 1 && d == 1) {
                        v[t][d][n] = x[t - 1];
                        z[t][d][n] = x[t - 1];
                    } else {
                        x[t - 1] = x[t - 1]; // Length of current trail
                        
                        if (d == 1) {
                            z[t][d][n] = Math.max(z[t - 1][d][n - 1], x[t-1]);
                            v[t][d][n] = v[t - 1][d][n - 1] - z[t - 1][d][n - 1] + z[t][d][n];
                        } else {
                            int maxLastTrail = Math.max(z[t - 1][d][n - 1], x[t-1]);
                            z[t][d][n] = maxLastTrail;
                            v[t][d][n] = v[t - 1][d][n - 1] - z[t - 1][d][n - 1] + z[t][d][n];
                        }
                    }
                }
            }
        }
        
        // Finding the optimal value
        int optimalValue = Integer.MAX_VALUE;
        for (int n = 1; n <= T; n++) {
            optimalValue = Math.min(optimalValue, v[T][D][n]);
        }
        
        //System.out.println("Optimal value: " + optimalValue);
        
        // Backtracking to construct optimal solution
        int daysLeft = D;
        int trailsLeft = T;
        int lastTrails = -1;
        
        for (int n = T; n >= 1; n--) {
            if (v[trailsLeft][daysLeft][n] == optimalValue) {
                lastTrails = n;
                break;
            }
        }
        
        List<Integer> hikePlan = new ArrayList<>();
        for (int t = T; t >= 1 && daysLeft >=0 && lastTrails >=0; t--) {
            if (v[t][daysLeft][lastTrails] != optimalValue) {
                trailsLeft = t + 1;
                daysLeft--;
                lastTrails--;
                hikePlan.add(trailsLeft);
            }
        }
        
        Collections.reverse(hikePlan);
        
        // Printing the hike plan
        int result = 0;
        for(int e : hikePlan) {
            result += x[e-1];
        }
        System.out.println(result);
    }
}

我尝试了Cary Swoveland提供的解决方案,但我不清楚我哪里出错了,我得到了错误的输出。
对于输入{3000, 1000, 4000}, 2,正确的结果是7000,但我得到了5000。同样地,对于输入{190, 200, 450, 499, 358, 160}, 3,正确的输出是849,但我得到了518。对于输入{150, 200, 400, 350, 250}, 3,正确的输出是750,但我得到了600

1
我不理解问题中的这部分内容,"......每天所走过的最长路径长度之和是可能的最小值......"。所以,每天他们必须至少徒步行走当前累积的徒步距离吗? - Reilas
@user16320675,对于[10,15,9,3,8,5]和记录2,我们选择10和15,所以输出是25。 - technotigr
2
为什么不是第一天:10、15、9、3、8,最长=15;第二天:只有5,最长=5 - 所以总和是15 + 5 = 20?对于第三次,给定的例子可能不涵盖所有情况(例如,为什么只有一次尝试/天,但用于最后一天)((而最后一个例子大多是无用的,假设它应该是记录=1))。 - user16320675
问题还在HackerRank上吗?如果是的话,能否请您提供一个链接? - Old Dog Programmer
@OldDogProgrammer,这是一个旧问题,所以链接已经过期了。 - technotigr
显示剩余2条评论
4个回答

2
用动态规划的方法似乎是正确的选择。我会使用一个只有两个维度的备忘录表,其中y坐标表示分区的数量,x坐标表示正在考虑的子数组的起始索引(并延伸到末尾)。表中找到的值应该提供所指定子数组和分区数量的最优解(最小值之和的最大值)。
如果你采用自底向上的方法,你应该:
- 当分区数量为零时,将其定义为0:无需进行任何操作。实际上,零并不是分区数量的有效值,但它将在算法的其余部分中发挥作用。 - 当分区数量为1时,定义结果:在这种情况下,必须存储子数组的最大值。可以通过从末尾到起始迭代值,并跟踪“运行”最大值来高效地完成此操作。 - 对于更大的分区数量,建立在先前的结果之上:当前分区可能在多个可能的索引处结束,因此逐个假设每个索引,并在该分区增长时保持最大值。分区结束后的内容可以从表中的先前结果中读取。
我想可能有几种优化方法。比如这个:
不需要连续存在两个拥有超过一个成员的分区。
证明:假设我们有两个这样的连续分区,并且左分区中的最大值大于右分区中的最大值。我们可以将左分区中的所有值(除了最左边的值)移动到右分区中。这不会增加任何分区中的最大值,尽管可能会降低第一个分区的最大值。因此,我们总是可以将原始分区替换为一个不包含连续存在两个拥有超过一个成员的分区的分区。
当左分区中的最大值小于或等于右分区中的最大值时,可以使用类似的推理方法。
这意味着我们可以考虑以下情况:
  • 将当前子数组的最左边元素作为一个大小为1的分区,并依赖于比当前分区少一个的分区结果,来观察剩余子数组的情况。
  • 查看当前分区可能结束的其他索引(即分区大小大于1的情况),然后将紧接着的元素作为一个大小为1的分区。我们可以依赖于比当前分区少两个的分区结果,来观察剩余子数组的情况。
  • 假设除了当前分区之外的所有分区都是大小为1的,并且它们都“挤在”子数组的右侧。然后,我们可以从比当前分区少一个的分区结果中获取剩余分区的结果。

以下是一种实现方法:

    public static int solve(int[] values, int numPartitions) {
        int n = values.length;
        int[][] dp = new int[numPartitions+1][n+1];
        // Initialise dp for when there is one partition 
        //    In that case the maximum value in the slice is the outcome
        for (int i = n - 1; i >= 0; i--) {
            dp[1][i] = Math.max(dp[1][i + 1], values[i]); // "running" maximum
        }
        // Initialise dp for when array size is the number of partitions
        for (int p = 1; p <= numPartitions; p++) {
            int i = n - p; // Size of slice is number of partitions
            dp[p][i] = values[i] + dp[p - 1][i + 1];
        }
        // Increment number of partitions: dynamic programming
        for (int p = 2; p <= numPartitions; p++) {
            for (int i = numPartitions - p; i <= n - p; i++) {
                // Split  (i)(i + 1...)...
                int max = values[i]; // Maintain the maximum of the current partition
                int result = max + dp[p - 1][i + 1];
                // Loop to place a singleton (so two splits) somewhere in the slice
                for (int j = i + 2; j <= n - p; j++) {
                    max = Math.max(max, values[j - 1]);
                    if (p > 2) { // Split (i..j-1)(j)(j+1...)...
                        result = Math.min(result, max + values[j] + dp[p - 2][j + 1]);
                    }
                }
                int j = n - p + 1;
                // Split  (i..j-1)(j)(j+1)...(n-1) all cramped to the right
                dp[p][i] = Math.min(result, Math.max(max, values[j - 1]) + dp[p - 1][j]);
            }
        }
        return dp[numPartitions][0];
    }

测试了以下测试用例:
    public static void main(String[] args) {
        System.out.printf("%d\n".repeat(7),
            solve(new int[]{1000, 500, 2000, 8000, 1500}, 3), // 9500
            solve(new int[]{3000, 1000, 4000}, 2), // 7000
            solve(new int[]{190, 200, 450, 499, 358, 160}, 3), // 849
            solve(new int[]{10,5,9,3,8,15}, 2), // 25
            solve(new int[]{150, 200, 400, 350, 250}, 3), // 750
            solve(new int[]{78, 45, 12, 56, 85, 45}, 1), // 85
            solve(new int[]{10, 5, 9, 3, 8, 15}, 2) // 25
        );
    }

输出:

9500
7000
849
25
750
85
25

1

你的代码立即添加了第一条路径的长度,但这不一定是正确的方法。在你的一个例子中是正确的,但它并不普遍适用。例如,如果你有[50,60,8]和记录=2,正确答案是68:第一天你做50和60,将60加入总和,第二天你做8并加上8。

关键是,如果你添加了“50”,你就会失败 - 这不是正确的方法。对于这样的练习,你的第一直觉可能是开始编码,但通常是错误的方法。先写下一堆测试案例,并分析如何获得正确答案。然后再开始编码。在这个问题中,你还没有做到这一点 - 你下一步应该是写下一堆自己构思出来的测试案例以及你认为正确的答案。

通常情况下,如果一个编程问题被赋予一个故事来使其易于理解,往往有助于首先根据这个相关的故事来思考问题并推导。但它们可能会误导;检查一下这个相关的故事是否有任何合理性可能会有所帮助。

这根本不是个好方法来解释问题。如果你试图在一天内完成整个任务,那你的得分将会非常低(也就是说,输入中最长的行程就是你的总得分)。在进行一次非常长的行程时,值得尝试做很多次行程来吞并其他长但不如此长的行程 - 如果输入中有两次非常长的行程,你真的希望将它们合并到同一天。
这与走一系列行程以创造世界纪录的想法毫无关联。
这类问题的目的实际上并不是测试你是否有能力意识到问题通过一个误导性的“相关”情境来欺骗你。所以,总的来说,我认为这个问题,以及让你回答这个问题的权威(无论是考试还是面试官),都非常糟糕。你应该庆幸自己没有在那里工作,作为一个程序员和负责招聘的人,我对有人在面试中敢问这种荒谬问题感到非常愤怒。
编辑:我举了一个例子,但是弄错了。我对这个问题的“形式”已经感到很烦恼了,考虑到楼主在面试失败后感到有些难过,我选择详细阐述这个问题是多么荒谬。

谢谢,我还在努力理解,看起来我误解了问题。如果输入是 [8,3,60,5,6,50,4,7,2,3]record=5,输出应该是什么?是 [8,3,60,5,6] = 82 还是 [60,5,6,50,4] = 125? - technotigr
@user16320675 其实,说得很对。这个问题真的很复杂;它试图让人们产生共鸣,但却失败了,因为它让你计算最长的行程,而不是当天所有行程的总和。 - rzwitserloot
对于[8,3,60,5,6,50,4,7,2,3]r=5,我认为他们想要将其分割为:8/3/60/5/6/504723,总共为60+4+7+2+3 = 76。问题周围的“故事”非常误导人(在一天内徒步旅行这么长的距离,然后在接下来的4天里几乎不走路没有任何意义,但这样做可以获得最好的“分数”,而且用更少的天数完成更容易)。 - rzwitserloot
@technotigr 我对这个答案进行了重大修改,提供了一个新的(这次是正确的!)例子,展示了你的问题无法解决的情况,并提供了一些额外的见解来解决这个问题。 - rzwitserloot
解释为什么第一天要走8/3/60/5/6/50:你必须按顺序进行。然而,你可以在一天内走尽可能多的段落,但是,你必须最终达到正好5个非空段落,所以如果你在第一天也走了'4',剩下的段落不足以填满5天。然而,通过在第一天同时完成60和50,你只需要“付出”当天最长的段落,因此将长段落合并在一天内是高效的。如果输入少一个段落(去掉那个3),答案会稍微增加:8+60+4+7+2。 - rzwitserloot

0
将前记录-1个值相加,然后再加上剩余范围中的最大值。
我在这里使用了Arrays#sort方法,以获取范围中的最大值。
public static int trekking(int[] array, int record) {
    int index, result = 0;
    for (index = 0; index < record - 1; index++)
        result += array[index];
    Arrays.sort(array, index, array.length);
    result += array[array.length - 1];
    return result;
}

这里是一个用法,附带提供的示例数据。

int[] arrayA = { 150, 200, 400, 350, 250 };
int recordA = 3;

int[] arrayB = { 78, 45, 12, 56, 85, 45 };
int recordB = 0;

int[] arrayC = { 10, 5, 9, 3, 8, 15 };
int recordC = 2;

System.out.println(trekking(arrayA, recordA));
System.out.println(trekking(arrayB, recordB));
System.out.println(trekking(arrayC, recordC));

输出

750
85
25

在这个答案中,为了获得最大项,你正在将剩余的元素排序到最大值。所以我在我的代码中使用的逻辑与你在此提供的相同。因此,这个答案无法解决问题。 - technotigr
@technotigr,在你的代码中,你在没有进行检查的情况下赋予了最大值。这将导致只使用最后两个项目的最大值。尝试在_max_赋值之前添加一个条件检查。 - Reilas
max = Math.max(max, array[i]); 我正在使用已经进行条件检查的 Math.max 函数,所以在赋值之前我已经进行了检查。你是指这个吗? - technotigr
@technotigr,你是对的,实际上我误读了。那样将可以。 - Reilas
从逻辑上讲,对于反转的数组,结果应该是相同的。当你反转数组时,你的第一个测试将产生不同的结果。因此,这个解决方案是不正确的。 - trincot

0

动态规划解法

这个问题可以使用动态规划来解决。

假设 "记录" 等于 D 天,试验的数量为 T

动态规划的制定将有 T阶段,每个阶段对应于每个试验 t,其中 1 <= t <= T。在每个阶段 t 中,状态变量将是第 t 个试验在前 d 天内(1 <= d <= min { D, t })徒步旅行的最优解(如下定义),并且其中的 n 个试验在最后一天进行。因此,每个三元组 (t,d,n) 将有一个状态变量。

对于每个这样的三元组,解决方案是在每一天上徒步旅行的次数(正数),这些徒步旅行的次数总和为t,并且其中n次徒步旅行在d天中的最后一天进行。解决方案的价值等于每天徒步旅行的最长路线长度之和。如果给定三元组的所有解决方案中,其价值最小,则该解决方案是最优的。
可以看出,第t+1阶段的每个状态变量的值可以从第t阶段的状态变量计算得出。这满足动态规划的最优性原则。
基本思想是从第一天开始徒步旅行第一条路线。第一天只有一个解决方案,即在第一天徒步旅行第一条路线,其中有1次徒步旅行在最后一天(即第一天)进行。
我们接下来要确定在一天或两天内徒步完成前两条路线的最佳方案。这也很简单。
接下来,我们要确定在一天、两天或三天内徒步完成前三条路线的最佳方案(假设D >= 3)。在这里,我们第一次需要考虑变量n,即最后一天徒步的次数。当三条路线需要在两天内完成时,最后一天可以选择徒步一次或两次。而在一天或三天内完成三条路线时,最后一天只有一种可能的徒步次数。
对于第四天及以后的计算稍微复杂一些。我不打算在这里解释,而是通过一个例子来说明,这样会更清楚明了。
例子:
假设D = 3,并且按顺序给出了徒步的长度,存储在数组中。
x = [350, 200, 400, 150, 250] 

变量

设定:

  • x(i) 表示第 i 条路径的长度(索引从 1 开始);

  • v(t,d,n) 表示在前 t 条路径上徒步旅行时,在 d 天内的最佳价值,当最后一天徒步旅行了 n 条路径;以及

  • z(t,d,n) 表示与 v(t,d,n) 相关的最后 n 条路径的长度的最大值。

确定第一条路径的最佳解决方案(t = 1

我们只需要考虑一天和最后一天的徒步次数(这里是第一天)。

x(1) = 350

v(1,1,1) = 350
x(1,1,1) = 350
    

确定前两个试验(t = 2)的最佳解决方案。
我们只需要考虑两天,以及每天最后一天的徒步次数。
x(2) = 200

z(2,1,2) = max { z(1,1,1), x(2) } = max { 350, 200 } = 350
v(2,1,2) = v(1,1,1) - z(1,1,1) + z(2,1,2) = 350 - 350 + 350 = 350 

z(2,2,1) = x(2) = 200
v(2,2,1) = v(1,1,1) + z(2,2,1) = 350 + 200 = 550

确定前三个试验(t = 3)的最佳解决方案。
从现在开始,我们需要考虑所有三天。
x(3) = 400   

z(3,1,3) = max { z(2,1,2), x(3) } = max { 350, 400 } = 400 
v(3,1,3) = v(2,1,2) - z(2,1,2) + z(3,1,3) = 350 - 350 + 400 = 400

z(3,2,2) = max { z(2,2,1), x(3) } = max { 200, 400 } = 400
v(3,2,2) = v(2,2,1) - z(2,2,1) + z(3,2,2) = 550 - 200 + 400 = 750

z(3,2,1) = x(3) = 400
v(3,2,1) = v(2,1,2) + z(3,2,1) = 350 + 400 = 750

z(3,3,1) = x(3) = 400
v(3,3,1) = v(2,2,1) + z(3,3,1) = 550 + 400 = 950

确定前四个试验(t = 4)的最佳解决方案。
无需计算v(4,1,4)(和z(4,1,4)),因为这将限制组数为2,D = 3(记录)。
x(4) = 150   

z(4,2,3) = max { z(3,2,2), x(4) } = max { 400, 150 } = 400
v(4,2,3) = v(3,2,2) - z(3,2,2) + z(4,2,3) = 750 - 400 + 400 = 750

z(4,2,2) = max { z(3,2,1), x(4) } = max { 400, 150 } = 400
v(4,2,2) = v(3,2,1) - z(3,2,1) + z(4,2,2) = 750 - 400 + 400 = 750

z(4,2,1) = x(4) = 150
v(4,2,1) = v(3,1,3) + z(4,2,1) = 400 + 150 = 550
             
z(4,3,2) = max { z(3,3,1), x(4) } = max { 400, 150 } = 400
v(4,3,2) = v(3,3,1) - z(3,3,1) + z(4,3,2) = 950 - 400 + 400 = 950 

z(4,3,1) = x(4) = 150 
v(4,3,1) = min { z(3,2,2), z(3,2,1) } + z(4,3,1) 
         = min { 750, 750 } + 150 = 900
                 ^^^

注意,在计算中,v(4,3,1)的第二天可以选择进行2次或1次徒步旅行。因此,我们计算z(3,2,2)z(3,2,1)的最小值,并记录下最小值是哪个。巧合的是,这两个值相同,所以选择哪个并没有影响。

确定所有五条路径(t = 5)的最佳解决方案

由于t = 5表示最后一条路径,且D = 3,我们只需要计算z(5,3,i)v(5,3,i),其中i等于321

x(5) = 250

z(5,3,3) = max { z(4,3,2), x(5) } = max { 400, 250 } = 400
v(5,3,3) = v(4,3,2) - z(4,3,2) + z(5,3,3) = 950 - 400 + 400 = 950

z(5,3,2) = max { z(4,3,1), x(5) } = max { 150, 250 } = 250
v(5,3,2) = v(4,3,1) - z(4,3,1) + z(5,3,2) = 900 - 150 + 250 = 1000

z(5,3,1) = x(5) = 250
v(5,3,1) = min { v(4,2,3), v(4,2,2), v(4,2,1) } + x(5)
         = min { 750, 750, 550 } + 250 = 800
                           ^^^

在计算中,v(5,3,1)表示第二天可以进行3次、2次或1次徒步旅行。正因为如此,我们计算z(4,2,3)z(4,2,2)z(4,2,1)的最小值,并记录下最小值是哪个。
确定最优值
最后,我们计算
min { v(5,3,3), v(5,3,2), v(5,3,1) } = min { 950, 1000, 800 } = 800
                          ^^^^^^^^

因此,最优解的值等于800

构建最优解

为了计算每天应该徒步的最佳次数,我们首先注意到上面的v(5,3,1)中的1表示在第3天我们应该只徒步最后一条路线,即第5条路线,其距离为250

在计算v(5,3,1)时(见上文),我们会记录下三个值中的最小值是v(4,2,1)。这告诉我们在第2天应该只徒步一条路线,即第4条路线,其长度为150。而v(4,2,1)的计算又确定了术语v(3,1,3)(见上文),它告诉我们前3条路线应该在第一天徒步。由于前三条路线的最大长度为400,我们可以看出这个解的最优值等于

400 + 150 + 250 = 800

正如预料的那样。通过这种回溯过程,我们能够计算出每天徒步旅行的最佳次数,而不论问题的规模如何。
可以推断出,即使有100次徒步旅行需要在30天内完成,最优解也可以在瞬间得到。

根据您提供的解决方案,我已经在我的帖子中添加了代码。但是我不清楚我犯了什么错误,因为输出结果是错误的。请您帮忙检查一下。 - technotigr
我会尽量找时间来审查你的代码,但是不能保证,因为我现在有很多其他任务要处理。 - Cary Swoveland

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接