垃圾ACMer的暑假训练220706

220706

Codeforces Round #803 (Div. 2) A. XOR Mixup

原题指路:https://codeforces.com/contest/1698/problem/A

题意

现有含 ( n − 1 ) (n-1) (n1)个元素的整数序列 a a a,令 x x x为序列元素的异或值,将 x x x添加到 a a a的末尾使其称为长度为 n n n的序列,随机调换 a a a中元素的位置(可能保持不变).

t    ( 1 ≤ t ≤ 1000 ) t\ \ (1\leq t\leq 1000) t  (1t1000)组测试数据.每组测试数据输入一个长度为 n    ( 2 ≤ n ≤ 100 ) n\ \ (2\leq n\leq 100) n  (2n100)的序列 a 1 , ⋯   , a n    ( 0 ≤ a i ≤ 127 ) a_1,\cdots,a_n\ \ (0\leq a_i\leq 127) a1,,an  (0ai127),表示由原始序列 a a a经上述操作后得到的序列.输出任一个满足条件的 x x x,可以证明这样的 x x x存在.

思路

因构建出新序列后随机调换 a a a中元素的位置,则新序列中各个位置等价,即都有可能是 x x x的位置.不失一般性,不妨设新序列的前 ( n − 1 ) (n-1) (n1)个元素为初始序列的元素,最后一个元素为 x x x.

n = 4 n=4 n=4为例,设初始序列 a : { a 1 , a 2 , a 3 } a:\{a_1,a_2,a_3\} a:{a1,a2,a3},则 x = a 1   x o r   a 2   x o r   a 3 x=a_1\ \mathrm{xor}\ a_2\ \mathrm{xor}\ a_3 x=a1 xor a2 xor a3,构成的新序列 a : { a 1 , a 2 , a 3 , x } a:\{a_1,a_2,a_3,x\} a:{a1,a2,a3,x}.考察序列的后 ( n − 1 ) (n-1) (n1)个元素,注意到 a 2   x o r   a 3   x o r   x = a 1 a_2\ \mathrm{xor}\ a_3\ \mathrm{xor}\ x=a_1 a2 xor a3 xor x=a1,即序列的后 ( n − 1 ) (n-1) (n1)个元素的异或值等于 a 1 a_1 a1,显然可通过调整 a 1 , a 2 , a 3 a_1,a_2,a_3 a1,a2,a3的位置使得后 ( n − 1 ) (n-1) (n1)个元素的异或值等于第一个元素,而这正是 x x x的定义.因新序列中各个位置等价,故新序列中每个元素都可能是 x x x,任意输出一个即可.

代码
int main() {
	CaseT{
		int n; cin >> n;
		int x;
		for (int i = 0; i < n; i++) cin >> x;	
		cout << x << endl;
	}
}


Codeforces Round #803 (Div. 2) B. Rising Sand

原题指路:https://codeforces.com/contest/1698/problem/B

题意

对含有 n n n个数的整数序列 a 1 , ⋯   , a n a_1,\cdots,a_n a1,,an.称一个数 a i    ( 1 < i < n ) a_i\ \ (1<i<n) ai  (1<i<n)是高的,如果 a i > a i − 1 + a i + 1 a_i>a_{i-1}+a_{i+1} ai>ai1+ai+1.注意首元素和尾元素不是高的.

t    ( 1 ≤ t ≤ 1000 ) t\ \ (1\leq t\leq 1000) t  (1t1000)组测试数据,每组测试数据输入一个 n , k    ( 3 ≤ n ≤ 2 e 5 , 1 ≤ k ≤ n ) n,k\ \ (3\leq n\leq 2\mathrm{e}5,1\leq k\leq n) n,k  (3n2e5,1kn),表示整数序列的长度.现可在序列中取一个长度为 k k k的连续区间,并将其中的元素 + 1 +1 +1.求经过若干次(可能是 0 0 0次)操作后序列中高的元素的个数的最大值.数据保证所有测试样例的 n n n之和不超过 2 e 5 2\mathrm{e}5 2e5.

思路

类似于 n n n个人站成一排,时间只对其中某个区间内的人流逝,问若干年后最多几个人的年龄是高的.注意到年龄差不变.

(1) k = 1 k=1 k=1时:

​ ①若 n n n是奇数,则可通过若干次操作使得序列中偶数位的数都是高的, a n s = ⌊ n − 1 2 ⌋ ans=\left\lfloor\dfrac{n-1}{2}\right\rfloor ans=2n1.

​ ②若 n n n为偶数,则可通过若干次操作使得序列中除尾元素的偶数位的数都是高的, a n s = ⌊ n − 1 2 ⌋ ans=\left\lfloor\dfrac{n-1}{2}\right\rfloor ans=2n1.

(2) k ≠ 1 k\neq 1 k=1时:

​ 考察序列 a 1 , ⋯   , a i − 1 , a i , a i + 1 , ⋯   , a n a_1,\cdots,a_{i-1},a_i,a_{i+1},\cdots,a_n a1,,ai1,ai,ai+1,,an,

​ 若选择区间 [ a i − 1 , a i ] [a_{i-1},a_i] [ai1,ai]进行操作,则 a i a_i ai的邻域变为 a i − 1 + 1 , a i + 1 , a i + 1 a_{i-1}+1,a_i+1,a_{i+1} ai1+1,ai+1,ai+1.若 a i + 1 a_i+1 ai+1是高的,则 a i + 1 > a i − 1 + 1 + a i + 1 a_i+1>a_{i-1}+1+a_{i+1} ai+1>ai1+1+ai+1,即 a i > a i − 1 + a i + 1 a_i>a_{i-1}+a_{i+1} ai>ai1+ai+1,亦即操作对 a i a_i ai是否是高的无贡献.同理选择区间 [ a i , a i + 1 ] [a_i,a_{i+1}] [ai,ai+1] [ a i − 1 , a i , a i + 1 ] [a_{i-1},a_i,a_{i+1}] [ai1,ai,ai+1]操作也无贡献.故 a i a_i ai是否是高的取决于其在原序列中是否是高的,对原序列统计一遍高的元素的个数即可.

代码
int main() {
	CaseT{
		int n, k; cin >> n >> k;
		vi a(n);
		for (int i = 0; i < n; i++) cin >> a[i];
		if (k == 1) {
			cout << (n - 1) / 2 << endl;
			continue;
		}
		else {
			int ans = 0;
			for (int i = 1; i < n - 1; i++) ans += (a[i] > a[i - 1] + a[i + 1]);
			cout << ans << endl;
		}
	}
}


Codeforces Round #803 (Div. 2)

原题指路:https://codeforces.com/contest/1698/problem/C

题意

称一个序列 a 1 , ⋯   , a n a_1,\cdots,a_n a1,,an是好的,如果对 ∀ 1 ≤ i < j < k ≤ n , ∃ 1 ≤ l ≤ n   s . t .   a i + a j + a k = a l \forall 1\leq i<j<k\leq n,\exists 1\leq l\leq n\ s.t.\ a_i+a_j+a_k=a_l 1i<j<kn,1ln s.t. ai+aj+ak=al

t    ( 1 ≤ t ≤ 1000 ) t\ \ (1\leq t\leq 1000) t  (1t1000)组测试数据.每组测试数据输入一个长度为 n    ( 3 ≤ n ≤ 2 e 5 ) n\ \ (3\leq n\leq 2\mathrm{e}5) n  (3n2e5)的整数序列 a 1 , ⋯   , a n    ( − 1 e 9 ≤ a i ≤ 1 e 9 ) a_1,\cdots,a_n\ \ (-1\mathrm{e}9\leq a_i\leq 1\mathrm{e}9) a1,,an  (1e9ai1e9).若该序列是好的,输出"YES",否则输出"NO".数据保证所有测试数据的 n n n之和不超过 2 e 5 2\mathrm{e}5 2e5.

思路

(1)若序列中至少有 3 3 3个正数,设其中前三大为 x , y , z x,y,z x,y,z,则数 x + y + z ∉ a x+y+z\notin a x+y+z/a.故序列中至多有 2 2 2个正数.同理至多有 2 2 2个负数.

(2)①若序列中有 1 1 1 0 0 0,则是否 ∃ a l ∈ a   s . t .   a i + a j + 0 = a l \exists a_l\in a\ s.t.\ a_i+a_j+0=a_l ala s.t. ai+aj+0=al取决于 a i + a j a_i+a_j ai+aj是否等于 a l a_l al.

​ ②若序列中有 2 2 2 0 0 0,注意到 a i + 0 + 0 = a i a_i+0+0=a_i ai+0+0=ai恒成立.

​ ③若序列中至少有 3 3 3 0 0 0,注意到除保证 a i + 0 + 0 = a i a_i+0+0=a_i ai+0+0=ai外,还保证 0 + 0 + 0 = 0 0+0+0=0 0+0+0=0,显然这是多余的.故序列中至多有 2 2 2 0 0 0.

综上,序列中至多有 6 6 6个数,暴力检查是否满足好的条件即可.

代码
void solve() {
	int n; cin >> n;
	vi positive, negative;  // 正数、负数
	vi arr;  // 最终序列
	while (n--) {
		int x; cin >> x;
		if (x > 0) positive.push_back(x);
		else if (x < 0) negative.push_back(x);
		else if (arr.size() < 2) arr.push_back(x);  // x=0
	}

	if (positive.size() > 2 || negative.size() > 2) {
		cout << "NO" << endl;
		return;
	}

	for (auto item : positive) arr.push_back(item);
	for (auto item : negative) arr.push_back(item);

	for (int i = 0; i < arr.size(); i++) {
		for (int j = i + 1; j < arr.size(); j++) {
			for (int k = j + 1; k < arr.size(); k++) {
				bool ok = false;
				for (int l = 0; l < arr.size(); l++) 
					if (arr[i] + arr[j] == arr[l] - arr[k]) ok = true;  // 防爆int
				if (!ok) {
					cout << "NO" << endl;
					return;
				}
			}
		}
	}
	cout << "YES" << endl;
}

int main() {
	CaseT{
		solve();
	}
}


ACWing PAT甲级

13. 链表

13.1 共享 ( 0.2   s 0.2\ \mathrm{s} 0.2 s)

题意

存储单词时,可用链表逐字符存储.若两单词有相同后缀,可共享一个子链表.

如"loading"和"beging"的存储如下:

在这里插入图片描述

输入第一行包含两个节点地址和一个整数 n    ( 2 ≤ n ≤ 1 e 5 ) n\ \ (2\leq n\leq 1\mathrm{e}5) n  (2n1e5),两地址分别是两单词首字母节点的地址(地址是一个 5 5 5位的正整数, N U L L NULL NULL的地址为 − 1 -1 1), n n n是总节点数.

接下来 n n n行每行包含一个节点信息,格式为 A d d r e s s   D a t a   N e x t Address\ Data\ Next Address Data Next,即当前节点地址、节点存储的字母( A ∼ Z , a ∼ z A\sim Z,a\sim z AZ,az)、下一节点的地址.

若两单词存在相同后缀,输出相同后缀的起始节点的地址;否则输出 − 1 -1 1.

思路

先建出两个链表,遍历一遍第一个链表,把地址存在哈希表中.再遍历第二个链表,若当前节点的地址在哈希表中,则该节点地址为相同后缀的起始节点的地址.若遍历完第二个链表,则两单词无相同后缀.

因地址是 5 5 5位数,可不用哈希表,直接用一个bool数组.

代码
const int MAXN = 1e5 + 5;
int n;  // 总节点数
int head1, head2, nxt[MAXN];
char val[MAXN];  // 节点的数据域
bool vis[MAXN];

int main() {
	cin >> head1 >> head2 >> n;
	for (int i = 0; i < n; i++) {
		int add, ne; char ch; cin >> add >> ch >> ne;
		val[add] = ch, nxt[add] = ne;
	}

	for (int i = head1; ~i; i = nxt[i]) vis[i] = true;  // 遍历第一个链表

	for (int i = head2; ~i; i = nxt[i]) 
		if (vis[i]) return printf("%05d", i), 0;  // 找到第一个公共节点
	
	cout << -1;
}


13.2 反转链表 ( 0.4   s 0.4\ \mathrm{s} 0.4 s)

题意

给定一个常数 k k k和单链表 l l l,对 l l l上每 k k k个元素做一次反转,若 l l l的最后不足 k k k个元素,则不反转.

输入第一行包含头节点的地址、总结点数 n    ( 1 ≤ n ≤ 1 e 5 ) n\ \ (1\leq n\leq 1\mathrm{e}5) n  (1n1e5)和常数 k    ( 1 ≤ k ≤ n ) k\ \ (1\leq k\leq n) k  (1kn).节点地址用一个 5 5 5位非负整数表示(可能含前导零), N U L L NULL NULL − 1 -1 1表示.

接下来 n n n行每行描述一个节点信息,格式为 A d d r e s s   D a t a   N e x t Address\ Data\ Next Address Data Next,即当前节点地址、节点存储的整数、下一节点的地址.

输出操作后的链表,从头节点开始依次输出,格式与输入相同.

思路

在链表上不易做反转.先将链表的节点地址依次存到一个vector里,在vector上做反转后,再将vector转为链表.

设操作后vector元素为 a 1 , ⋯   , a n a_1,\cdots,a_n a1,,an.每个节点的地址、数据域不变,令 n x t [ a 1 ] = a 2 , n x t [ a 2 ] = a 3 , ⋯   , n x t [ a n − 1 ] = a n , n x t [ a n ] = − 1 nxt[a_1]=a_2,nxt[a_2]=a_3,\cdots,nxt[a_{n-1}]=a_n,nxt[a_n]=-1 nxt[a1]=a2,nxt[a2]=a3,,nxt[an1]=an,nxt[an]=1即可.

代码
const int MAXN = 1e5 + 5;
int n, k;  // 总节点数、常数
int head, val[MAXN], nxt[MAXN];

int main() {
	cin >> head >> n >> k;
	for (int i = 0; i < n; i++) {
		int add, data, ne; cin >> add >> data >> ne;
		val[add] = data, nxt[add] = ne;
	}

	vi arr;
	for (int i = head; ~i; i = nxt[i]) arr.push_back(i);

	for (int i = 0; i + k - 1 < arr.size(); i += k)   // 有k个元素才反转
		reverse(arr.begin() + i, arr.begin() + i + k);
	
	for (int i = 0; i < arr.size(); i++) {
		printf("%05d %d ", arr[i], val[arr[i]]);
		if (i + 1 == arr.size()) cout << -1 << endl;
		else printf("%05d\n", arr[i + 1]);
	}
}


13.3 删除链表重复数据 ( 0.4   s 0.4\ \mathrm{s} 0.4 s)

题意

给定一个单链表 l l l,其每个节点上存有一个键值.现需删除有重复键值的绝对值的节点,即对每个键值 k k k,只保留键值或其绝对值为 k k k的第一个节点,同时将被删掉的节点存在另一个单链表中.

输入第一行包含头节点的地址和节点总数 n    ( 1 ≤ n ≤ 1 e 5 ) n\ \ (1\leq n\leq 1\mathrm{e}5) n  (1n1e5),节点地址是一个 5 5 5位非负整数(可能含前导零), N U L L NULL NULL − 1 -1 1表示.

接下来 n n n行每行描述描述一个节点信息,格式为 A d d r e s s   K e y   N e x t Address\ Key\ Next Address Key Next,即当前节点地址、节点的键值(绝对值不超过 1 e 4 1\mathrm{e}4 1e4)、下一节点的地址.

按顺序输出结果链表和被删除的节点的链表,格式与输入相同.

思路

建出链表,遍历一遍,若该键值的绝对值未出现过,将其节点地址插入到第一个vector中,否则插入到第二个vector中,再将vector转为链表.

代码
const int MAXN = 1e5 + 5;
int n;  // 总节点数、常数
int head, val[MAXN], nxt[MAXN];
bool vis[MAXN];

int main() {
	cin >> head >> n;
	for (int i = 0; i < n; i++) {
		int add, key, ne; cin >> add >> key >> ne;
		val[add] = key, nxt[add] = ne;
	}

	vi arr1, arr2;
	for (int i = head; ~i; i = nxt[i]) {
		int cur = abs(val[i]);
		if (vis[cur]) arr2.push_back(i);
		else {
			vis[cur] = true;
			arr1.push_back(i);
		}
	}
	
	for (int i = 0; i < arr1.size(); i++) {
		printf("%05d %d ", arr1[i], val[arr1[i]]);
		if (i + 1 == arr1.size()) cout << -1 << endl;
		else printf("%05d\n", arr1[i + 1]);
	}
	for (int i = 0; i < arr2.size(); i++) {
		printf("%05d %d ", arr2[i], val[arr2[i]]);
		if (i + 1 == arr2.size()) cout << -1 << endl;
		else printf("%05d\n", arr2[i + 1]);
	}
}


13.4 链表元素分类 ( 0.4   s 0.4\ \mathrm{s} 0.4 s)

题意

给定一个单链表,对其元素进行分类排列,使得所有负值元素都排在非负值元素的前面,且区间 [ 0 , k ] [0,k] [0,k]内的元素都排在大于 k k k的元素前面,要求不改变每一类内部元素的顺序.

输入第一行包含头节点的地址、总结点数 n    ( 1 ≤ n ≤ 1 e 5 ) n\ \ (1\leq n\leq 1\mathrm{e}5) n  (1n1e5)和常数 k    ( 1 ≤ k ≤ n ) k\ \ (1\leq k\leq n) k  (1kn).节点地址用一个 5 5 5位非负整数表示(可能含前导零), N U L L NULL NULL − 1 -1 1表示.

接下来 n n n行每行描述一个节点信息,格式为 A d d r e s s   D a t a   N e x t Address\ Data\ Next Address Data Next,即当前节点地址、节点存储的整数(数的绝对值不超过 1 e 5 1\mathrm{e}5 1e5)、下一节点的地址.

输出操作后的链表,从头节点开始依次输出,格式与输入相同.

思路

最后排序结果分为三段: < 0 <0 <0的元素、 [ 0 , k ] [0,k] [0,k]的元素、 > k >k >k的元素.

开3个vector存三类元素,再合并.

代码
const int MAXN = 1e5 + 5;
int n, k;  // 总节点数、常数
int head, val[MAXN], nxt[MAXN];

int main() {
	cin >> head >> n >> k;
	for (int i = 0; i < n; i++) {
		int add, key, ne; cin >> add >> key >> ne;
		val[add] = key, nxt[add] = ne;
	}

	vi arr1, arr2, arr3;
	for (int i = head; ~i; i = nxt[i]) {
		int cur = val[i];
		if (cur < 0) arr1.push_back(i);
		else if (cur <= k) arr2.push_back(i);
		else arr3.push_back(i);
	}
	
	arr1.insert(arr1.end(), arr2.begin(), arr2.end());
	arr1.insert(arr1.end(), arr3.begin(), arr3.end());

	for (int i = 0; i < arr1.size(); i++) {
		printf("%05d %d ", arr1[i], val[arr1[i]]);
		if (i + 1 == arr1.size()) cout << -1 << endl;
		else printf("%05d\n", arr1[i + 1]);
	}
}


10. 并查集

10.1 战争中的城市 ( 0.4   s 0.4\ \mathrm{s} 0.4 s)

题意

假设战争中,若一个城市被敌人占领,则从该城市出发和通往该城市的公路都将关闭.现需判断是否需要维修其他公路来保证其余城市连通.

输入第一行包含三个整数 n , m , k    ( 2 ≤ n ≤ 1000 , 1 ≤ m ≤ n ( n − 1 ) 2 , 1 ≤ k ≤ n , k m ≤ 3.5 e 6 ) n,m,k\ \ \left(2\leq n\leq 1000,1\leq m\leq\dfrac{n(n-1)}{2},1\leq k\leq n,km\leq 3.5\mathrm{e}6\right) n,m,k  (2n1000,1m2n(n1),1kn,km3.5e6),分别表示城市总数、公路总数和重点关注城市的数量,城市编号 1 ∼ n 1\sim n 1n.

接下来 m m m行每行输入两个整数 a , b    ( 1 ≤ a , b ≤ n ) a,b\ \ (1\leq a,b\leq n) a,b  (1a,bn),表示城市 a a a b b b间存在公路.数据保证初始时所有城市连通.

最后一行包含 k k k个整数,表示重点关注城市的编号.

输出 k k k行,每行输出当该重点城市被占领时,为保证其余城市的连通性至少需维修多少条公路.

思路

不要将问题看成带删除一个节点的并查集,而是对每个询问,以删去节点后剩下的节点和边建立并查集.

用并查集维护连通块,若最后有 c n t cnt cnt个连通块,则 a n s = c n t − 1 ans=cnt-1 ans=cnt1.

时间复杂度 O ( k ( n + m ) ) O(k(n+m)) O(k(n+m)).

代码
const int MAXN = 1005, MAXM = 5e5 + 5;
int n, m, k;  // 城市数、公路数、查询数
struct Edge { int u, v; }edges[MAXM];
int fa[MAXN];

int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }

int main() {
	cin >> n >> m >> k;
	for (int i = 0; i < m; i++) cin >> edges[i].u >> edges[i].v;

	while (k--) {
		int x; cin >> x;
		
		for (int i = 1; i <= n; i++) fa[i] = i;
		int cnt = n - 1;  // 当前连通块数
		for (int i = 0; i < m; i++) {  // 枚举所有边
			int u = edges[i].u, v = edges[i].v;
			if (u != x && v != x) {  // 不是重点关注城市
				u = find(u), v = find(v);
				if (u != v) {
					fa[u] = v;
					cnt--;  // 更新当前连通块数
				}
			}
		}
		cout << cnt - 1 << endl;
	}
}


10.2 家产 ( 0.2   s 0.2\ \mathrm{s} 0.2 s)

题意

给定每个人的家庭成员和ta自己名下的不动产信息,求每个家庭的成员数、人均不动产面基、人均房产套数.

第一行输入整数 n    ( 1 ≤ n ≤ 1000 ) n\ \ (1\leq n\leq 1000) n  (1n1000).接下来 n n n行,每行输入一个拥有房产的人员的信息,格式为 I D   F a t h e r   M o t h e r   k   C h i l d 1   ⋯   C h i l d k   M _ e s t a t e   A r e a ID\ Father\ Mother\ k\ Child_1\ \cdots\ Child_k\ M\_estate\ Area ID Father Mother k Child1  Childk M_estate Area,其中 I D ID ID是每个人独一无二的 4 4 4位标识号, F a t h e r Father Father M o t h e r Mother Mother是其父母的 I D ID ID号(父母去世用 − 1 -1 1表示), k k k是孩子数, C h i l d i Child_i Childi是第 i    ( 1 ≤ i ≤ k ≤ 5 ) i\ \ (1\leq i\leq k\leq 5) i  (1ik5) I D ID ID, M _ e s t a t e M\_estate M_estate是其名下房产的数量, A r e a Area Area是其名下房产的面积.数据保证每个人名下的房产不超过 100 100 100套,每个人名下的房产总面积不超过 5 e 4 5\mathrm{e}4 5e4.

按人均房产面积降序输出所有家庭信息,若人均房产面积相等,按 I D ID ID升序排序.输出格式为 I D   M   A V G _ s e t s   A C G _ a r e a ID\ M\ AVG\_sets\ ACG\_area ID M AVG_sets ACG_area,其中 I D ID ID为家庭成员中编号最小的成员的编号, M M M为家庭成员数, A V G _ s e t s AVG\_sets AVG_sets为人均房地产数, A V G _ a r e a AVG\_area AVG_area为人均房地产面积.

思路

最后按 I D ID ID升序排序输出,可在集合的根节点多开一个变量来记录集合的最小 I D ID ID,或在合并集合时将根节点编号较大的集合合并到根节点较小的集合,这样保证最后的根节点的 I D ID ID最小.

人的 I D ID ID 4 4 4位数,则需枚举 0001 ∼ 9999 0001\sim 9999 00019999,可用一个bool数组标记每个 I D ID ID是否出现过,或存在set中.

每个人有 1 1 1个父亲、 1 1 1个母亲和至多 5 5 5和孩子,则每个节点至多有 7 7 7条边, 1000 1000 1000个人最多 7000 7000 7000条边.

代码
const int MAXN = 1e4 + 5;
int n;  // 人数
int fa[MAXN];  // 并查集的fa[]数组
int people[MAXN], house[MAXN], area[MAXN];  // 人数、房产数、房产面积
set<int> ids;  // 出现过的ID
struct Edge { int u, v; }edges[MAXN];
struct Family {
	int id, people, house, area;  // 最小ID、人数、房产数、房产面积

	bool operator<(const Family& p)const {
		// 以人均房产面积降序排序,即比较area/people和p.area/p.people
		// 交叉相乘可避免浮点数比较
		if (area * p.people != p.area * people) return area * p.people > p.area * people;
		else return id < p.id;  // 人均房产面积相等,以ID升序排序
	}
};

int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }

int main() {
	for (int i = 0; i < MAXN; i++) fa[i] = i, people[i] = 1;  // 初始时每个家庭有1个人
	
	cin >> n;

	int m = 0;  // 边数
	for (int i = 0; i < n; i++) {
		int id, father, mother, k; cin >> id >> father >> mother >> k;
		ids.insert(id);
		if (~father) edges[m++] = { id,father };
		if (~mother) edges[m++] = { id,mother };
		for (int j = 0; j < k; j++) {  // 孩子
			int son; cin >> son;
			edges[m++] = { id,son };
		}
		cin >> house[id] >> area[id];
	}

	for (int i = 0; i < m; i++) {
		int u = edges[i].u, v = edges[i].v;
		ids.insert(u), ids.insert(v);

		u = find(u), v = find(v);
		if (u != v) {  // 将u的集合合并到v的集合中
			if (u < v) swap(u, v);  // 保证v的ID小
			people[v] += people[u], house[v] += house[u], area[v] += area[u];
			fa[u] = v;
		}
	}

	vector<Family> families;  // 存每个集合的根节点
	for (auto item : ids)  // 只遍历出现过的ID
		if (fa[item] == item) families.push_back({ item,people[item],house[item],area[item] });
	
	sort(all(families));
	cout << families.size() << endl;
	for (auto item : families)
		printf("%04d %d %.3lf %.3lf\n",
			item.id, item.people, (double)item.house / item.people, (double)item.area / item.people);
}


10.3 森林里的鸟 ( 0.15   s 0.15\ \mathrm{s} 0.15 s)

题意

有很多照片,假设同一张照片上的鸟属于同一棵树.求森林中树的最大数量.给定一对鸟,判断它们是否在同一棵树上.

第一行输入包含整数 n    ( 1 ≤ n ≤ 1 e 4 ) n\ \ (1\leq n\leq 1\mathrm{e}4) n  (1n1e4)表示照片数.接下来 n n n行每行描述一张照片,格式为 k   B 1   ⋯   B k k\ B_1\ \cdots\ B_k k B1  Bk,其中 k k k表示照片中鸟的数量, B i    ( 1 ≤ i ≤ k ≤ 10 ) B_i\ \ (1\leq i\leq k\leq 10) Bi  (1ik10)表示鸟的编号.数据保证所有照片中的鸟被连续编号 1 1 1到某个不超过 1 e 4 1\mathrm{e}4 1e4的整数.接下来一行输入一个询问数 q    ( 1 ≤ q ≤ 1 e 4 ) q\ \ (1\leq q\leq 1\mathrm{e}4) q  (1q1e4),接下来 q q q行每行输入两个鸟的编号,表示一组询问.

第一行输出森林中树的最大数量和鸟的数量.接下来 q q q行对每个询问,若被询问的两鸟在同一棵树上,输出"YES";否则输出"NO".

思路

森林中的树有最大数量的解释:极端情况时照片是一棵树的不同角度.

为了使树的数量最多,合并集合时只合并必须合并的集合.

代码
const int MAXN = 1e4 + 5;
int n;  // 照片数
int fa[MAXN];
int birds[MAXN];  // 临时存一张照片中的鸟的编号
set<int> ids;  // 出现过的id

int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }

int main() {
	for (int i = 0; i < MAXN; i++) fa[i] = i;
	
	cin >> n;
	int merge = 0;  // 合并次数
	for (int i = 0; i < n; i++) {
		int k; cin >> k;
		for (int j = 0; j < k; j++) {
			cin >> birds[j];
			ids.insert(birds[j]);
		}

		for (int j = 1; j < k; j++) {  // 每次合并相邻两堆鸟,把前一堆合并到后一堆中
			int a = birds[j - 1], b = birds[j];
			a = find(a), b = find(b);
			if (a != b) {
				fa[a] = b;
				merge++;
			}
		}
	}

	cout << ids.size() - merge << ' ' << ids.size() << endl;  // 树数、鸟数

	CaseT{
		int a,b; cin >> a >> b;
		cout << (find(a) == find(b) ? "Yes" : "No") << endl;
	}
}


10.4 社会集群 ( 1.2   s 1.2\ \mathrm{s} 1.2 s)

题意

社会集群指一群有共同爱好的人.假设爱好有传递性.给定社交网络中的人的兴趣爱好,找到所有社会集群.

第一行输入一个 n    ( 1 ≤ n ≤ 1000 ) n\ \ (1\leq n\leq 1000) n  (1n1000)表示社交网络人数,人编号 1 ∼ n 1\sim n 1n.接下来 n n n行每行包含一个人的爱好信息,格式为 k   h [ 1 ]   ⋯   h [ k ] k\ h[1]\ \cdots\ h[k] k h[1]  h[k],其中 k k k为爱好数, h [ i ]    ( 1 ≤ i ≤ k ≤ n , k > 0 ) h[i]\ \ (1\leq i\leq k\leq n,k>0) h[i]  (1ikn,k>0)为第 i i i个爱好的编号.

第一行输出社会集群数.第二行按非增序输出每个集群的人数.

代码
const int MAXN = 1005;
int n;  // 人数
vi hobbies[MAXN];  // 每个人的爱好
int fa[MAXN];
int people[MAXN];  // 每个集群的人数

int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }

int main() {
	for (int i = 0; i < MAXN; i++) fa[i] = i;
	
	cin >> n;
	for (int i = 0; i < n; i++) {
		int k; scanf("%d:", &k);
		while (k--) {
			int h; cin >> h;
			hobbies[h].push_back(i);
		}
	}
	
	for (int i = 1; i <= 1000; i++) {
		for (int j = 1; j < hobbies[i].size(); j++) {
			int a = hobbies[i][0], b = hobbies[i][j];
			fa[find(a)] = find(b);  // 把爱好都合并到后一个
		}
	}
	
	int ans = 0;  // 集群数
	for (int i = 0; i < n; i++) {
		if (!people[find(i)]) ans++;  // 更新集群数
		people[find(i)]++;  // 更新每个集群的人数
	}
	cout << ans << endl;
	
	sort(people, people + n, greater<int>());
	cout << people[0];
	for (int i = 1; i < ans; i++) cout << ' ' << people[i];
	cout << endl;
}


5. 线段树与树状数组

5.1 动态求连续区间和

题意

给定一个整数数组,数组下标从 1 1 1开始,要求支持两种操作:①修改某一元素的值;②求子序列 [ a , b ] [a,b] [a,b]的和.

第一行输入两个整数 n    ( 1 ≤ n ≤ 1 e 5 ) n\ \ (1\leq n\leq 1\mathrm{e}5) n  (1n1e5) m    ( 1 ≤ m ≤ 1 e 5 ) m\ \ (1\leq m\leq 1\mathrm{e}5) m  (1m1e5),分别表示数组长度和操作个数.第二行输入 n n n个数表示原数组.接下来 m m m行每行输入三个数 k , a , b    ( 1 ≤ a ≤ b ≤ n ) k,a,b\ \ (1\leq a\leq b\leq n) k,a,b  (1abn),当 k = 0 k=0 k=0是表示求子序列 [ a , b ] [a,b] [a,b]的和; k = 1 k=1 k=1时表示第 a a a个数加 b b b.数据保证任意时刻数组内的元素之和在int内.

输出 k = 0 k=0 k=0的操作的结果.

代码
const int MAXN = 1e5 + 5;
int n, m;  // 数组长度、操作数
int BIT[MAXN];

void add(int x, int v) {  // arr[x]+=v
	for (int i = x; i <= n; i += lowbit(i)) BIT[i] += v;
}

int query(int x) {  // 求arr[1...x]的前缀和
	int res = 0;
	for (int i = x; i; i -= lowbit(i)) res += BIT[i];
	return res;
}

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		int x; cin >> x;
		add(i, x);
	}

	while (m--) {
		int k, a, b; cin >> k >> a >> b;
		if (k) add(a, b);
		else cout << query(b) - query(a - 1) << endl;
	}
}


5.2 数星星 ( 0.2   s 0.2\ \mathrm{s} 0.2 s)

题意

天上有一些星星,任两个星星位置不同,每个星星有坐标.若一个星星的左下方(含正左和正下)有 k k k颗星星,则称该星星是 k k k级的.

第一行输入一个整数 n    ( 1 ≤ n ≤ 1.5 e 4 ) n\ \ (1\leq n\leq 1.5\mathrm{e4}) n  (1n1.5e4),表示星星的个数.接下来 n n n行输入星星的坐标 ( x , y )    ( 0 ≤ x , y ≤ 3.2 e 4 ) (x,y)\ \ (0\leq x,y\leq 3.2\mathrm{e}4) (x,y)  (0x,y3.2e4).数据保证无星星重叠,星星坐标按 y y y升序给出, y y y相等时按 x x x升序给出.

输出 n n n行,第 i    ( 0 ≤ i ≤ n − 1 ) i\ \ (0\leq i\leq n-1) i  (0in1)行输出一个数表示 i i i级的星星的个数.

思路

因输入按 y y y升序、 x x x升序给出,则在当前点 ( x , y ) (x,y) (x,y)的左下方的点只能是在其之前输入的点,即只需考虑之前输入的点中有多少个点的横坐标小于等于 x x x,亦即求横坐标范围为 [ 0 , x ] [0,x] [0,x]的星星的个数.用 a [ i ] a[i] a[i]表示横坐标为 i i i的点的个数,则 x x x级星星的个数为 a [ 1 ] + ⋯ + a [ x ] a[1]+\cdots+a[x] a[1]++a[x].添加一个星星时,只需将对应横坐标的点的个数 + 1 +1 +1.先查询输入的点的左下方的星星个数,再将其插入BIT中,以免将自己也算进去.总时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn).

因输入的坐标 x x x 0 0 0开始,而BIT的下标从 1 1 1开始,可整体加一个偏移量.

代码
const int MAXN = 3.2e4 + 5;
int n;  // 星星数
int BIT[MAXN];
int ans[MAXN];  // 每个等级的星星个数

void add(int x) {
	for (int i = x; i < MAXN; i += lowbit(i)) BIT[i]++;
}

int query(int x) {
	int res = 0;
	for (int i = x; i; i -= lowbit(i)) res += BIT[i];
	return res;
}

int main() {
	cin >> n;
	for (int i = 0; i < n; i++) {
		int x, y; cin >> x >> y;
		x++;  // 下标从1开始
		ans[query(x)]++;  // 更新该横坐标的点数
		add(x);  // 将该点的横坐标插入BIT
	}

	for (int i = 0; i < n; i++) cout << ans[i] << endl;
}


25.3.3 公约数的和

例题

给定 n    ( 2 ≤ n ≤ 2 e 6 ) n\ \ (2\leq n\leq 2\mathrm{e}6) n  (2n2e6),求 ∑ i = 1 n ∑ j = i + 1 n gcd ⁡ ( i , j ) \displaystyle \sum_{i=1}^n\sum_{j=i+1}^n \gcd(i,j) i=1nj=i+1ngcd(i,j).

思路

[定理1] ∑ i ≤ n ∑ d ∣ i μ ( d ) = ∑ d ≤ n ⌊ n d ⌋ μ ( d ) \displaystyle \sum_{i\leq n}\sum_{d\mid i}\mu(d)=\sum_{d\leq n}\left\lfloor\dfrac{n}{d}\right\rfloor\mu(d) indiμ(d)=dndnμ(d).

[] ∑ i ≤ n ∑ d ∣ i \displaystyle\sum_{i\leq n}\sum_{d\mid i} indi表示先枚举 1 ∼ n 1\sim n 1n内的 i i i,再枚举 i i i的所有约数,

这等价于先枚举 1 ∼ n 1\sim n 1n内的 d d d,再枚举其在 1 ∼ n 1\sim n 1n内的倍数,显然倍数有 ⌊ n d ⌋ \left\lfloor\dfrac{n}{d}\right\rfloor dn个.

[定理2] ∑ i ≤ n ∑ j ≤ m [ gcd ⁡ ( i , j ) = = 1 ] = ∑ d ≤ min ⁡ { n , m } μ ( d ) ⌊ n d ⌋ ⌊ m d ⌋ \displaystyle\sum_{i\leq n}\sum_{j\leq m}[\gcd(i,j)==1]=\sum_{d\leq\min\{n,m\}}\mu(d)\left\lfloor\dfrac{n}{d}\right\rfloor\left\lfloor\dfrac{m}{d}\right\rfloor injm[gcd(i,j)==1]=dmin{n,m}μ(d)dndm.

[] 注意到 [ gcd ⁡ ( i , j ) = = 1 ] = ∑ d ∣ gcd ⁡ ( i , j ) μ ( d ) \displaystyle [\gcd(i,j)==1]=\sum_{d\mid \gcd(i,j)}\mu(d) [gcd(i,j)==1]=dgcd(i,j)μ(d),

定理1: L H S = ∑ i ≤ n ∑ j ≤ m ∑ d ∣ gcd ⁡ ( i , j ) μ ( d ) = ∑ i ≤ n ∑ j ≤ m ∑ d ∣ i ∑ d ∣ j μ ( d ) = ∑ i ≤ n ∑ d ∣ i ∑ j ≤ m ∑ d ∣ j μ ( d ) \displaystyle \mathrm{LHS}=\sum_{i\leq n}\sum_{j\leq m}\sum_{d\mid\gcd(i,j)}\mu(d)=\sum_{i\leq n}\sum_{j\leq m}\sum_{d\mid i}\sum_{d\mid j}\mu(d)=\sum_{i\leq n}\sum_{d\mid i}\sum_{j\leq m}\sum_{d\mid j}\mu(d) LHS=injmdgcd(i,j)μ(d)=injmdidjμ(d)=indijmdjμ(d).

定理2: = ∑ d ≤ n ⌊ n d ⌋ ∑ d ≤ m ⌊ m d ⌋ μ ( d ) = ∑ d ≤ min ⁡ { n , m } μ ( d ) ⌊ n d ⌋ ⌊ m d ⌋ \displaystyle =\sum_{d\leq n}\left\lfloor\dfrac{n}{d}\right\rfloor\sum_{d\leq m}\left\lfloor\dfrac{m}{d}\right\rfloor\mu(d)=\sum_{d\leq\min\{n,m\}}\mu(d)\left\lfloor\dfrac{n}{d}\right\rfloor\left\lfloor\dfrac{m}{d}\right\rfloor =dndndmdmμ(d)=dmin{n,m}μ(d)dndm.

为求 ∑ i = 1 n ∑ j = i + 1 n gcd ⁡ ( i , j ) \displaystyle \sum_{i=1}^n\sum_{j=i+1}^n \gcd(i,j) i=1nj=i+1ngcd(i,j),先求 ∑ i ≤ n ∑ j ≤ n gcd ⁡ ( i , j ) \displaystyle\sum_{i\leq n}\sum_{j\leq n}\gcd(i,j) injngcd(i,j).

考虑枚举 d ∈ [ 1 , n ] d\in[1,n] d[1,n],则 ∑ i ≤ n ∑ j ≤ n gcd ⁡ ( i , j ) \displaystyle\sum_{i\leq n}\sum_{j\leq n}\gcd(i,j) injngcd(i,j)等于 d d d gcd ⁡ ( i , j ) = d \gcd(i,j)=d gcd(i,j)=d的数的个数,即 ∑ d ≤ n d ⋅ ∑ i ≤ n ∑ j ≤ n [ gcd ⁡ ( i , j ) = = d ] \displaystyle\sum_{d\leq n}d\cdot \sum_{i\leq n}\sum_{j\leq n}[\gcd(i,j)==d] dndinjn[gcd(i,j)==d].

∑ d ≤ n d ⋅ ∑ i ≤ n ∑ j ≤ n [ gcd ⁡ ( i , j ) = = d ] = ∑ d ≤ n d ⋅ ∑ i ≤ ⌊ n d ⌋ ∑ j ≤ ⌊ n d ⌋ [ gcd ⁡ ( i , j ) = = 1 ] = t = n d ∑ d ≤ n d ⋅ ∑ d ′ ≤ t μ ( d ′ ) ⌊ t d ′ ⌋ 2 \displaystyle\sum_{d\leq n}d\cdot \sum_{i\leq n}\sum_{j\leq n}[\gcd(i,j)==d]=\sum_{d\leq n}d\cdot \sum_{i\leq\left\lfloor\frac{n}{d}\right\rfloor}\sum_{j\leq\left\lfloor\frac{n}{d}\right\rfloor}[\gcd(i,j)==1]\xlongequal{t=\frac{n}{d}}\sum_{d\leq n}d\cdot \sum_{d'\leq t}\mu(d')\left\lfloor\dfrac{t}{d'}\right\rfloor^2 dndinjn[gcd(i,j)==d]=dndidnjdn[gcd(i,j)==1]t=dn dnddtμ(d)dt2,用整除分块求即可.

对比 ∑ i = 1 n ∑ j = i + 1 n gcd ⁡ ( i , j ) \displaystyle \sum_{i=1}^n\sum_{j=i+1}^n \gcd(i,j) i=1nj=i+1ngcd(i,j) ∑ i ≤ n ∑ j ≤ n gcd ⁡ ( i , j ) \displaystyle\sum_{i\leq n}\sum_{j\leq n}\gcd(i,j) injngcd(i,j)枚举的 ( i , j ) (i,j) (i,j)对:

∑ i = 1 n ∑ j = i + 1 n gcd ⁡ ( i , j ) \displaystyle \sum_{i=1}^n\sum_{j=i+1}^n \gcd(i,j) i=1nj=i+1ngcd(i,j) ∑ i ≤ n ∑ j ≤ n gcd ⁡ ( i , j ) \displaystyle\sum_{i\leq n}\sum_{j\leq n}\gcd(i,j) injngcd(i,j)
i = 1 i=1 i=1 j = 2 , 3 , ⋯   , n j=2,3,\cdots,n j=2,3,,n j = 1 , 2 , ⋯   , n j=1,2,\cdots,n j=1,2,,n
i = 2 i=2 i=2 j = 3 , 4 , ⋯   , n j=3,4,\cdots,n j=3,4,,n j = 1 , 2 , ⋯   , n j=1,2,\cdots,n j=1,2,,n
⋮ \vdots ⋮ \vdots ⋮ \vdots
i = n − 1 i=n-1 i=n1 j = n j=n j=n j = 1 , 2 , ⋯   , n j=1,2,\cdots,n j=1,2,,n
i = n i=n i=n ∅ \varnothing j = 1 , 2 , ⋯   , n j=1,2,\cdots,n j=1,2,,n

观察知:第 1 ∼ ( n − 1 ) 1\sim (n-1) 1(n1)行后者都比前者多枚举了一倍,且后者比前者多了 i = n i=n i=n的枚举.

i = n i=n i=n时, ∑ i ≤ n ∑ j ≤ n gcd ⁡ ( i , j ) = ∑ j ≤ n gcd ⁡ ( n , j ) = 1 + 2 + ⋯ + n = n ( n + 1 ) 2 \displaystyle\sum_{i\leq n}\sum_{j\leq n}\gcd(i,j)=\sum_{j\leq n}\gcd(n,j)=1+2+\cdots+n=\dfrac{n(n+1)}{2} injngcd(i,j)=jngcd(n,j)=1+2++n=2n(n+1),这是因为 n n n 1 ∼ n 1\sim n 1n中最大的,则 gcd ⁡ ( j , n ) = min ⁡ { j , n }    ( 1 ≤ j ≤ n ) \gcd(j,n)=\min\{j,n\}\ \ (1\leq j\leq n) gcd(j,n)=min{j,n}  (1jn).

∑ i ≤ n ∑ j ≤ n gcd ⁡ ( i , j ) = 2 ∑ i = 1 n ∑ j = i + 1 n gcd ⁡ ( i , j ) + n ( n + 1 ) 2 \displaystyle\sum_{i\leq n}\sum_{j\leq n}\gcd(i,j)=2\sum_{i=1}^n\sum_{j=i+1}^n \gcd(i,j)+\dfrac{n(n+1)}{2} injngcd(i,j)=2i=1nj=i+1ngcd(i,j)+2n(n+1).

代码
const int MAXN = 2e6 + 5;
int n;
int primes[MAXN], cnt = 0;
bool vis[MAXN];
int mu[MAXN];
int pre[MAXN];  // mu[]的前缀和

void init() {  // 预处理mu[]及其前缀和
	mu[1] = 1;
	for (int i = 2; i < MAXN; i++) {
		if (!vis[i]) primes[cnt++] = i, mu[i] = -1;
		for (int j = 0; primes[j] * i < MAXN; j++) {
			vis[primes[j] * i] = true;
			if (i % primes[j] == 0) break;
			mu[primes[j] * i] = -mu[i];
		}
	}
	
	for (int i = 1; i < MAXN; i++) pre[i] = pre[i - 1] + mu[i];
}

ll cal(int n, int d) {
	n /= d;
	ll res = 0;
	for (int l = 1, r = 0; l <= n; l = r + 1) {
		int tmp = n / l;
		r = min(n, n / tmp);
		res += (ll)(pre[r] - pre[l - 1]) * tmp * tmp;  // 注意ll
	}
	return res;
}

int main() {
	init();

	cin >> n;
	ll ans = 0;
	for (int d = 1; d <= n; d++) ans += (ll)d * cal(n, d);

	ans -= (ll)n * (n + 1) / 2, ans >>= 1;
	cout << ans;
}

25.3.4 YY的GCD ( 4 s 4 \mathrm{s} 4s)

例题

t    ( 1 ≤ t ≤ 1 e 4 ) t\ \ (1\leq t\leq 1\mathrm{e}4) t  (1t1e4)组测试数据.每组测试数据给定 n , m    ( 1 ≤ n , m ≤ 1 e 7 ) n,m\ \ (1\leq n,m\leq 1\mathrm{e}7) n,m  (1n,m1e7),求 1 ≤ x ≤ n , 1 ≤ y ≤ m 1\leq x\leq n,1\leq y\leq m 1xn,1ym gcd ⁡ ( x , y ) \gcd(x,y) gcd(x,y)为素数的 ( x , y ) (x,y) (x,y)的对数.

思路

不妨设 n ≤ m n\leq m nm,若不然则交换.设素数 k    ( 2 ≤ k ≤ n ) k\ \ (2\leq k\leq n) k  (2kn).

∑ i = 1 n ∑ j = 1 m [ gcd ⁡ ( i , j ) ∈ p r i m e s ] = ∑ k = 1 n ∑ i = 1 n ∑ j = 1 m [ g c d ( i , j ) = = k ] = ∑ k = 1 n ∑ i = 1 ⌊ n k ⌋ ∑ j = 1 ⌊ m k ⌋ [ gcd ⁡ ( i , j ) = = 1 ] \displaystyle\sum_{i=1}^n \sum_{j=1}^m[\gcd(i,j)\in primes]=\sum_{k=1}^n \sum_{i=1}^n \sum_{j=1}^m [gcd(i,j)==k]=\sum_{k=1}^n \sum_{i=1}^{\left\lfloor\frac{n}{k}\right\rfloor}\sum_{j=1}^{\left\lfloor\frac{m}{k}\right\rfloor}[\gcd(i,j)==1] i=1nj=1m[gcd(i,j)primes]=k=1ni=1nj=1m[gcd(i,j)==k]=k=1ni=1knj=1km[gcd(i,j)==1]

= ∑ k = 1 n ∑ i = 1 ⌊ n k ⌋ ∑ j = 1 ⌊ m k ⌋ ∑ d ∣ gcd ⁡ ( i , j ) μ ( d ) \displaystyle=\sum_{k=1}^n \sum_{i=1}^{\left\lfloor\frac{n}{k}\right\rfloor}\sum_{j=1}^{\left\lfloor\frac{m}{k}\right\rfloor}\sum_{d\mid \gcd(i,j)}\mu(d) =k=1ni=1knj=1kmdgcd(i,j)μ(d).

先枚举 d d d,因 d ∣ gcd ⁡ ( i , j ) d\mid \gcd(i,j) dgcd(i,j),则 i i i j j j都是 d d d的倍数.不妨将 i i i j j j的枚举上限都除以 d d d,相当于将 i i i变为 ⌊ i d ⌋ ⋅ d \left\lfloor\dfrac{i}{d}\right\rfloor\cdot d did,将 j j j变为 ⌊ j d ⌋ ⋅ d \left\lfloor\dfrac{j}{d}\right\rfloor\cdot d djd.

原 式 = ∑ k = 1 n ∑ d = 1 ⌊ n k ⌋ μ ( d ) ⋅ ⌊ n k d ⌋ ⌊ m k d ⌋ = T = k d ∑ T = 1 n ⌊ n T ⌋ ⌊ m T ⌋ ⋅ ∑ k ∣ T , k ∈ p r i m e s μ ( T k ) \displaystyle 原式=\sum_{k=1}^n \sum_{d=1}^{\left\lfloor\frac{n}{k}\right\rfloor}\mu(d)\cdot\left\lfloor\dfrac{n}{kd}\right\rfloor\left\lfloor\dfrac{m}{kd}\right\rfloor\xlongequal{T=kd}\sum_{T=1}^n \left\lfloor\dfrac{n}{T}\right\rfloor\left\lfloor\dfrac{m}{T}\right\rfloor\cdot\sum_{k\mid T,k\in primes}\mu\left(\dfrac{T}{k}\right) =k=1nd=1knμ(d)kdnkdmT=kd T=1nTnTmkT,kprimesμ(kT).

上式的 ∑ k ∣ T , k ∈ p r i m e s μ ( T k ) \displaystyle\sum_{k\mid T,k\in primes}\mu\left(\dfrac{T}{k}\right) kT,kprimesμ(kT)可预处理,用一个数组 f [ ] f[] f[]记录素数及其倍数累加的 μ \mu μ值,即对每个素数 k k k,将其在范围内的所有倍数 T T T f f f值加 μ ( T k ) \mu\left(\dfrac{T}{k}\right) μ(kT),再求 f [ ] f[] f[]的前缀和 p r e [ ] pre[] pre[]即可.

代码
const int MAXN = 1e7 + 5;
int n;
int primes[MAXN], cnt = 0;
bool vis[MAXN];
int mu[MAXN];
int f[MAXN];  // 记录素数及其倍数累加的mu值
int pre[MAXN];  // f[]的前缀和

void init() {
	mu[1] = 1;
	for (int i = 2; i < MAXN; i++) {
		if (!vis[i]) primes[cnt++] = i, mu[i] = -1;
		for (int j = 0; primes[j] * i < MAXN; j++) {
			vis[primes[j] * i] = true;
			if (i % primes[j] == 0) break;
			mu[primes[j] * i] = -mu[i];
		}
	}
	
	for (int i = 0; i < cnt; i++) {  // 枚举素数
		for (int j = 1; primes[i] * j <= 1e7; j++)  // 枚举倍数
			f[primes[i] * j] += mu[j];
	}

	for (int i = 1; i < MAXN; i++) pre[i] = pre[i - 1] + f[i];
}

int main() {
	init();

	CaseT{
		int n,m; cin >> n >> m;
		ll ans = 0;
		if (n > m) swap(n, m);  // 保证n≤m

		for (int l = 1, r = 0; l <= n; l = r + 1) {
			r = min(n / (n / l), m / (m / l));
			ans += (ll)(pre[r] - pre[l - 1]) * (n / l) * (m / l);
		}
		cout << ans << endl;
	}
}


常见的完全积性函数: ϵ ( n ) = [ n = = 1 ] , I ( n ) = 1 , I d ( n ) = n \epsilon(n)=[n==1],I(n)=1,Id(n)=n ϵ(n)=[n==1],I(n)=1,Id(n)=n.

对两个数论函数 f f f g g g,定义它们的Dirichlet卷积 ( f ∗ g ) ( n ) = ∑ d ∣ n f ( d ) g ( n d ) \displaystyle (f*g)(n)=\sum_{d\mid n}f(d)g\left(\dfrac{n}{d}\right) (fg)(n)=dnf(d)g(dn).

性质:①交换律: f ∗ g = g ∗ f f*g=g*f fg=gf;②结合律 ( f ∗ g ) ∗ h = f ∗ ( g ∗ h ) (f*g)*h=f*(g*h) (fg)h=f(gh);③单位元为 ϵ \epsilon ϵ,即 f ∗ ϵ = ϵ ∗ f = f f*\epsilon=\epsilon*f=f fϵ=ϵf=f.

常用结论:

μ ∗ I = ϵ \mu*I=\epsilon μI=ϵ.

​ [] u ∗ I = ∑ d ∣ n μ ( d ) I ( n d ) = ∑ d ∣ n μ ( d ) = [ n = = 1 ] = ϵ \displaystyle u*I=\sum_{d\mid n}\mu(d)I\left(\dfrac{n}{d}\right)=\sum_{d\mid n}\mu(d)=[n==1]=\epsilon uI=dnμ(d)I(dn)=dnμ(d)=[n==1]=ϵ.

φ ∗ I = I d ( n ) \varphi*I=Id(n) φI=Id(n).

​ [引理] ∑ d ∣ n φ ( d ) = n \displaystyle\sum_{d\mid n}\varphi(d)=n dnφ(d)=n.

​ [引.证] 考察与 n n n gcd ⁡ \gcd gcd分别为 1 , 2 , ⋯   , n 1,2,\cdots,n 1,2,,n的数的个数.

gcd ⁡ ( n , i ) = 1 \gcd(n,i)=1 gcd(n,i)=1 i i i φ ( n ) \varphi(n) φ(n)个, gcd ⁡ ( n , i ) = 2 \gcd(n,i)=2 gcd(n,i)=2 i i i φ ( n 2 ) \varphi\left(\dfrac{n}{2}\right) φ(2n)个, ⋯ \cdots .共 n n n个数,故 ∑ d ∣ n φ ( d ) = n \displaystyle\sum_{d\mid n}\varphi(d)=n dnφ(d)=n.

​ [] φ ∗ I = ∑ d ∣ n φ ( d ) I ( n d ) = ∑ d ∣ n φ ( d ) = n = I d ( n ) \displaystyle \varphi*I=\sum_{d\mid n}\varphi(d)I\left(\dfrac{n}{d}\right)=\sum_{d\mid n}\varphi(d)=n=Id(n) φI=dnφ(d)I(dn)=dnφ(d)=n=Id(n).

I d ∗ μ = φ ( n ) Id*\mu=\varphi(n) Idμ=φ(n).

​ [] ②两边卷 μ \mu μ得: φ ( n ) ∗ I ∗ μ = I d ∗ μ \varphi(n)*I*\mu=Id*\mu φ(n)Iμ=Idμ,则 φ ( n ) ∗ ( I ∗ μ ) = I d ∗ μ \varphi(n)*(I*\mu)=Id*\mu φ(n)(Iμ)=Idμ,即 φ ( n ) = I d ∗ μ \varphi(n)=Id*\mu φ(n)=Idμ.

[Mobius反演] 若 f ( n ) = ∑ d ∣ n g ( d ) \displaystyle f(n)=\sum_{d\mid n}g(d) f(n)=dng(d),即 f = g ∗ I f=g*I f=gI,则 g ( n ) = ∑ d ∣ n f ( n d ) ∗ μ ( d ) \displaystyle g(n)=\sum_{d\mid n}f\left(\dfrac{n}{d}\right)*\mu(d) g(n)=dnf(dn)μ(d),即 g = f ∗ μ g=f*\mu g=fμ.

[] f ∗ μ = g ∗ I ∗ μ = g ∗ ϵ = g f*\mu=g*I*\mu=g*\epsilon=g fμ=gIμ=gϵ=g.


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值