牛客网J题在比赛时是通过率最高的一道题,但是这道题对于时间的复杂度要求比较高。在比赛的时候,很多队伍提交的程序都以”运行超时“而结束。那就让我们先来看看这道看似简单的题。
Given a sequence of integers a1, a2, ..., an and q pairs of integers (l1, r1), (l2, r2), ..., (lq, rq), find count(l1, r1), count(l2, r2), ..., count(lq, rq) where count(i, j) is the number of different integers among a 1, a2, ..., ai, aj, aj + 1, ..., an.
The input consists of several test cases and is terminated by end-of-file. The first line of each test cases contains two integers n and q. The second line contains n integers a1, a2, ..., an. The i-th of the following q lines contains two integers li and ri.
For each test case, print q integers which denote the result.
* 1 ≤ n, q ≤ 105
* 1 ≤ ai ≤ n
* 1 ≤ li, ri ≤ n
* The number of test cases does not exceed 10.
3 2
1 2 1
1 2
1 3
4 1
1 2 3 4
1 3
2
1
3
输入长度为n的数列,且数列中的元素大小小于n。求[0,li]U[ri,n)区间内不同元素的个数。
分析
树状数组+离线处理进行维护
我的想法
在比赛时,我想的是在输入数列时记录下每个元素的位置。接着遍历这个数组,如果这个元素第一次出现的位置小于li或者最后一次出现的位置大于ri,则不同元素的个数+1。下面是我比赛时提交的代码,但是很遗憾只通过了50%,时间复杂度还是在。
#include
#include
using namespace std;
int main()
{
//取消cin与stdin的同步
ios::sync_with_stdio(false);
int n, q, num;
int r[10];
vectorres;
while (cin >> n >> q)
{
//二维vector数组,存放首位置和末位置
vector> v(n);
int k = 1;
for (int i = 0; i < n; i++)
{
cin >> num;
//向数组中加入位置
v[num - 1].push_back(k++);
}
int left, right, sum = 0;
for (int e = 0; e < q; e++)
{
sum = 0;
cin >> left >> right;
for (int i = 0; i < n; i++)
{
//如果数组为空 continue
if (!v[i].size())
continue;
//如果首位置right 个数++
if (v[i][0] <= left || v[i].back() >= right)
sum++;
}
//将个数加入数组中
res.push_back(sum);
}
}
//遍历输出
for (auto i : res)
cout << i << endl;
//system("pause");
}
在我的算法中,还是遍历了整个数组。赛后看了AC的代码,其中用了树状数组,将时间复杂度从降低到了。还不会树状数组的童鞋可以先看看树状数组详解。
整体的思路差不多,在输入数列时,同时记录元素的首次出现的位置和最后出现的位置。输入区间后,将区间的边界值和id存放在容器中,并依据右边界升序排序,接着对树状数组进行维护即可。count数组记录没有出现的元素个数,在元素第一次出现的位置之前的count[i]值加1。result数组记录最后答案,初始值为数列中元素的个数。将result数组中的值进行维护后,即可得到答案。
#include
#include
#include
using namespace std;
//用于存储左边界值和右边界值,id号
struct Region
{
int left, right, id;
};
//重载运算符 < , 依据右边界升序
bool operator < (const Region& u, const Region& v)
{
return u.right < v.right;
}
int main()
{
int n, q;
while (cin >> n >> q)
{
vector a(n), first(n, -1), last(n), count(n), result(q);
int total = 0;
for (int i = 0; i < n; ++i)
{
cin >> a[i];
a[i] --;
//记录元素最后出现的位置
last[a[i]] = i;
//如果元素之前未出现 则记录第一次出现的位置
if (first[a[i]] == -1)
{
total++;
first[a[i]] = i;
}
}
vector regions;
for (int i = 0, l, r; i < q; ++i)
{
cin >> l >> r;
//将区间和id加到容器中
regions.push_back(Region{ l - 1, r - 1, i });
}
//以区间的右边界 升序排序
sort(regions.begin(), regions.end());
for (int i = 0, k = 0; i < n; ++i)
{
/*==================================================================
树状数组维护
记录维护区间的次数k && 此区间右边界之前的区域全部维护完成后进入循环
===================================================================*/
while (k < q && regions[k].right == i)
{
//将result数组初始化为数列中不同元素的个数
//res引用
int& res = result[regions[k].id] = total;
/*===================================================
总数- 从该区间左边界至右边界,所有不出现的元素的个数
~j & j+1 +的优先级比&高
=====================================================*/
for (int j = regions[k].left; j < i; j += ~j & j + 1)
res -= count[j];
k++;
}
if (last[a[i]] == i)
{
/*===================================================
在该元素左端点以前都没有出现该元素
count数组用于统计没有出现的元素个数
====================================================*/
for (int j = first[a[i]] - 1; ~j; j -= ~j & j + 1)
count[j] ++;
}
}
for (auto i : result)
cout << i << endl;
}
//system("pause");
}
我读完题的第一反应就是遍历区间,用桶记录出现的次数。但是用桶就需要遍历2次,这里的时间复杂度就是。这样的做法肯定是不行的。接着就想到了用容器记录元素的首次出现的位置和末位置,但是最后我还是遍历了容器,时间复杂度没有减少。引入树状数组后,就可以将时间复杂度从降低到。主要的难点在于树状数组的维护,利用树状数组降低时间复杂度。