【问题标题】:Minimum Cost to reduce the size of array to 1将数组大小减小到 1 的最小成本
【发布时间】:2021-10-21 17:26:13
【问题描述】:

给定一个包含 N 个数字的数组(不一定是排序的)。我们可以将任意两个数字合并为一个,合并这两个数字的成本等于两个值的总和。任务是找到合并所有数字的最小总成本。

示例:
设数组 A = [1,2,3,4]

然后,我们可以删除 1 和 2,将它们相加并将总和保留在数组中。此步骤的成本为 (1+2) = 3。

现在,A = [3,3,4],成本 = 3

在第二步中,我们可以将 3 和 3 相加,并将总和保留在数组中。此步骤的成本为 (3+3) = 6。

现在,A = [4,6],成本 = 6

在第三步中,我们可以从数组中删除两个元素并将总和再次保留在数组中。此步骤的成本为 (4+6) = 6。

现在,A = [10],成本 = 10

所以,总成本是 19 (10+6+3)。

我们必须选择 2 个最小的元素来最小化我们的总成本。一个简单的方法是使用最小堆结构。我们将能够在 O(1) 中获得最小元素,插入将是 O(log n)。

这种方法的时间复杂度是 O(n log n)。

但我尝试了另一种方法,但无法找到失败的情况。基本思想是,我们将在任何时候选择的两个最小元素的总和总是大于之前选择的一对元素的总和。因此“temp”数组将始终被排序,并且我们将能够访问 O(1) 中的最小元素。

当我对输入数组进行排序然后简单地遍历数组时,我的方法的复杂性是 O(n log n)。

int minCost(vector<int>& arr) {
    sort(arr.begin(), arr.end());
    // temp array will contain the sum of all the pairs of minimum elements
    vector<int> temp;
    // index for arr
    int i = 0;
    // index for temp
    int j = 0;
    int cost = 0;

    // while we have more than 1 element combined in both the input and temp array
    while(arr.size() - i + temp.size() - j > 1) {
        int num1, num2;
        // selecting num1 (minimum element)
        if(i < arr.size() && j < temp.size()) {
            if(arr[i] <= temp[j])
                num1 = arr[i++];
            else
                num1 = temp[j++];
        }
        else if(i < arr.size())
            num1 = arr[i++];
        else if(j < temp.size())
            num1 = temp[j++];

        // selecting num2 (second minimum element)
        if(i < arr.size() && j < temp.size()) {
            if(arr[i] <= temp[j])
                num2 = arr[i++];
            else
                num2 = temp[j++];
        }
        else if(i < arr.size())
            num2 = arr[i++];
        else if(j < temp.size())
            num2 = temp[j++];

        // appending the sum of the minimum elements in the temp array
        int sum = num1 + num2;
        temp.push_back(sum);
        cost += sum;
    }
    return cost;
}

这种方法正确吗?如果没有,请告诉我我缺少什么,以及该算法失败的测试用例。

SPOJ Link for the same problem

【问题讨论】:

  • 这种方法正确吗? -- 你怎么知道你的程序是否有逻辑错误?仅仅因为它编译并不意味着它没有错误。算法仅与编写的假定实现算法的程序一样好。比如Djikstra的最短路径算法是没有bug的,但是如果我实现不正确,那么问题就出在实现上,而不是算法上。
  • 欢迎来到 Stack Overflow。请阅读the help pages,接受SO tour,阅读How to Ask,以及this question checklist。最后,请不要发送垃圾邮件无关的标签。例如,这个问题与 Python 有什么关系?
  • 我无法在我的方法中找到逻辑错误,这就是我发布问题的原因。我想知道我的方法在逻辑上是如何错误的。
  • 但我尝试了另一种方法...... -- 好吧,你承担了这个负担,而不是坚持你声称有效的实现(使用堆)。如果您的算法失败,则无需 C++ 代码来证明它可能会失败。在你写一行代码之前,它应该已经在纸上很明显了。
  • 我在纸上尝试了多个测试用例,每次都得到正确答案,但是当我尝试在 SPOJ 上提交实现的代码时,结果是 WA。我只想知道我的算法无法给出正确结果的情况。如果您能在这方面帮助我,那就太好了。

标签: c++ arrays algorithm data-structures


【解决方案1】:

首先,想得简单!
使用priority queue 时,问题很简单!
在第一个测试用例中:

1 6 3 20
// after pushing to Q
1 3 6 20
// and sum two top items and pop and push!
(1 + 3) 6 20    cost = 4
(4 + 6) 20      cost = 10 + 4 
(10 + 20)       cost = 30 + 14
30              cost = 44
#include<iostream>
#include<queue>
using namespace std;


int main()
{
    int t;
    cin >> t;
    while (t--) {
        int n;
        cin >> n;
        priority_queue<long long int, vector<long long int>, greater<long long int>> q;

        for (int i = 0; i < n; ++i) {
            int k;
            cin >> k;
            q.push(k);
        }

        long long int sum = 0;
        while (q.size() > 1) {
            long long int a = q.top();
            q.pop();
            long long int b = q.top();
            q.pop();
            q.push(a + b);
            sum += a + b;
        }

        cout << sum << "\n";
    }
}

【讨论】:

    【解决方案2】:

    逻辑对我来说似乎非常可靠...所有计算的和永远不会减少,因此您只需将最旧的两个计算和、接下来的两个元素或最旧的和和下一个元素相加。

    我只是简化代码:

    #include <vector>
    #include <algorithm>
    #include <stdio.h>
    
    int hsum(std::vector<int> arr) {
        int ni = arr.size(), nj = 0, i = 0, j = 0, res = 0;
        std::sort(arr.begin(), arr.end());
        std::vector<int> temp;
        auto get = [&]()->int {
            if (j == nj || (i < ni && arr[i] < temp[j])) return arr[i++];
            return temp[j++];
        };
        while ((ni-i)+(nj-j)>1) {
            int a = get(), b = get();
            res += a+b;
            temp.push_back(a + b); nj++;
        }
        return res;
    }
    
    int main() {
        fprintf(stderr, "%i\n", hsum(std::vector<int>{1,4,2,3}));
        return  0;
    }
    

    好主意!

    另一个改进是注意到正在处理的两个数组(原始数组和保存总和的临时数组)的累积长度将在每一步都减少。 由于第一步将使用两个输入元素,临时数组在每一步增加一个元素的事实仍然不足以使数组本身中分配的“步行队列”到达读取指针。 这意味着不需要临时数组,并且可以在数组本身中找到总和的空间...

    int hsum(std::vector<int> arr) {
        int ni = arr.size(), nj = 0, i = 0, j = 0, res = 0;
        std::sort(arr.begin(), arr.end());
        auto get = [&]()->int {
            if (j == nj || (i < ni && arr[i] < arr[j])) return arr[i++];
            return arr[j++];
        };
        while ((ni-i)+(nj-j)>1) {
            int a = get(), b = get();
            res += a+b;
            arr[nj++] = a + b;
        }
        return res;
    }
    

    关于 SPOJ 上的错误...我尝试简单地搜索问题,但没有成功。但是,我尝试生成随机长度的随机数组,并使用直接从规范中实现的“蛮力”解决方案来检查这个解决方案,我有理由相信该算法是正确的。

    我知道至少有一个编程领域(Topcoder),有时问题是经过精心设计的,因此如果使用unsigned,但如果使用int(或者如果使用unsigned long long,但如果使用,则不会),计算得出正确的结果long long) 因为整数溢出。 不知道SPOJ是不是也做这种废话(1)...可能是某些隐藏测试用例失败的原因...

    编辑

    使用 SPOJ 检查算法是否通过 long long 值...这是我使用的条目:

    #include <stdio.h>
    #include <algorithm>
    #include <vector>
    
    int main(int argc, const char *argv[]) {
        int n;
        scanf("%i", &n);
        for (int testcase=0; testcase<n; testcase++) {
            int sz; scanf("%i", &sz);
            std::vector<long long> arr(sz);
            for (int i=0; i<sz; i++) scanf("%lli", &arr[i]);
    
            int ni = arr.size(), nj = 0, i = 0, j = 0;
            long long res = 0;
            std::sort(arr.begin(), arr.end());
            auto get = [&]() -> long long {
                if (j == nj || (i < ni && arr[i] < arr[j])) return arr[i++];
                return arr[j++];
            };
            while ((ni-i)+(nj-j)>1) {
                long long a = get(), b = get();
                res += a+b;
                arr[nj++] = a + b;
            }
            printf("%lli\n", res);
        }
        return 0;
    }
    

    PS:在给定符号频率表的情况下,这种计算也是构建用于熵编码的 Huffman 树所需要的,因此它不仅仅是随机练习,而是具有实际应用。


    (1) 我说的是“废话”,因为在 Topcoder 中,它们从不给出需要 65 位的问题;因此,它不是真正关心溢出,而只是为新手设置陷阱。 另一个我认为是我在 TC 上看到的不好的做法是,有些问题是经过精心设计的,因此正确的算法如果使用 C++ 将几乎不符合超时限制:只需使用另一种语言(并获得例如2 倍减速),您无法解决问题。

    【讨论】:

    • 非常感谢您的努力!喜欢你的解释。
    猜你喜欢
    • 2019-04-09
    • 1970-01-01
    • 2017-01-28
    • 2010-10-03
    • 2019-02-14
    • 2019-04-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多