牛客周赛 Round 111
第一次尝试倒开 qwq,没想到还意外的挺成功的,最后也是成功 ak 了喵 qwq
这里写的都是我赛时的做法,并非最佳做法,但是还是希望能帮到你 qwq,提供一个参考喵
A 小红的阶梯
直接判断就好了 qwq
void solve() {
int a, b, c;
std::cin >> a >> b >> c;
if (b == a + 1 && c == b + 1) {
std::cout << "Yes\n";
} else {
std::cout << "No\n";
}
}
B 小红的数组取数
简单推下式子
我们要使这个式子尽可能大。
可以发现第一项是固定的,是两个数组和的差值;而第二项要尽可能大,让 尽可能大就好了,也就是选最大的
和最小的
。我这里用的是
和
,感觉方便一点 。
void solve() {
int n;
std::cin >> n;
std::vector<int> a(n), b(n);
for (int i = 0; i < n; i++) {
std::cin >> a[i];
}
for (int i = 0; i < n; i++) {
std::cin >> b[i];
}
std::cout << std::min_element(a.begin(), a.end()) - a.begin() + 1 << " " << std::max_element(b.begin(), b.end()) - b.begin() + 1 << "\n";
}
C 小红抽卡
这是一个比较经典的问题。
题目要求是需要把第 个元素放到第一位,但是这个
特别大,我们一次次手动模拟肯定是会超时的。能不能不改变数组呢?
当然是可以的!我们可以观察一下,假设我们一开始数组头的位置是在 ,每次把第
个元素搬到前面去,就相当于在模
的意义下,让
往左移一格,也就是
(这里
是为了不让下标出现负数),这样的话,每移动
次,
其实都会回到开始的位置,那么我们只需要先把
模上
(每左移
格就回到原点),再在模
的意义下让
减去
(在模
的意义下左移
格),得到的就是最终的数组头的下标,这里用取模的话,建议数组使用
的下标,会方便很多!
输出的时候,先把 的元素输出完,再输出没有输出过的元素就好了,因为题目里的操作只会对
的元素造成影响。
void solve() {
i64 n, k, x;
std::cin >> n >> k >> x;
k %= x;
std::vector<int> a(n);
for (int i = 0; i < n; i++) {
std::cin >> a[i];
}
int p = (x - k) % x;
std::vector<int> vis(n);
while (!vis[p]) {
std::cout << a[p] << " ";
vis[p] = 1;
p = (p + 1) % x;
}
p = 0;
while (vis[p]) {
p++;
}
while (p < n) {
std::cout << a[p] << " ";
p++;
}
std::cout << "\n";
}
D 小红的好数对
(update : 这题好像题面没说清楚qwq,但是根据样例 (样例
貌似也是赛后加的 qwq),题意应该是把
时的
和
认作两种不同的拼接方式)
这题挺好的 qwq,很多写法,这里提供的是 写法,从除法的本质入手,感觉挺对的 qwq
我们要让拼接之后的数是 的倍数,我们不妨思考一下,怎么样才能让拼接后的数是
的倍数。
我们可以回顾一下我们小时候学的列竖式除法的计算过程,对于第一位,除以 ,余了一个数,再把后面一位抄下来,再除 ...... 其实这样想就不难发现,我们只要让
模
的余数的后面接上
(前面的位不会对结果产生任何影响),如果这样产生的新的数还能被
整除,那么这个数也就一定能被
整除。
而 模完
之后,得到的余数一定是在
之间的,那么我们就可以
啦,记
表示在
之前的所有数里(不包含第
个数),有多少个数模
的余数是
,
表示在
之后的,其他都和前面一样。对于
数组,每次转移的时候,先把前面第
个位置的所有
值全部搬过来,然后再更新这一位的余数所带来的影响,
数组同理。
然后我们再遍历一遍原数组,对于每一位 ,我们遍历所有
的余数,计算这些余数后面拼接
之后的数值,看看能不能被
整除,可以的话,我们就直接加上
的对应数值。然后我们还要倒着跑一遍,一样的计算。(注意:
和
拼接的顺序不同,结果也会不同!这两个数不一定都会被
整除!我就在这里获得了
,
分的好成绩 qwq )
对于数的拼接,我们记前面的数为 , 接在后面的数为
,且
在十进制下的长度为
(也就是这个数有多少位),那么拼接起来的数就是
。可以快速幂优化一下,但是不优化应该也没问题 qwq,应该还是能过的。
i64 power(i64 a, i64 b) { // 快速幂
i64 res = 1;
for (; b; a *= a, b >>= 1) {
if (b & 1) {
res *= a;
}
}
return res;
}
void solve() {
int n;
std::cin >> n;
std::vector<int> a(n + 1);
for (int i = 1; i <= n; i++) {
std::cin >> a[i];
}
std::vector<std::vector<int>> pre(n + 1, std::vector<int>(10 + 1));
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= 10; j++) {
pre[i][j] = pre[i - 1][j];
}
pre[i][a[i] % 11]++;
}
std::vector<std::vector<int>> suf(n + 2, std::vector<int>(10 + 1));
for (int i = n; i >= 1; i--) {
for (int j = 0; j <= 10; j++) {
suf[i][j] = suf[i + 1][j];
}
suf[i][a[i] % 11]++;
}
i64 ans = 0;
for (int i = 2; i <= n; i++) { // 顺着跑一遍
int len = 0, x = a[i];
while (x) {
len++;
x /= 10;
}
for (int j = 0; j <= 10; j++) {
if ((1LL * j * power(10, len) + a[i]) % 11 == 0) {
ans += pre[i - 1][j];
}
}
}
for (int i = n - 1; i >= 1; i--) { // 倒着跑一遍
int len = 0, x = a[i];
while (x) {
len++;
x /= 10;
}
for (int j = 0; j <= 10; j++) {
if ((1LL * j * power(10, len) + a[i]) % 11 == 0) {
ans += suf[i + 1][j];
}
}
}
std::cout << ans << "\n";
}
E 小芳的排列构造
构造题。
由于下面的讨论会用到 这个条件,这里我们先分类讨论一下
时的答案:
时,只有
这一个数,
在算
的时候会被考虑一次,算
的时候也会被考虑一次,所以答案只能是
,其他的数都不行,此时数组为
。
否则 ,我们先考虑最大和最小能达到的和是多少。
总体分析一下,首先 一定会被考虑两次,算
会被考虑到,算
也会被考虑到,因为
大于
的所有数。
然后对于其他数,一定 至多被考虑一次,因为无论怎么放,总有一边有 这个数存在,所以不能大于这一边的所有数。而对于数
,它一定会被考虑到一次,因为一边有
这个数,不会被考虑到,另一边的所有数都一定小于
,所以有一个固定的贡献。其他的数如果放到
与
的外侧,就可能产生一次贡献,否则一定不会产生贡献,因为一侧有
, 另一侧有
,都不会让这些数产生贡献。
那么,答案最小的情况就是, 产生两次贡献,
产生一次贡献,其他数都不产生贡献,此时答案为
;答案最大的情况是,
产生两次贡献,其他数均产生一次贡献,此时答案为
。
所以答案的范围就是 。
然后我们考虑怎么构造对应值的数组 。
首先数组的初态是 ,这样数组能产生最小贡献。
如果 在上面说的范围内,我们可以开一个变量
表示还需要多少贡献才能到
,于是我们可以贪心地从
这个范围内从大到小取数,可以证明是一定能凑出来的,这一部分的严谨数学证明放在了整篇题解的结尾。每取一个数,如果小于等于当前的
,就把它加到数组的最后面,并且让
减去这个数,否则放到
和
之间 。这样构造,
后面的数是递减的,每个都会产生一次贡献,其他的数不会产生贡献 。
void solve() {
i64 n, k;
std::cin >> n >> k;
if (n == 1) {
std::cout << 1 << "\n";
return;
}
if (k < 3 * n - 1 || k > n * (n + 1) / 2 + n) {
std::cout << -1 << "\n";
return;
}
std::vector<int> ans;
ans.push_back(n);
i64 dif = k - 3 * n + 1;
std::vector<int> f; // 开个辅助数组,不然真的往中间插复杂度太高了
for (int i = n - 2; i >= 1; i--) {
if (i <= dif) {
f.push_back(i);
dif -= i;
} else {
ans.push_back(i);
}
}
ans.push_back(n - 1);
for (int i = 0; i < f.size(); i++) {
ans.push_back(f[i]);
}
for (int i = 0; i < n; i++) {
std::cout << ans[i] << " \n"[i == n - 1];
}
}
F 小红的排列构造
构造题。
先来考虑最少和最多需要多少次才能把数组还原成顺序。
首先是最小次数,很明显,一次最多改变两个数的位置,也最多能使两个数回到自己的位置上,对于 是偶数的情况,最少需要
次,此时数组为,把
数组,第一个和最后一个交换,第二个和倒数第二个交换 ...... ,而对于
是奇数的情况,在进行完和偶数一样的操作后,中间的数还满足
,所以还需要再加一次,假设中间三个数从小到大是
,那么可以排成
,这样只比原来多一次,总结一下,可以化成一个式子,也就是
次。
然后是最大次数,因为题目意思是一定会采取最优方式交换,那么最优方式最劣的情况就是 次,每次让一个数回到位置上,剩下的不会回到位置上,在交换完第
个数后,最后一个数会自动归位,所以是
次,此时数组可能是
,这种应该是
次的,**但是我们下面的构造不会这么构造 **。
这样就能筛选掉所有的无解的情况。
否则有解,我们考虑从初态,也就是最小次数的情况开始扩展。
记变量 ,表示还要多加多少次数,由于按照上面的构造方式,现在第一个只和最后一个交换,第二只和倒数第二个交换 ...... 那么,只要交换
和
,就能让次数加一,因为,你最后一定要换回来再和最后面的交换,这样不断交换最终能达到最大的
次 。
void solve() {
int n, k;
std::cin >> n >> k;
if (k < (n + 1) / 2 || k > n - 1) {
std::cout << -1 << "\n";
return;
}
std::vector<int> a(n);
std::iota(a.begin(), a.end(), 1); // 这个函数表示把数组 a 的所有元素填充为 [1, 2, ..., n]
for (int i = 0; i < n / 2; i++) { // 先和后面的交换
std::swap(a[i], a[n - 1 - i]);
}
std::swap(a[n / 2], a[(n + 1) / 2]); // n 为奇数就多交换一次,n 为偶数时这两个下标是相等的
for (int i = 0; i < k - (n + 1) / 2; i++) { // 补充次数
std::swap(a[i], a[i + 1]);
}
for (int i = 0; i < n; i++) {
std::cout << a[i] << " \n"[i == n - 1];
}
}
结尾附上 E 题,对于 内的所有数都能被
选部分个数恰好一次求和所得到的证明喵 qwq,证明由 yanami_anna 提供,太强啦 qwq,"贪心一定要证明啊!".jpg