220706
Codeforces Round #803 (Div. 2) A. XOR Mixup
原题指路:https://codeforces.com/contest/1698/problem/A
题意
现有含 ( n − 1 ) (n-1) (n−1)个元素的整数序列 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 (1≤t≤1000)组测试数据.每组测试数据输入一个长度为 n ( 2 ≤ n ≤ 100 ) n\ \ (2\leq n\leq 100) n (2≤n≤100)的序列 a 1 , ⋯ , a n ( 0 ≤ a i ≤ 127 ) a_1,\cdots,a_n\ \ (0\leq a_i\leq 127) a1,⋯,an (0≤ai≤127),表示由原始序列 a a a经上述操作后得到的序列.输出任一个满足条件的 x x x,可以证明这样的 x x x存在.
思路
因构建出新序列后随机调换 a a a中元素的位置,则新序列中各个位置等价,即都有可能是 x x x的位置.不失一般性,不妨设新序列的前 ( n − 1 ) (n-1) (n−1)个元素为初始序列的元素,最后一个元素为 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) (n−1)个元素,注意到 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) (n−1)个元素的异或值等于 a 1 a_1 a1,显然可通过调整 a 1 , a 2 , a 3 a_1,a_2,a_3 a1,a2,a3的位置使得后 ( n − 1 ) (n-1) (n−1)个元素的异或值等于第一个元素,而这正是 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>ai−1+ai+1.注意首元素和尾元素不是高的.
有 t ( 1 ≤ t ≤ 1000 ) t\ \ (1\leq t\leq 1000) t (1≤t≤1000)组测试数据,每组测试数据输入一个 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 (3≤n≤2e5,1≤k≤n),表示整数序列的长度.现可在序列中取一个长度为 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=⌊2n−1⌋.
②若 n n n为偶数,则可通过若干次操作使得序列中除尾元素的偶数位的数都是高的, a n s = ⌊ n − 1 2 ⌋ ans=\left\lfloor\dfrac{n-1}{2}\right\rfloor ans=⌊2n−1⌋.
(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,⋯,ai−1,ai,ai+1,⋯,an,
若选择区间 [ a i − 1 , a i ] [a_{i-1},a_i] [ai−1,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} ai−1+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>ai−1+1+ai+1,即 a i > a i − 1 + a i + 1 a_i>a_{i-1}+a_{i+1} ai>ai−1+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}] [ai−1,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 ∀1≤i<j<k≤n,∃1≤l≤n s.t. ai+aj+ak=al。
有 t ( 1 ≤ t ≤ 1000 ) t\ \ (1\leq t\leq 1000) t (1≤t≤1000)组测试数据.每组测试数据输入一个长度为 n ( 3 ≤ n ≤ 2 e 5 ) n\ \ (3\leq n\leq 2\mathrm{e}5) n (3≤n≤2e5)的整数序列 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 (−1e9≤ai≤1e9).若该序列是好的,输出"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 ∃al∈a 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 (2≤n≤1e5),两地址分别是两单词首字母节点的地址(地址是一个 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 A∼Z,a∼z)、下一节点的地址.
若两单词存在相同后缀,输出相同后缀的起始节点的地址;否则输出 − 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 (1≤n≤1e5)和常数 k ( 1 ≤ k ≤ n ) k\ \ (1\leq k\leq n) k (1≤k≤n).节点地址用一个 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[an−1]=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 (1≤n≤1e5),节点地址是一个 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 (1≤n≤1e5)和常数 k ( 1 ≤ k ≤ n ) k\ \ (1\leq k\leq n) k (1≤k≤n).节点地址用一个 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 (2≤n≤1000,1≤m≤2n(n−1),1≤k≤n,km≤3.5e6),分别表示城市总数、公路总数和重点关注城市的数量,城市编号 1 ∼ n 1\sim n 1∼n.
接下来 m m m行每行输入两个整数 a , b ( 1 ≤ a , b ≤ n ) a,b\ \ (1\leq a,b\leq n) a,b (1≤a,b≤n),表示城市 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=cnt−1.
时间复杂度 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 (1≤n≤1000).接下来 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 (1≤i≤k≤5)的 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 0001∼9999,可用一个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 (1≤n≤1e4)表示照片数.接下来 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 (1≤i≤k≤10)表示鸟的编号.数据保证所有照片中的鸟被连续编号 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 (1≤q≤1e4),接下来 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 (1≤n≤1000)表示社交网络人数,人编号 1 ∼ n 1\sim n 1∼n.接下来 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] (1≤i≤k≤n,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 (1≤n≤1e5)和 m ( 1 ≤ m ≤ 1 e 5 ) m\ \ (1\leq m\leq 1\mathrm{e}5) m (1≤m≤1e5),分别表示数组长度和操作个数.第二行输入 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 (1≤a≤b≤n),当 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 (1≤n≤1.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) (0≤x,y≤3.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 (0≤i≤n−1)行输出一个数表示 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 (2≤n≤2e6),求 ∑ i = 1 n ∑ j = i + 1 n gcd ( i , j ) \displaystyle \sum_{i=1}^n\sum_{j=i+1}^n \gcd(i,j) i=1∑nj=i+1∑ngcd(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) i≤n∑d∣i∑μ(d)=d≤n∑⌊dn⌋μ(d).
[证] ∑ i ≤ n ∑ d ∣ i \displaystyle\sum_{i\leq n}\sum_{d\mid i} i≤n∑d∣i∑表示先枚举 1 ∼ n 1\sim n 1∼n内的 i i i,再枚举 i i i的所有约数,
这等价于先枚举 1 ∼ n 1\sim n 1∼n内的 d d d,再枚举其在 1 ∼ n 1\sim n 1∼n内的倍数,显然倍数有 ⌊ 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 i≤n∑j≤m∑[gcd(i,j)==1]=d≤min{n,m}∑μ(d)⌊dn⌋⌊dm⌋.
[证] 注意到 [ 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]=d∣gcd(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=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).
由定理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 =d≤n∑⌊dn⌋d≤m∑⌊dm⌋μ(d)=d≤min{n,m}∑μ(d)⌊dn⌋⌊dm⌋.
为求 ∑ i = 1 n ∑ j = i + 1 n gcd ( i , j ) \displaystyle \sum_{i=1}^n\sum_{j=i+1}^n \gcd(i,j) i=1∑nj=i+1∑ngcd(i,j),先求 ∑ i ≤ n ∑ j ≤ n gcd ( i , j ) \displaystyle\sum_{i\leq n}\sum_{j\leq n}\gcd(i,j) i≤n∑j≤n∑gcd(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) i≤n∑j≤n∑gcd(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] d≤n∑d⋅i≤n∑j≤n∑[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 d≤n∑d⋅i≤n∑j≤n∑[gcd(i,j)==d]=d≤n∑d⋅i≤⌊dn⌋∑j≤⌊dn⌋∑[gcd(i,j)==1]t=dnd≤n∑d⋅d′≤t∑μ(d′)⌊d′t⌋2,用整除分块求即可.
对比 ∑ i = 1 n ∑ j = i + 1 n gcd ( i , j ) \displaystyle \sum_{i=1}^n\sum_{j=i+1}^n \gcd(i,j) i=1∑nj=i+1∑ngcd(i,j)和 ∑ i ≤ n ∑ j ≤ n gcd ( i , j ) \displaystyle\sum_{i\leq n}\sum_{j\leq n}\gcd(i,j) i≤n∑j≤n∑gcd(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=1∑nj=i+1∑ngcd(i,j) | ∑ i ≤ n ∑ j ≤ n gcd ( i , j ) \displaystyle\sum_{i\leq n}\sum_{j\leq n}\gcd(i,j) i≤n∑j≤n∑gcd(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=n−1 | 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∼(n−1)行后者都比前者多枚举了一倍,且后者比前者多了 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} i≤n∑j≤n∑gcd(i,j)=j≤n∑gcd(n,j)=1+2+⋯+n=2n(n+1),这是因为 n n n是 1 ∼ n 1\sim n 1∼n中最大的,则 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} (1≤j≤n).
故 ∑ 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} i≤n∑j≤n∑gcd(i,j)=2i=1∑nj=i+1∑ngcd(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 (1≤t≤1e4)组测试数据.每组测试数据给定 n , m ( 1 ≤ n , m ≤ 1 e 7 ) n,m\ \ (1\leq n,m\leq 1\mathrm{e}7) n,m (1≤n,m≤1e7),求 1 ≤ x ≤ n , 1 ≤ y ≤ m 1\leq x\leq n,1\leq y\leq m 1≤x≤n,1≤y≤m且 gcd ( x , y ) \gcd(x,y) gcd(x,y)为素数的 ( x , y ) (x,y) (x,y)的对数.
思路
不妨设 n ≤ m n\leq m n≤m,若不然则交换.设素数 k ( 2 ≤ k ≤ n ) k\ \ (2\leq k\leq n) k (2≤k≤n).
∑ 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=1∑nj=1∑m[gcd(i,j)∈primes]=k=1∑ni=1∑nj=1∑m[gcd(i,j)==k]=k=1∑ni=1∑⌊kn⌋j=1∑⌊km⌋[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=1∑ni=1∑⌊kn⌋j=1∑⌊km⌋d∣gcd(i,j)∑μ(d).
先枚举 d d d,因 d ∣ gcd ( i , j ) d\mid \gcd(i,j) d∣gcd(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 ⌊di⌋⋅d,将 j j j变为 ⌊ j d ⌋ ⋅ d \left\lfloor\dfrac{j}{d}\right\rfloor\cdot d ⌊dj⌋⋅d.
原 式 = ∑ 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=1∑nd=1∑⌊kn⌋μ(d)⋅⌊kdn⌋⌊kdm⌋T=kdT=1∑n⌊Tn⌋⌊Tm⌋⋅k∣T,k∈primes∑μ(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) k∣T,k∈primes∑μ(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) (f∗g)(n)=d∣n∑f(d)g(dn).
性质:①交换律: f ∗ g = g ∗ f f*g=g*f f∗g=g∗f;②结合律 ( f ∗ g ) ∗ h = f ∗ ( g ∗ h ) (f*g)*h=f*(g*h) (f∗g)∗h=f∗(g∗h);③单位元为 ϵ \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 u∗I=d∣n∑μ(d)I(dn)=d∣n∑μ(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 d∣n∑φ(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 d∣n∑φ(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=d∣n∑φ(d)I(dn)=d∣n∑φ(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)=d∣n∑g(d),即 f = g ∗ I f=g*I f=g∗I,则 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)=d∣n∑f(dn)∗μ(d),即 g = f ∗ μ g=f*\mu g=f∗μ.
[证] f ∗ μ = g ∗ I ∗ μ = g ∗ ϵ = g f*\mu=g*I*\mu=g*\epsilon=g f∗μ=g∗I∗μ=g∗ϵ=g.