按操作倒序逆推数组,普通加乘分别用减除还原,而自加与自乘要特判成除以 2 和开平方。
OJ: luogu
题目 ID: P8318
难度:普及-
标签:模拟思维
日期: 2026-06-18 23:28
题意
给定猴子操作后的最终数组 b1..bn,以及猴子执行过的 m 次操作:
1 x y:第x个数加上第y个数2 x y:第x个数乘上第y个数
要求还原出最初的数组 a1..an。
思路
先看一个最直观的朴素解:
如果数据很小,可以枚举原数组的每一项,再把所有操作正着模拟一遍,看最后能不能变成题目给出的数组。
cpp
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10;
const int MAXM = 20;
int n, m;
long long b[MAXN]; // 输入给出的最终数组
int op[MAXM], x[MAXM], y[MAXM];
long long cur[MAXN];
long long ans[MAXN];
int found;
void simulate() {
for (int i = 1; i <= m; i++) {
if (op[i] == 1) {
cur[x[i]] = cur[x[i]] + cur[y[i]];
}
else {
cur[x[i]] = cur[x[i]] * cur[y[i]];
}
}
}
void dfs(int dep) {
if (found) {
return;
}
if (dep > n) {
long long bak[MAXN];
for (int i = 1; i <= n; i++) {
bak[i] = cur[i];
}
simulate();
bool ok = true;
for (int i = 1; i <= n; i++) {
if (cur[i] != b[i]) {
ok = false;
break;
}
}
if (ok) {
found = 1;
for (int i = 1; i <= n; i++) {
ans[i] = bak[i];
}
}
for (int i = 1; i <= n; i++) {
cur[i] = bak[i];
}
return;
}
for (int v = 1; v <= 4; v++) {
cur[dep] = v;
dfs(dep + 1);
if (found) {
return;
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> b[i];
}
for (int i = 1; i <= m; i++) {
cin >> op[i] >> x[i] >> y[i];
}
dfs(1);
for (int i = 1; i <= n; i++) {
if (i > 1) {
cout << ' ';
}
cout << ans[i];
}
cout << '\n';
return 0;
}这个办法只能做很小的数据,但它给了我们一个关键启发:
既然正向操作是确定的,那么更自然的办法就是从最终数组开始,按操作倒序还原。
倒着看每一步:
- 若原操作是
1 x y- 当
x != y时,之前的x = 现在的 x - 现在的 y - 当
x == y时,这一步其实是x = old_x + old_x,所以之前的x = 现在的 x / 2
- 当
- 若原操作是
2 x y- 当
x != y时,之前的x = 现在的 x / 现在的 y - 当
x == y时,这一步其实是x = old_x * old_x,所以之前的x = sqrt(现在的 x)
- 当
注意 x == y 时不能直接套“减当前值”或“除当前值”,因为这个位置本身被这一步改掉了。
所以我们只要从第 m 个操作一直逆推到第 1 个操作,就能恢复原数组。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
const int MAXM = 205;
int n, m;
long long a[MAXN]; // 当前维护的数组,初始读入的是最终结果 b
int op[MAXM], x[MAXM], y[MAXM];
// 还原平方操作时,求一个整数平方根。
long long isqrt_ll(long long v) {
long long r = (long long) sqrtl((long double) v);
while ((r + 1) * (r + 1) <= v) {
r++;
}
while (r * r > v) {
r--;
}
return r;
}
void solve() {
for (int i = m; i >= 1; i--) {
if (op[i] == 1) {
if (x[i] == y[i]) {
a[x[i]] /= 2;
}
else {
a[x[i]] -= a[y[i]];
}
}
else {
if (x[i] == y[i]) {
a[x[i]] = isqrt_ll(a[x[i]]);
}
else {
a[x[i]] /= a[y[i]];
}
}
}
for (int i = 1; i <= n; i++) {
if (i > 1) {
cout << ' ';
}
cout << a[i];
}
cout << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= m; i++) {
cin >> op[i] >> x[i] >> y[i];
}
solve();
return 0;
}复杂度
每次操作只需要常数时间逆推一次,总时间复杂度是
总结
这题的核心不是正着模拟,而是意识到“最终状态 + 操作记录”足够支持倒着还原。
真正容易错的地方只有一个:x == y 时要单独处理,自加对应除以 2,自乘对应开平方。
