Codeforces Round 1108 (Div. 2)
在复健补题,由于多年退化水平比较低下,所以没有 E 和 F。
A. farmpiggie and Subset Sum
题意
考虑一个长度为偶数 \(n\) 的排列 \(p\),这个排列会与它的下标相乘,得到一组数:
对于这 \(n\) 个数,每一个数你都可以选择赋予它正号(\(+\))、负号(\(-\))或者不选(即乘以 \(0\)),然后对它们求和。
构造任意一个排列 \(p\) 并输出,使得不管怎么拼凑这几个数的符号,都无法让求和结果等于 1。
思路
考虑奇偶性,最容易想到的就是令每个 \(|i \cdot p_i|\) 都是偶数,即要么 \(i\) 为偶数,要么 \(p_i\) 为偶数,此时求和结果一定为偶数。
由这个思路,可以立即想到,从 \(\{1, 2, 3, 4, \dots, 2k-1, 2k\}\) 这样的标准排列开始构造,可以考虑:
- 偶数位置上的数和奇数位置上的数进行两两交换。特别地,为了写代码方便,可以让相邻数之间交换:\(\{2, 1, 4, 3, \dots, 2k, 2k-1\}\)
- 直接倒序输出:\(\{2k, 2k-1, \dots, 2, 1\}\)
即可通过本题。
B. ezraft and Array
题意
给定一个正整数 \(n\),构造一个含 \(n\) 个不相同正整数的数组,使得数组之和能被所有数组元素整除。
思路
考虑迭代构造。\(n = 1\) 略,\(n = 2\) 显然无解。
从 \(n = 3\) 开始考虑,显然有 \([1, 2, 3]\) 满足题意。
现在考虑从 \(n\) 递推到 \(n+1\):对于满足题意的数组 \(a_n\),一个很自然的想法是把 \(\sum(a_n)\) 塞入到原来的 \(a_n\) 数组里来构造新的数组 \(a_{n+1}\),此时新数组的和为 \(2 \sum{a_n}\),而:
- \(\sum{a_n}\) 能够被原数组里所有的正整数 \(a_1, \dots, a_n\) 整除,这是原数组 \(a_n\) 满足的条件
- \(2 \sum{a_n}\) 又显然能够被 \(a_{n + 1} = \sum{a_n}\) 整除
于是直接从 \(\{1, 2, 3\}\) 开始递推求解数组就可以了。
C. 0mar and Alternating Sums
题意
定义一个数组 \(b\) 的 交错和(alternating sum) 为:\(\sum_{i = 1}^k (-1)^{i+1} b_i\)。
给定一个长度为 \(n\) 的不下降数组 \(a\),元素 \(a_i\) 满足:要么是 \(-1\),要么是正整数。现在找出数组 \(a\) 中所有满足交错和为 \(0\) 的子序列,对结果取模 \(10^9 + 7\)。
思路
先考虑第一种情况:数组中没有 \(-1\)。
首先,显然子序列的长度必须为偶数,设其长度为 \(2k\)。任取子序列中的两项 \(c_{2m-1}, c_{2m}\),对交错和的贡献为 \(c_{2m - 1} - c_{2m}\);又因为原数组不下降且都为正,则 \(c_{2m - 1} - c_{2m} \le 0\)。要想子序列的交错和为 0,则进一步必须满足 \(\forall m, c_{2m-1} = c_{2m}\)。
有了这个结论之后,回到原数组筛选子序列。考虑 \(a\) 中所有元素相同的一个极大子数组 \(sa\),假设其长度为 \(k\),必须在其中选择偶数个值才能保证交错和为 0,否则 \(c_{2m - 1} \ne c_{2m}\)。方案数量为:
考虑乘法原理,统计所有元素相同的极大子数组的大小 \(k_1, k_2, k_3, \dots\),此时 \(\prod_i 2^{k_i-1}\) 即为答案。
现在考虑第二种情况:数组中有 \(-1\)。
如果在子序列中引入偶数个 \(-1\),则这部分对交错和的贡献会相互抵消成 \(0\),因此子序列中 \(-1\) 之后的部分对交错和的贡献仍然需要为 \(0\),这退化到了第一种情况;否则,如果在子序列中引入奇数个 \(-1\),此时需要后半部分的交错和贡献需要为 \(+1\) 。
很容易想到这怎么实现:对于数组内元素相同的两个极大子数组 \(sa_1, sa_2\),如果它们的元素值分别为 \(a_{sa_1}, a_{sa_2}\) 且 \(a_{sa_2} - a_{sa_1} = 1\),即整数值相邻,那么可以从这两个子数组里各取奇数个值,其他子数组里取偶数个值,方案数仍然为 \(\prod_i 2^{k_i-1}\)。
综合这两种情况,答案计算如下:
- 当数组中不存在 \(-1\) 时,方案数为 \(\prod_i 2^{k_i-1}\)
- 当数组中存在 \(-1\) 时,方案数为 \((d + 1) \cdot \prod_i 2^{k_i-1}\),\(d\) 为原数组元素去重之后,满足整数值相邻的对数
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef long double ld;
typedef std::vector<int> vi;
#define forn(i, a, b) for(int i = a; i < b; i++)
#define readi(x) scanf("%d", &x)
#define readv(x, n) forn(i,0,n) scanf("%d", &x[i])
#ifdef DEBUG
#define printi(x) printf("DEBUG: %d\n", x)
#define prints(x) printf("%s", x)
#define printv(x, n) printf("DEBUG: "); forn(i,0,n) printf("%d ", x[i])
#else
#define printi(x) printf("%d\n", x)
#define prints(x) cout << x << endl
#define printv(x, n) forn(i,0,n) printf("%d ", x[i])
#endif
const ll MOD = 1e9 + 7;
const int N = 2e5 + 5;
ll _pow[N];
void pre(void) {
_pow[0] = 1;
forn (i, 1, N)
_pow[i] = (_pow[i - 1] << 1) % MOD;
}
int main(void){
pre();
int t;
readi(t);
while (t--) {
int n;
readi(n);
ll ans = 0, _exp = 1;
bool is_minus_one_exist = false;
int last_num = -100, cnt = 0, adj_num = 0;
while (n--){
int tmp;
readi(tmp);
if (tmp == -1)
is_minus_one_exist = true;
if (tmp == last_num)
cnt++;
else {
if (tmp - last_num == 1)
adj_num++;
_exp += cnt - 1;
last_num = tmp;
cnt = 1;
}
}
_exp += cnt - 1;
if (is_minus_one_exist)
ans = (adj_num + 1) * _pow[_exp];
else
ans = _pow[_exp];
printf("%lld\n", ans % MOD);
}
}
D. diss_quack and Array Game
题意
给定一个含有 \(n\) 个整数的数组 \(a\),Alice 和 Bob 将在这个数组上进行游戏。
在与 Bob 的游戏正式开始之前,Alice 可以任意多次选择数组中的一个元素,将其加 \(1\)。每次对一个元素加 \(1\) 都算作 Alice 的一步操作。
随后游戏开始,Bob 和 Alice 轮流行动,Bob 先手。
在 Bob 的回合中,他可以任意选择两个下标 \(i\) 和 \(j\),交换 \(a_i\) 和 \(a_j\)。特别地,Bob 可以选择 \(i=j\),此时数组不会发生变化。
在 Alice 的回合中:
- 如果 \(a_1\) 是偶数,她会找到最大的 \(j\le |a|\),使得对于所有 \(i\le j\),\(a_i\) 都是偶数;随后对所有 \(i\le j\),令 \(a_i:=a_i/2\)。
- 否则,令 \(a_1:=a_1-1\)。
如果某个元素在操作后变成了 \(0\),它会立刻从数组中被删除,其余元素的下标也会相应地重新排列。
当数组变为空数组时,游戏结束。
Alice 希望最小化自己执行的操作次数,而 Bob 希望最大化 Alice 的操作次数。这里 Alice 的总操作次数等于:
- 游戏开始前,Alice 对数组元素执行加 \(1\) 的次数;
- 游戏开始后,Alice 行动的回合数。
求双方都采取最优策略时,Alice 最终需要执行多少次操作。
思路
考虑游戏正式开始之后(即 \(a\) 数组固定),Bob 会如何决策,使得 Alice 的操作次数最大化。
不难想到,如果数组 \(a\) 中存在奇数,Bob 可以在整个游戏过程中一直把这个奇数放在第二个位置,这样不管 Alice 怎么操作,都只会影响第一个数;当其他数都被消耗完之后,再把这个奇数消耗掉即可。记 \(a_i \in a\) 的二进制长度为 \(l_i\),二进制形式中 \(1\) 的个数为 \(c_i\),容易知道此时 Alice 的总步数为 \(\sum_i f(a_i) = \sum_i (l_i + c_i - 1)\)。
而如果数组 \(a\) 中全为偶数,则不管 Bob 怎么操作,Alice 都会先把整个数组除以 \(2\),直到数组 \(a\) 中出现奇数为止,此时又回到了第一种情况。
既然 Alice 可以在游戏开始之前,对数组 \(a\) 中的元素执行加 \(1\) 操作,那么该怎么样才能让操作次数减小呢?
第一次错误
我最开始考虑,如果能在较少的加 \(1\) 操作下,让数组的所有元素都能被 \(2^k\) 次方整除,那么应该就会省下一些操作次数,而这个 \(k\) 是可以穷举的。比如考虑以下样例:
很显然,对第二个数加 \(1\) 之后,因为整个数组的元素都被 \(4\) 整除,可以很大程度上减少操作次数,而且在这种极端数据下,省下的操作次数是十万量级的。
然而,这个贪心是不充分的,考虑以下反例:
当 \(k = 0\) 时,不改变数组,此时操作次数为 \(9 + 1 = 10\);当 \(k = 1\) 时,第一个数和第二个数都 \(+1\),此时操作次数为 \(2 + 1 + 5 + 1 = 9\)。然而,更优的做法是只给第一个数 \(+1\),此时操作次数为 \(1 + 6 + 1 = 8\)。
这个反例暴露的问题是,除了要考虑整除对答案的全局贡献(对 \(l_i\) 生效)之外,还需要考虑局部贡献,即减少每个元素的二进制形式中 \(1\) 的个数对答案的贡献(对 \(c_i\) 生效),而局部贡献是各元素独立的,也就是可以分别枚举。
在同时考虑这两个变量的情况下,应该如何贪心?
第二次错误
这里我又想错了。首先每个数能省下的步数不多,具体来说,最多也就是 \(17 + 17 - 1 = 33\) 步。那我想先单独处理局部贪心,也就是先找到 \([a_i, a_i + 33]\) 里 \(l_i + c_i - 1\) 最小的数,记为 \(a_{i_2}\),再考虑全局贪心,把每个 \(a_{i_2}\) 都拉到比它大的第一个能被 \(2^k\) 整除的数 \(a_{i_3}\)。
但是这个局部优化没有考虑到全局影响,事实上也确实能构造反例:
在局部优化阶段,\(f(1017) = 17, f(1024) = 11\),而 \(11 + 7 = 18 > 17\),会多出一个操作次数,所以没有数会被改变;而在全局优化阶段,当 \(k=2\) 时,如果将 \(1017\) 变成 \(1020\),则操作次数会变多 1(\(3 + 2 - 2 \times 2 = 1\)),所以也不会触发优化。
但事实上,如果只把 \(1017\) 拉到 \(1024\),虽然会多出 \(7\) 步操作,但是局部优化部分(\(c_i\))减少了 \(6\) 步操作,全局优化部分(\(l_i\))减少了 \(2\) 步操作,两个优化加起来是更优的。也就是说这里的全局优化出现了短视的问题,没有考虑局部优化。
正解
那当然是在全局优化里考虑局部优化。具体地:
- 全局优化(外层循环):枚举 \(k\),将所有的数都对齐到能被 \(2^k\) 整除,这个被对齐的数记为 \(b_i\)
- 局部优化(内层循环):在 \(b_i\) 的基础上,枚举 \([b_i. b_i + 33]\) 中所有能被 \(2^k\) 整除的数,然后找到其中操作次数最少的数
代码
#include <bits/stdc++.h>
#include <bit>
using namespace std;
typedef long long ll;
typedef long double ld;
typedef std::vector<int> vi;
#define INT_INF 2147483647
#define forn(i, a, b) for(int i = a; i < b; i++)
#define readi(x) scanf("%d", &x)
#define readv(x, n) forn(i,0,n) scanf("%d", &x[i])
#ifdef DEBUG
#define printi(x) printf("DEBUG: %d\n", x)
#define prints(x) printf("%s", x)
#define printv(x, n) vi x(n); printf("DEBUG: "); forn(i,0,n) printf("%d ", x[i])
#else
#define printi(x) printf("%d\n", x)
#define prints(x) cout << x << endl
#define printv(x, n) forn(i,0,n) printf("%d ", x[i]); printf("\n")
#endif
int main(void){
int t;
readi(t);
while (t--) {
int n;
readi(n);
vi x(n);
readv(x, n);
ll ans = INT_INF;
for (int v = 1, w = 0; v <= 200000; v <<= 1, w += 1) {
ll _ans = 0;
forn (i, 0, n) {
int gap = (v - x[i] % v) % v;
_ans += gap;
int _min_step = INT_INF;
for (int k = 0; k <= 33; k++) {
unsigned t = x[i] + gap + k;
if (t % v == 0) {
_min_step = min(_min_step, k + std::popcount(t) + std::bit_width(t) - 1);
}
}
_ans += _min_step;
}
_ans -= (n - 1) * w;
ans = min(ans, _ans);
}
printf("%lld\n", ans);
}
}

浙公网安备 33010602011771号