【问题标题】:Min sum of distances (absolute differences) between array element and set of k array elements数组元素与 k 个数组元素的集合之间的最小距离总和(绝对差)
【发布时间】:2021-05-15 05:39:58
【问题描述】:

我需要找到数组中的元素与数组的 k 元素集之间的距离之和的最小总和,不包括该索引。

例如:

arr = {5, 7, 4, 9}

k = 2

min_sum(5) = |5-4| + |5-7| = 3

min_sum(7) = |7-9| + |7-5| = 4

min_sum(4) = |4-5| + |4-7| = 4

min_sum(9) = |9-7| + |9-5| = 6

因此,一个简单的解决方案是从数组的每个元素中减去第 i 个元素,然后对数组进行排序并计算排序后的数组中前 k 个元素的总和。但这需要很长时间......我相信这是一个 dp 问题或类似的问题(可能是陷阱)。

输入:

n - 数组元素的数量

k - 集合中的元素数

数组

约束:

2

1

1

时间限制:2秒

输入:

4

2

5 7 4 9

输出:

3 4 4 6

解决这个问题最有效的方法是什么?如何优化最小和的搜索?

这是我用 C++ 编写的代码,对于 n = 350 000, k = 150 000 大约需要 3 分钟:

#include <bits/stdc++.h>

using namespace std;

int main() {

    int n, k, tp;
    unsigned long long temp;
    cin >> n >> k;

    vector<unsigned int> org;
    vector<unsigned int> a;
    vector<unsigned long long> cum(n, 0);
    //unordered_map <int, long long> ans;
    unordered_map <int, long long> mp;


    for (int i = 0; i < n; i++){
        cin >> tp;
        org.push_back(tp);
        a.push_back(tp);
    }

/*
    srand(time(0));

    for (int i = 0; i < n; i++){
        org.push_back(rand());
        a.push_back(org[i]);
    }
*/

    sort(a.begin(), a.end());
    partial_sum(a.begin(), a.end(), cum.begin());

    mp[a[0]] = cum[k] - cum[0] - a[0] * k;
    //ans[a[0]] = mp[a[0]];

    for (int i = 1; i <= k; i++) {
       mp[a[i]] = a[i] * i - cum[i-1] + cum[k] - cum[i] - a[i] * (k-i);
    }

    for (int i = 1; i < n-k; i++){
        for (int j = 0; j <= k; j++){
            //if (ans.find(a[i+j]) != ans.end()) {continue;}
            temp = ( (a[i+j] * j) - (cum[i+j-1] - cum[i-1]) ) + ( cum[i+k] - cum[i+j] - a[i+j] * (k-j) );
            if (mp.find(a[i+j]) == mp.end()) { mp[a[i+j]] = temp; }
            else if (mp[a[i+j]] > temp) { mp[a[i+j]] = temp; }
            //else { ans[a[i+j]] = mp[a[i+j]]; }
        }
    }

    for (int i = 0; i < n; i++) {
        cout << mp[org[i]] << " ";
    }

    return 0;
}

【问题讨论】:

  • 解决这个问题最有效的方法是什么?如何优化最小总和的搜索? -- 老实说,显示代码的目的是什么,正如您所说的那样,效率低下?这是一个纯算法问题,与 C++ 关系不大。
  • 如果问题上的标签只有algorithm,那么就不存在不贴代码的问题,因为这不是“代码”问题。

标签: c++ arrays algorithm heap dynamic-programming


【解决方案1】:

我们可以通过滑动窗口的方法有效地解决这个问题。

假设数组中没有重复项似乎是安全的。如果它包含重复项,那么我们可以在HashSet 的帮助下简单地丢弃它们。

下一步是对数组进行排序,以保证对于每个索引 i,最接近的 k 个元素将在窗口 [i - k; i + k] 内。

我们将为窗口保留三个变量:leftrightcurrentSum。它们将在每次迭代时进行相应调整。最初,left = 0right = k(因为索引 0 处的元素在其左侧没有元素)和currentSum = result 用于索引 0。

关键考虑因素是变量 left 和 right 在迭代期间不太可能发生“显着”变化。更准确地说,在每次迭代中,我们应该通过比较nums[i + right + 1] - nums[i]nums[i] - nums[i - left] 的距离来尝试将窗口向右移动。 (您可以从数学上证明,尝试将窗口向左移动是没有意义的。)如果前者小于后者,我们会在更新 currentSum 的同时向右递增和向左递减。

为了重新计算 currentSum,我建议写下两个相邻迭代的表达式,并仔细查看它们之间的差异。
例如,如果 result[i] = nums[i + 1] + ... + nums[i + right] - (nums[i - 1] + ... + nums[i - left]) + (left - right) * nums[i], 然后
result[i] = nums[i + 2] + ... + nums[i + right] - (nums[i] + ... + nums[i - left]) + (left - right + 2) * nums[i + 1].
正如我们所看到的,这些表达式非常相似。该解决方案的时间复杂度为O(n * log(n))。 (我的 n ~ 500_000k ~ 400_000 在 Java 中的解决方案在 300 毫秒内工作)我希望这与上述考虑对您有所帮助。

假设我们已经对原始数组nums 进行了排序并计算了映射element-&gt;its index in the sorted array(例如,通过二分查找),我们可以继续查找距离。

public long[] findMinDistances(int[] nums, int k) {
    long[] result = new long[nums.length];
    long currentSum = 0;
    for (int i = 1; i <= k; i++) {
        currentSum += nums[i];
    }
    result[0] = currentSum - (long) k * nums[0];

    int left = 0;
    int right = k;
    currentSum = result[0];
    for (int i = 1; i < nums.length; i++) {
        int current = nums[i];
        int previous = nums[i - 1];
        currentSum -= (long) (left - right) * previous;
        currentSum -= previous;

        if (right >= 1) {
            currentSum -= current;
            left++;
            right--;
        } else {
            currentSum += nums[i - 1 - left];
        }
        currentSum += (long) (left - right) * current;

        while (i + right + 1 < nums.length && i - left >= 0 &&
                nums[i + right + 1] - current < current - nums[i - left]) {
            currentSum += nums[i + right + 1] - current;
            currentSum -= current - nums[i - left];
            right++;
            left--;
        }
        result[i] = currentSum;
    }
    return result;
}

对于原始数组中的每个元素e,其最小距离总和将为result[mapping.get(e)]

【讨论】:

  • 不应丢弃重复项,因为我们也必须计算到它们的距离。在我上面的解决方案中,我实现了与您的描述类似的东西。显然,我重新计算了很长时间的总和。我不知道您是如何设法为滑动窗口获得 O( log(n) ) 的。您可以在答案中发布您的解决方案或发布代码链接。这对这些标签来说是可以的。并感谢您的努力。
  • 我得到了滑动窗口的概念。我试图实现这一点,但现在我在 C++ 中的 n=350000k=150000 解决方案可以在 6-7 秒内工作。您能否发布一个运行时间为 300 毫秒的 Java 解决方案的链接?
  • @OliverWayne 我刚刚更新了我的答案。事实证明它有效,无论数组是否包含重复项。
【解决方案2】:

我觉得这个更好:

先对数组进行排序,然后你就可以知道这个事实 - 对于数组中的每个元素 i,它与其他元素的 k 最小距离将是与数组中 k 中围绕它的元素的距离。 (当然它可能是向右或向左或从两侧)。

所以对于每个元素 i 来计算 min_sum(a[i]) 这样做:

首先,min_sum(a[i]) = 0。

然后,使用两个索引,我们将它们标记为 r(在 i 的右侧)和 l(在 i 的左侧) 并将距离 (a[i]-a[r]) 与距离 (a[i]-a[l]) 进行比较。 您将最小的添加到 min_sum(a[i]) 如果它是正确的然后 增加索引 r,如果是左边的则减少索引 l。 当然,如果左侧为 0 或右侧为 n,您将最容易从另一侧获取元素。 不管怎样,你一直这样做,直到你对 k 元素求和,就是这样。

这样你除了主数组之外什么都不排序。

【讨论】:

  • 是的,我在问题描述中的 C++ 解决方案中做了类似的事情,但它适用于 O(NK)。 N 和 K 可以达到 10^5,所以 NK = 10^10,操作太多了。我需要一种适用于 O(nlog(k)) 或 O(nlog^2(k)) 的算法。
猜你喜欢
  • 2016-12-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-28
  • 1970-01-01
相关资源
最近更新 更多