【问题标题】:Codility MinAbsSumCodility MinAbsSum
【发布时间】:2023-04-09 10:53:01
【问题描述】:

我尝试了这个 Codility 测试:MinAbsSum。 https://codility.com/programmers/lessons/17-dynamic_programming/min_abs_sum/

我通过搜索整个可能性树解决了这个问题。结果还可以,但是,由于大输入超时,我的解决方案失败了。换句话说,时间复杂度没有预期的那么好。我的解决方案是 O(nlogn),这对于树木来说是正常的。但是这个编码测试在“动态编程”一节中,必须有一些方法来改进它。我尝试先对整个集合求和,然后使用这些信息,但我的解决方案中总是缺少一些东西。有人知道如何使用 DP 改进我的解决方案吗?

#include <vector>
using namespace std;

int sum(vector<int>& A, size_t i, int s)
{
   if (i == A.size())     
      return s;

   int tmpl = s + A[i];
   int tmpr = s - A[i];
   return min (abs(sum(A, i+1, tmpl)), abs(sum(A, i+1, tmpr)));
}

int solution(vector<int> &A) {
   return sum(A, 0, 0);   
}

【问题讨论】:

  • 您的解决方案是O(2^n)
  • 动态编程通常意味着某种查找表的参与

标签: c++ algorithm


【解决方案1】:

我发明了另一种解决方案,比以前的解决方案更好。我不再使用递归。 这个解决方案工作正常(所有逻辑测试都通过了),并且还通过了一些性能测试,但不是全部。我还能如何改进它?

#include <vector>
#include <set> 
using namespace std;

int solution(vector<int> &A) {
    if (A.size() == 0) return 0;

    set<int> sums, tmpSums;        
    sums.insert(abs(A[0]));
    for (auto it = begin(A) + 1; it != end(A); ++it)
    {        
        for (auto s : sums)
        {
            tmpSums.insert(abs(s + abs(*it)));
            tmpSums.insert(abs(s - abs(*it)));            
        }
        sums = tmpSums;
        tmpSums.clear();
    }

    return *sums.begin();
}

【讨论】:

  • 递归的时间复杂度是 pow(2,N)。这就是为什么你需要使用 DP
  • 我喜欢你使用set 删除重复结果。要改进解决方案,您需要找到一种在一个地方计算重复数字的方法,因为范围在 -100 到 100 之间,在一个长数组中有很多。
  • 72% 结果:app.codility.com/demo/results/trainingZ5EMB7-N9S 这对您的解决方案来说是一个不错的分数。
【解决方案2】:

我无法解决它。不过here's官方回答。

引用它:

请注意,数字的范围非常小(最多 100)。因此, 一定有很多重复的数字。让 count[i] 表示 值 i 的出现次数。我们可以处理所有事件 一次具有相同的价值。首先我们计算值 count[i] 然后我们 创建数组 dp 使得:

  • dp[j] = -1 如果我们不能得到总和 j,
  • dp[j] >= 0 如果我们可以得到和 j。

最初,所有 j 的 dp[j] = -1(dp[0] = 0 除外)。然后我们扫描 通过所有出现在 A 中的值 a;我们认为所有 a 都是这样的 计数[a]>0。对于每一个这样的a,我们更新 dp[j] 表示的 dp 在达到和 j 之后(最大)还有多少值 a。笔记 如果 dp[j] >= 0 的前一个值,那么我们可以设置 dp[j] = count[a] 因为不需要值 a 来获得总和 j。否则我们 必须先得到 sum j-a,然后用 aa 得到 sum j。在 这样的情况 dp[j] = dp[j-a]-1。使用这个算法,我们可以 标记所有总和值并选择最好的(最接近 S 的一半, A) 的绝对值之和。

def MinAbsSum(A):
    N = len(A)
    M = 0
    for i in range(N):
        A[i] = abs(A[i])
        M = max(A[i], M)
    S = sum(A)
    count = [0] * (M + 1)
    for i in range(N):
        count[A[i]] += 1
    dp = [-1] * (S + 1)
    dp[0] = 0
    for a in range(1, M + 1):
        if count[a] > 0:
            for j in range(S):
                if dp[j] >= 0:
                    dp[j] = count[a]
                elif (j >= a and dp[j - a] > 0):
                    dp[j] = dp[j - a] - 1
    result = S
    for i in range(S // 2 + 1):
        if dp[i] >= 0:
            result = min(result, S - 2 * i)
    return result

(请注意,由于最终迭代只考虑到 S // 2 + 1 的总和,因此我们也可以通过只创建一个 DP 缓存直到该值来节省一些空间和时间)

fladam 提供的 Java 答案为输入 [2, 3, 2, 2, 3] 返回了错误的结果,尽管它获得了 100% 的分数。

Java 解决方案

import java.util.Arrays;

public class MinAbsSum{
    static int[] dp;

    public static void main(String args[]) {
        int[] array = {1, 5,  2, -2};

        System.out.println(findMinAbsSum(array));
    }
    
    public static int findMinAbsSum(int[] A) {
        int arrayLength = A.length;
        int M = 0;
        for (int i = 0; i < arrayLength; i++) {
            A[i] = Math.abs(A[i]);
            M = Math.max(A[i], M);
        }
        
        int S = sum(A);
        dp = new int[S + 1];
        int[] count = new int[M + 1];
        for (int i = 0; i < arrayLength; i++) {
            count[A[i]] += 1;
        }
        Arrays.fill(dp, -1);
        dp[0] = 0;
        for (int i = 1; i < M + 1; i++) {
            if (count[i] > 0) {
                for(int j = 0; j < S; j++) {
                    if (dp[j] >= 0) {
                        dp[j] = count[i];
                    } else if (j >= i && dp[j - i] > 0) {
                        dp[j] = dp[j - i] - 1;
                    }
                }
            }
        }
        int result = S;
        for (int i = 0; i < Math.floor(S / 2) + 1; i++) {
            if (dp[i] >= 0) {
                result = Math.min(result, S - 2 * i);
            }
        }
        return result;
    }

    public static int sum(int[] array) {
        int sum = 0;
        for(int i : array) {
            sum += i;
        }

        return sum;
    }
}

【讨论】:

    【解决方案3】:

    这个解决方案(用 Java 编写)在(正确性和性能)这两个方面都获得了 100% 的分数

    public int solution(int[] a){
        if (a.length == 0) return 0;
        if (a.length == 1) return a[0];
        int sum = 0;
        for (int i=0;i<a.length;i++){
            sum += Math.abs(a[i]);
        }
        int[] indices = new int[a.length];
        indices[0] = 0;
        int half = sum/2;
        int localSum = Math.abs(a[0]);
        int minLocalSum = Integer.MAX_VALUE;
        int placeIndex = 1;
        for (int i=1;i<a.length;i++){
            if (localSum<half){
                if (Math.abs(2*minLocalSum-sum) > Math.abs(2*localSum - sum))
                    minLocalSum = localSum;
                localSum += Math.abs(a[i]);
                indices[placeIndex++] = i;
            }else{
                if (localSum == half)
                    return Math.abs(2*half - sum);
    
                if (Math.abs(2*minLocalSum-sum) > Math.abs(2*localSum - sum))
                    minLocalSum = localSum;
                if (placeIndex > 1) {
                    localSum -= Math.abs(a[indices[placeIndex--]]);
                    i = indices[placeIndex];
                }
            }
        }
        return (Math.abs(2*minLocalSum - sum));
    
    }
    

    此解决方案将所有元素视为正数,并希望尽可能接近所有元素的 sum除以 2(在这种情况下我们知道所有其他元素的总和也将是相同的增量,远离一半 -> abs 总和将是可能的最小值)。 它通过从第一个元素开始并连续将其他元素添加到“本地”总和(并记录总和中元素的索引)直到达到 x >= sumAll/2 的总和。如果 x 等于 sumAll/2 我们有一个最优解。如果不是,我们退回到索引数组中并继续选择在该位置的最后一次迭代结束的其他元素。结果将是 abs((sumAll - sum) - sum) 最接近 0 的“本地”总和;

    固定解决方案:

    public static int solution(int[] a){
        if (a.length == 0) return 0;
        if (a.length == 1) return a[0];
        int sum = 0;
        for (int i=0;i<a.length;i++) {
            a[i] = Math.abs(a[i]);
            sum += a[i];
        }
        Arrays.sort(a);
    
        int[] arr = a;
        int[] arrRev = new int[arr.length];
        int minRes = Integer.MAX_VALUE;
    
        for (int t=0;t<=4;t++) {
            arr = fold(arr);
            int res1 = findSum(arr, sum);
            if (res1 < minRes) minRes = res1;
    
            rev(arr, arrRev);
            int res2 = findSum(arrRev, sum);
            if (res2 < minRes) minRes = res2;
    
            arrRev = fold(arrRev);
            int res3 = findSum(arrRev, sum);
            if (res3 < minRes) minRes = res3;
        }
    
        return minRes;
    }
    
    private static void rev(int[] arr, int[] arrRev){
        for (int i = 0; i < arrRev.length; i++) {
            arrRev[i] = arr[arr.length - 1 - i];
        }
    }
    
    private static int[] fold(int[] a){
        int[] arr = new int[a.length];
        for (int i=0;a.length/2+i/2 < a.length && a.length/2-i/2-1 >= 0;i+=2){
            arr[i] = a[a.length/2+i/2];
            arr[i+1] = a[a.length/2-i/2-1];
        }
        if (a.length % 2 > 0) arr[a.length-1] = a[a.length-1];
        else{
            arr[a.length-2] = a[0];
            arr[a.length-1] = a[a.length-1];
        }
        return arr;
    }
    
    private static int findSum(int[] arr, int sum){
        int[] indices = new int[arr.length];
        indices[0] = 0;
        double half = Double.valueOf(sum)/2;
        int localSum = Math.abs(arr[0]);
        int minLocalSum = Integer.MAX_VALUE;
        int placeIndex = 1;
        for (int i=1;i<arr.length;i++){
            if (localSum == half)
                return 2*localSum - sum;
            if (Math.abs(2*minLocalSum-sum) > Math.abs(2*localSum - sum))
                minLocalSum = localSum;
    
            if (localSum<half){
                localSum += Math.abs(arr[i]);
                indices[placeIndex++] = i;
            }else{
                if (placeIndex > 1) {
                    localSum -= Math.abs(arr[indices[--placeIndex]]);
                    i = indices[placeIndex];
                }
            }
        }
    
        return Math.abs(2*minLocalSum - sum);
    }
    

    【讨论】:

      【解决方案4】:

      您已接近实际解决方案的 90%。看来您非常了解递归。现在,您应该在此处对您的程序应用动态编程。

      动态编程只不过是对递归的记忆,这样我们就不会一次又一次地计算相同的子问题。如果遇到相同的子问题,我们返回之前计算和记忆的值。记忆可以在二维数组的帮助下完成,例如 dp[][],其中第一个状态表示数组的当前索引,第二个状态表示求和。

      对于这个特定的问题,您有时可以贪婪地决定跳过一个调用,而不是从每个状态调用两个状态。

      【讨论】:

        【解决方案5】:

        以下是C++官方答案的渲染图(在任务、正确性和性能方面得分100%):

        #include <cmath>
        #include <algorithm>
        #include <numeric>
        using namespace std;
        
        int solution(vector<int> &A) {
            // write your code in C++14 (g++ 6.2.0)
            const int N = A.size();
            int M = 0;
            for (int i=0; i<N; i++) {
                A[i] = abs(A[i]);
                M = max(M, A[i]);
            }
            int S = accumulate(A.begin(), A.end(), 0);
            vector<int> counts(M+1, 0);
            for (int i=0; i<N; i++) {
                counts[A[i]]++;
            }
            vector<int> dp(S+1, -1);
            dp[0] = 0;
            for (int a=1; a<M+1; a++) {
                if (counts[a] > 0) {
                    for (int j=0; j<S; j++) {
                        if (dp[j] >= 0) {
                            dp[j] = counts[a];
                        } else if ((j >= a) && (dp[j-a] > 0)) {
                            dp[j] = dp[j-a]-1;
                        }
                    }
                }
            }
            int result = S;
            for (int i =0; i<(S/2+1); i++) {
                if (dp[i] >= 0) {
                    result = min(result, S-2*i);
                }
            }
            return result;
        }
        

        【讨论】:

          【解决方案6】:

          我想提供算法,然后提供我在 C++ 中的实现。 Idea 与官方的 codility 解决方案大致相同,并添加了一些不断优化。

          1. 计算输入的最大绝对元素。
          2. 计算输入的绝对和。
          3. 计算输入中每​​个数字的出现次数。将结果存储在向量哈希中。
          4. 检查每个输入。
          5. 对于每个输入,遍历任意数量输入的所有可能总和。只达到可能总和的一半是一个轻微的持续优化。
          6. 对于之前的每个求和,设置当前输入的出现次数。
          7. 检查每个潜在总和是否等于或大于当前输入,无论此输入以前是否已被使用过。相应地更新当前总和的值。在本次迭代中,我们不需要检查小于当前输入的潜在总和,因为很明显它以前没有被使用过。
          8. 上述嵌套循环将使用大于 -1 的值填充每个可能的总和。
          9. 再次检查这个可能的总和哈希,以寻找最接近可能得到的一半的总和。最终,最小绝对总和将是这个与一半的差乘以 2,因为该差将在两组中相加,作为与中位数的差。

          此算法的运行时复杂度为O(N * max(abs(A)) ^ 2),或简称为O(N * M ^ 2)。那是因为外部循环正在迭代M 次,而内部循环正在迭代总和时间。在最坏的情况下,总和基本上是N * M。因此,它是O(M * N * M)

          该解决方案的空间复杂度为O(N * M),因为我们为计数分配了N 项的散列,为总和分配了S 项的散列。 S又是N * M

          int solution(vector<int> &A)                                                    
          {                                                                               
            int M = 0, S = 0;                                                             
            for (const int e : A) { M = max(abs(e), M); S += abs(e); }                    
            vector<int> counts(M + 1, 0);                                                 
            for (const int e : A) { ++counts[abs(e)]; }                                   
            vector<int> sums(S + 1, -1);                                                  
            sums[0] = 0;                                                                  
            for (int ci = 1; ci < counts.size(); ++ci) {                                  
              if (!counts[ci]) continue;                                                  
              for (int si = 0; si < S / 2 + 1; ++si) {                                    
                if (sums[si] >= 0) sums[si] = counts[ci];                                 
                else if (si >= ci and sums[si - ci] > 0) sums[si] = sums[si - ci] - 1;    
              }                                                                           
            }                                                                             
                                                                                          
            int min_abs_sum = S;                                                          
            for (int i = S / 2; i >= 0; --i) if (sums[i] >= 0) return S - 2 * i;          
            return min_abs_sum;                                                           
          }
          

          【讨论】:

            猜你喜欢
            • 2020-09-19
            • 2014-12-21
            • 2014-05-31
            • 2015-02-19
            • 2013-10-28
            • 1970-01-01
            • 1970-01-01
            • 2016-03-10
            • 2014-02-10
            相关资源
            最近更新 更多