折叠一次后 t 变成关于中心对称的向量,之后每折叠一次只翻倍;于是 b 只能是 x·t + H·(t+reverse(t)) 的形式,解出唯一的非负整数 x,H 再验证即可。
OJ: luogu
题目 ID: P9714
难度:普及
标签:数学推导思维枚举线性组合
创建: 2026-10-03 12:04
更新: 2026-10-03 12:04
形式化题目
给定长度为
- 操作一:
,对每个 同时进行; - 操作二:
。
判断是否存在操作序列使
注意约束
正解
思路
直接去想"先做几次操作一、再做几次操作二"是行不通的:操作可以任意交错,序列有无穷多条。我们的目标是把这无穷多条序列,压成有限个整数解。
第一步:两种操作互不干扰。
- 操作一只改写
,完全不动 ; - 操作二只改写
,完全不动 。
所以操作序列里,只有"在
一遍操作序列唯一对应一组非负整数
第二步:算出每个
令
也就是
一次折叠之后,
就变成关于中心对称的向量了。
因为
那么对
同理
看似要无限枚举的折叠过程,其实只有第一步是"新"的,后面全是
第三步:合并成一个方程。
把上式代回
现在看
所以
恰好可以取遍全体非负整数,不多也不少。
于是整个问题等价于一个非常干净的说法:
是否存在非负整数
,使得对一切 都有 ?
只剩下两个未知量,解方程就够了。
第四步:解出
两个未知量需要两条方程。把
因为
情形 A:存在
要求它是非负整数(
同样要求非负整数。两个未知量都被钉死后,
情形 B:
记
为什么这样是对的? 上面每一步都是等价的(“存在操作序列”
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-03 12:04
* update_at: 2026-10-03 12:04
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 2005;
int n;
ll t[maxn]; // 题目给出的 t 数列
ll b[maxn]; // 目标数列
// 记 mirror(i) = n + 1 - i。
// 设第 k 个版本的 t 为 T^k(T^0 = t,每做一次操作一变成 fold(T^k)),
// 并且 x_k 表示用 T^k 做了几次操作二,则
// a_i = x_0 * t_i + H * (t_i + t_mirror(i)), H = sum_{k>=1} x_k * 2^(k-1) >= 0
// 其中 x_0 >= 0 与 H 都是可以任意取的非负整数。
// 于是答案就是:是否存在非负整数 x_0, H 使所有等式同时成立。
bool check() {
// 情形 1:存在 i 使 t_i != t_mirror(i)。
// 用 i 与 mirror(i) 两条等式相减,可以直接反解出 x_0。
int pos = -1;
for (int i = 1; i <= n; i++) {
if (t[i] != t[n + 1 - i]) {
pos = i;
break;
}
}
if (pos != -1) {
int j = n + 1 - pos;
ll num = b[pos] - b[j]; // = x_0 * (t_pos - t_j)
ll den = t[pos] - t[j]; // 由 t_pos != t_j 保证 den != 0
if (num % den != 0) return false;
ll x0 = num / den;
if (x0 < 0) return false;
// 用 i = 1 的等式反解 H
ll rest = b[1] - x0 * t[1];
ll coef = t[1] + t[n];
if (rest % coef != 0) return false;
ll H = rest / coef;
if (H < 0) return false;
// 逐条验证
for (int i = 1; i <= n; i++) {
if (b[i] != x0 * t[i] + (t[i] + t[n + 1 - i]) * H) return false;
}
return true;
}
// 情形 2:t 完全对称,即所有 t_i == t_mirror(i)。
// 此时等式退化为 b_i = t_i * (x_0 + 2H),只要所有 b_i / t_i 是同一个整数即可
// (取 x_0 等于这个整数、H = 0 就能实现)。
if (b[1] % t[1] != 0) return false;
ll K = b[1] / t[1];
for (int i = 1; i <= n; i++) {
if (b[i] != K * t[i]) return false;
}
return true;
}
void read_data() {
int T;
cin >> T;
while (T--) {
cin >> n;
for (int i = 1; i <= n; i++) cin >> t[i];
for (int i = 1; i <= n; i++) cin >> b[i];
if (check()) cout << "Yes\n";
else cout << "No\n";
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
read_data();
return 0;
}实现上有两个容易写错的地方:
- 反解
时用的是 。这没问题,但必须在解完之后把所有 重新验证一遍——因为解方程只用了两条等式,其余位置还没有被约束过。 - 情形 A 的除数为
, 必须是那两个不对称点之一,不能拿中间元素当除点( 为奇数时中间的自减恒为 ,会除零)。
复杂度
每组数据只需 int 也不会溢出(代码中仍使用 long long 以防万一)。
关于暴力解法
对拍用的 brute.cpp 是直接搜索操作序列:状态为当前的
它依赖一个剪枝:
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-03 12:04
* update_at: 2026-10-03 12:04
*/
// brute.cpp:小数据暴力解(仅用于理解题意与对拍,不做任何优化)。
// 直接搜索操作序列:状态就是当前的 (a, t) 两个数列,每一步尝试操作一或操作二。
//
// 剪枝依据:a 只会变大,t 在操作一之后也只会变大(新 t_i = t_i + t_{n+1-i} >= t_i),
// 所以只要某一刻出现 a_i + t_i > b_i,以后永远补不回来,可以立刻停止这条分支。
// 因此搜索树是有限的。本文件只在 t_i、b_i 都很小的数据上使用。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1005;
int n;
int a[maxn]; // 当前的 a
int t[maxn]; // 当前的 t
int b[maxn]; // 目标的 b
bool dfs() {
// 已经达到目标
bool same = true;
for (int i = 1; i <= n; i++) {
if (a[i] != b[i]) same = false;
}
if (same) return true;
// 如果现在这个 t 都已经加不动,以后的 t 只会更大,更不可能加
for (int i = 1; i <= n; i++) {
if (a[i] + t[i] > b[i]) return false;
}
// 选择一:执行操作二,a += t
for (int i = 1; i <= n; i++) a[i] += t[i];
if (dfs()) return true;
for (int i = 1; i <= n; i++) a[i] -= t[i];
// 选择二:执行操作一,t 与自己的倒序相加。
// 用本层的局部数组保存 t,避免不同递归层互相覆盖。
int save_t[maxn];
for (int i = 1; i <= n; i++) save_t[i] = t[i];
for (int i = 1; i <= n; i++) t[i] = save_t[i] + save_t[n + 1 - i];
if (dfs()) return true;
for (int i = 1; i <= n; i++) t[i] = save_t[i];
return false;
}
void read_data() {
int T;
cin >> T;
while (T--) {
cin >> n;
for (int i = 1; i <= n; i++) cin >> t[i];
for (int i = 1; i <= n; i++) cin >> b[i];
for (int i = 1; i <= n; i++) a[i] = 0;
if (dfs()) cout << "Yes\n";
else cout << "No\n";
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
read_data();
return 0;
}总结
这道题的关键不是"怎么搜索操作序列",而是先消除操作序列的无限性:
- 两种操作互不干扰,于是任意操作序列等价于每个折叠版本各执行若干次操作二的计数;
- 折叠一次后
就变成中心对称向量,此后每折叠一次只是整体乘 ,即 ; - 二进制表示让"各计数的加权和"
恰好取遍所有非负整数,问题坍缩为 这一个方程; - 利用
的对称性作差消去 ,反解出唯一的 和 ,非负整数 + 逐项验证即可。
值得记住的两个技巧:把"操作序列"转换成"各状态的执行次数",以及用二进制把一个任意大的加权和解释成可以取遍所有非负整数。两者合起来就把无穷搜索变成了两条除法。