#include <iostream> #include <vector> #include <map> #define int long long using namespace std; const int mod = 1e9 + 7; signed main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int t; cin >> t; while(t --) { int n; cin >> n; vector<bool> b(n + 1, true); vector<int> a(n + 1, 0); map<int,int> mp; for(int i = 1; i <= n; i ++) { cin >> a[i]; mp[a[i]] ++; } for(int i = 1; i <= n; i ++) { if(mp[a[i]] >= 2) b[i] = false; } int l = 0, r = 0; int len = 0; for(int i = 1; i <= n; i ++) { if(b[i]) { for(int j = i; j <= n; j ++) { if(b[j]) { if(j - i + 1 > len) { len = j - i + 1; l = i; r = j; } } else { i = j; break; } } } } if(len == 0) cout << 0 << '\n'; else cout << l << ' ' << r << '\n'; } }这段代码的时间复杂度是多少
首先,代码的大致结构是:处理多个测试用例(t次循环),每个测试用例中,输入一个长度为n的数组a,然后进行一些处理,最后输出结果。
让我一步步拆解代码:
输入处理部分:
ios::sync_with_stdio(false); cin.tie(0);
这些是加速输入输出的语句,不影响时间复杂度。- 读取t,然后进入t次循环。
每个测试用例内的操作:
读取n,然后创建两个vector:b和a。b的大小是n+1,初始化为true。a的大小是n+1,之后读取n个元素存入a数组。
接下来,使用一个map
mp
统计每个元素在a中出现的次数。遍历数组a,每个元素a[i]的计数增加,这一步的时间是O(n)的,因为每个元素被处理一次,而map的插入和更新操作平均是O(1)(假设使用哈希表实现的unordered_map,但这里用的是map,即红黑树实现的,所以每次插入是O(log n)。不过题目中用的是map,所以这部分的时间复杂度是O(n log n))。然后,再次遍历数组a,检查每个元素的出现次数是否大于等于2。如果是,就将对应的b[i]设为false。这一步是O(n)时间,因为遍历一次数组,每次操作是O(1)。
寻找最长连续子数组部分:
- 初始化变量l, r, len。
- 外层循环从i=1到n。如果当前b[i]为true,则进入内层循环,从j=i开始,一直到n。如果b[j]为true,就检查j-i+1是否比当前len大,更新len、l、r。如果遇到b[j]为false,就将i设置为j,并跳出内层循环,继续外层循环。
这里的循环结构需要仔细分析。外层循环变量i从1到n,但内层循环中,当遇到b[j]为false时,i会被设置为j,然后break,这样外层循环的i会跳到j的位置继续。假设最坏情况下,所有b[i]都是true,那么外层循环的i会逐个增加,而内层循环j从i到n,总次数是O(n^2)。如果b数组中存在很多连续的true段,那么总的循环次数可能接近O(n^2)。例如,如果数组b全为true,那么内层循环每次都会执行到n,总次数是n + (n-1) + ... +1 = O(n²)。因此,这部分的时间复杂度在最坏情况下是O(n²)。
综合以上步骤,每个测试用例的时间复杂度由三个部分组成:
- 统计频率的O(n log n)(因为使用map)。
- 处理b数组的O(n)。
- 寻找最长子数组的O(n²)。
所以总的时间复杂度是这三个部分的最大值,即O(n²)。因为当n很大时,O(n²)会主导整个时间。
不过,我需要再仔细检查一下第三部分的循环是否正确。例如,当i被设置为j后,外层循环的下一个i会是j+1吗?因为外层循环是for(int i=1; i <=n; i++)。假设在内部循环中i被设置为j,然后break,之后外层循环的i会自增到j+1吗?
例如,假设当前i=1,进入内层循环,j从1到n,假设在某个j=k时b[k]为false,那么i会被设置为k,然后break。此时,外层循环的i在本次循环结束后,会执行i++,也就是i=k+1。这样,外层循环的i每次跳过了多个元素,所以这部分的时间复杂度实际上是O(n),而不是O(n²)。比如,假设每个i对应的内层循环只执行到某个点,然后i被跳转到后面的位置,这样总体循环次数是O(n)的。
这里可能存在理解错误。例如,假设对于每个i,内层循环运行直到遇到第一个false的位置j,然后i被设置为j,之后外层循环的i会递增到j+1。因此,整个过程中,每个元素最多被访问两次:一次作为i的起始点,另一次作为j的位置。这可能导致总体时间复杂度是O(n)的。或者,是否存在这样的情况?
举个例子,假设数组b中有一段连续的true,长度为m。那么对于这段连续的true,外层循环的i会从该段的起点开始,内层循环j从i到该段的终点,然后i被设置为该段的终点,break。外层循环的i递增到终点+1。这样,这段连续的true会被处理一次,整个循环的时间复杂度是O(n)。因为每个元素只被处理一次。
因此,这部分的时间复杂度实际上是O(n),而不是O(n²)。那这里我之前分析有误?
让我再仔细看一下代码的结构:
外层循环是for(int i=1; i <=n; i++)。在每次循环中,当i的b[i]为true时,进入内层循环。内层循环从j=i开始,逐个检查b[j]。当遇到b[j]为false时,将i设为j,然后break。这样,外层循环的下一次i会是j+1吗?
因为外层循环是for循环,i的值在每次循环结束后会自增。例如,假设当前i=1,进入循环体,执行到i=j(比如j=5),然后break。接着,外层循环的i会变成j+1吗?
不,不是的。因为for循环的第三个表达式(i++)会在每次循环体结束后执行。例如:
假设当前i=1,执行循环体中的代码,在循环体内将i设置为j=5,然后break。此时,循环体结束,然后i会执行i++,变成5+1=6。因此,外层循环的下一次i是6,而不是原来的i=1的下一个i=2。这样,这段代码的外层循环实际上只会遍历每个可能的连续true块的起始位置,而每个块只会被处理一次。因此,总的循环次数是O(n)的。
例如,假设数组b的结构是:一段true,接着一段false,再一段true,等等。那么外层循环i会依次跳到每个true段的起始位置,处理该段,然后i会被设置为该段的结束位置+1,从而跳过中间的部分。因此,整个处理过程的时间复杂度是O(n),因为每个元素最多被访问两次:一次在i的起始点,一次在j的循环中。
因此,这部分的时间复杂度实际上是O(n),而不是O(n²)。那这样的话,整个程序的时间复杂度主要取决于统计map的部分,也就是O(n log n),因为后面的部分是O(n)。
那这样,总的时间复杂度应该是每个测试用例的O(n log n + n) = O(n log n)。但是,这里可能存在错误,需要重新验证。
或者,我是不是哪里弄错了?
让我们再仔细分析循环部分:
例如,考虑下面的代码结构:
for (i=1; i<=n; i++) { if (b[i]) { for (j=i; j<=n; j++) { if (b[j]) { // do something } else { i = j; break; } } // after break, i is j // then, the for loop's i++ will make i = j +1 } }
假设当前i=1,并且b[1]是true。进入内层循环,j从1开始,直到找到一个j使得b[j]为false,例如j=5。此时,设置i=5,break。外层循环的i会自增到5+1=6。因此,下一次循环i=6。因此,中间的那些i=2,3,4,5都被跳过了。
所以,每个连续为true的块只会被处理一次。对于每个连续的true块,内层循环的运行次数等于该块的长度。例如,块长度为m,内层循环执行m次。而所有块的总长度等于整个数组中true的元素的数量。假设最坏情况下,所有元素都是true,那么内层循环的总次数是n次(因为j从i到n,执行n-i+1次,但此时i会被设置为n,然后外层循环i++变成n+1,结束循环)。所以总的内层循环次数是O(n)。
因此,这部分的时间复杂度是O(n)。那这样的话,整个测试用例的时间复杂度由map的统计部分O(n log n)主导。加上后面的处理O(n),所以总的时间复杂度是O(n log n)。
那之前的分析错误在于认为这部分是O(n²),但实际上通过i的跳跃,这部分是O(n)。所以,总的时间复杂度是O(n log n) per test case。
那这样的话,每个测试用例的时间复杂度是O(n log n),而总的时间复杂度是O(t * n log n),其中t是测试用例的数量。
那么,现在的问题是,原代码的时间复杂度是多少?
现在需要确认几个点:
map的统计部分:每次插入a[i]的次数。因为map是基于红黑树的,所以每次插入和查找的时间是O(log k),其中k是当前map中的元素数目。最坏情况下,所有元素都不同,所以k = n,每次操作是O(log n)。因此,统计部分的时间复杂度是O(n log n)。
处理b数组的循环是O(n)。
寻找最长子数组的部分,通过上述分析是O(n)。
因此,总的时间复杂度是O(n log n) + O(n) + O(n) = O(n log n) per test case。
那原代码的时间复杂度应为O(t * n log n)。对吗?
那用户的问题是,这段代码的时间复杂度是多少?
所以正确的答案应该是,每个测试用例的时间复杂度是O(n²),还是O(n log n)?
这时候需要重新审视循环部分,是否真的如我之前分析的那样是O(n)。
或者,我是否在分析中哪里出错了?
让我们举一个例子,假设n=5,数组b全为true:
i=1进入循环,内层循环j从1到5,全部处理。此时,在j循环结束后,i仍为1,因为当内层循环结束后,没有执行到i=j的代码(因为所有b[j]都是true,所以不会进入else分支)。因此,内层循环执行完后,i仍然是1。然后,外层循环的i++,i变成2。
此时,i=2再次进入循环,内层循环j从2到5,执行处理。这将导致内层循环执行了4次(j=2到5),而总的时间复杂度在这种情况下是O(n²),因为每个i都需要执行j从i到n的循环。
哦,这似乎与之前的结论相矛盾。这时候,我的分析出现了错误。这时候,假设所有b[i]都是true,那么当处理i=1时,内层循环j从1到n,然后循环结束后,由于没有遇到else分支,i没有被修改。因此,外层循环i++后,i=2,再次进入循环,处理j从2到n。此时,内层循环每次都会执行到n,导致总次数为n + (n-1) + ... +1 = O(n²)。
这时候,这种情况下的时间复杂度确实是O(n²)。那这与我之前的分析矛盾了,这时候必须重新审视代码。
哦,问题出在代码的结构上。当内层循环处理完所有j=i到n的true元素时,即整个子数组都是true的情况,此时内层循环的j会一直走到n,然后循环结束。此时,外层循环的i仍然是原来的i,之后i会自增1,进入下一次循环。这会导致,当整个数组都是true时,外层循环i从1到n,每个i都会进入内层循环,j从i到n,导致O(n²)的时间复杂度。此时,这部分的时间复杂度在最坏情况下是O(n²)。
那之前的分析错误在于,假设每次进入内层循环后,i会被调整,从而跳过后续的元素。但实际情况是,当所有b[i]都是true时,内层循环不会触发else分支,因此i不会被修改,外层循环的i会正常递增,导致每个i都要处理,从而内层循环的时间复杂度为O(n²)。
这说明之前的分析是错误的。这时候,正确的做法是重新分析这段代码的循环结构:
代码段:
for(int i = 1; i <= n; i ++) { if(b[i]) { for(int j = i; j <= n; j ++) { if(b[j]) { // 更新len, l, r } else { i = j; // 这里修改了外层循环的i break; } } // 这里,如果内层循环因为j到达n而结束,则不会执行i=j的操作 } }
当处理一个连续的true块时,例如从i到m都是true,那么当j循环到m+1时,可能发现b[m+1]是false,这时i会被设置为m+1,break,然后外层循环的i会自增为m+2。因此,处理这个连续块的时间是O(m)次循环,然后i跳过了整个块。
但是如果整个数组都是true,那么对于每个i,内层循环j从i到n都会执行,并且不会触发else分支。因此,每个i对应的内层循环都会运行n - i + 1次,导致总的时间复杂度为O(n²)。
所以,这部分的时间复杂度在最坏情况下是O(n²),比如当所有元素都是true时,而这种情况是可能的。例如,当数组中的所有元素都是唯一的,那么mp[a[i]]的值都是1,此时b数组中的每个元素都是true。这时候,代码的第三部分将遍历所有可能的i,每个i对应j从i到n,导致O(n²)的时间复杂度。
因此,正确的总时间复杂度应该是:每个测试用例的时间复杂度为O(n² + n log n),即O(n²),因为n²在n较大时比n log n增长得更快。
此时,统计map的部分是O(n log n),处理b数组是O(n),寻找最长子数组是O(n²)。因此,总时间复杂度是O(n²) per test case.
那最终的结论是,这段代码的时间复杂度是O(t * n²),其中t是测试用例的数量,而每个测试用例的n是该用例的输入大小。
因此,用户提供的代码的时间复杂度是O(t * n²),其中t是测试用例的个数,n是每个测试用例的数组长度。该代码的时间复杂度为$O(t \cdot n^2)$,其中$t$表示测试用例数量,$n$表示每个测试用例的数组长度。以下是具体分析步骤:
关键步骤解析
输入处理与统计频率
map<int, int> mp; for (int i = 1; i <= n; i++) { cin >> a[i]; mp[a[i]]++; }
- 使用
map
统计元素频率,时间复杂度为$O(n \log n)$(map
基于红黑树,每次插入/查询为$O(\log n)$)。
- 使用
标记唯一性
for (int i = 1; i <= n; i++) { if (mp[a[i]] >= 2) b[i] = false; }
- 遍历数组标记非唯一元素,时间复杂度为$O(n)$。
寻找最长连续唯一子数组
for (int i = 1; i <= n; i++) { if (b[i]) { for (int j = i; j <= n; j++) { if (b[j]) { /* 更新长度 */ } else { i = j; // 跳转到非唯一元素位置 break; } } } }
- 最坏情况(所有元素唯一,即
b[i]
全为true
):
外层循环遍历每个$i$,内层循环从$i$到$n$,总时间复杂度为$O(n^2)$。
- 最坏情况(所有元素唯一,即
时间复杂度总结
- 主要瓶颈:寻找最长子数组的双重循环。
- 公式推导:
$$T(n) = O(n \log n) + O(n) + O(n^2) = O(n^2)$$
对于$t$个测试用例,总复杂度为$O(t \cdot n^2)$。
优化建议
若需优化至$O(n)$或$O(n \log n)$,可:
- 改用哈希表:如
unordered_map
统计频率,将统计部分复杂度降至$O(n)$。 - 单次遍历找最长子数组:通过记录当前连续段的起点,避免双重循环。
总结:代码的时间复杂度主要由双重循环主导,实际应用中需注意数据规模以避免超时。
相关推荐

















