题意:给定一个包含n n n 个正整数的数组a a a 。任意次操作:选择两个索引i i i 和j j j ,然后将a i a_i a i 的值更新为gcd ( a i , a j ) \gcd(a_i, a_j) g cd( a i , a j ) 。求出让数组中所有元素都相等所需的最少操作次数。
所有元素最终必然会相等,且等于整个初始数组的 GCD。问题的核心就变成了如何求得最小的 GCD。
方法一 :看到 5000 考虑O ( n 2 ) \mathcal O(n^{2}) O ( n 2 ) 的 DP。设d p i , x dp_{i,x} d p i , x 表示前i i i 个数中至少需要选出几个数才能组合出 GCD 等于x x x 。复杂度O ( n V log V ) \mathcal O(nV\log V) O ( n V log V ) ,V = 5000 V=5000 V = 5 0 0 0 ,能过。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <bits/stdc++.h> using namespace std;using uint = unsigned ;using ll = long long ;using ull = unsigned long long ;using ulll = unsigned __int128;constexpr ll inf = 0x3f3f3f3f3f3f3f3f ;void solve () { int n; cin >> n; vector<int > a (n) ; int d = 0 ; for (int i = 0 ; i < n; i++) { cin >> a[i]; d = gcd (d, a[i]); } int cnt = 0 ; int has_one = 0 ; for (int i = 0 ; i < n; i++) { cnt += a[i] != d; if (a[i] == d) has_one = 1 ; } const int N = 5001 ; vector<int > dp (N, inf) ; for (int i = 0 ; i < n; i++) { auto ndp = dp; ndp[a[i]] = 0 ; for (int j = 1 ; j < N; j++) { ndp[gcd (j, a[i])] = min (ndp[gcd (j, a[i])], dp[j] + 1 ); } dp = ndp; } cout << (dp[d] + cnt - 1 + has_one) << endl; } int main () { cin.tie (nullptr )->sync_with_stdio (false ); for (int t = (cin >> t, t); t--; ) solve (); return 0 ; }
题意:给定一个包含n n n 个正整数的数组a a a 。任意次操作:选择两个索引i i i 和j j j ,然后将a i a_i a i 的值更新为gcd ( a i , a j ) \gcd(a_i, a_j) g cd( a i , a j ) 。求出让数组中所有元素都相等所需的最少操作次数。
官方题解给出了本题的 Bonus 版本:Try to solven , a i ≤ 2 × 1 0 5 n, a_i \leq 2 \times 10^5 n , a i ≤ 2 × 1 0 5 with only one test case. 设值域为V V V ,期望复杂度O ( V log V ) \mathcal{O}(V\log V) O ( V log V ) 。
所有元素最终必然会相等,且等于整个初始数组的 GCD。问题的核心就变成了如何求得最小的 GCD。
考虑到求得最小的 GCD 需要的数不会很多,事实上最大只会是ω ( V ) = 8 \omega(V)=8 ω ( V ) = 8 ,即V V V 以内一个数的最大素因子个数。
我们采用迭代的方式求解。迭代t t t 轮次后得到: 恰好选出t t t 个数(允许重复),它们的 GCD 可能为哪些 。循环ω ( V ) \omega(V) ω ( V ) 轮次后就找出答案,每轮迭代期望复杂度O ( V log V ) \mathcal{O}(V\log V) O ( V log V ) 。
设
f [ i ] f[i] f [ i ] : 初始数组中数字i i i 的出现次数,g [ t ] [ i ] g[t][i] g [ t ] [ i ] : 迭代t t t 轮次后,数字i i i 当前是否可以被表示为若干个数的 GCD。 每一轮迭代需要用f f f 来更新g [ t ] g[t] g [ t ] ,具体来说是g [ t ] [ i ] g[t][i] g [ t ] [ i ] 将被赋值为:是否有可能从f f f 中选一个数x x x ,再从g [ t − 1 ] g[t-1] g [ t − 1 ] 选一个数y y y ,且其最大公约数gcd ( x , y ) = i \gcd(x, y)=i g cd( x , y ) = i 。x x x 和y y y 都一定是i i i 的倍数,枚举i i i 的倍数(两个 log),加上 gcd 的判断(一个 log),复杂度O ( V log 3 V ) \mathcal{O}(V\log^{3} V) O ( V log 3 V ) 。
总的复杂度O ( V log 3 V ω ( V ) ) \mathcal{O}(V\log^{3} V\omega(V)) O ( V log 3 V ω ( V ) ) ,跑 2e5 的数据还是有点危险的。
进一步优化,引入辅助数组
h [ t ] [ i ] h[t][i] h [ t ] [ i ] 表示:是否有可能从f f f 中选一个数x x x ,再从g [ t − 1 ] g[t-1] g [ t − 1 ] 选一个数y y y ,它们的最大公约数gcd ( x , y ) \gcd(x, y) g cd( x , y ) 是i i i 的倍数。 这样x x x 和y y y 都一定是i i i 的倍数,通过枚举倍数在O ( V log V ) \mathcal{O}(V\log V) O ( V log V ) 的时间就能求出h h h 数组。显然h [ t ] [ n ] = ∑ k g [ t ] [ n k ] \displaystyle h[t][n]=\sum_{k}g[t][nk] h [ t ] [ n ] = k ∑ g [ t ] [ n k ] ,为用h [ t ] h[t] h [ t ] 表示g [ t ] g[t] g [ t ] 需要用到莫比乌斯反演,
g [ t ] [ n ] = ∑ k μ [ k ] ⋅ h [ t ] [ n k ] \displaystyle g[t][n] = \sum_{k}\mu[k] \cdot h[t][n k] g [ t ] [ n ] = k ∑ μ [ k ] ⋅ h [ t ] [ n k ]
总的复杂度O ( V log V ⋅ ω ( V ) ) \mathcal{O}(V\log V \cdot\omega(V)) O ( V log V ⋅ ω ( V ) ) 。
注:莫比乌斯反演
f [ n ] = ∑ d ∣ n g [ d ] ⟺ g [ n ] = f [ n ] ∗ μ [ n ] = ∑ d ∣ n μ [ d ] ⋅ f [ n d ] f [ n ] = ∑ d = 1 ∞ g [ n d ] ⟺ g [ n ] = ∑ d = 1 ∞ μ [ d ] ⋅ f [ n d ] \begin{aligned} f[n]=\displaystyle \sum_{d \mid n} g[d] &\quad\iff\quad g[n]=f[n]\ast\mu[n]=\sum_{d \mid n} \mu[d]\cdot f\Big[\frac{n}{d}\Big] \\ f[n] = \sum_{d=1}^{\infty} g[n d] &\quad\iff\quad g[n] = \sum_{d=1}^{\infty} \mu[d] \cdot f[n d] \end{aligned} f [ n ] = d ∣ n ∑ g [ d ] f [ n ] = d = 1 ∑ ∞ g [ n d ] ⟺ g [ n ] = f [ n ] ∗ μ [ n ] = d ∣ n ∑ μ [ d ] ⋅ f [ d n ] ⟺ g [ n ] = d = 1 ∑ ∞ μ [ d ] ⋅ f [ n d ]
其中莫比乌斯函数μ [ n ] = { 1 , n = 1 0 , ∃ d > 1 , d 2 ∣ n ( − 1 ) ω ( n ) , otherwise \mu[n] = \begin{cases} 1, & n = 1 \\ 0, & \exists d > 1, d^{2} \mid n \\ (-1)^{\omega(n)}, & \text{otherwise}\end{cases} μ [ n ] = ⎩ ⎪ ⎪ ⎨ ⎪ ⎪ ⎧ 1 , 0 , ( − 1 ) ω ( n ) , n = 1 ∃ d > 1 , d 2 ∣ n otherwise 可以用欧拉筛线性预处理。
核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 vector<int > f (V) ; vector<int > g (V) ; for (auto x : a) { f[x] = 1 ; g[x] = 1 ; } for (int t = 1 ; ; t++) { vector<int > h (V) ; for (int i = 1 ; i < V; i++) { int sumf = 0 ; int sumg = 0 ; for (int j = i; j < V; j += i) { sumf += f[j]; sumg += g[j]; } h[i] = sumf * sumg; } for (int i = 1 ; i < V; i++) { g[i] = 0 ; for (int k = 1 ; i * k < V; k++) { g[i] += mu[k] * h[i * k]; } } if (g[d] > 0 ) { cout << (t + n - 1 ) << endl; return ; } }
Zeta - Möbius Transform 用于计算偏序集上一个元素的所有上级(或下级)元素的某个函数值的总和。在这里我们将其应用到「倍数——因子」这个上下级关系,即
X [ n ] = ∑ d = 1 ∞ x [ n d ] X[n] = \sum_{d=1}^{\infty} x[nd] X [ n ] = d = 1 ∑ ∞ x [ n d ]
代码里算 sumf
和 sumg
就是在求 Mobius 变换。
上面讲解中的h [ t ] h[t] h [ t ] 实际上是f f f 和g [ t − 1 ] g[t-1] g [ t − 1 ] GCD 卷积的变换域表示,一个域的卷积对应于另一个域的乘法,h [ t ] [ k ] = F [ k ] ⋅ G [ t ] [ k ] h[t][k]=F[k]\cdot G[t][k] h [ t ] [ k ] = F [ k ] ⋅ G [ t ] [ k ] ;而从h [ t ] h[t] h [ t ] 倒推g [ t ] g[t] g [ t ] 的过程,实际做了一次反 Mobius 变换。
从这个角度看,没有必要每次循环都先正变换再反变换。直接一直在变换域做点值乘法,只判断g [ t ] [ d ] g[t][d] g [ t ] [ d ] 这一项是否大于 0。
复杂度O ( V log V + V ⋅ ω ( V ) ) \mathcal{O}(V\log V+V\cdot\omega(V)) O ( V log V + V ⋅ ω ( V ) ) 。
核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 vector<int > f (V) ; for (auto x : a) { f[x] = 1 ; } vector<int > F (V) ; for (int i = 1 ; i < V; i++) { for (int j = i; j < V; j += i) { F[i] += f[j]; } } auto G = F; for (int t = 1 ; ; t++) { for (int i = 1 ; i < V; i++) { G[i] *= F[i]; } int res = 0 ; for (int k = 1 ; d * k < V; k++) { res += mu[k] * G[d * k]; } if (res > 0 ) { cout << (t + n - 1 ) << endl; return ; } }
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 #include <bits/stdc++.h> using namespace std;constexpr int N = 1e4 ;vector<int > prime; int minp[N]; int mu[N]; void solve () { int n; cin >> n; vector<int > a (n) ; int d = 0 ; for (int i = 0 ; i < n; i++) { cin >> a[i]; d = gcd (d, a[i]); } int cnt = count (a.begin (), a.end (), d); if (cnt) { cout << (n - cnt) << endl; return ; } const int V = *max_element (a.begin (), a.end ()) + 1 ; vector<int > f (V) ; for (auto x : a) { f[x] = 1 ; } vector<int > F (V) ; for (int i = 1 ; i < V; i++) { for (int j = i; j < V; j += i) { F[i] += f[j]; } } auto G = F; for (int t = 1 ; ; t++) { for (int i = 1 ; i < V; i++) { G[i] *= F[i]; } int res = 0 ; for (int k = 1 ; d * k < V; k++) { res += mu[k] * G[d * k]; } if (res > 0 ) { cout << (t + n - 1 ) << endl; return ; } } } int main () { cin.tie (nullptr )->sync_with_stdio (false ); minp[1 ] = 1 ; mu[1 ] = 1 ; for (int i = 2 ; i < N; i++) { if (!minp[i]) { minp[i] = i; prime.push_back (i); mu[i] = -1 ; } for (auto p : prime) { if (i * p >= N) break ; minp[i * p] = p; if (p == minp[i]) { mu[i * p] = 0 ; break ; } mu[i * p] = -mu[i]; } } for (int t = (cin >> t, t); t--; ) solve (); return 0 ; }
评注:
数论中很多东西不太大,比如素数距离和素因子个数等,看似有好几层循环实际上很小。 「恰好」不好做,尝试转化为「至少」或「至多」,再反演得到结果。本题是把 GCD 恰好等于某数转化为 GCD 是某数的倍数。 大概想法是先找出必要条件,再验证充分性。
倒着求出每个数的下界(a x ⩾ a z , a y ⩾ a z a_{x} \geqslant a_z,\ a_{y} \geqslant a_{z} a x ⩾ a z , a y ⩾ a z ),取a a a 为这个下界,再验证构造出的这个a a a 是否合法。复杂度O ( n + q ) \mathcal O(n+q) O ( n + q ) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <bits/stdc++.h> using namespace std;void solve () { int n, q; cin >> n >> q; vector<int > b (n) ; for (int i = 0 ; i < n; i++) { cin >> b[i]; } vector<array<int , 3>> que (q); for (auto & [x, y, z] : que) { cin >> x >> y >> z; x--, y--, z--; } reverse (que.begin (), que.end ()); auto a = b; for (auto & [x, y, z] : que) { a[y] = max (a[y], a[z]); a[x] = max (a[x], a[z]); if (z != x && z != y) { a[z] = 0 ; } } reverse (que.begin (), que.end ()); auto c = a; for (auto & [x, y, z] : que) { c[z] = min (c[x], c[y]); } if (b != c) { cout << -1 << endl; return ; } for (int i = 0 ; i < n; i++) { cout << a[i] << " " ; } cout << endl; } int main () { cin.tie (nullptr )->sync_with_stdio (false ); for (int t = (cin >> t, t); t--; ) solve (); return 0 ; }
这题方法应该不少,写一个相对好理解的。
先置a i : = a i ⊕ b i , r e s : = r e s ⊕ b i a_{i}:=a_{i}\oplus b_{i},\mathrm{res}:=\mathrm {res}\oplus b_{i} a i : = a i ⊕ b i , r e s : = r e s ⊕ b i ,这样相当于每个人可以选择要不要a i a_{i} a i 。
考虑a a a 只有 01 怎么做:最后一个拿到a = 1 a =1 a = 1 的人有决定权,可以改成他想要的数。因此检查最后一个a = 1 a =1 a = 1 的位置,如果是c = 1 c=1 c = 1 ,那么答案为 1,否则答案为 0。
如果有多位,就要知道每一个二进制位的决定权在谁手上。倒序遍历(因为要找最后一个位置)a a a 并插入线性基,设将a i a_{i} a i 插入线性基后的最高位为2 d 2^{d} 2 d ,那么d d d 的决定权就在c i c_{i} c i 手上。
现在,从高到低遍历每个二进制位,这一位d d d 的决定权在谁手上,这个人就能把这一位变成他想要的数(c = 1 c=1 c = 1 则变成 1,否则变成 0)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include <bits/stdc++.h> using namespace std;using uint = unsigned ;using ll = long long ;using ull = unsigned long long ;using ulll = unsigned __int128;constexpr int B = 60 ;void solve () { int n; cin >> n; vector<ull> a (n) ; for (int i = 0 ; i < n; i++) { cin >> a[i]; } ull res = 0 ; for (int i = 0 ; i < n; i++) { ull x; cin >> x; a[i] ^= x; res ^= x; } string c; cin >> c; array<ull, B> h{}; array<bool , B> op{}; for (int i = n - 1 ; i >= 0 ; i--) { auto & x = a[i]; for (int bit = B - 1 ; bit >= 0 ; bit--) { if (x >> bit & 1 ) { if (!h[bit]) { op[bit] = c[i] - '0' ; h[bit] = x; break ; } else { x ^= h[bit]; } } } } for (int bit = B - 1 ; bit >= 0 ; bit--) { if (op[bit] ^ (res >> bit & 1 )) { res ^= h[bit]; } } cout << res << endl; } int main () { cin.tie (nullptr )->sync_with_stdio (false ); for (int t = (cin >> t, t); t--; ) solve (); return 0 ; }