#include <iostream> #include <vector> #include <algorithm> #define int long long using namespace std; signed main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int t; cin >> t; while(t --) { int n, m; cin >> n >> m; vector<int> a(m, 0); for(int i = 0; i < m; i ++) cin >> a[i]; sort(a.begin(), a.end()); int ans = 0; for(int i = 0; i < m; i ++) { int k = n - a[i]; // 后面的元素至少要出几个 int l = i + 1, r = m; while(l < r) { int mid = (l + r) >> 1; if(a[mid] >= k) r = mid; else l = mid + 1; } for(int j = r; j < m; j ++) { int tmp = n - a[j]; // a[i]自己至少要出几个 int cnt = a[i] - tmp + 1; ans = ans + (cnt * 2); } } cout << ans << '\n'; } return 0; }优化一下这段代码的时间复杂度
时间: 2025-03-18 14:04:20 浏览: 59
<think>好的,我现在要帮用户优化这段代码的时间复杂度。首先,我得仔细看看这段代码是做什么的。看起来这是一个处理数组的问题,可能是计算某种配对的数量或者总和?代码结构大致是读取t个测试用例,每个用例里处理n和m,然后读取数组a,排序之后进行一些计算。
用户给出的代码中,外层循环遍历每个元素a[i],然后对于每个a[i],用二分查找找到r的位置,之后从r到m-1遍历j,计算tmp和cnt,然后累加到ans。这部分的内层循环可能会带来较高的时间复杂度,特别是当m很大的时候,比如m=1e5,这样的双重循环会是O(m^2),显然无法通过时间限制。
我需要分析这个内层循环的作用。假设对于每个i,找到j的起始位置r,然后对每个j >= r,计算cnt = a[i] - (n - a[j]) +1,然后ans += cnt*2。这里可能是在计算满足某种条件的配对,然后将它们的贡献乘以2累加。
首先,我需要明确这里的条件是什么。对于每个i和j(i < j),可能a[i]和a[j]需要满足某种关系。根据代码中的k = n - a[i],然后二分查找找到第一个j的位置r,使得a[j] >=k。这意味着对于j >= r,a[j] >=n -a[i]。同时,在计算tmp的时候,tmp =n -a[j],而a[i]自己至少要出tmp,所以a[i] >= tmp,即a[i] >=n -a[j]。这可能涉及到双方的某种条件,比如a[i] +a[j] >=n?
但这里可能需要更仔细地推导数学关系。假设对于每个i < j,如果a[i] + a[j] >=n,那么这对的贡献是某个值。或者可能,这里的问题要求的是当两个元素满足各自的条件时,计算它们的贡献总和。
现在,假设原题的问题描述是:给定数组a,求所有满足i<j且a[i]+a[j]>=n的有序对的某种贡献总和。或者可能是其他条件,比如a[i] >=k和a[j] >=某个值。需要根据代码的逻辑来推断原问题的条件。
在用户的代码中,对于每个i,k =n -a[i],然后找到r的位置是第一个j的位置使得a[j] >=k。然后对于每个j >=r,计算tmp =n -a[j],也就是这个j对应的要求a[i] >=tmp。因为tmp =n -a[j],所以a[i] >=n -a[j] → a[i] +a[j] >=n。这可能意味着在i<j的情况下,当a[i]+a[j] >=n时,这对的贡献是cnt*2,其中cnt= a[i] - (n -a[j]) +1 =a[i] +a[j] -n +1。
所以,每个符合条件的i<j的对的贡献是2*(a[i] +a[j] -n +1)。这可能对应于原问题中每个这样的对需要计算两个方向的贡献,比如x和y各有一种情况,所以乘以2。或者可能题目中的贡献计算本身需要乘以2,比如每个对有两个情况。
现在的问题是如何优化这个双重循环,因为原来的代码对于每个i,遍历从r到m-1的所有j,这导致时间复杂度为O(m log m + m^2),当m很大时,比如1e5,这会超时。
我需要找到一种方法,将内层的循环转化为数学表达式,从而可以用前缀和或双指针等方法,将时间复杂度降低到O(m log m)或O(m)。
首先,观察到数组已经排序。因为一开始对a进行了排序,所以a是递增的。然后对于每个i,k =n -a[i],找到r是第一个j >=i+1的位置,使得a[j] >=k。那么对于每个j >=r,都满足a[j] >=k → a[j] >=n -a[i]。同时,在这些j中,计算cnt =a[i] - (n -a[j]) +1 =a[i] +a[j] -n +1,然后累加这个值的两倍到ans中。
所以,对于每个i,我们需要计算所有j >=r的(a[i]+a[j]-n+1)之和,然后乘以2。原来的代码对每个i和j都进行遍历,导致O(m^2)的时间复杂度。
要优化这个部分,可以将每个i对应的j的范围找到后,计算这部分的和,而不用逐个遍历j。例如,对于每个i,找到r的位置,然后对于j >=r到m-1,计算sum_{j=r}^{m-1} (a[i]+a[j] -n +1)。这个和可以分解为sum_{j=r}^{m-1} (a[i] -n +1) + sum_{j=r}^{m-1} a[j]。这两个部分都可以用前缀和数组来快速计算。
具体来说,假设我们预先计算了a数组的前缀和数组pre_sum,其中pre_sum[j]表示前j个元素的和(或者从某个起点开始)。那么sum_{j=r}^{m-1} a[j]就等于pre_sum[m] - pre_sum[r]。而sum_{j=r}^{m-1} (a[i]-n+1)等于 (a[i] -n +1) * (m - r)。这样,整个内层循环的总和就可以快速计算,而不需要遍历每个j。
因此,优化后的代码对于每个i,只需要O(1)的时间来计算这部分的和,从而将总时间复杂度降为O(m log m),因为每个i的二分查找是O(log m)的时间,而每个i的计算是O(1)。
所以,具体步骤如下:
1. 预处理前缀和数组pre_sum,其中pre_sum[i]表示数组a从索引0到i-1的和。例如,pre_sum[0]=0,pre_sum[1]=a[0],pre_sum[2]=a[0]+a[1],依此类推。这样,sum a[j]从r到m-1等于pre_sum[m] - pre_sum[r]。
2. 对于每个i,计算k =n -a[i],然后用二分查找找到r的位置,即第一个j >=i+1且a[j] >=k的位置。
3. 计算当前i对应的有效j的数量cnt_j =m -r。然后,总和中的第一部分是cnt_j * (a[i] -n +1),第二部分是pre_sum[m] - pre_sum[r]。将这两部分相加后乘以2,累加到ans中。
这样,每个i的处理时间变为O(log m),总时间复杂度为O(m log m)。
现在需要检查原代码是否有其他问题。例如,原代码中,当找到r之后,循环j从r到m-1,但原代码中的循环变量j的范围是否正确?原代码中的数组a是排序后的,所以是正确的。
另外,原代码中的二分查找是否正确?原代码中,初始的l是i+1,r是m。在二分查找中,当a[mid] >=k时,r=mid,否则l=mid+1。最终,r是找到的第一个>=k的位置。这部分是正确的。
现在,将内层循环替换为数学计算,并利用前缀和数组来加速。
因此,优化后的代码大致如下:
预处理前缀和数组:
vector<int> a(m);
sort后,计算pre_sum,其中pre_sum[0]=0,pre_sum[i] = pre_sum[i-1]+a[i-1]。
或者,可能索引需要调整。例如,pre_sum[0] =0,pre_sum[1]=a[0], pre_sum[2]=a[0]+a[1], ..., pre_sum[m] = sum_{0到m-1} a[i]。这样,sum a[j]从r到m-1是pre_sum[m] - pre_sum[r]。因为数组a的索引是0到m-1,而pre_sum的索引是0到m。所以在代码中,需要正确计算pre_sum数组。
例如:
vector<int> pre_sum(m + 1, 0);
for (int i = 0; i < m; ++i) {
pre_sum[i+1] = pre_sum[i] + a[i];
}
然后,对于每个i,当找到r之后,如果r >=m,那么没有j满足条件,贡献为0。否则,有效的j的数量是m -r。sum_a = pre_sum[m] - pre_sum[r]。sum_part = (a[i] -n +1) * (m -r) + sum_a。这部分的总和乘以2就是当前的贡献。
因此,原代码中的内层循环可以被替换为:
int cnt_j = m - r;
if (cnt_j <=0) continue;
int sum_part = (a[i] - n +1) * cnt_j + (pre_sum[m] - pre_sum[r]);
ans += sum_part * 2;
这样,整个时间复杂度就降到了O(m log m),因为每个i的二分查找是O(log m),预处理前缀和是O(m),总时间为O(m log m)。
现在,检查是否还有其他可能的优化。例如,是否可以利用双指针法来替代二分查找?因为数组是排序后的,对于i递增的情况,k =n -a[i]随着a[i]的递增而递减。因此,当i增加时,k减少,那么下一个r的位置可能在之前的位置的左边。这可能导致双指针的可能。
例如,初始时i=0,k =n -a[0]。找到r的位置。当i增加到1时,k=n -a[1],因为a是递增的,所以a[1] >=a[0],所以k会变小。此时,r的位置可能在之前找到的位置的左边。所以可以用一个指针r从m-1开始,逐渐左移。
这可能可以用双指针法,将寻找每个i的r的时间从O(log m)降到O(1)总时间,从而总时间复杂度降为O(m)。但需要确认是否可行。
比如,初始时,r =m(即初始的右边界)。然后,对于每个i从0到m-1,k =n -a[i]。因为数组是递增的,当i增大时,k减小,所以对于下一个i来说,r的位置应该小于等于当前的r。这样,可以维护一个指针r,初始为m,然后每次i增大时,将r左移到合适的位置。
例如,初始时r=m。然后,对于i=0,找到最大的j>=i+1的最小r,使得a[r]>=k。当i增加到1时,k=n -a[1] <=n -a[0],所以原来的r可能仍然有效,或者需要左移。
但具体来说,如何维护r的位置?
假设我们从i=0到i=m-1遍历,同时维护r为当前最小的索引,使得a[r] >=k_i(其中k_i =n -a[i])。由于数组a是递增的,当i增加时,a[i]增加,k_i减小。因此,对于i+1来说,k_{i+1}=n -a[i+1] <=k_i。因此,对应的r的位置应该不小于之前的r的位置,或者说可能需要更小的r?或者可能更左边?
举个例子:假设数组a排序后是 [1,2,3,4,5], n=5。当i=0时,a[i]=1,k=5-1=4,所以r是第一个j>=1(i+1=1)且a[j]>=4的位置,即j=3(a[3]=4)。当i=1时,a[i]=2,k=5-2=3,此时r需要找到第一个j>=2且a[j]>=3的位置,即j=2。所以在这种情况下,当i增加时,k减小,对应的r的位置可能左移(即更小)。这时候,如果原来的r在i=0时是3,那么当i=1时,需要从j>=2开始找,找到第一个>=3的位置。这时候,原来的r可能大于新的r的位置。
这说明,在这种情况下,随着i的增加,r的位置可能向左移动。因此,双指针方法可能可行,但需要以i为起点,或者需要从后向前处理?
或者,可能可以用两个指针,一个从i+1开始,另一个从m-1开始,寻找满足条件的r。
这可能比较复杂,因此可能还是用二分查找更简单,但时间复杂度已经足够优化。或者,是否可以将i从大到小处理,从而让k_i递增,这样r的位置可以随着i的减小而递增,从而可以用双指针?
比如,数组a是递增的,如果i从大到小遍历,那么a[i]递增,所以k_i =n -a[i]递减。这可能不太容易。或者,反过来,如果遍历i的顺序是逆序的,可能可以更有效地维护r的位置。但原题中的循环是i从0到m-1的顺序,所以可能需要调整。
综上,可能用二分查找的方法已经足够优化,将时间复杂度从O(m^2)降到O(m log m),而使用前缀和数组来加速内层循环的计算。
现在,需要检查原代码中的其他可能的错误或优化点。例如,原代码中的变量k =n -a[i],然后寻找j >=i+1的位置,且a[j]>=k。那么,当a[j] >=k时,也就是a[j] >=n -a[i]。而同时,在计算cnt的时候,tmp =n -a[j],所以a[i] >=tmp → a[i] >=n -a[j] → a[i] +a[j] >=n。这说明,对于i<j,当a[i] +a[j] >=n时,这对会被计算。但因为i<j,所以每个这样的对会被计算两次吗?或者原问题中的每个对是否需要被计算两次?
在原来的代码中,对于每个i和j(i<j),当满足条件时,贡献是cnt*2。这可能是因为i和j的顺序对调的情况,或者因为题目中每个对的贡献需要考虑两个方向。例如,当i和j满足条件时,i对j的贡献和j对i的贡献都要计算?但原代码中的循环结构是i从0到m-1,然后j从i+1到m-1,所以每个i<j的对会被处理一次。然后,在代码中,贡献是乘以2的。这可能表示,每个满足条件的i<j的对的贡献是2*(a[i] +a[j] -n +1)。或者,原问题可能存在某种对称性,例如两个元素之间的互动需要计算两次。
但不管怎样,原代码的逻辑是正确的,现在需要优化时间复杂度。
所以,总结优化步骤:
1. 预处理前缀和数组pre_sum。
2. 在循环每个i时,找到r的位置,计算对应的和,利用pre_sum数组快速求和,避免内层循环。
这样,修改后的代码的时间复杂度为O(m log m)。
现在,检查是否还有其他可能的优化点。例如,原代码中的排序是必要的吗?是的,因为后面的二分查找和前缀和都需要数组有序。
此外,是否可以将变量类型改为更合适的类型?例如,原代码中使用的是int long long,这可能是因为n和a的元素可能很大,导致结果溢出。所以这可能是必要的。
现在,编写优化后的代码:
首先,在读取数组a之后进行排序,并计算pre_sum数组。
然后,循环每个i,进行二分查找,计算对应的sum_part,然后累加到ans。
具体修改如下:
在代码中,在排序之后,计算pre_sum:
vector<int> a(m);
for(int i=0; i<m; i++) cin >>a[i];
sort(a.begin(), a.end());
vector<int> pre_sum(m+1, 0);
for(int i=0; i<m; i++) pre_sum[i+1] = pre_sum[i] + a[i];
然后,在循环i的时候:
for(int i=0; i<m; i++){
int k = n -a[i];
int l =i+1, r =m;
while(l <r){
int mid = (l +r)>>1;
if(a[mid] >=k) r=mid;
else l=mid +1;
}
int cnt_j = m -r;
if(cnt_j <=0) continue;
int sum_part = (a[i] -n +1)*cnt_j + (pre_sum[m] - pre_sum[r]);
ans += sum_part *2;
}
这样,时间复杂度就降到了O(m log m)。因为每个i的二分查找是O(log m),而其他操作都是O(1)。
需要注意的是,pre_sum的索引是否正确。例如,a的索引是0到m-1,而pre_sum的索引0到m,其中pre_sum[i]是前i个元素的和(即a[0]到a[i-1]的和)。因此,sum a[j]从r到m-1是pre_sum[m] - pre_sum[r],因为a[r]到a[m-1]共有m -r个元素,它们的和是pre_sum[m](前m个元素的和)减去pre_sum[r](前r个元素的和)。
这样,优化后的代码应该可以正确处理问题,并且时间复杂度大大降低。
现在,还需要测试这种优化是否正确。例如,原来的内层循环中,每个j对应的cnt是a[i] +a[j] -n +1,然后总和是sum (a[i] +a[j] -n +1) *2。而优化后的代码将sum拆分为(a[i] -n +1)的总和加上a[j]的总和,即sum (a[i]-n+1) + sum a[j] → 正确,因为每个项是(a[i]-n+1 +a[j])。因此,总和等于sum (a[i]+a[j] -n +1),所以优化后的代码是正确的。
因此,这种优化是正确的,可以显著降低时间复杂度。</think>我们可以通过数学推导和前缀和预处理将时间复杂度从 $O(m^2)$ 优化到 $O(m \log m)$。以下是分步说明及优化后的代码:
### 问题分析
原代码针对每个元素 $a[i]$,使用二分查找确定有效区间后,遍历计算区间内所有元素的贡献值。内层循环导致时间复杂度达到 $O(m^2)$。
### 优化思路
1. **前缀和预处理**:预先计算数组前缀和,将区间求和操作优化至 $O(1)$
2. **数学表达式转换**:将累加操作转换为数学表达式,利用前缀和快速计算区间和
3. **保持二分查找**:利用排序后数组的有序性,继续使用二分查找确定有效区间
### 数学推导
对于每个 $a[i]$,找到满足 $a[j] \geq n-a[i]$ 的最小 $j$ 后:
$$
\text{贡献值} = 2 \times \sum_{j=r}^{m-1} \left[ (a[i] + a[j] - n + 1) \right]
$$
可分解为:
$$
2 \times \left[ (a[i]-n+1) \times (m-r) + \sum_{j=r}^{m-1} a[j] \right]
$$
### 优化代码
```cpp
#include <iostream>
#include <vector>
#include <algorithm>
#define int long long
using namespace std;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int t;
cin >> t;
while(t--) {
int n, m;
cin >> n >> m;
vector<int> a(m);
for(int i = 0; i < m; ++i) cin >> a[i];
sort(a.begin(), a.end());
// 预处理前缀和数组
vector<int> pre_sum(m+1, 0);
for(int i = 0; i < m; ++i)
pre_sum[i+1] = pre_sum[i] + a[i];
int ans = 0;
for(int i = 0; i < m; ++i) {
int k = n - a[i];
// 二分查找右边界
int l = i + 1, r = m;
while(l < r) {
int mid = (l + r) >> 1;
if(a[mid] >= k) r = mid;
else l = mid + 1;
}
if(r >= m) continue; // 无有效元素
int cnt = m - r;
int sum_part = (a[i] - n + 1) * cnt + (pre_sum[m] - pre_sum[r]);
ans += sum_part * 2;
}
cout << ans << '\n';
}
return 0;
}
```
### 复杂度分析
1. **排序操作**:$O(m \log m)$
2. **前缀和预处理**:$O(m)$
3. **主循环**:每个元素的二分查找 $O(\log m)$,总时间 $O(m \log m)$
总时间复杂度从原始 $O(m^2)$ 优化到 $O(m \log m)$,可处理 $m \leq 10^5$ 规模的数据。
### 关键优化点
1. **前缀和数组**:将区间求和复杂度降为 $O(1)$
2. **表达式分解**:将双重循环转换为数学表达式计算
3. **保持二分查找**:利用有序性快速定位有效区间
该优化在保持算法正确性的前提下,显著提升了执行效率。
阅读全文
相关推荐


















