牛客網暑期ACM多校訓練營(第一場) J題詳解
牛客網J題在比賽時是通過率最高的一道題,但是這道題對於時間的複雜度要求比較高。在比賽的時候,很多隊伍提交的程式都以”執行超時“而結束。那就讓我們先來看看這道看似簡單的題。
Different Integers
題目描述
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.
示例1:
輸入
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<iostream>
#include<vector>
using namespace std;
int main()
{
//取消cin與stdin的同步
ios::sync_with_stdio(false);
int n, q, num;
int r[10];
vector<int>res;
while (cin >> n >> q)
{
//二維vector陣列,存放首位置和末位置
vector<vector<int>> 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;
//如果首位置<left||末位置>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 <iostream>
#include<vector>
#include<algorithm>
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<int> 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<Region> 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次,這裡的時間複雜度就是。這樣的做法肯定是不行的。接著就想到了用容器記錄元素的首次出現的位置和末位置,但是最後我還是遍歷了容器,時間複雜度沒有減少。引入樹狀陣列後,就可以將時間複雜度從降低到。主要的難點在於樹狀陣列的維護,利用樹狀陣列降低時間複雜度。