1 条题解
-
0
题目回顾
我们有两个数组 和 ,长度均为 。
必须执行恰好一次操作:选择 ,交换 和 对于所有 。操作后,定义:
$$\text{值} = \gcd(a_1, \dots, a_n) + \gcd(b_1, \dots, b_n) $$求这个值的最大值,以及达到最大值的不同 对的数量。
第一步:寻找最大可能的 和
关键观察
设 ,。
则 要么等于 ,要么 (因为 至少减半)。
所以 和 中不同的值最多只有 个,其中 。同样,后缀的 也满足这个性质。
固定 和 后的分析
假设我们想让操作后 的 为 , 的 为 。
考虑最长的前缀,使得其 为 (对 )或 (对 )。
如果交换区间和这个前缀有交集,我们可以不交换交集部分,因为它不会使 变大。
同理对于后缀。因此,交换区间应该完全落在“中间部分”,即前缀结束之后、后缀开始之前。
而中间部分的第一个和最后一个元素必须被交换,否则我们可以延长前缀或后缀。
充分的前缀/后缀位置
因此,我们只需要考虑那些“变化点”作为可能的前缀/后缀边界:
- 对前缀: 满足 或
- 对后缀:类似
这样的 最多 个。
暴力枚举中间区间
对于每个这样的前缀边界 (表示前缀 不交换,交换从 开始),我们枚举右边界 从 到 :
- 维护
- 维护
操作后:
- 的
- 的
用前缀 数组 和后缀 数组 可以 计算:
更新最大值。
因为 只会移动 次,每次 更新 ,所以这部分复杂度 。
第二步:计算方案数
我们找到了最大和 。
现在要统计有多少 能达到 。
方法:动态规划
固定 和 (),计算有多少区间使得最终 分别为 和 。
定义 ,其中 表示:
- :还没开始交换
- :正在交换(区间已开始,未结束)
- :已结束交换
初始 (第一个位置不交换)。
转移( 从 到 ):
对于当前 ,检查交换或不交换时, 是否能被 整除, 是否能被 整除:
-
如果不交换():只能从 来
如果满足条件 -
如果开始交换():可以从 (开始)或 (继续)来
如果满足条件(交换后 满足整除) -
如果结束交换():可以从 (不交换)、(刚好结束)、(已结束)来
如果满足条件(不交换的情况,即原始 满足整除)
最终,固定 的方案数 = 。
特殊情况处理
- 如果不交换任何区间(即 不可能,因为必须交换一次)?
不对,我们允许 吗?题目说选择 ,交换 ,所以 是允许的(只交换一个位置)。
因此“不交换”不是合法操作,必须交换至少一个元素。
但我们的 DP 包含了 (始终不交换)的情况,这对应 吗?不,这是无效的,因为必须交换一次。
所以最后要减去 吗?不,因为 对应的是整个数组都没交换,这是不允许的。
但标程中, 被计入,然后最后特殊处理了“没有交换”的情况。
实际上,标程的做法:
- 最后
- 如果 (即不交换也能达到最大值),则减去 (因为 算了一次,另外 次来自 从 转移,即交换 等价于没交换?这需要仔细分析,但按标程处理即可)
枚举
因为 和 不会被交换(,但交换 会交换 ,所以 可以被交换)。
所以 必须是 的某个值,即 是 的约数(因为最终 可能被换掉,所以 不一定整除 ?这里要小心)。
实际上,最终 必须整除每个 (在交换后),所以 整除 或 (如果 在交换区间内)。
所以枚举 为 的约数和 的约数即可。标程只枚举 的约数,并令 ,然后检查 是否整除 。
时间复杂度
- 枚举 : 个约数
- 每个 调用 : DP
- 加上前面找最大值的
总复杂度 ,对 可行。
代码实现(带注释)
#include <bits/stdc++.h> using namespace std; const int N = 200005; int a[N], b[N]; int pa[N], pb[N], sa[N], sb[N]; int n; ll dp[N][3]; ll solve_one(ll ga, ll gb) { if (ga <= 0 || gb <= 0) return 0; if (b[1] % gb) return 0; // b1 必须被 gb 整除 memset(dp, 0, sizeof(dp)); dp[1][0] = 1; for (int i = 2; i <= n; i++) { for (int j = 0; j < 3; j++) { for (int k = 0; k <= j; k++) { bool ok = true; if (j == 1) swap(a[i], b[i]); // 模拟交换 if (a[i] % ga) ok = false; if (b[i] % gb) ok = false; if (j == 1) swap(a[i], b[i]); // 换回来 if (ok) dp[i][j] += dp[i-1][k]; } } } return dp[n][0] + dp[n][1] + dp[n][2]; } int main() { ios::sync_with_stdio(0); cin.tie(0); int t; cin >> t; while (t--) { cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= n; i++) cin >> b[i]; // 前缀 gcd pa[0] = pb[0] = 0; for (int i = 1; i <= n; i++) { pa[i] = gcd(pa[i-1], a[i]); pb[i] = gcd(pb[i-1], b[i]); } // 后缀 gcd sa[n+1] = sb[n+1] = 0; for (int i = n; i >= 1; i--) { sa[i] = gcd(sa[i+1], a[i]); sb[i] = gcd(sb[i+1], b[i]); } // 小数据暴力 if (n <= 300) { int max_gcd = 0, ways = 0; for (int l = 1; l <= n; l++) { int ga = 0, gb = 0; for (int r = l; r <= n; r++) { ga = gcd(ga, a[r]); gb = gcd(gb, b[r]); int ca = gcd(pa[l-1], gcd(sa[r+1], gb)); int cb = gcd(pb[l-1], gcd(sb[r+1], ga)); if (ca + cb > max_gcd) { max_gcd = ca + cb; ways = 1; } else if (ca + cb == max_gcd) { ways++; } } } cout << max_gcd << " " << ways << "\n"; continue; } // 大数据:找最大和 int max_gcd = pa[n] + pb[n]; for (int i = 2; i <= n; i++) { if (pa[i] == pa[i-1] && pb[i] == pb[i-1]) continue; int ga = 0, gb = 0; for (int j = i; j <= n; j++) { ga = gcd(ga, a[j]); gb = gcd(gb, b[j]); int ca = gcd(pa[i-1], gcd(sa[j+1], gb)); int cb = gcd(pb[i-1], gcd(sb[j+1], ga)); max_gcd = max(max_gcd, ca + cb); } } // 统计方案数 ll ans = 0; for (int d = 1; d * d <= a[1]; d++) { if (a[1] % d) continue; ans += solve_one(d, max_gcd - d); if (d * d != a[1]) { ans += solve_one(a[1] / d, max_gcd - a[1] / d); } } // 减去没有交换的情况 if (pa[n] + pb[n] == max_gcd) ans -= n; // 加上只交换前缀的情况 for (int i = 1; i <= n; i++) { if (gcd(pa[i], sb[i+1]) + gcd(pb[i], sa[i+1]) == max_gcd) ans++; } cout << max_gcd << " " << ans << "\n"; } return 0; }
总结
- 利用 的前缀/后缀快速变化性质, 个关键点。
- 枚举所有可能的交换区间, 找到最大和。
- 用 DP 统计方案数,枚举 为 的约数。
- 小数据用 暴力,大数据用优化方法。
这样就能在 内解决问题。
- 1
信息
- ID
- 6253
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 2
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者