抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

今日、海を見た。もう怖くない

记录

比赛的那天 KS 酱办生日聚会,所以玩的有点晚——于是为了避免掉分就又开了个小号——结果还真的算是预防成功了((只能说不愧是我==

上一场一样的出题人,也是熟悉的五个题目;但是这场总感觉比之前那一场要难一些——可能出题人不经意间触及了我较多的知识盲区吧(

题解

A - Tit for Tat

右手就行可是我白给了一发;只要取出前面的加到最后一个元素上就行了:

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 这种代码呢?以为很对称🐎(

B - AGAGA XOOORRR

考虑到最后只可能剩下两个相同的元素或三个相同的元素——因为更多的元素都可以合并到这两种情况上,所以只需要分别处理即可:

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;
}

之前白给成了直接排除一个元素,显然这是不科学的(

C - Baby Ehab Partitions Again

提供长度为n100n \leq 100 的数组aa,你要从中删除一些元素,使得不可以将这个数组拆分成两个部分,使得两个部分的和相等;要求最小化删除元素的数量并输出删除元素的坐标。

首先,显然当i=1nai\sum_{i = 1}^n a_i 是奇数的时候不用删除任何元素;然后,为偶数情况下可以进行背包DPDP 来判断是否可能完成这样的划分;如果可以,那么问题就变成了如何删除元素。

不难想到最多只会删除一个元素,那么问题就是删除什么样的元素;我最开始因为删除最小的元素然后白给了一罚时,因为这样是不正确的——比如删除 2,可以通过交换两个 2 和一个 5 来使得再次平衡;那么我们在考虑其他的一定可行的情况,就不难想到删除一个奇数。

如果整个数组都是偶数怎么办?那么我们可以整体右移11 位,显然和原数组是等价的;我们可以一直右移,直到我们找到了可以删除的奇数为止;显然,这一定可以找到;

从右移等价,我们可以联想到整个数组除以任何同一个数都是等价的;所以,一个最简单的方法就是首先约去整个数组的gcd\gcd,然后找到一个奇数删除就行了——因为是等价的,所以这样的删除是合理的;

代码实现

这个使用 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;
}

于是这个卵题我白给了近十发,,我是真的菜(

D - Cut

给一个长度为n105n \leq 10^5 的数组aa,进行q105q\leq10^5 次询问:每次询问关于一个区间[l,r][l, r],将它分成的最少的段数,使得每一段的LCM\text{LCM} 都和连乘的乘积相等;求这个段数。

首先,不难意识到LCM\text{LCM} 和连乘积相等就是说这一段的gcd\gcd11。那么问题就转化为了对于任意段[l,r][l, r],将它划分成互质的最小的段数。

那么一种很显然的想法就是对于范围内的所有质数(约96009600 个)分别维护一个列表,包含了包含它为质因数的数字的下标;然后对于一个位置,对于它的每一个质因子在对应的表上二分查找出下一个位置,取最小值作为下一个区间开始的备选位置;但是这样做显然非常的啥b,因为有显而易见地简单优化但是我也显而易见的没有想到

  • 我们可以倒着维护每一个质数的下一个位置,这样就不用对每个质数二分查找了
  • 倒着转移的时候也考虑后一个位置的备选位置,这样就不用考虑到备选区间之间的冲突了

那么这样,我们就可以维护出一个表FiF_i,表示从ii 开始可以转移到的最远的备选位置;

但是这样还存在一个问题:如果所有的数字都是11,那么上面的做法会被卡成n2n^2;因此,为了能够快速的跳转求出区段数,我们可以利用倍增的思想,维护下两个、下四个备选位置;这样,就可以在logn\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;
}

即使可以想到正确的维护方法,但是倍增的思想也不能不说是十分的高妙…… 学到许多(

E - Baby Ehab Plays with Permutations

现在有长度为n109n \leq 10^9 的排列{1,,n}\{1, \dots,n\},可以进行对于两个不同的下标两两交换的操作k200k \leq 200 次;问对于[1,k][1, k] 次操作可以得到的不同的排列数量;

首先,先说明一种贪心地将长度为nn 的排序pp 复位的做法——对于排列的最后一个位置[n][n]

  • 如果pn=np_n = n,那么可以忽略这个位置;将原排列看作长度为n1n - 1
  • 否则,将pnp_nppnp_{p_n} 交换位置;此时至少可以使得pnp_n 复位,继续递归,但是操作次数+1+1

可以证明,这样处理完整个序列就可以得到将序列pp 复位的最小操作次数。

那么,基于这种思想,我们可以构造出一种递推关系——假设Fn,jF_{n, j} 表示了进行jj 次交换后可以复位的、长度为nn 的排列(或者说从复位的排列开始,进行jj 次交换可以产生的排列)的数量:

  • j=0j = 0:此时,只有初始复位的排列一种情况;因此总是为11
  • 现在,我们考虑通过Fn1F_{n - 1}FnF_n 转移;即每次将nn 放入长度为n1n - 1 的排列中:
    • 如果新放入的nn 不进行交换,那么对答案没有贡献:可以直接转移
    • 如果和前面的n1n - 1 个位置中的一个进行交换:那么贡献11 次次数,有n1n - 1 种转移方法
  • 综上所述,可以得到递推公式Fn,j=Fn1,j+(n1)Fn1,j1F_{n, j} = F_{n - 1, j} + (n - 1) \cdot F_{n - 1, j - 1}

那么,基于这个动态规划,我们可以有两种不同的做法:

阳间做法

注意到题目中的kk 非常的小,所以即使进行kk 次交换,最多只会使得2k2k 个位置错位;所以我们每次只需要能选出错位的位置长度,然后对于在这个范围内的长度求Fn,jF_{n, j} 即可;那么,一种很显然的做法就是确定一个允许的错位位置的长度ii,对于这个长度求FF,最后统一考虑——

那么,答案长下面这样吗?

ans?j=i=0min(2j,n)Cni×Fi,j\text{ans?}_{j} = \sum_{i = 0}^{\min(2j, n)} \mathbf{C}_n^{i} \times F_{i, j}

不,当然不对——因为我们的Fn,jF_{n, j} 可能实际上只变动了其中很少一部分的位置——而这样的话就会不可避免的和其他情况重合,导致计数的不准确;所以为了解决这个问题,我们可以考虑在错排问题种采用的解决方法——使用容斥原理包含/排除重复的部分:

  • 现在,我们定义Gn,jG_{n, j}Fn,jF_{n, j} 类似,但是每一个位置都是错排的情况数量
  • 然后,我们在Fn,jF_{n, j} 中选择一个位置固定,然后剩下的位置的任何排序就都符合要求,需要排除
  • 但是这样的话,就会导致两个位置固定的情况被多排除了一次,需要重新包含
  • ……

所以,我们就可以在O(k2)\mathcal{O}(k^2) 的时间内完成一次GG 的求解;算法总复杂度是O(k3)\mathcal{O}(k^3)

代码实现

一个很容易注意到的事情就是ansi\text{ans}_i 可以由ansi2\text{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;
}

一些说明:很显然Cij=Ci1j+Ci1j1\mathbf{C}_i^j = \mathbf{C}_{i - 1}^j + \mathbf{C}_{i - 1}^{j - 1}

阴间做法

首先,我们需要先观察上面得到的那个递推公式,并考虑更深刻的理解它:

  • 考虑到Fn,0=1F_{n, 0} = 1,除了递推的转移引入之外,都是通过n1n - 1 的形式引入的;
  • 什么情况下会引入n1n - 1?当新加入的nn 不放在[n][n] 而是参与了与前面的位置交换的场合下;
  • 引入时产生了什么副作用?因为进行了交换,所以奉献了一次交换次数,j=j+1j = j + 1
  • 所以,我们可以将Fn,jF_{n, j} 看作从“下标”集合[0,n1][0, n - 1] 中选出一个大小jj 的子集求积后求和的结果;

形式化的说,我们可以得到下面的Fn,jF_{n, j} 的表示形式:

Fn,j=s[0,n1]s=j i=1jsiF_{n, j} = \sum_{s \subset [0, n - 1] \and |s| = j} \ \prod_{i = 1}^j s_i

那么接下来,对于全部需要的jj,我们考虑从FnF_{n} 转移到F2nF_{2n}

  • 一个很显然的想法,就是我们从[0,n1][0, n - 1] 中选择ll 个,从[n,2n1][n, 2n - 1] 中选出剩下的,组成上面所提到的从集合[0,2n1][0, 2n-1] 中选出的大小为jj 子集;形式化地说就是将两部分的结果乘起来

  • 前半部分的答案很显然是Fn,lF_{n, l},而后半部分的答案我们没有维护;但是我们可以把它看作从[0,n1][0, n - 1] 中选择了jlj - l 个,然后对于每一个都加上了nn,也就是下面这样:

    Fn,j=s[n,2n1]s=j i=0jsi=s[0,n1]s=j i=0j(si+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)

    那么这个式子展开是什么样的呢?因为这个连乘长得非常像二项式展开但是不是,残念(,所以我们可以想象一下它展开后的样子:

    Fn,j=l=0jCnljl×njl×s[0,n1]s=li=0lsiF'_{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

    上面的式子中,有Cnljl=Cnj×Cjl\mathbf{C}_{n - l}^{j' - l} = \mathbf{C}_n^{j'} \times \mathbf{C}_{j'}^l;首先是选出坐标的组合数,然后乘上“二项式系数”。

  • 然后,观察上面的式子的后半部分,我们会惊喜地发现它就是Fn,lF_{n, l},是我们已经维护的东西!

综上所述,我们可以通过下面的转移方程完成从FnF_{n}F2nF_{2n} 的转移:

F2n,J=j=0JFn,j×Fn,j,  j+j=JF_{2n, J} = \sum_{j = 0}^J F_{n, j} \times F'_{n, j'}, \ \ j + j' = J

上式中的Fn,jF'_{n, j} 可以通过Fn,jF_{n, j} 通过下面的多项式乘法转移得到:

Fn,j=l=0jCnljl×njl×Fn,lF'_{n, j} = \sum_{l = 0}^j \mathbf{C}_{n - l}^{j - l} \times n^{j - l} \times F_{n, l}

当然,基础的从Fn1,jF_{n - 1, j}Fn1,j1F_{n - 1, j - 1}Fn,jF_{n, j} 的转化仍然有效;因此我们可以转化到任何的nn:相当于我们从n=1n = 1 出发,然后使用×2\times2+1+1 操作构造任何的nn ——在构造的过程中完成上面的转移就可以了;一种很显然的思路就是二进制拆位,然后按位构造nn

代码实现

基本的思路就是每次扩增×2\times2,如果这一位为 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 的所在的位置的低一位的下标;我们使用它作为扩增起点——不使用最高位的原因是我们已经从11 出发了,所以不需要对于首位的11 进行额外的扩增。

当然,上面的实现中的每次转移的时间复杂度是O(k2)\mathcal{O}(k^2) 的,进行log2n\log_2n 次转移;如果可以使用各种手段加快单次转移的速度,理论上可以做到k105k \leq 10^5(时间复杂度O(klogklogn)\mathcal{O}(k\cdot \log k\log n)但是我不会,原作者也不会

后记

罚时是真的多,,明明还是可以回到 1700 的场嗯给我打成了防掉分场,只能说非常地不行了(

JVS2TR2_F28UCRC3K_YG_3B.jpg

最近的准度不行啊,还是以后要多加注意才行;补题也要进行的更加迅速才行,不然题目真的补不完力昨天半夜的 Div.1 + Div.2 还没有下落呜呜(呜呜呜

评论