记录比赛的那天 KS 酱办生日聚会,所以玩的有点晚——于是为了避免掉分就又开了个小号——结果还真的算是预防成功了((只能说不愧是我==
和上一场 一样的出题人,也是熟悉的五个题目;但是这场总感觉比之前那一场要难一些——可能出题人不经意间触及了我较多的知识盲区吧(
题解右手就行可是我白给了一发 ;只要取出前面的加到最后一个元素上就行了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const int N = 100 ;int a[N];signed main () { ios::sync_with_stdio (false ); cin.tie (null), cout.tie (null); int T = $.nextInt (), n, k; while (T --) { input, n, k; $.nextArray (a + 1 , a + 1 + n); int l = 1 , r = n; while (k && l < r) { auto ded = min (k, a[l]); k -= ded, a[l] -= ded, a[r] += ded; ++ l; } $.putArray (a + 1 , a + 1 + n); } return 0 ; }
到底是什么样的小天才才能写出 ++ l, -- r
这种代码呢?以为很对称🐎(
考虑到最后只可能剩下两个相同的元素或三个相同的元素——因为更多的元素都可以合并到这两种情况上,所以只需要分别处理即可:
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 const int N = 2060 ;uint a[N], sum[N]; signed main () { ios::sync_with_stdio (false ); cin.tie (null), cout.tie (null); int T = $.nextInt (), n; while (T --) { $(n).nextArray (a + 1 , a + 1 + n); for (int i = 1 ; i <= n; ++ i) sum[i] = sum[i - 1 ] ^ a[i]; bool found = false ; for (int i = 1 ; i <= n; ++ i) if (sum[i] == (sum[n] ^ sum[i])) found = true ; for (int i = 1 ; i <= n; ++ i) for (int j = i; j <= n; ++ j) if (sum[i] == (sum[j] ^ sum[i]) && sum[i] == (sum[n] ^ sum[j])) found = true ; $.put (found ? "YES" : "NO" ); } return 0 ; }
之前白给成了直接排除一个元素,显然这是不科学的(
提供长度为n ≤ 100 n \leq 100 n ≤ 100 的数组a a a ,你要从中删除一些元素,使得不可以将这个数组拆分成两个部分,使得两个部分的和相等;要求最小化删除元素的数量并输出删除元素的坐标。
首先,显然当∑ i = 1 n a i \sum_{i = 1}^n a_i ∑ i = 1 n a i 是奇数的时候不用删除任何元素;然后,为偶数情况下可以进行背包D P DP D P 来判断是否可能完成这样的划分;如果可以,那么问题就变成了如何删除元素。
不难想到最多只会删除一个元素,那么问题就是删除什么样的元素;我最开始因为删除最小的元素然后白给了一些 罚时,因为这样是不正确的——比如删除 2
,可以通过交换两个 2
和一个 5
来使得再次平衡;那么我们在考虑其他的一定可行的情况,就不难想到删除一个奇数。
如果整个数组都是偶数怎么办?那么我们可以整体右移1 1 1 位,显然和原数组是等价的;我们可以一直右移,直到我们找到了可以删除的奇数为止;显然,这一定可以找到;
从右移等价,我们可以联想到整个数组除以任何同一个数都是等价的;所以,一个最简单的方法就是首先约去整个数组的gcd \gcd g cd ,然后找到一个奇数删除就行了——因为是等价的,所以这样的删除是合理的;
代码实现这个使用 bitset
的可行性背包实现属实颇有雅趣(
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 const int N = 2050 , M = 110 ;int a[N]; bool check (int n) { int sum = accumulate (a + 1 , a + 1 + n, 0 ); if (sum % 2 ) return false ; bitset<N * M> dp; dp.set (0 ); for (int i = 1 ; i <= n; ++ i) dp |= (dp << a[i]); return dp[sum / 2 ]; } signed main () { ios::sync_with_stdio (false ); cin.tie (null), cout.tie (null); int n = $.nextInt (); $.nextArray (a + 1 , a + 1 + n); if (check (n)) { int g = 0 ; for (int i = 1 ; i <= n; ++ i) g = gcd (g, a[i]); for (int i = 1 ; i <= n; ++ i) if (a[i] / g % 2 ) { $.put (1 ).put (i); break ; } } else $.put (0 ); return 0 ; }
于是这个卵题我白给了近十发,,我是真的菜(
给一个长度为n ≤ 1 0 5 n \leq 10^5 n ≤ 1 0 5 的数组a a a ,进行q ≤ 1 0 5 q\leq10^5 q ≤ 1 0 5 次询问:每次询问关于一个区间[ l , r ] [l, r] [ l , r ] ,将它分成的最少的段数,使得每一段的LCM \text{LCM} LCM 都和连乘的乘积相等;求这个段数。
首先,不难意识到LCM \text{LCM} LCM 和连乘积相等就是说这一段的gcd \gcd g cd 为1 1 1 。那么问题就转化为了对于任意段[ l , r ] [l, r] [ l , r ] ,将它划分成互质的最小的段数。
那么一种很显然的想法就是对于范围内的所有质数(约9600 9600 9600 个)分别维护一个列表,包含了包含它为质因数的数字的下标;然后对于一个位置,对于它的每一个质因子在对应的表上二分查找出下一个位置,取最小值作为下一个区间开始的备选位置;但是这样做显然非常的啥b,因为有显而易见地简单优化但是我也显而易见的没有想到 :
我们可以倒着维护每一个质数的下一个位置,这样就不用对每个质数二分查找了 倒着转移的时候也考虑后一个位置的备选位置,这样就不用考虑到备选区间之间的冲突了 那么这样,我们就可以维护出一个表F i F_i F i ,表示从i i i 开始可以转移到的最远的备选位置;
但是这样还存在一个问题:如果所有的数字都是1 1 1 ,那么上面的做法会被卡成n 2 n^2 n 2 ;因此,为了能够快速的跳转求出区段数,我们可以利用倍增的思想,维护下两个、下四个备选位置;这样,就可以在log n \log n log n 的时间内完成转移,并且像二进制拆位那样构造出任何一个数字。
代码实现使用类筛的方法完成质因数分解。
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 const int N = 1e5 + 5 ;const int M = 9600 ;int a[N], f[20 ][N], to[M];auto decomposition (const int n) { static vector<int > p; static vector<vector<int >> de (n); for (int i = 2 ; i < n; ++i) if (de[i].empty ()) { for (int j = i; j < n; j += i) de[j].push_back (i); p.push_back (i); } return make_pair (ref (p), ref (de)); } signed main () { ios::sync_with_stdio (false ); cin.tie (null), cout.tie (null); int n, q, l, r; $(n, q).nextArray (a + 1 , a + 1 + n); auto [p, de] = decomposition (N); unordered_map<int , int > desc; const auto ps = p.size (); const auto bound = n + 1 ; desc.reserve (ps * 4 ); for (int i = 0 ; i < ps; ++ i) { desc[p[i]] = i, to[i] = bound; } f[0 ][bound] = bound; for (int i = n; i; -- i) { f[0 ][i] = f[0 ][i + 1 ]; for (auto j : de[a[i]]) { minimize (f[0 ][i], to[desc[j]]); to[desc[j]] = i; } } for (int i = 1 ; i < 20 ; ++ i) for (int j = 1 ; j <= bound; ++ j) f[i][j] = f[i - 1 ][f[i - 1 ][j]]; while (q --) { $(l, r); uint ans = 0 ; for (int i = 19 ; i >= 0 ; -- i) if (f[i][l] <= r) { ans += (1u << i); l = f[i][l]; } $.put (ans + 1 ); } return 0 ; }
即使可以想到正确的维护方法,但是倍增的思想也不能不说是十分的高妙…… 学到许多(
现在有长度为n ≤ 1 0 9 n \leq 10^9 n ≤ 1 0 9 的排列{ 1 , … , n } \{1, \dots,n\} { 1 , … , n } ,可以进行对于两个不同的下标两两交换的操作k ≤ 200 k \leq 200 k ≤ 200 次;问对于[ 1 , k ] [1, k] [ 1 , k ] 次操作可以得到的不同的排列数量;
首先,先说明一种贪心地将长度为n n n 的排序p p p 复位的做法——对于排列的最后一个位置[ n ] [n] [ n ] :
如果p n = n p_n = n p n = n ,那么可以忽略这个位置;将原排列看作长度为n − 1 n - 1 n − 1 否则,将p n p_n p n 和p p n p_{p_n} p p n 交换位置;此时至少可以使得p n p_n p n 复位,继续递归,但是操作次数+ 1 +1 + 1 可以证明,这样处理完整个序列就可以得到将序列p p p 复位的最小操作次数。
那么,基于这种思想,我们可以构造出一种递推关系——假设F n , j F_{n, j} F n , j 表示了进行j j j 次交换后可以复位的、长度为n n n 的排列(或者说从复位的排列开始,进行j j j 次交换可以产生的排列)的数量:
j = 0 j = 0 j = 0 :此时,只有初始复位的排列一种情况;因此总是为1 1 1 现在,我们考虑通过F n − 1 F_{n - 1} F n − 1 向F n F_n F n 转移;即每次将n n n 放入长度为n − 1 n - 1 n − 1 的排列中:如果新放入的n n n 不进行交换,那么对答案没有贡献:可以直接转移 如果和前面的n − 1 n - 1 n − 1 个位置中的一个进行交换:那么贡献1 1 1 次次数,有n − 1 n - 1 n − 1 种转移方法 综上所述,可以得到递推公式F n , j = F n − 1 , j + ( n − 1 ) ⋅ F n − 1 , j − 1 F_{n, j} = F_{n - 1, j} + (n - 1) \cdot F_{n - 1, j - 1} F n , j = F n − 1 , j + ( n − 1 ) ⋅ F n − 1 , j − 1 ; 那么,基于这个动态规划,我们可以有两种不同的做法:
阳间做法注意到题目中的k k k 非常的小,所以即使进行k k k 次交换,最多只会使得2 k 2k 2 k 个位置错位;所以我们每次只需要能选出错位的位置长度,然后对于在这个范围内的长度求F n , j F_{n, j} F n , j 即可;那么,一种很显然的做法就是确定一个允许的错位位置的长度i i i ,对于这个长度求F F F ,最后统一考虑——
那么,答案长下面这样吗?
ans? j = ∑ i = 0 min ( 2 j , n ) C n i × F i , j \text{ans?}_{j} = \sum_{i = 0}^{\min(2j, n)} \mathbf{C}_n^{i} \times F_{i, j} ans? j = i = 0 ∑ m i n ( 2 j , n ) C n i × F i , j
不,当然不对——因为我们的F n , j F_{n, j} F n , j 可能实际上只变动了其中很少一部分的位置——而这样的话就会不可避免的和其他情况重合,导致计数的不准确;所以为了解决这个问题,我们可以考虑在错排问题 种采用的解决方法——使用容斥原理包含/排除重复的部分:
现在,我们定义G n , j G_{n, j} G n , j 和F n , j F_{n, j} F n , j 类似,但是每一个位置都是错排的情况数量 然后,我们在F n , j F_{n, j} F n , j 中选择一个位置固定,然后剩下的位置的任何排序就都符合要求,需要排除 但是这样的话,就会导致两个位置固定的情况被多排除了一次,需要重新包含 …… 所以,我们就可以在O ( k 2 ) \mathcal{O}(k^2) O ( k 2 ) 的时间内完成一次G G G 的求解;算法总复杂度是O ( k 3 ) \mathcal{O}(k^3) O ( k 3 ) 。
代码实现一个很容易注意到的事情就是ans i \text{ans}_i ans i 可以由ans i − 2 \text{ans}_{i - 2} ans i − 2 转移过来——因为你可以连续两次进行相同的交换来浪费两次交换机会== 由此,也很容易联想到奇偶分开(
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 const int N = 500 ;const int mod = 1e9 + 7 ;longs inv[N], c[N][N], f[N][N]; void initInverse (int n, longs p) { inv[0 ] = inv[1 ] = 1 ; for (int i = 2 ; i <= n; i++) inv[i] = (p - p / i) * inv[p % i] % p; inv[0 ] = 0 ; } longs nCr (int n, int r) { longs ret = 1 ; for (int i = n - r + 1 ; i <= n; ++ i) ret = ret * i % mod; for (int i = 1 ; i <= r; ++ i) ret = ret * inv[i] % mod; return ret; } signed main () { ios::sync_with_stdio (false ); cin.tie (null), cout.tie (null); int n = $.nextInt (), k = $.nextInt (); c[0 ][0 ] = 1 ; for (int i = 1 ; i <= 2 * k; ++ i) { f[i][0 ] = 1 , c[i][0 ] = 1 ; for (int j = 1 ; j <= 2 * k; ++ j) { f[i][j] = (f[i - 1 ][j] + (i - 1 ) * f[i - 1 ][j - 1 ]) % mod; c[i][j] = (c[i - 1 ][j] + c[i - 1 ][j - 1 ]) % mod; } } initInverse (2 * k, mod); longs ans[] = {1 , 0 }; vector<int > out; for (int j = 1 ; j <= k; ++ j) { const auto lim = min (n, 2 * j); for (int i = 1 ; i <= lim; ++ i) { longs cnt = 0 , fix, fl; for (fix = 0 , fl = 1 ; fix <= i; ++ fix, fl = -fl) { cnt = cnt + fl * c[i][fix] * f[i - fix][j] % mod; do cnt = (cnt + mod) % mod; while (cnt < 0 ); } ans[j % 2 ] = (ans[j % 2 ] + nCr (n, i) * cnt) % mod; } out.push_back ((int ) ans[j % 2 ]); } $.putArray (out.begin (), out.end ()); return 0 ; }
一些说明:很显然C i j = C i − 1 j + C i − 1 j − 1 \mathbf{C}_i^j = \mathbf{C}_{i - 1}^j + \mathbf{C}_{i - 1}^{j - 1} C i j = C i − 1 j + C i − 1 j − 1 ;
阴间做法首先,我们需要先观察上面得到的那个递推公式,并考虑更深刻的理解它:
考虑到F n , 0 = 1 F_{n, 0} = 1 F n , 0 = 1 ,除了递推的转移引入之外,都是通过n − 1 n - 1 n − 1 的形式引入的; 什么情况下会引入n − 1 n - 1 n − 1 ?当新加入的n n n 不放在[ n ] [n] [ n ] 而是参与了与前面的位置交换的场合下; 引入时产生了什么副作用?因为进行了交换,所以奉献了一次交换次数,j = j + 1 j = j + 1 j = j + 1 ; 所以,我们可以将F n , j F_{n, j} F n , j 看作从“下标”集合[ 0 , n − 1 ] [0, n - 1] [ 0 , n − 1 ] 中选出一个大小j j j 的子集求积后求和的结果; 形式化的说,我们可以得到下面的F n , j F_{n, j} F n , j 的表示形式:
F n , j = ∑ s ⊂ [ 0 , n − 1 ] ∧ ∣ s ∣ = j ∏ i = 1 j s i F_{n, j} = \sum_{s \subset [0, n - 1] \and |s| = j} \ \prod_{i = 1}^j s_i F n , j = s ⊂ [ 0 , n − 1 ] ∧ ∣ s ∣ = j ∑ i = 1 ∏ j s i
那么接下来,对于全部需要的j j j ,我们考虑从F n F_{n} F n 转移到F 2 n F_{2n} F 2 n :
一个很显然的想法,就是我们从[ 0 , n − 1 ] [0, n - 1] [ 0 , n − 1 ] 中选择l l l 个,从[ n , 2 n − 1 ] [n, 2n - 1] [ n , 2 n − 1 ] 中选出剩下的,组成上面所提到的从集合[ 0 , 2 n − 1 ] [0, 2n-1] [ 0 , 2 n − 1 ] 中选出的大小为j j j 子集;形式化地说就是将两部分的结果乘起来
前半部分的答案很显然是F n , l F_{n, l} F n , l ,而后半部分的答案我们没有维护;但是我们可以把它看作从[ 0 , n − 1 ] [0, n - 1] [ 0 , n − 1 ] 中选择了j − l j - l j − l 个,然后对于每一个都加上了n n n ,也就是下面这样:
F n , j ′ ′ = ∑ s ⊂ [ n , 2 n − 1 ] ∧ ∣ s ∣ = j ′ ∏ i = 0 j ′ s i = ∑ s ⊂ [ 0 , n − 1 ] ∧ ∣ s ∣ = j ′ ∏ i = 0 j ′ ( s i + n ) F'_{n, j'} = \sum_{s\subset [n, 2n-1] \and |s| = j'} \ \prod_{i = 0}^{j'}s_{i} = \sum_{s\subset [0, n-1] \and |s| = j'} \ \prod_{i = 0}^{j'}(s_{i} + n) F n , j ′ ′ = s ⊂ [ n , 2 n − 1 ] ∧ ∣ s ∣ = j ′ ∑ i = 0 ∏ j ′ s i = s ⊂ [ 0 , n − 1 ] ∧ ∣ s ∣ = j ′ ∑ i = 0 ∏ j ′ ( s i + n )
那么这个式子展开是什么样的呢?因为这个连乘长得非常像二项式展开但是不是,残念( ,所以我们可以想象一下它展开后的样子:
F n , j ′ ′ = ∑ l = 0 j ′ C n − l j ′ − l × n j ′ − l × ∑ s ⊂ [ 0 , n − 1 ] ∧ ∣ s ∣ = l ∏ i = 0 l s i F'_{n, j'} = \sum_{l = 0}^{j'} \mathbf{C}_{n - l}^{j'-l} \times n^{j'-l} \times \sum_{s\subset [0, n-1] \and |s| = l} \prod_{i = 0}^{l}s_i F n , j ′ ′ = l = 0 ∑ j ′ C n − l j ′ − l × n j ′ − l × s ⊂ [ 0 , n − 1 ] ∧ ∣ s ∣ = l ∑ i = 0 ∏ l s i
上面的式子中,有C n − l j ′ − l = C n j ′ × C j ′ l \mathbf{C}_{n - l}^{j' - l} = \mathbf{C}_n^{j'} \times \mathbf{C}_{j'}^l C n − l j ′ − l = C n j ′ × C j ′ l ;首先是选出坐标的组合数,然后乘上“二项式系数”。
然后,观察上面的式子的后半部分,我们会惊喜地发现它就是F n , l F_{n, l} F n , l ,是我们已经维护的东西!
综上所述,我们可以通过下面的转移方程完成从F n F_{n} F n 到F 2 n F_{2n} F 2 n 的转移:
F 2 n , J = ∑ j = 0 J F n , j × F n , j ′ ′ , j + j ′ = J F_{2n, J} = \sum_{j = 0}^J F_{n, j} \times F'_{n, j'}, \ \ j + j' = J F 2 n , J = j = 0 ∑ J F n , j × F n , j ′ ′ , j + j ′ = J
上式中的F n , j ′ F'_{n, j} F n , j ′ 可以通过F n , j F_{n, j} F n , j 通过下面的多项式乘法转移得到:
F n , j ′ = ∑ l = 0 j C n − l j − l × n j − l × F n , l F'_{n, j} = \sum_{l = 0}^j \mathbf{C}_{n - l}^{j - l} \times n^{j - l} \times F_{n, l} F n , j ′ = l = 0 ∑ j C n − l j − l × n j − l × F n , l
当然,基础的从F n − 1 , j F_{n - 1, j} F n − 1 , j 和F n − 1 , j − 1 F_{n - 1, j - 1} F n − 1 , j − 1 向F n , j F_{n, j} F n , j 的转化仍然有效;因此我们可以转化到任何的n n n :相当于我们从n = 1 n = 1 n = 1 出发,然后使用× 2 \times2 × 2 和+ 1 +1 + 1 操作构造任何的n n n ——在构造的过程中完成上面的转移就可以了;一种很显然的思路就是二进制拆位,然后按位构造n n n :
代码实现基本的思路就是每次扩增× 2 \times2 × 2 ,如果这一位为 1
就再额外进行一次+ 1 +1 + 1 扩增:
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 const int N = 500 ;const int mod = 1e9 + 7 ;longs inv[N], f[N], big[N], np[N], tmp[N]; void initInverse (int n, longs p) { inv[0 ] = inv[1 ] = 1 ; for (int i = 2 ; i <= n; i++) inv[i] = (p - p / i) * inv[p % i] % p; inv[0 ] = 0 ; } longs nCr (longs n, longs r) { longs ret = 1 ; for (auto i = n - r + 1 ; i <= n; ++ i) ret = ret * i % mod; for (int i = 1 ; i <= r; ++ i) ret = ret * inv[i] % mod; return ret; } void calculateBig (int k, longs n) { memset (big, 0 , sizeof (longs) * (k + 1 )); memset (np, 0 , sizeof (longs) * (k + 1 )); np[0 ] = 1 ; for (int i = 1 ; i <= k; ++ i) np[i] = n * np[i - 1 ] % mod; const auto lim = min ((longs)k, n); for (int i = 0 ; i <= lim; ++ i) for (int j = 0 ; j <= i; ++ j) { auto t = nCr (n - j, i - j) * f[j] % mod * np[i - j] % mod; big[i] = (big[i] + t) % mod; } } signed main () { ios::sync_with_stdio (false ); cin.tie (null), cout.tie (null); int n = $.nextInt (), k = $.nextInt (); const bitset<32> binary = n; initInverse (N - 1 , mod); longs now = f[0 ] = 1 ; for (int i = 30 - __builtin_clz(n); i >= 0 ; -- i) { calculateBig (k, now); memset (tmp, 0 , sizeof (longs) * (k + 1 )); for (int s = 0 ; s <= k; ++ s) for (int b = 0 ; b <= k; ++ b) if (s + b <= k) tmp[s + b] = (tmp[s + b] + f[s] * big[b]) % mod; else break ; memcpy (f, tmp, sizeof (longs) * (k + 1 )); now *= 2 ; if (binary.test (i)) { memset (tmp, 0 , sizeof (longs) * (k + 1 )); tmp[0 ] = 1 ; for (int j = 1 ; j <= k; ++ j) tmp[j] = (f[j] + now * f[j - 1 ]) % mod; memcpy (f, tmp, sizeof (longs) * (k + 1 )); ++ now; } } longs ans[] = {1 , 0 }; vector<int > out; for (int j = 1 ; j <= k; ++ j) { ans[j % 2 ] = (ans[j % 2 ] + f[j]) % mod; out.push_back ((int ) ans[j % 2 ]); } $.putArray (out.begin (), out.end ()); return 0 ; }
上面的代码中的 30 - __builtin_clz(n)
的含义是: int
类型的 n
的最高位的 1
的所在的位置的低一位的下标;我们使用它作为扩增起点——不使用最高位的原因是我们已经从1 1 1 出发了,所以不需要对于首位的1 1 1 进行额外的扩增。
当然,上面的实现中的每次转移的时间复杂度是O ( k 2 ) \mathcal{O}(k^2) O ( k 2 ) 的,进行log 2 n \log_2n log 2 n 次转移;如果可以使用各种手段加快单次转移的速度,理论上可以做到k ≤ 1 0 5 k \leq 10^5 k ≤ 1 0 5 (时间复杂度O ( k ⋅ log k log n ) \mathcal{O}(k\cdot \log k\log n) O ( k ⋅ log k log n ) )但是我不会,原作者也不会
后记罚时是真的多,,明明还是可以回到 1700 的场嗯给我打成了防掉分场,只能说非常地不行了(
最近的准度不行啊,还是以后要多加注意才行;补题也要进行的更加迅速才行,不然题目真的补不完力昨天半夜的 Div.1 + Div.2 还没有下落呜呜 (呜呜呜