【问题标题】:Count number of ways for choosing two numbers in efficient algorithm计算高效算法中选择两个数的方法数
【发布时间】:2017-04-17 17:50:10
【问题描述】:

我解决了这个问题,但我在在线裁判上得到了 TLE Time Limit Exceed

程序的输出是正确的,但我认为可以改进方法以提高效率!

问题:

给定n整数,计算我们可以选择两个元素的方式的数量,例如 他们的绝对差值小于32。

以更正式的方式,计算(i, j) (1 ≤ i < j ≤ n)的对数,使得

|V[i] - V[j]| < 32。 |X| 是X 的绝对值。

输入

输入的第一行包含一个整数T,测试用例数(1 ≤ T ≤ 128)。

每个测试用例都以整数 n (1 ≤ n ≤ 10,000) 开头。

下一行包含n 整数(1 ≤ V[i] ≤ 10,000)。

输出

对于每个测试用例,在一行上打印对数。

我的 C++ 代码:

int main() {
    int T,n,i,j,k,count;
    int a[10000];
    cin>>T;

for(k=0;k<T;k++)
 {   count=0;
     cin>>n;
    for(i=0;i<n;i++)
    {
      cin>>a[i];
    }
    for(i=0;i<n;i++)
    {
        for(j=i;j<n;j++)
        {
          if(i!=j)
          {
            if(abs(a[i]-a[j])<32)
                count++;
          }
        }
    }
    cout<<count<<endl;
 }
    return 0;
}

我需要帮助如何用更有效的算法解决它?

【问题讨论】:

  • 我认为对输入进行排序会有所帮助。这样你就不必每次都做 O(n^2)。

标签: c++ algorithm


【解决方案1】:

尽管我之前的(愚蠢的)回答,根本不需要对数据进行排序。相反,您应该计算数字的频率。

那么您需要做的就是跟踪要配对的可行数字的数量,同时迭代可能的值。抱歉没有 c++,但 java 也应该是可读的:

int solve (int[] numbers) {                                                 
    int[] frequencies = new int[10001];                                     
    for (int i : numbers) frequencies[i]++;                                 
    int solution = 0;                                                       
    int inRange = 0;                                                        
    for (int i = 0; i < frequencies.length; i++) {                          
        if (i > 32) inRange -= frequencies[i - 32];                         
        solution += frequencies[i] * inRange;                               
        solution += frequencies[i] * (frequencies[i] - 1) / 2;              
        inRange += frequencies[i];                                           
    }                                                                        
    return solution;                                                         
}    

【讨论】:

  • 你的想法是正确的频率数组是最好的方法,但你的代码输出不正确
  • 您认为我的代码在什么输入上失败了?因为我测试的所有内容(包括您链接中提供的示例)似乎都给出了正确的结果。
  • 抱歉 Stef 我错了,您的解决方案完美运行您能否更详细地解释您的算法以供参考。
  • 你能解释一下从这个循环到结束的代码行吗for (int i = 0; i &lt; frequencies.length; i++)
【解决方案2】:
#include <bits/stdc++.h> 
using namespace std;

int a[10010];
int N;
int search (int x){
int low = 0;
int high = N;
while (low < high)
{
    int mid = (low+high)/2;
    if (a[mid] >= x) high = mid;
    else low = mid+1;
}
return low;
}

int main() {
    cin >> N;
    for (int i=0 ; i<N ; i++) cin >> a[i];
    sort(a,a+N);
    long long ans = 0;
    for (int i=0 ; i<N ; i++)
    {
        int t = search(a[i]+32);
        ans += (t -i - 1);
    }

    cout << ans << endl;
    return 0;
}

【讨论】:

    【解决方案3】:

    您可以对数字进行排序,然后使用滑动窗口。从最小的数字开始,用不大于最小数字 + 31 的数字填充std::deque。然后在每个数字的外循环中,更新滑动窗口并添加新的滑动窗口大小到柜台。滑动窗口的更新可以在内循环中进行,首先pop_front每一个小于外循环当前数的数,然后push_back每一个不大于外循环当前数的数+ 31。

    【讨论】:

    • 你也可以用频率数组或二分搜索来解决它
    【解决方案4】:

    一种更快的解决方案是首先对数组进行排序,然后遍历排序后的数组,并且对于每个元素,仅访问其右侧的元素,直到差值超过 31。

    排序可能通过计数排序来完成(因为你有 1 ≤ V[i] ≤ 10,000)。所以你得到排序部分的线性时间。不过可能没有必要(也许快速排序就足以获得所有分数)。

    此外,您可以为内部循环(“转到当前元素的右侧”部分)做一个技巧。请记住,如果 S[i+k]-S[i]

    【讨论】:

      【解决方案5】:

      这可以通过对数据进行恒定次数的传递来完成,并且实际上可以在不受“间隔”值(在您的情况下为 32)的影响下完成。 这是通过填充一个数组来完成的,其中a[i] = a[i-1] + number_of_times_i_appears_in_the_data - 非正式地,a[i] 包含小于/等于i 的元素总数。

      代码(针对单个测试用例):

      static int UPPER_LIMIT = 10001;
      static int K = 32;
      int frequencies[UPPER_LIMIT] = {0}; // O(U)
      int n;
      std::cin >> n;
      for (int i = 0; i < n; i++) { // O(n)
       int x;
       std::cin >> x;
       frequencies[x] += 1;
      }
      for (int i = 1; i < UPPER_LIMIT; i++) { // O(U)
          frequencies[i] += frequencies[i-1];
      }
      int count = 0;
      for (int i = 1; i < UPPER_LIMIT; i++) { // O(U)
        int low_idx = std::max(i-32, 0);
        int number_of_elements_with_value_i = frequencies[i] - frequencies[i-1];
        if (number_of_elements_with_value_i == 0) continue;
        int number_of_elements_with_value_K_close_to_i =
            (frequencies[i-1] - frequencies[low_idx]);
        std::cout << "i: " << i << " number_of_elements_with_value_i: " << number_of_elements_with_value_i << " number_of_elements_with_value_K_close_to_i: " << number_of_elements_with_value_K_close_to_i << std::endl;
        count += number_of_elements_with_value_i * number_of_elements_with_value_K_close_to_i;
        // Finally, add "duplicates" of i, this is basically sum of arithmetic 
        // progression with d=1, a0=0, n=number_of_elements_with_value_i
        count += number_of_elements_with_value_i * (number_of_elements_with_value_i-1) /2;
      }
      std::cout << count;
      

      IDEone 上的完整示例。

      【讨论】:

        【解决方案6】:

        您可以排序,然后在范围超出时使用 break 来结束循环。

        int main()
        {
            int t;
            cin>>t;
        
            while(t--){
                int n,c=0;
                cin>>n;
        
                int ar[n];
                for(int i=0;i<n;i++)
                    cin>>ar[i];
        
                sort(ar,ar+n);
        
                for(int i=0;i<n;i++){
                    for(int j=i+1;j<n;j++){
                        if(ar[j]-ar[i] < 32)
                            c++;
                        else
                            break;
                    }
                }
                cout<<c<<endl;
            }
        }
        

        或者,您可以使用哈希数组作为范围并标记每个元素的出现,然后循环并检查每个元素,即 x = 32 - y 是否存在。

        【讨论】:

        • 感谢您的回答,但它返回 Time limit Exceeded 因为它的复杂性是 O(n^2)。
        【解决方案7】:

        这里的一个好方法是将数字分成单独的桶:

        constexpr int limit = 10000;
        constexpr int diff = 32;
        constexpr int bucket_num = (limit/diff)+1;
        std::array<std::vector<int>,bucket_num> buckets;
        
        cin>>n;
        int number;
        for(i=0;i<n;i++)
        {
            cin >> number;
            buckets[number/diff].push_back(number%diff);
        }
        

        显然,在同一个桶中的数字彼此足够接近以满足要求,所以我们可以计算所有对:

        int result = std::accumulate(buckets.begin(), buckets.end(), 0,
                          [](int s, vector<int>& v){ return s + (v.size()*(v.size()-1))/2; });  
        

        不相邻的桶中的数字不能形成任何可接受的对,所以我们可以忽略它们。

        这留下了最后一个极端情况 - 相邻的桶 - 可以通过多种方式解决:

        for(int i=0;i<bucket_num-1;i++)
           if(buckets[i].size() && buckets[i+1].size())
              result += adjacent_buckets(buckets[i], buckets[i+1]);
        

        我个人喜欢单桶规模的“出现频率”方法,但可能有更好的选择:

        int adjacent_buckets(const vector<int>& bucket1, const vector<int>& bucket2)
        {
            std::array<int,diff> pairs{};
            for(int number : bucket1)
             {
                 for(int i=0;i<number;i++)
                    pairs[i]++;
             }
        
            return std::accumulate(bucket2.begin(), bucket2.end(), 0,
                                   [&pairs](int s, int n){ return s + pairs[n]; }); 
        }
        

        此函数首先构建一个“下层桶中与i 足够接近的数字”的数组,然后将该数组中与上层桶号对应的值相加。

        一般来说,这种方法的复杂度为 O(N),在最好的情况下,它几乎只需要一次通过,总体上应该足够快。

        Working Ideone example

        【讨论】:

        • 感谢您的努力,也是一个具有 O(n) 时间复杂度的不错的解决方案。
        【解决方案8】:

        这个解决方案可以被认为是 O(N) 来处理 N 个输入数字,并且处理输入的时间是常数:

        #include <iostream>
        using namespace std;
        
        void solve()
        {
            int a[10001] = {0}, N, n, X32 = 0, ret = 0;
            cin >> N;
            for (int i=0; i<N; ++i)
            {
                cin >> n;
                a[n]++;
            }
        
            for (int i=0; i<10001; ++i)
            {
                if (i >= 32)
                    X32 -= a[i-32];
                if (a[i])
                {
                    ret += a[i] * X32;
                    ret += a[i] * (a[i]-1)/2;
                    X32 += a[i];
                }
            }
            cout << ret << endl;
        }
        
        int main()
        {
            int T;
            cin >> T;
            for (int i=0 ; i<T ; i++)
                solve();
        }
        

        run this code on ideone

        解决方案说明:a[i] 表示i 在输入序列中出现的次数。 然后你遍历整个数组,X32 会跟踪范围从i 的元素数量。唯一棘手的部分是在多次重复某些 i 时正确计算:a[i] * (a[i]-1)/2。就是这样。

        【讨论】:

        • @MohammadOghli 此解决方案是否返回预期结果?
        • 这个 O(1) 怎么样?它有一个从 0 到 N-1 的循环
        • 我认为你是对的,有 O(N) 来处理 N 个输入数字。我更新了答案。我认为反向循环不会以任何方式影响复杂性。
        【解决方案9】:

        您应该首先对输入进行排序。

        如果你的内部循环检测到距离超过 32,你可以从中中断。

        【讨论】:

        • 如果有许多相等的数字,那仍然会很慢,仍然是 O(N^2)。
        【解决方案10】:

        感谢大家为解决这个问题付出的努力和时间。

        感谢所有尝试解决它。​​

        经过在线法官的测试,我发现正确和最有效的解决方案算法是Stef's Answer和AbdullahAhmedAbdelmonem's answer,pavel解决方案也是正确的,但它与不同语言C++的Stef解决方案完全相同。

        Stef 的代码在 codeforces 在线判断中得到了执行时间 358 ms 并被接受。

        同时 AbdullahAhmedAbdelmonem 的代码在 codeforces 在线判断中获得了执行时间 421 ms 并被接受。

        如果他们对那里的算法进行详细解释,那么赏金将是其中之一。

        选择问题E. Time Limit Exceeded?E. Time Limit Exceeded?后,您可以在此链接尝试您的解决方案并提交给codeforces在线评委

        我还发现了一个很棒的算法解决方案,使用频率数组更容易理解,它的复杂度为 O(n)。

        在此算法中,您只需将每个插入元素的特定范围带到数组中:

        begin = element - 32  
        end = element + 32
        

        然后计算频率数组中每个插入元素在此范围内的对数:

        int main() {
            int T,n,i,j,k,b,e,count;
            int v[10000];
            int freq[10001];
        
            cin>>T;   
         for(k=0;k<T;k++)
         { 
            count=0;
            cin>>n;
            for(i=1;i<=10000;i++)
            {
              freq[i]=0;
            }
            for(i=0;i<n;i++)
            {
             cin>>v[i];
            }   
            for(i=0;i<n;i++)
            {
             count=count+freq[v[i]];
        
             b=v[i]-31;
             e=v[i]+31;
        
             if(b<=0)
                 b=1;
             if(e>10000)
                e=10000;
        
             for(j=b;j<=e;j++)
              {
                freq[j]++;
              }
            }
            cout<<count<<endl;
         }
            return 0;
        }
        

        最后我认为解决此类问题的最佳方法是使用频率数组并计算特定范围内的对数,因为它的时间复杂度是 O(n)。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-09-27
          • 1970-01-01
          • 1970-01-01
          • 2016-09-11
          • 2021-01-30
          • 1970-01-01
          相关资源
          最近更新 更多