Codeforces Round 988 div3 个人题解(A~G)
Dashboard - Codeforces Round 988 (Div. 3) - Codeforces
火车头
#include <bits/stdc++.h>using namespace std;#define ft first
#define sd second#define yes cout << "yes\n"
#define no cout << "no\n"#define Yes cout << "Yes\n"
#define No cout << "No\n"#define YES cout << "YES\n"
#define NO cout << "NO\n"#define pb push_back
#define eb emplace_back#define all(x) x.begin(), x.end()
#define all1(x) x.begin() + 1, x.end()
#define unq_all(x) x.erase(unique(all(x)), x.end())
#define unq_all1(x) x.erase(unique(all1(x)), x.end())
#define inf 0x3f3f3f3f
#define infll 0x3f3f3f3f3f3f3f3fLL#define RED cout << "\033[91m" // 红色
#define GREEN cout << "\033[92m" // 绿色
#define YELLOW cout << "\033[93m" // 蓝色
#define BLUE cout << "\033[94m" // 品红
#define MAGENTA cout << "\033[95m" // 青色
#define CYAN cout << "\033[96m" // 青色
#define RESET cout << "\033[0m" // 重置typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
// typedef __int128_t i128;typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<ll, int> pli;
typedef pair<string, ll> psl;typedef tuple<int, int, int> ti3;
typedef tuple<ll, ll, ll> tl3;
typedef tuple<ld, ld, ld> tld3;typedef vector<bool> vb;
typedef vector<int> vi;
typedef vector<ll> vl;
typedef vector<string> vs;
typedef vector<vi> vvi;
typedef vector<vl> vvl;// std::mt19937_64 rng(std::chrono::steady_clock::now().time_since_epoch().count());template <typename T>
inline T read()
{T x = 0;int y = 1;char ch = getchar();while (ch > '9' || ch < '0'){if (ch == '-')y = -1;ch = getchar();}while (ch >= '0' && ch <= '9'){x = (x << 3) + (x << 1) + (ch ^ 48);ch = getchar();}return x * y;
}template <typename T>
inline void write(T x)
{if (x < 0){putchar('-');x = -x;}if (x >= 10){write(x / 10);}putchar(x % 10 + '0');
}/*#####################################BEGIN#####################################*/
void solve()
{
}int main()
{ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);// freopen("test.in", "r", stdin);// freopen("test.out", "w", stdout);int _ = 1;std::cin >> _;while (_--){solve();}return 0;
}/*######################################END######################################*/
// 链接:
A. Twice
Kinich 醒来迎接新一天的开始。他打开手机,查看邮箱,发现一份神秘的礼物。他决定拆开礼物。
Kinich 拆开了一个包含 \(n\) 个整数的数组 \(a\)。最初,Kinich 的得分是 \(0\)。他将执行以下任意次操作:
选择两个索引 \(i\) 和 \(j\) ( \(1 \leq i < j \leq n\) ),使得 \(i\) 和 \(j\) 在任何先前的操作中均未被选择,并且 \(a_i = a_j\)。然后,将 \(1\) 添加到他的得分中。输出 Kinich 在执行上述任意次操作后可以获得的最高分数。
输入
第一行包含一个整数 \(t\) ( \(1 \leq t \leq 500\) ) — 测试用例的数量。
每个测试用例的第一行包含一个整数 \(n\) ( \(1 \leq n \leq 20\) ) — \(a\) 的长度。
每个测试用例的下一行包含 \(n\) 个空格分隔的整数 \(a_1, a_2, \ldots, a_n\) ( \(1 \leq a_i \leq n\) )。
输出
对于每个测试用例,在新行上输出可获得的最高分数。
示例
输入
5
1
1
2
2 2
2
1 2
4
1 2 3 1
6
1 2 3 1 2 3
输出
0
1
0
1
3
提示
在第一个和第三个测试用例中,Kinich 不能执行任何操作。
在第二个测试用例中,Kinich 可以执行一次操作,选择 \(i=1\) 和 \(j=2\)。
在第四个测试用例中,Kinich 可以执行一次操作,选择 \(i=1\) 和 \(j=4\)。
解题思路
使用map统计一下有多少相同的数对即可。
题解代码
void solve()
{int n;cin >> n;map<int, int> mp;for (int i = 0; i < n; i++){int x;cin >> x;mp[x]++;}int ans = 0;for (auto it : mp){ans += it.sd / 2;}cout << ans << "\n";
}
B. Intercepted Inputs
为了帮助您为即将到来的 Codeforces 竞赛做准备,Citlali 设置了一个网格问题,并试图通过您的输入流为您提供一个 \(n \times m\) 的网格。具体来说,您的输入流应包含以下内容:
第一行包含两个整数 \(n\) 和 \(m\) — 网格的尺寸。
以下 \(n\) 行分别包含 \(m\) 个整数 — 网格的值。
但是,有人截取了您的输入流,打乱了所有给定的整数,并将它们全部放在一行上!现在,一行上有 \(k\) 个整数,而您不知道每个整数原本属于哪里。您决定自己确定 \(n\) 和 \(m\) 的值,而不是要求 Citlali 重新发送输入。
输入
第一行包含一个整数 \(t\) ( \(1 \leq t \leq 10^4\) ) — 测试用例的数量。
每个测试用例的第一行包含一个整数 \(k\) ( \(3 \leq k \leq 2 \cdot 10^5\) ) — 输入流中的输入总数。
每个测试用例的下一行包含 \(k\) 个整数 \(a_1, a_2, \ldots, a_k\) ( \(1 \leq a_i \leq k\) ) — 输入流的混洗输入。保证 \(n\) 和 \(m\) 包含在 \(k\) 个整数内。
保证所有测试用例的 \(k\) 之和不超过 \(2 \cdot 10^5\)。
输出
对于每个测试用例,输出两个整数,一个可能值为 \(n\) 和 \(m\)。如果存在多个可能的答案,则输出任意一个。
示例
输入
5
3
1 1 2
11
3 3 4 5 6 7 8 9 9 10 11
8
8 4 8 3 8 2 8 1
6
2 1 4 5 3 3
8
1 2 6 3 8 5 5 3
输出
1 1
3 3
2 3
4 1
1 6
提示
在第一个测试用例中,初始输入可能是:
1 1
2
在第二个测试用例中,初始输入可能是:
3 3
4 5 6
7 8 9
9 10 11
解题思路
枚举所有 \(a_i\) 查找是否存在 \(a_j\) 使得 \(a_i\times a_j = k-2\) 即可。
题解代码
void solve()
{int k;cin >> k;map<int, int> mp;for (int i = 1; i <= k; i++){int x;cin >> x;mp[x]++;}k -= 2;for (auto it : mp){if (k % it.ft == 0 && mp[k / it.ft]){cout << it.ft << " " << k / it.ft << "\n";return;}}
}
C. Superultra's Favorite Permutation
小熊猫 Superultra 非常想要原石。在梦中,一个声音告诉他,他必须解决以下任务才能获得一生所需的原石。帮助 Superultra!
构造一个长度为 \(n\) 的排列 \(p\),使得 \(p_i + p_{i+1}\) 是整个 \(1 \leq i \leq n-1\) 的复合数。如果不可能,则输出 \(-1\)。
一个长度为 \(n\) 的排列是一个由 \(n\) 个不同整数组成的数组,这些整数从 \(1\) 到 \(n\) 以任意顺序排列。例如, \([2, 3, 1, 5, 4]\) 是排列,但 \([1, 2, 2]\) 不是排列(\(2\) 在数组中出现两次),\([1, 3, 4]\) 也不是排列(\(n=3\) 但数组中有 \(4\))。
如果整数 \(x\) 除了 \(1\) 和 \(x\) 之外至少还有一个其他除数,则该整数为合数。例如,\(4\) 为合数,因为 \(2\) 是除数。
输入
第一行包含 \(t\) ( \(1 \leq t \leq 10^4\) ) — 测试用例的数量。
每个测试用例包含一个整数 \(n\) ( \(2 \leq n \leq 2 \cdot 10^5\) ) — 排列的长度。
保证所有测试用例的 \(n\) 之和不超过 \(2 \cdot 10^5\)。
输出
对于每个测试用例,如果无法构造 \(p\),则在新行上输出 \(-1\)。否则,在新行上输出 \(n\) 个整数 \(p_1, p_2, \ldots, p_n\)。
示例
输入
2
3
8
输出
-1
1 8 7 3 6 2 4 5
解题思路
对于相邻两个数,如果它们奇偶性相同,则相加一定为偶数,所以我们可以把所有奇数放前面,偶数放后面,只需要考虑奇数和偶数相加的情况即可。发现对于 \(n\ge8\) 的情况,我们只需要把 \(1\) 和 \(8\) 放在一起即可,对于 \(n\lt 8\) 我们写个程序暴力打表计算即可。
打表程序:
#include <bits/stdc++.h>using namespace std;bool isPrime(int x)
{for (int i = 2; i * i <= x; i++){if(x % i==0)return false;}return true;
}int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);vector<int> a ;for (int i = 1; i <= 7; i++){a.push_back(i);bool flag = true;do {flag = true;for (int i = 0; i < a.size() - 1; i++){if (isPrime(a[i] + a[i + 1])){flag = false;break;}}if (flag){for (auto x : a){cout << x << " ";}cout << "\n";break;}} while (next_permutation(a.begin(), a.end()));if (!flag)cout << "-1\n";}return 0;
}
题解代码
void solve()
{int n;cin >> n;vi ans;if (n < 5){cout << -1 << "\n";return;}else if (n == 7){cout << "1 3 5 4 6 2 7\n";return;}else if (n == 6){cout << "1 3 5 4 2 6\n";return;}else if (n == 5){cout << "1 3 5 4 2\n";return;}for (int i = 3; i <= n; i += 2){ans.pb(i);}ans.pb(1);ans.pb(8);for (int i = 2; i <= n; i += 2){if (i == 8)continue;ans.pb(i);}for (int i = 0; i < n; i++){cout << ans[i] << " \n"[i == n - 1];}
}
D. Sharky Surfing
Mualani 喜欢在她的鲨鱼冲浪板上冲浪!
Mualani 的冲浪路径可以用数字线建模。她从位置 \(1\) 开始,路径在位置 \(L\) 结束。当她处于位置 \(x\) 且跳跃能力为 \(k\) 时,她可以跳到区间 \([x,x+k]\) 中的任何位置。最初,她的跳跃能力为 \(1\)。
但是,她的冲浪路径并不完全平坦。她的路径上有 \(n\) 个障碍。每个障碍都由一个区间 \([l,r]\) 表示,这意味着她无法跳到区间 \([l,r]\) 中的任何位置。
路径上的某些位置还有 \(m\) 个强化道具。道具 \(i\) 位于位置 \(x_i\),其值为 \(v_i\)。当 Mualani 位于位置 \(x_i\) 时,她可以选择收集道具,以增加她的跳跃力 \(v_i\)。同一位置可能有多个道具。当她位于具有一些道具的位置时,她可以选择获取或忽略每个道具。任何障碍的间隔内均没有道具。
她必须收集的最少道具数量是多少才能到达位置 \(L\) 并完成路径?如果无法完成冲浪路径,则输出 \(-1\)。
输入
第一行包含一个整数 \(t\) ( \(1 \leq t \leq 10^4\) ) — 测试用例的数量。
每个测试用例的第一行包含三个整数 \(n\)、\(m\) 和 \(L\) ( \(1 \leq n,m \leq 2 \cdot 10^5, 3 \leq L \leq 10^9\) ) — 障碍数量、强化道具数量和终点位置。
接下来的 \(n\) 行包含两个整数 \(l_i\) 和 \(r_i\) ( \(2 \leq l_i \leq r_i \leq L-1\) ) — 第 \(i\) 个障碍的区间界限。保证所有 \(1 \leq i < n\) 的 \(r_i + 1 < l_{i+1}\) (即所有障碍都不重叠,按升序排序,并且前一个障碍的终点与下一个障碍的起点不连续)。
以下 \(m\) 行包含两个整数 \(x_i\) 和 \(v_i\) ( \(1 \leq x_i, v_i \leq L\) ) — 第 \(i\) 个道具的位置和值。可能有多个道具具有相同的 \(x\)。保证所有 \(1 \leq i < m\) 的 \(x_i \leq x_{i+1}\) (即道具按非降序位置排序),并且没有道具位于任何障碍的间隔内。
保证所有测试用例的 \(n\) 之和与 \(m\) 之和不超过 \(2 \cdot 10^5\)。
输出
对于每个测试用例,输出她必须收集的最少能量提升数量才能到达位置 \(L\)。如果不可能,则输出 \(-1\)。
示例
输入
4
2 5 50
7 14
30 40
2 2
3 1
3 5
18 2
22 32
4 3 50
4 6
15 18
20 26
34 38
1 2
8 2
10 2
1 4 17
10 14
1 6
1 2
1 2
16 9
1 2 10
5 9
2 3
2 2
输出
4
-1
1
2
提示
在第一个测试用例中,她可以收集道具 \(1\)、\(2\)、\(3\) 和 \(5\) 来清除所有障碍。
在第二个测试用例中,她无法跳过第一个障碍。
在第四个测试用例中,通过收集两个道具,她可以跳过障碍。
解题思路
对于每一次拿去,我们肯定是贪心的拿取可以拿但还未拿的道具中对能力提升最大的那一个。所以我们可以使用堆来维护道具。枚举没一个障碍,将这个障碍之前的所有道具加入堆中,然后从堆中拿取道具直到可以跳过障碍。
题解代码
#define l first
#define r secondvoid solve()
{int n, m, L;cin >> n >> m >> L;vector<pii> a(n + 1);for (int i = 1; i <= n; i++){cin >> a[i].l >> a[i].r;}vector<pii> b(m + 1);int ans = -1;for (int i = 1; i <= m; i++){cin >> b[i].ft >> b[i].sd;}ll val = 1;int cnt = 0;int j = 1;priority_queue<int> q;for (int i = 1; i <= n; i++){while (j <= m && b[j].ft < a[i].l){q.push(b[j].sd);j++;}while (q.size() && val < a[i].r - a[i].l + 2){val += q.top();cnt++;q.pop();}if (val < a[i].r - a[i].l + 2){cout << "-1\n";return;}}cout << cnt << "\n";
}
E. Kachina's Favorite Binary String
这是一个交互式问题。
Kachina 挑战您猜出她最喜欢的长度为 \(n\) 的二进制字符串 \(s\)。她将 \(f(l,r)\) 定义为 \(s_l s_{l+1} \ldots s_r\) 中 \(01\) 的子序列数。如果两个子序列是通过从原始字符串的不同位置删除字符形成的,则它们被视为不同,即使生成的子序列由相同的字符组成。
要确定 \(s\),您可以问她一些问题。在每个问题中,您可以选择两个索引 \(l\) 和 \(r\) ( \(1 \leq l < r \leq n\) ),并询问她 \(f(l,r)\) 的值。
在向 Kachina 询问不超过 \(n\) 个问题后,确定并输出 \(s\)。但是,可能存在无法确定 \(s\) 的情况。在这种情况下,您需要改为报告 IMPOSSIBLE。
正式来说,如果在询问 \(n\) 个问题后,无论询问什么问题,对于 \(s\) 总是存在多个可能的字符串,则无法确定 \(s\)。请注意,如果您报告 IMPOSSIBLE 当存在最多 \(n\) 个查询序列可以唯一地确定二进制字符串时,您将得到错误答案的判定。
输入
输入的第一行包含一个整数 \(t\) ( \(1 \leq t \leq 10^3\) ) — 测试用例的数量。
每个测试用例的第一行包含一个整数 \(n\) ( \(2 \leq n \leq 10^4\) ) — \(s\) 的长度。
保证所有测试用例的 \(n\) 的总和不超过 \(10^4\)。
交互
要提出问题,请按以下格式输出一行(不包括引号)
"? l r"
陪审团将返回一个整数 \(f(l,r)\)。
当您准备打印答案时,请按以下格式输出一行:
如果无法确定 \(s\),则输出 "! IMPOSSIBLE"
否则,输出 "! s"
之后,继续处理下一个测试用例,如果这是最后一个测试用例,则终止程序。打印答案不算作查询。
交互器不自适应,这意味着答案在参与者提出查询之前就已经知道,并且不依赖于参与者提出的问题。
如果您的程序对一个测试用例进行了超过 \(n\) 次查询,则程序应立即终止以接收判定“错误答案”。否则,您可能会得到任意判定,因为您的解决方案将继续从封闭的流中读取。
打印查询后,不要忘记输出行尾并刷新输出。否则,您可能会得到“空闲限制超出”的判定。为此,请使用:
- C++ 中的
fflush(stdout)
或cout.flush()
; - Java 中的
System.out.flush()
; - Pascal 中的
flush(output)
; - Python 中的
stdout.flush()
。
示例
输入
2
5
4
输出
? 1 5
? 2 4
? 4 5
? 3 5
! 01001
? 1 2
! IMPOSSIBLE
提示
在第一个测试用例中,您可以通过询问 Kachina 关于不同区间的 \(f(l,r)\) 值来确定字符串。
在第二个测试用例中,由于只有一个可能的查询,无法区分字符串 \(00\) 和 \(11\),因此报告 IMPOSSIBLE。
解题思路
如果字符串不存在 \(01\) 子序列,我们查询什么都是 \(0\) ,因此一定无法确定字符串。
对于字符 \(s\) ,我们可以从 \(1\) 开始枚举所有的 \(i\) 和 \(i+1\) 直到找到 第一个 \(01\) 子字符串的位置 \(p\),然后我们通过询问 \([1,p]\) 即可知道 \([1,p]\) 中有多少个 \(0\) ,把这些零放在最后面,剩下的放 \(1\) 就得到了符合条件的 \([1,p]\) 的子字符串。
然后我们再从 \(p+1\) 开始枚举,查询 \([1,i]\) ,如果查询的 \(01\) 序列数增多了,说明该位置为 \(1\) 否则 为 \(0\) 。
题解代码
const string noFound = "IMPOSSIBLE";
int query(int l, int r)
{printf("? %d %d\n", l, r);fflush(stdout);return read<int>();
}void answer(string x)
{printf("! %s\n", x.c_str());fflush(stdout);
}void solve()
{int n = read<int>();int p = 0;for (int i = 2; i <= n; i++){if (query(i - 1, i)){p = i;break;}}if (!p){answer(noFound);return;}int now = query(1, p);string ans(n + 1, '0');for (int i = 1; i < p - now; i++){ans[i] = '1';}ans[p] = '1';for (int i = p + 1; i <= n; i++){int temp = query(1, i);if (temp > now)ans[i] = '1';now = temp;}answer(ans.substr(1));
}
F. Ardent Flames
您已获得新的限时活动角色 Xilonen。您决定在战斗中使用她。
一行中有 \(n\) 个敌人。左侧第 \(i\) 个敌人的生命值为 \(h_i\),当前位置为 \(x_i\)。Xilonen 的攻击伤害为 \(m\),您已准备好用她击败敌人。
Xilonen 具有强大的“地面践踏”攻击。在执行任何攻击之前,您选择一个整数 \(p\) 并将 Xilonen 定位在那里(\(p\) 可以是任何整数位置,包括当前有敌人的位置)。之后,每次攻击时,她都会对位置 \(p\) 处的敌人造成 \(m\) 伤害(如果有),对位置 \(p-1\) 和 \(p+1\) 处的敌人造成 \(m-1\) 伤害,对位置 \(p-2\) 和 \(p+2\) 处的敌人造成 \(m-2\) 伤害,依此类推。距离 Xilonen 至少为 \(m\) 的敌人不会受到任何伤害。
正式来说,如果位置 \(x\) 处有敌人,她每次击中都会对该敌人造成 \(\max(0,m-|p-x|)\) 伤害。请注意,您不能为不同的攻击选择不同的 \(p\)。
在所有可能的 \(p\) 中,输出 Xilonen 必须执行的最少攻击次数,以击败至少 \(k\) 个敌人。如果无法找到 \(p\),使得最终至少有 \(k\) 个敌人会被击败,则输出 \(-1\)。请注意,如果敌人的生命值达到 \(0\) 或以下,则视为被击败。
输入
第一行包含一个整数 \(t\) ( \(1 \leq t \leq 10^4\) ) - 测试用例的数量。
每个测试用例的第一行包含三个整数 \(n\)、\(m\) 和 \(k\) ( \(1 \leq k \leq n \leq 10^5\) 、 \(1 \leq m \leq 10^9\) )。
下一行包含 \(n\) 个整数 \(h_1,h_2,\ldots,h_n\) ( \(1 \leq h_i \leq 10^9\) )。
每个测试用例的最后一行包含 \(n\) 个整数 \(x_1,x_2,\ldots,x_n\) ( 对于所有 \(1 \leq i < n\) ,则为 \(1 \leq x_i \leq 10^9\) , \(x_i < x_{i+1}\) )。
保证所有测试用例的 \(n\) 的总和不超过 \(10^5\)。
输出
对于每个测试用例,在新行上输出一个整数,即击败至少 \(k\) 个敌人所必须执行的最少攻击次数。如果无法找到 \(p\),使得最终至少 \(k\) 个敌人将被击败,则输出 \(-1\)。
示例
输入
6
5 5 3
7 7 7 7 7
1 2 3 4 5
9 5 9
2 4 6 8 10 8 6 4 2
1 2 3 4 5 6 7 8 9
2 10 2
1 1
1 20
2 10 1
69696969 420420420
1 20
2 10 2
10 15
1 19
2 2 2
1000000000 1
1 3
输出
2
2
-1
6969697
15
1000000000
提示
在第一个测试用例中,选择 \(p=2\) 是最优的。每次攻击,第一个敌人受到 \(5-|2-1|=4\) 伤害,第二个敌人受到 \(5\) 伤害,第三个敌人受到 \(4\) 伤害,第四个敌人受到 \(3\) 伤害,第五个敌人受到 \(2\) 伤害。经过 \(2\) 次攻击,前 \(3\) 个敌人将被击败。可以证明,无论选择哪个 \(p\),都无法在少于 \(2\) 次攻击中击败 \(3\) 个敌人。
在第二个测试用例中,我们必须击败所有 \(9\) 个敌人。选择 \(p=5\),所有九个敌人将在 \(2\) 次攻击中被击败。
在第三个测试用例中,我们必须击败两个敌人。然而,可以证明没有选择的 \(p\) 能同时对两个敌人造成伤害,因此答案是 \(-1\)。
在第四个测试用例中,选择 \(p=1\) 将使我们在 \(6969697\) 次攻击中击败第一个敌人。
在第五个测试用例中,选择 \(p=10\) 将使每个敌人每次攻击受到 \(1\) 点伤害。两个敌人将在 \(15\) 次攻击中被击败。
解题思路
观察发现,这是一道求最小可行解的问题,具有二段性,考虑二分。
我们可以二分最小的攻击次数 \(q\),使得存在一个位置 \(p\) ,使得至少 \(k\) 个敌人在 \(q\) 次攻击后被击败。
设计检查函数。
对于给定的攻击次数 \(q\) ,我们需要检查是否存在一个位置 \(p\) ,使得至少 \(k\) 个敌人被击败。
对于每个敌人,计算其被击败所需的最小每次攻击伤害 \(t = \lceil \frac {h_i}{q} \rceil )\) ,如果 \(m \ge t\),则该敌人可以在 \(q\) 次攻击内被击败。
对于每个可被击败的敌人,计算需要位于的位置范围 \(p\in[l, r]\),使得 \(|p - x_i| \le m - t\) 。
将所有这些位置范围转化为区间事件,使用扫描线算法检查是否存在一个位置 \(p\) 被至少 \(k\) 个区间覆盖。
题解代码
void solve()
{ll n, m, k;cin >> n >> m >> k;vl h(n);for (int i = 0; i < n; i++){cin >> h[i];}vl x(n);for (int i = 0; i < n; i++){cin >> x[i];}ll l = 1;ll r = *max_element(all(h));ll ans = -1;auto check = [&](ll mid){vector<pll> v;for (int i = 0; i < n; ++i){ll t = (h[i] + mid - 1) / mid;if (m >= t){ll re = m - t;ll l = x[i] - re;ll r = x[i] + re;v.eb(l, 1);v.eb(r + 1, -1);}}if (v.empty())return false;sort(all(v));ll sum = 0;for (auto &[_, t] : v){sum += t;if (sum >= k)return true;}return false;};while (l <= r){ll mid = (l + r) >> 1;if (check(mid)){r = mid - 1;ans = mid;}else{l = mid + 1;}}cout << ans << "\n";
}
G. Natlan Exploring
您正在探索令人惊叹的 Natlan 地区!该地区由 \(n\) 个城市组成,每个城市的吸引力评级为 \(a_i\)。当且仅当 \(i < j\) 和 \(\text{gcd}(a_i, a_j) \neq 1\) 时,从城市 \(i\) 到城市 \(j\) 存在有向边,其中 \(\text{gcd}(x, y)\) 表示整数 \(x\) 和 \(y\) 的最大公约数 (GCD)。
从城市 1 出发,您的任务是确定到达城市 \(n\) 可以采取的不同路径总数,模数为 \(998244353\)。当且仅当访问的城市集合不同时,两条路径才是不同的。
输入
第一行包含一个整数 \(n\) ( \(2 \leq n \leq 2 \cdot 10^5\) ) — 城市数量。
第二行包含 \(n\) 个整数 \(a_1, a_2, \ldots, a_n\) ( \(2 \leq a_i \leq 10^6\) ) — 每个城市的吸引力。
输出
输出到达城市 \(n\) 可以采取的不同路径总数,模数为 \(998244353\)。
示例
输入
5
2 6 3 4 6
输出
5
输入
5
4 196 2662 2197 121
输出
2
输入
7
3 6 8 9 11 12 20
输出
7
输入
2
2 3
输出
0
提示
在第一个示例中,五条路径如下:
城市 1 → 城市 5
城市 1 → 城市 2 → 城市 5
城市 1 → 城市 2 → 城市 3 → 城市 5
城市 1 → 城市 2 → 城市 4 → 城市 5
城市 1 → 城市 4 → 城市 5
在第二个示例中,两条路径如下:
城市 1 → 城市 3 → 城市 5
城市 1 → 城市 2 → 城市 3 → 城市 5
解题思路
看到计数问题,我们先考虑暴力 \(dp\),我们设 \(f[i]\) 到达 \(i\) 城市的路径数,易得状态转移方程如下:
时间复杂度为 \(O(n^2)\) 由于 \(n\le 100000\),因此肯定超时,考虑优化。
观察发现,对于一个数 \(x=p_{1}^{k_1} \times p_{2}^{k_2} \times \dots \times p_{x}^{k_x}\) 。我们设 \(P = \bigcup_{i=1}^{n} p_i\) ,到达 \(x\) 的路径实际上可以归类为$v_S=\prod_{j\in S}j ,
S \subseteq P,S\neq \emptyset \(。\)v_S$ 为集合 \(S\) 的乘积。因为两个数要互相可达当且仅当不互质,即存在一个相同的质因数,因此对于一个数 \(x\) ,\(y=p_{1}^2\times p_{2}^1\) 和 \(z=p_{1}^3\times p_{2}^4\) 到达它实际上是可以等价的,即\(v_{S_z}=v_{S_y}\) 。因为\(2\times 3\times 5\times 7\times 11\times 13\times 17\times19 =9699690 \gt 1000000\),所以一个数最多只有 \(7\) 个质因子,它的转移状态只有 \(2^7=128\) 种,我们可以极大的简化每一个数从哪里转移过来所需的计算次数,时间复杂度为 \(O(128n)\) 。
如果我们直接加和计算 \(x\) 的转移状态,会发现对于从 \(p_1\times p_2\) 转移和从 \(p_1\times p_2 \times p_3\) 转移我们会进行重复计算。因此我们需要使用容斥原理来进行计算。对于质因子数为奇数的我们加上,偶数的我们减去,可以发现,这就是莫比乌斯函数(一开始想的是枚举质因子倍数然后调和级数求和算路径总数,用莫比乌斯函数做容斥,发现时间复杂度会超一点,后来发现直接算多重集容斥即可)。
我们设 \(g[v_S]\) 表示所有之前城市 \(j,j < i\),其 \(a_j\) 的质因数包含子集 \(S\) 中所有质因数的路径数之和,其中 \(v_S\) 为集合 \(S\) 中所以数的乘积 \(S \subseteq P_i,S\neq \emptyset\) , \(P_i\) 为 \(a_i\) 的质因数的全集。
易得状态转移方程:
推得最终的状态转移方程
题解代码
const int N = 1e6 + 5; // 假设最大范围为 1000000
int pri[N]; // 存储所有素数
int minp[N]; // minp[x] 存储 x 的最小质因子
int cnt; // 记录素数数量const int mod = 998244353;
// 欧拉筛函数
void sieve(int n)
{cnt = 0; // 初始化素数计数器for (int i = 2; i <= n; i++){if (minp[i] == 0){ // 如果 minp[i] 为 0,说明 i 是素数minp[i] = i; // 记录 i 的最小质因子为自身pri[cnt++] = i; // 存储素数}// 利用已找到的素数进行筛选for (int j = 0; j < cnt && pri[j] <= n / i; j++){minp[pri[j] * i] = pri[j]; // 筛掉 i * primes[j],并记录最小质因子if (i % pri[j] == 0)break; // 防止重复筛选}}
}vi func(int x)
{vi f;while (x > 1){int p = minp[x];f.eb(p);while (x % p == 0)x /= p;}return f;
}
void solve()
{int n;cin >> n;vi a(n);for (int i = 0; i < n; i++){cin >> a[i];}int maxA = *max_element(all(a));sieve(maxA);vl f(n + 1);f[1] = 1;vl g(maxA + 1);vi primes = func(a[0]);int m = primes.size();for (int mask = 0; mask < (1 << m); mask++){ll d = 1;for (int b = 0; b < m; b++){if (mask & (1 << b))d *= primes[b];}if (d > maxA)continue;g[d] = (g[d] + f[1]) % mod;}ll tot = f[1];for (int i = 2; i <= n; i++){vi p = func(a[i - 1]);int num = p.size();ll sum = 0;for (int mask = 0; mask < (1 << num); mask++){ll d = 1;bool flag = true;for (int b = 0; b < num; b++){if (mask & (1 << b)){d *= p[b];if (d > maxA){flag = false;break;}}}if (!flag)continue;int mu = (__builtin_popcount(mask) % 2 == 0) ? -1 : 1;sum = (sum + mu * g[d] + mod) % mod;}f[i] = (tot + sum + mod) % mod;for (int mask = 0; mask < (1 << num); mask++){ll d = 1;for (int b = 0; b < num; b++){if (mask & (1 << b))d *= p[b];}if (d > maxA)continue;g[d] = (g[d] + f[i]) % mod;}tot = (tot + f[i]) % mod;}cout << f[n] << "\n";
}
这场超神发挥,首次d3ak,不过感觉这场d3也比以往的更简单。