1. 程式人生 > >牛客網暑期ACM多校訓練營(第一場) J題詳解

牛客網暑期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%,時間複雜度還是在n^{2}

#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的程式碼,其中用了樹狀陣列,將時間複雜度從n^{2}降低到了nlog(n)。還不會樹狀陣列的童鞋可以先看看樹狀陣列詳解

整體的思路差不多,在輸入數列時,同時記錄元素的首次出現的位置和最後出現的位置。輸入區間後,將區間的邊界值和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次,這裡的時間複雜度就是n^{2}。這樣的做法肯定是不行的。接著就想到了用容器記錄元素的首次出現的位置和末位置,但是最後我還是遍歷了容器,時間複雜度沒有減少。引入樹狀陣列後,就可以將時間複雜度從n^{2}降低到nlog(n)。主要的難點在於樹狀陣列的維護,利用樹狀陣列降低時間複雜度。