Ynoi 盼君勿望

Trie_Tree / 2023-08-24 / 原文

1.1 前言

在太阳西斜的这个世界里,置身天上之森,等这场战争结束之后,等这场战争结束之后,人人本着正义之名,长存不灭的过去,逐渐消逝的未来,我回来了,纵使日薄西山,即使看不到未来,此时此刻的光辉,盼君勿忘,世界上最幸福的女孩

珂朵莉要永远幸福的呀~

题目链接。

1.2 题目描述

珂朵莉给了你一个序列,每次查询一个区间 \([l,r]\) 中所有子序列分别去重后的和 \(\bmod\ p\)

输入格式

第一行两个整数 \(n,m\)

第二行 \(n\) 个整数表示这个序列。

之后 \(m\) 行,每行三个整数 \(l,r,p\) 表示查询的区间与模数。

输出格式

\(m\) 行,每行输出一个整数表示答案。

样例 #1

样例输入 #1

5 5
1 2 2 3 4
1 2 233333
2 3 333333
1 5 5
3 5 15
2 4 8

样例输出 #1

6
6
1
6
0

提示

Idea:nzhtl1477,Solution:nzhtl1477,Code:nzhtl1477,Data:nzhtl1477

对于 \(100\%\) 的数据,\(1\leq n,m,a_i \leq 10^5\)\(1\leq p\leq 10^9\)\(1\leq l\leq r\leq n\)

1.3 前置知识——莫队

我们一旦看到区间问题,自然而然的就想到了莫队(本蒟蒻也是刚学)不了解都可以看看 Oi-wiki,上面有详细解答,我们贴一段莫队算法的时间复杂度证明过来,不然有些同学一看到莫队就说它很暴力(恼)。

Oi-wiki莫队链接:Link

时间复杂度证明(Oi-wiki)

证:令每一块中 L 的最大值为 \(\max_1,\max_2,\max_3, \cdots , \max_{\lceil\sqrt{n}\rceil}\)

由第一次排序可知,\(\max_1 \le \max_2 \le \cdots \le \max_{\lceil\sqrt{n}\rceil}\)

显然,对于每一块暴力求出第一个询问的时间复杂度为 \(O(n)\)

考虑最坏的情况,在每一块中,\(R\) 的最大值均为 \(n\),每次修改操作均要将 \(L\)\(\max_{i - 1}\) 修改至 \(\max_i\) 或由 \(\max_i\) 修改至 \(\max_{i - 1}\)

考虑 \(R\):因为 \(R\) 在块中已经排好序,所以在同一块修改完它的时间复杂度为 \(O(n)\)。对于所有块就是 \(O(n\sqrt{n})\)

重点分析 \(L\):因为每一次改变的时间复杂度都是 \(O(\max_i-\max_{i-1})\) 的,所以在同一块中时间复杂度为 \(O(\sqrt{n}\cdot(\max_i-\max_{i-1}))\)

将每一块 L 的时间复杂度合在一起,可以得到:

对于 L 的总时间复杂度为

\[\begin{aligned} & O(\sqrt{n}(\max{}_1-1)+\sqrt{n}(\max{}_2-\max{}_1)+\sqrt{n}(\max{}_3-\max{}_2)+\cdots+\sqrt{n}(\max{}_{\lceil\sqrt{n}\rceil}-\max{}_{\lceil\sqrt{n}\rceil-1))} \\ = \phantom{} & O(\sqrt{n}\cdot(\max{}_1-1+\max{}_2-\max{}_1+\max{}_3-\max{}_2+\cdots+\max{}_{\lceil\sqrt{n}\rceil-1}-\max{}_{\lceil\sqrt{n}\rceil-2}+\max{}_{\lceil\sqrt{n}\rceil}-\max{}_{\lceil\sqrt{n}\rceil-1)}) \\ = \phantom{} & O(\sqrt{n}\cdot(\max{}_{\lceil\sqrt{n}\rceil-1}))\\ \end{aligned} \]

(裂项求和)

由题可知 \(\max_{\lceil\sqrt{n}\rceil}\) 最大为 n,所以 L 的总时间复杂度最坏情况下为 \(O(n\sqrt{n})\)

1.4 光速幂

由于 \(Ynoi\) 系列是由清华大学教授 lxl 编写的,所以数据十分的毒瘤,会卡我们许多东西,所以,我们要保证在最短的时间内完成\(幂\)的操作,大家可以先看看百度,再来理解下面的代码讲解:

我们求 \(2^n\) 幂时,可以把它拆成很多个幂,当然要预处理,然后就能做到在 \(O(1)\) 时间内把幂求出来,代码如下:

预处理代码
inline void chuli(int mod)
{
	f[0][0] = 1;
	for(int i = 1;i <= bl;i ++ ) f[i][0] = 2ll * f[i - 1][0] % mod;
	f[0][1] = 1;
	for(int i = 1;i <= bl;i ++ ) f[i][1] = 1ll * f[i - 1][1] * f[bl][0] % mod;
}
幂代码

inline ll query(int x, int mod){
	return f[x % bl][0] * f[x / bl][1] % mod;
}

2.1 题目分析

设当前区间为 \(l、r\),值 v 在 \(l、r\) 内的出现次数为 \(cnt_v\)

顺便补充一点,莫队是以 \(l\) 为第一关键词,\(r\) 为第二关键词。

我们画图举例:

我们要排区间 \([1,3]\),转换得 \([1,3]\)(左边为区间,右边为位置)

我们要拍区间 \([1,6]\),转换得 \([1,6]\)

我们要排区间 \([6]\),转换得 \([6]\)

所以排序顺序应该是:\(② -> ① -> ③\)

\(cnt\) 为一个桶。

很难直接求去重后包含值 \(k\) 的子序列个数,不妨稍加转化。如果将这些子序列中的 \(k\) 删除,则这些子序列中一定不出现 \(k\)。容易看出 \(l、r\) 内不包含 \(k\) 的子序列个数共有 \(2^{r-l + 1 -cnt_k}\) 个。这意味着包含值 k 的子序列共有 \(2^{r-l + 1-2^r-l+ 1-cnt_k}\) 个。出现次数直接用普通莫队维护即可,复杂度是 \(O(n\sqrt{q})\)

对于出现次数相同的值,我们可以将它们的贡献一起统计。这里的实现可以维护一个针对出现次数的值域双向链表,每次从当前的出现次数开始向下一个存在的出现次数跳。双向链表的插入和删除可以 \(O(1)\) 做。显然询问的复杂度和值出现次数的个数有关,又易知对于不同的值出现次数,其个数是 \(O(\sqrt{n})\) 的。所以这一部分的复杂度是 \(O(q\sqrt{n})\)

我们要求子序列的长度 \(len\),不妨把它看为 \(x\) 的贡献,那 \(x\) 的贡献数量就是 \(tong_x * (2 ^ {len} - 2 ^ {len - tong_x})\)

3.1 代码实现

代码十分长,写了 \(4\) 个多小时,就是一个普通莫队板子 + 光速幂。

代码运行时间 22.78ms
代码长度 2.41KB

#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
typedef long long ll;
template<typename T>
T read(T x)
{
	T opt = 1, tong = 0;
	char ch = getchar();
	while(!isdigit(ch)){
		opt = (tong == '-') ? -1 : 1, ch = getchar();
	}	
	while( isdigit(ch)){
		tong = (tong << 1) + (tong << 3) + (ch ^ 48), ch = getchar();
	}
	return opt * tong;
}
#define read read(0)
int n, m;
int bl;
const int N = 1e5 + 5;
int a[N];
ll  ans[N];
int pre[N], nxt[N];
int cnt[N];
int bel[N];
ll tongg[N];
ll f[N + 10][2];
struct node
{
	int l, r, m, id;
	bool operator < (const node &rhs) const
	{
		if(bel[l] ^ bel[rhs.l]) return bel[l] < bel[rhs.l];
		return (bel[l] & 1 ? r < rhs.r : r > rhs.r);
	}
}q[N];

int tot;
inline void ins(int x)
{
	nxt[tot] = x;
	pre[x] = tot;
	tot = x;
}
inline void erase(int x)
{
	if(x == tot)
	{
		nxt[pre[x]] = 0;
		tot = pre[x];
	}
	else 
	{
		nxt[pre[x]] = nxt[x];
		pre[nxt[x]] = pre[x];
	}
	pre[x] = 0;
	nxt[x] = 0;
}
inline void add(int x)
{
	tongg[cnt[a[x]]] -= a[x];
	if(!tongg[cnt[a[x]]]) 
		erase(cnt[a[x]]);
	cnt[a[x]] ++;
	if(!tongg[cnt[a[x]]]) 
		ins(cnt[a[x]]);
	tongg[cnt[a[x]]] += a[x];
}
inline void delta(int x)
{
	tongg[cnt[a[x]]] -= a[x];
	if(!tongg[cnt[a[x]]]) 
		erase(cnt[a[x]]);
	cnt[a[x]] -- ;
	if(!tongg[cnt[a[x]]]) 
		ins(cnt[a[x]]);
	tongg[cnt[a[x]]] += a[x];
}
inline void chuli(int mod)
{
	f[0][0] = 1;
	for(int i = 1;i <= bl;i ++ ) 
		f[i][0] = 2ll * f[i - 1][0] % mod;
	f[0][1] = 1;
	for(int i = 1;i <= bl;i ++ ) 
		f[i][1] = 1ll * f[i - 1][1] * f[bl][0] % mod;
}
inline ll query(int x, int mod){
	return f[x % bl][0] * f[x / bl][1] % mod;
}
int main()
{
	n = read, m = read;

	int block = n / sqrt(m);
	bl = block + 1;
	for(int i = 1;i <= n;i ++ ) {
		a[i] = read;
		 bel[i] = (i - 1) / block + 1;
	}
	for(int i = 1;i <= m;i ++ ){
		q[i].l = read, q[i].r = read, q[i].m = readl;
		 q[i].id = i;
	}
	sort(q + 1,q + 1 + m);
	int l = 1, r = 0;
	for(int i = 1;i <= m;i ++ ){
//		cout << 'j';
		while(r < q[i].r) add(++ r);
		while(l > q[i].l) add(-- l);
		while(l < q[i].l) delta(l ++ );
		while(r > q[i].r) delta(r -- );
		int len = r - l + 1;
		chuli(q[i].m);
		for(int j = nxt[0];j ;j = nxt[j]){
			ll v = tongg[j] * (query(len, q[i].m) - query(len - j, q[i].m) + q[i].m) % q[i].m;
			ans[q[i].id] = (ans[q[i].id] + v) % q[i].m;
		}
	}
	for(int i = 1;i <= m;i ++ ){
		printf("%lld\n", ans[i]);
	}
	return 0;
}