Luogu P4062 Yazid 的新生舞会(待填坑)

题目大意

(n) 个非负整数 (a_1,a_2,dots,a_n) ,求有多少个区间 ([l,r]) ,满足 (a_l,a_{l+1},dots,a_r) 中的众数出现次数大于 (dfrac{r-l+1}{2})
其中, (1 leq n leq 5 imes 10^5)(0 leq a_i leq n - 1)

题解

一、根号做法

来自初二dalao (color{black}{ extbf{l}}color{red}{ extbf{by}}) 的巧妙做法OrzOrzOrz。
真·比你小还比你强



观察部分分,当 (type = 2) 时,容易发现,满足条件的子区间长度不超过 (2 imes 15)
于是我们可以从 (1)(n) 枚举 (l) ,在从 (l)(l + 30 - 1) 枚举 (r) ,枚举时不断更新当前子区间的众数,并判断是否满足条件。
时间复杂度为 (O(30n))

继续观察部分分,当 (type = 3) 时( (type = 1) 的情况实际上是 (type = 3) 的情况的子集),不同 (a_i) 的数量很少,最多只有 (8) 种。
思考若一个区间众数为 (m) ,众数的数量为 (c(m)) ,若这个区间满足要求,则必须保证 (c(m)) 大于区间长度的一半,即 (c(m) > (r - l + 1) - c(m))
移项,得 (2c(m) > r - l + 1)
我们设区间 ([1,i]) 中出现 (m) 的数量为 (s(i)) ,代入上式,得 (2left(s(r) - s(l - 1) ight) > r - l + 1)
移项,得 (2s(r) - r > 2s(l - 1) - (l - 1))
显然我们可以用树状数组等数据结构维护 (2s(i) - i)
所以我们可以先从 (0)(7) 枚举,设当前枚举到的数为 (num),再从 (1)(n) 枚举 (a_i) ,算出当前的 (2s(i) - i)insert ,每次贡献的答案为 (2s(j) - j) 的数量,其中 (0 leq j < i)(2s(j) - j < 2s(i) - i)
时间复杂度为 (O(8nlog n))

然而上面的做法貌似不能过部分分,所以还要继续优化。
观察得 (2s(i) - i = egin{cases} 2s(i - 1) - (i - 1) + 1 & a_i = m \ 2s(i - 1) - (i - 1) - 1 & a_i ot = m end{cases})
所以我们可以不用树状数组来维护。
我们设 (sum = sumlimits_{j = 1}^{i}left[2s(j) - j leq 2s(i) - i ight]) ,设 (t(2s(i) - i) = sumlimits_{j = 1}^{i}left[2s(j) - j = 2s(i) - i ight])
显然,每枚举一个 (a_i)(t(2s(i) - i) gets t(2s(i) - i) + 1)
(a_i = m) 时, (sum gets sum + t(2s(i) - i))
(a_i ot = m) 时, (sum gets sum - t(2s(i - 1) - (i - 1)) + 1)(sum gets sum - t(2s(i) - i + 1) + 1)
显然时间复杂度为 (O(8n))

此时我们考虑 (type = 0) 的情况即正解。 (type = 2) 时满足条件的子区间长度较短, (type = 3) 时满足条件的子区间内众数的种类较少。
设满足条件的子区间的最大长度为 (p) ,则按照 (type = 2) 的做法来做时间复杂度为 (O(pn))
设满足条件的子区间的众数最多数量为 (q) ,其实就是不同 (a_i) 的数量,则按照 (type = 3) 做法来做时,时间复杂度为 (O(qn))
考虑正解,我们是否可以综合两种做法的优点?
直接说正解,我们设 (a_i) 在序列里出现的次数为 (C(a_i)) ,则我们可以先做一次 (type = 2) 的做法,只计算 (m) 满足 (C(m) leq sqrt{n}) 的贡献,显然 (p leq 2sqrt{n}) ,则这一次操作的复杂度为 (O(2nsqrt{n}))
然后我们从 (0)(n - 1) 枚举数 (num) ,若 (C(num) > sqrt{n}) ,则做一次 (type = 3) 的做法。
因为 (sum C(num)left[C(num) > sqrt{n} ight] leq n) 且我们只有满足 (C(num) > sqrt{n}) 时才进行操作,所以 $ q leq dfrac{n}{sqrt{n}} = sqrt{n}$ 。所以做 (type = 3) 的做法的时间复杂度为 (O(nsqrt{n}))
综合起来,总的时间复杂度为 (O(nsqrt{n}))
(不过我的常数太大了,在洛谷上会T一个点。。。)

#include <cstdio>
#include <cstring>
#include <cmath>

#define MAX_N (500000 + 5)

using std::sqrt;
using std::memset;

int n;
int a[MAX_N];
int cnt[MAX_N], vis[MAX_N];
int t[MAX_N * 2];
long long ans;

int Read()
{
	int res = 0;
	char ch = getchar();
	while (ch < '0' || ch > '9') ch = getchar();
	while (ch >= '0' && ch <= '9')
	{
		res = res * 10 + ch - '0';
		ch = getchar();
	}
	return res;
}

int main()
{
	n = Read();
	Read();
	for (int i = 1; i <= n; ++i)
	{
		a[i] = Read();
		++cnt[a[i]];
	}
	const int plus = n + 1;
	const int sqrtn = sqrt(n);
	int num, mode;
	for (int i = 1; i <= n; ++i)
	{
		num = 0;
		for (int j = i; j <= n && j - i + 1 <= sqrtn + sqrtn; ++j)
		{
			++vis[a[j]];
			if (vis[a[j]] > num) num = vis[a[j]], mode = a[j];
			if (num + num - (j - i + 1) > 0 && cnt[mode] <= sqrtn) ++ans;
		}
		for (int j = i; j <= n && j - i + 1 <= sqrtn + sqrtn; ++j)
		{
			--vis[a[j]];
		}
	}
	int cur, sum;
	for (int i = 0; i < n; ++i)
	{
		if (cnt[i] <= sqrtn) continue;
		memset(t, 0, sizeof t);
		cur = plus;
		sum = 1;
		t[cur] = 1;
		for (int j = 1; j <= n; ++j)
		{
			if (a[j] == i)
			{
				++cur;
				++t[cur];
				sum += t[cur];
			}
			else
			{
				--cur;
				++t[cur];
				sum = sum - t[cur + 1] + 1;
			}
			ans += sum - t[cur];
		}
	}
	printf("%lld", ans);
	return 0;
}

更多做法?见下!


二、分治做法

来自初一dalao (color{black}{ extbf{c}}color{red}{ extbf{zz}}) 的巧妙做法OrzOrzOrz。
真·比你小还比你强


我们先证明下面的结论:

  • 对于 ([l,r]) , 若 (exists k in [l,r))([l,k]) 中不存在出现次数大于 (dfrac{k-l+1}{2}) 的众数, ((k,r]) 中不存在出现次数大于 (dfrac{r - k}{2}) 的众数,则 ([l,k]) 中不存在出现次数大于 (dfrac{r- l+1}{2}) 的众数。

证明:
([l,r])(a_i) 的出现次数为 (c_{[l,r]}(a_i))
由上得 (forall c_{[l,k]}(a_i) leq dfrac{k - l + 1}{2},c_{(k,r]}(a_i) leq dfrac{r - k}{2})
两个加起来得到 (forall c_{[l,r]}(a_i) leq dfrac{r - l + 1}{2})
故命题得证。

  • 对于 ([l,r]) , 若 (exists k in [l,r))([l,k]) 中存在出现次数大于 (dfrac{k-l+1}{2}) 的众数, ((k,r]) 中不存在出现次数大于 (dfrac{r - k}{2}) 的众数,则 ([l,k])可能存在出现次数大于 (dfrac{r- l+1}{2}) 的众数,这个众数若存在,一定为 ([l,k]) 中的众数。

  • 对于 ([l,r]) , 若 (exists k in [l,r))([l,k]) 中不存在出现次数大于 (dfrac{k-l+1}{2}) 的众数, ((k,r]) 中存在出现次数大于 (dfrac{r - k}{2}) 的众数,则 ([l,k])可能存在出现次数大于 (dfrac{r- l+1}{2}) 的众数,这个众数若存在,一定为 ((k,r]) 中的众数。

  • 对于 ([l,r]) , 若 (exists k in [l,r))([l,k]) 中存在出现次数大于 (dfrac{k-l+1}{2}) 的众数, ((k,r]) 中存在出现次数大于 (dfrac{r - k}{2}) 的众数,则 ([l,k])可能存在出现次数大于 (dfrac{r- l+1}{2}) 的众数,这个众数若存在,一定为 ([l,k])((k,r]) 中的众数。

下三条证明过程与第一条类似,这里就不写了(其实是懒)

所以我们可以设这个 (k)(mid)(leftlceildfrac{r - l + 1}{2} ight ceil) ,然后先分治处理 ([l,mid])((mid,r]) ,再处理 ([l,r])
处理 ([l,r]) 的具体过程如下。
我们先分别从 (mid)(l) 枚举,从 (mid + 1)(r) 枚举,记录 ([i,mid])((mid,i]) 中出现次数大于区间长度一半的众数。
然后枚举刚刚记录的每一个众数 (q_i),从 (mid)(l) 枚举,用一个桶记录 (2c_{[i,mid]}(q_i) - (mid - i + 1)) 出现次数,然后从 (mid + 1)(r) 枚举,算出当前的 (2c_{(mid,i]}(q_i) - (i - mid)) ,然后贡献答案,做法与上面的根号做法中的 (type=3) 部分相似。

#include <cstdio>

#define MAX_N (500000 + 5)

int n;
int a[MAX_N];
int cnt[MAX_N];
int q[MAX_N], vis[MAX_N], len;
int s[MAX_N * 2], t[MAX_N * 2];
long long ans;

int Read()
{
	int res = 0;
	char ch = getchar();
	while (ch < '0' || ch > '9') ch = getchar();
	while (ch >= '0' && ch <= '9')
	{
		res = res * 10 + ch - '0';
		ch = getchar();
	}
	return res;
}

void Solve(int l, int r)
{
	if (l == r) 
	{
		++ans;
		return;
	}
	int mid = l + r >> 1;
	Solve(l, mid);
	Solve(mid + 1, r);
	for (int i = mid; i >= l; --i)
	{
		++cnt[a[i]];
		if (cnt[a[i]] + cnt[a[i]] > mid - i + 1 && !vis[a[i]])
		{
			q[++len] = a[i];
			vis[a[i]] = 1;
		}
	}
	for (int i = mid; i >= l; --i)
	{
		--cnt[a[i]];
	}
	for (int i = mid + 1; i <= r; ++i)
	{
		++cnt[a[i]];
		if (cnt[a[i]] + cnt[a[i]] > i - mid && !vis[a[i]])
		{
			q[++len] = a[i];
			vis[a[i]] = 1;
		}
	}
	for (int i = mid + 1; i <= r; ++i)
	{
		--cnt[a[i]];
	}
	const int plus = n + 1;
	int p, cur;
	for (int i = 1; i <= len; ++i)
	{
		p = q[i];
		vis[p] = 0;
		cur = plus;
		s[cur] = 0;
		for (int j = mid; j >= l; --j)
		{
			if (a[j] == p) ++cur;
			else --cur;
			++t[cur];
			if (cur >= plus) ++s[plus];
		}
		cur = plus;
		for (int j = mid + 1; j <= r; ++j)
		{
			if (a[j] == p)
			{
				--cur;
				s[cur] = s[cur + 1] + t[cur];
			}
			else
			{
				++cur;
				s[cur] = s[cur - 1] - t[cur - 1];
			}
			ans += s[cur] - t[cur];
		}
		cur = 0 + plus;
		for (int j = mid; j >= l; --j)
		{
			if (a[j] == p) ++cur;
			else --cur;
			--t[cur];
		}
	}
	len = 0;
}

int main()
{
	n = Read();
	Read();
	for (int i = 1; i <= n; ++i)
	{
		a[i] = Read();
	}
	Solve(1, n); 
	printf("%lld", ans);
	return 0;
}

还要更多做法?见下!


三、线段树做法

来自 (color{grey}{huge{ ext{蒟蒻}}}) 自己yy出来的sb做法。。。
然而在考场上想到了,结果以为是个暴力就懒得打,考后才发现是正解。
马上就高一了,被上面两位初中dalao爆踩,要努力啊



(待填坑)

真没了。

原文地址:https://www.cnblogs.com/kcn999/p/13455118.html