P9017 [USACO23JAN] Lights Off G
前言
困了一下午, 仅仅只搞懂了个大概, 我们赶紧把这些题补了, 冷静一点
思路
观察大样例可以发现, 答案好像都不大
容易证明的是先用最多 \(n\) 次关闭所有开关, 然后在 \(2n\) 次打开每个灯, 这样一定不超过 \(3n\) 次就可以成功的打开所有灯
那么我们考虑以这个为突破口, 枚举操作次数解决问题
那么考虑对于一种操作次数 \(m\) , 怎样判断其是否可行
首先你需要知道操作的小转化 : 对于每个 \(2\) 操作, 我们发现其相当于一个 \(\oplus\) , 那么我们可以利用 \(\oplus\) 的交换律和自反律, 得出 \(a\) 的初始状态和 \(b\) 的变换是互相不影响的两个部分, 下面我们只考虑 \(b\) 变换的异或积
稍微观察一下第 \(i\) 次操作的性质, 你发现最终第 \(i\) 次操作会覆盖到 \(m - i + 1\) 大小的区间, \(\rm{belike}\) :
那么对于 \(m\) 次操作, 相当于一串长度为 \(1, 2, 3 \cdots m\) 的区间取反
这个时候问题就更加符合中国宝宝的体质, 每次操作次数 \(m \gets m + 1\) , 转化过来仅仅是多了一个长为 \(m + 1\) 的区间取反
这个时候看似没法处理了, 但是你发现即使是多组测试数据, 但是 \(n\) 都是相通的, 进一步思考可以发现, 我们可以考虑来一个全局的预处理
考虑令 \(f_{i, S}\) 表示第 \(i\) 次操作能否转化到 \(S\) , 其中 \(f_{i, S} \in \{0, 1\}\)
容易发现每次其实就是从 \(f_{i - 1, S^{\prime}}\) 通过新增加一个长为 \(i\) 的区间取反变换而来的
我们可以容易的列出柿子
即可
感觉有点混乱, 我们再理一遍思路
首先你观察操作, 发现 \(a\) 的初始状态和 \(b\) 的变换是两个部分, 可以分开讨论
观察大样例发现答案很小, 我们考虑证明出答案 \(\leq 3n\) , 然后枚举答案(及操作次数)
假设操作次数为 \(m\)
稍微观察一下第 \(i\) 次操作的性质, 你发现最终第 \(i\) 次操作会覆盖到 \(m - i + 1\) 大小的区间, \(\rm{belike}\) :
那么对于 \(m\) 次操作, 相当于一串长度为 \(1, 2, 3 \cdots m\) 的区间取反
这个时候问题就更加符合中国宝宝的体质, 每次操作次数 \(m \gets m + 1\) , 转化过来仅仅是多了一个长为 \(m + 1\) 的区间取反
然后我们考虑全局预处理
结束
复习
考虑复习
- 定义操作 (约束) 和开销 / 收益, 要求最值化开销 / 收益
- 先排除无效元素
- 将约束条件数学化
- 模拟操作情况, 贪心处理最好开销 (将简单情况先处理, 然后在基础上处理最值 / 简化操作)
- 考虑操作对答案的影响 (推式子) , 据此对操作进行处理
- 推导每个元素对答案的贡献 \((\)拆贡献\()\)
- 推导动态规划
- 判断是否存在操作满足约束
- 模拟, 对于重复情况去重
- 要求一个数列的多个部分 \((\)前缀, 子串 \(\cdots\)\()\) 的成本
- 贪心找到最优操作的构造方法, 加上优化 / 找共同点
- 找到所有情况统一的构造方案
- 然后逐操作贪心
题意
给定长为 的 串
一次操作定义为
- 对 的一位异或
- 把 异或上
- 将 循环右移一位
求最少多少次操作使得 变成全
肯定要先模拟操作
重要性质
不难发现每次都把 异或上 这个操作
例如 , 可以简单地化成
也就是说, 问题简化为
- 对 的一位异或
- 把 异或上
- 将 循环右移一位
求 的最小次数
不难发现对于每一个 , 次数的最小化问题实际上是比较确定的, 因为操作并未留下人为调整的空间
发现 \(b\) 的变化可以看做简单的滚动之后翻转
但是其异或和怎么变换?
不妨设操作次数为 \(k\) , 如果没有每次操作的翻转 \(b\) 一位, 那么对于 \(b\) 中的每一个 \(1\) , 就等效于一个长为 \(k\) 的区间取反
加上每次操作的翻转 \(b\) 一位, 也就是多了一个长为 \(k - i + 1\) 的区间取反
那么你发现, 问题简化为判定类
对于操作次数 \(k\) , 求是否存在 \(|b|\) 个长为 \(k\) 的和 \(k\) 个长度分别为 \(1 \sim k\) 的区间, 对这些区间取反之后, 使得 \(b \to a\)
其中 \(|b|\) 个长为 \(k\) 的区间是固定的, 可以提前处理变成 \(b'\)
进一步简化为 \(k\) 个长度分别为 \(1 \sim k\) 的区间, 对这些区间取反之后, 使得 \(b' \to a\)
求最小的可行 \(k\)
发现 \(n \leq 20\) , 那可以用很经典的 \(\mathcal{O} (n 2^n)\) 算法
令 \(f_{i, \mathbb{B}}\) 表示 \(i\) 次操作之后 \(b\) 的可行性
直接模拟不太好做, 巧妙地一点是只要一个 \(\mathbb{B}\) 可以被表示为 \(b\) 进行 \(k\) 个长度分别为 \(1 \sim k\) 的区间取反, 就一定可行
所以每次加上一个长为 \(i\) 的区间取反即可, 很有生活
至于多测, 简单地值域处理一般化即可
实现
难点不在实现
#include <bits/stdc++.h>
const int MAXVAL = 2e6 + 10;
int t, n;
int p[MAXVAL]; // 同族元素
int f[45][MAXVAL]; // 递推数组
std::string a, b;
/*旋转操作*/
int rotate(int x) {
return ((x & 1) << n - 1) | (x >> 1);
}
signed main()
{
scanf("%d %d", &t, &n);
memset(p, -1, sizeof p);
for (int i = 0; i < (1 << n); i++) {
int x = i;
while (!(~p[x])) p[x] = i, x = rotate(x);
}
f[0][0] = 1;
for (int i = 1, s = 0; i <= 2 * n; i++) {
s ^= (1 << ((i - 1) % n));
for (int j = 0; j < (1 << n); j++) f[i][p[j]] |= f[i - 1][p[j ^ s]];
}
while (t--)
{
std::cin >> a >> b;
int A = 0, B = 0;
for (int i = 0; i < n; i++) A |= ((a[i] - '0') << (n - i - 1)), B |= ((b[i] - '0') << (n - i - 1));
if (A == 0) { puts("0"); continue; }
for (int i = 1; ; i++) {
A ^= B, B = rotate(B);
if (f[i][p[A]]) { printf("%d\n", i); break; }
}
}
return 0;
}
总结
对于 \(01\) 串的操作, 我们考虑用数学语言表示
善于利用运算律简化问题
当观察到答案较小时, 考虑
- 分讨答案
- 枚举答案检查
玩样例可以找到一些有用的性质
如果多组测试数据有相通之处, 也许你可以全局的处理?
感觉这个题到紫了, 很难 \((\)现在看还好\()\)
枚举确定长度串的小 \(\rm{trick}\)
模拟操作情况, 贪心处理最好开销 (将简单情况先处理, 然后在基础上处理最值 / 简化操作)