夜空笑传之猜猜变
考场上看到吓哭了。赛后补题补了两个月,今天来补篇题解。
本题前期的思维构造特别抽象,文字详细说明会显得十分繁琐,所以下文会配有大量插图以帮助理解。
已经看过其他题解或自行推出大部分性质的可以直接在右侧标题栏点 天降之物 跳到完整图示。(如果找不到标题栏,点击右上角查看文章即可;对于博客园读者,请使用目录导航功能)
建模
转链为环
把 \(A\) 和 \(B\) 当成环会好理解很多。
平凡地,设置一个「分界点」指针代表原链中首尾的区分,操作转化为指针往一个地方移动两位并将经过的两位异或成一位。

构造划分
环 \(B\) 中每一个数必然对应环 \(A\) 中的一个区间,将 \(A\) 划分成 \(m\) 个区间,区间 \(j\) 的异或和即为 \(b_j\)。
可以理解为钦定一个辅助数组 \(d\),其中 \(b_i=\bigoplus_{d_j=i}a_j\)。要求等价于所有相同的 \(d_j\) 必须在环上连续。
不是所有划分都是合法的,有些划分是不能通过操作达成的。考虑什么样的划分能由操作达成。
对于一段区间,限定分界点从一端到另一端,它的长度 \(l\) 有哪些限制?
区间长度的余数限制

- \(2\equiv 2 \pmod 3\)
- \(1\equiv 1 \pmod 3\)
- \(2 + 2\equiv 1 \pmod 3\)
- \(1 + 1\equiv 2 \pmod 3\)
任何满足 \(l>1,l \equiv 1\pmod 3\) 的区间都可以由两个满足 \(l \equiv 2\pmod 3\) 的区间构造;任何满足 \(l \equiv 2\pmod 3\) 都可以由一个满足 \(l \equiv 1\pmod 3\) 的区间额外拼上一段 \(l=1\) 构造。
完整的一段 \(3|l\) 的区间是不合法的,它们无法被构造出来。
然而,只要不存在完整的一段 \(3|l\) 的区间,划分就一定是合法的吗?

由上图可知,如果分界点一开始在 \(l\equiv 2\pmod 3\) 区间的左侧,那么这一段合成后它会在这一段的右侧;如果分界点一开始在 \(l > 1, l\equiv 1\pmod 3\) 区间的左侧,那么这一段合成后它还会在这一段的左侧。(反过来同理)
最后要求的 \(B\) 是序列意义上的,因此要求 \(B\) 最后的分界点也要达标。并且各个区间之间不能相交,所以需要注意处理区间时移动的位置关系。
先考虑一开始分界点就在一段的端点上的情况:

可以看到大致分成了四个部分,分别是两边若干段 \(l>1,l \equiv 1\pmod 3\),若干段 \(l\equiv 2\pmod 3\) 拼在一起,以及剩下的 \(l=1\)。
那如果一开始分界点不在一段的端点呢?通过操作将其转到一段的端点上即可解决。

在序列上表现为一个前缀和一个后缀拼在一起。
这也是唯一出现 \(l\equiv 0\pmod 3\) 的机会。
再套几次可得:

发现把区间从分界点处切开,则切开得到的两个区间的模 \(3\) 意义下的余数必然不等。反过来考虑,只要切开得到的两个区间的模 \(3\) 意义下的余数不等则必然可以构造一个可行的方案。容易证明两者互为充要条件。
天降之物
(空降成功,感谢指挥部!)
完整的图长这样:

在序列上表现为:

欧拉回路
所以这题跟欧拉回路有什么关系?
部分分
观察特殊性质 B 和 C,容易转化为下图所示两个栈的结构,操作为弹出一个栈栈顶两个元素,将其异或和放入另一个栈。\(1\) 因除 \(0\) 外不能合并充当了栈底的作用。B 则为一个栈为空的情况。


尝试归约到欧拉回路问题。
右栈顶属于块 \(k\),左栈顶属于块 \(k'=k-1\) 的状态称为状态 \(k\)。

状态 \(k+1\)。

观察到如果想要合并块 \(k\) 内的元素,就必须在两个栈内“倒腾”,即在状态 \(k\) 和状态 \(k+1\) 间反复切换。只要确定了划分,这个次数就是固定的。
对每个状态建一个点,每需要在状态 \(k\) 和状态 \(k+1\) 间切换一次,连一条边;同时在起始状态和结束状态(可由 \(A\) 和 \(B\) 的起点判断)间连一条边,那么整个连通图是一个欧拉回路。
推广
对 \(B\) 在 \(A\) 中对应的每个分段建点,再在分段之间建边,最后在 \(A\) 分界处对应的点作为起点与 \(B\) 分界处对应的点作为终点连边即可构成欧拉回路。
注意欧拉回路中,两个点之间有三条及以上边的情况同时删去两条边是和原图等价的,这个转化需要显式地标明否则有概率让人感到困惑。

动态规划 DP
建模了这么久,是时候 DP 了!
断环为链
枚举 \(A'\) 为 \(A\) 的一个循环移位。设 \(b_1\) 的对应块是 \(A'\) 断环为链后的一个前缀。如果这里不断环为链的话后面就要处理多层环,为了易于编写代码于是这么做了。实际方便!
分段 DP
\(B\) 起始位置后 \(l > 1, l \equiv 1\pmod 3\) 为一段,中间 \(l = 1\) 的部分特殊转移,接下来 \(l > 1\) 一直到 \(A\) 起始位置为一段,最后 \(A\) 起始位置到 \(B\) 起始位置为一段。
细节处理
具体转移可参考 chenbs 大佬的题解。
镜像的情况翻转再做一遍即可。构造答案需要保存下每次转移的过程。
点击查看代码
#include <bits/stdc++.h>
#include <cstring>
using namespace std;
typedef long long ll;
const int maxn = 250;
int c, t;
int n, m;
int a[2 * maxn + 5], b[maxn + 5];
int Axorpre[2 * maxn + 5];
int f[3][maxn + 5]; // b 前 i 个位置能匹配到的最短 a 前缀 分段 DP
// 0 b_1 后双边 1 前双边 2 单边
int g[2 * maxn + 5][maxn + 5];
unordered_map<int, int> mp[3];
struct tran_t
{
int type;
int id;
} prest[3][2 * maxn + 5];
int A_beg; // 因为要作为接下来两个函数的参数就干脆作为全局变量了
struct segT
{
int l, r, tp;
segT(int l_ = 0, int r_ = 0, int tp_ = 0) : l(l_), r(r_), tp(tp_) {}
};
vector<int> ans;
vector<segT> seg;
struct air
{
int first;
int second;
int third;
air(int fi = 0, int se = 0, int th = 0) : first(fi), second(se), third(th) {}
bool operator < (const air & obj) const
{
if(first != obj.first) return first < obj.first;
if(second != obj.second) return second < obj.second;
return third < obj.third;
}
bool operator > (const air & obj) const
{
return obj < *this;
}
};
void init()
{
memset(g, 0, sizeof g); // a[x, n] b[y, m] LCP
for(int i = 2 * n; i >= 1; i--)
{
for(int j = m; j >= 1; j--)
{
g[i][j] = (a[i] == b[j] ? g[i + 1][j + 1] + 1 : 0);
g[i][j] = min(g[i][j], max(0, n - i + 1)); // 边界条件呃呃的说
}
}
Axorpre[0] = 0; // 边界情况
for(int i = 1; i <= 2 * n; i++) Axorpre[i] = Axorpre[i - 1] ^ a[i]; // 这样就已经可以方便计算任意一段异或和的说
mp[0].clear(), mp[1].clear(), mp[2].clear();
for(int i = n + 1; i <= 2 * n; i++) if(!mp[i % 3].count(Axorpre[i])) mp[i % 3][Axorpre[i]] = i; // 介个特定长度介个异或和最短的系啦一国
// 跨过环后每种 s 值第一次出现的位置
}
bool solve()
{
priority_queue<air, vector<air>, greater<air> > pq; // 小根堆
memset(f, 0x3f, sizeof f);
if(A_beg == n + 1) f[2][0] = A_beg - 1;
else f[0][0] = f[1][0] = A_beg - 1; // 循环节的前一位,表示当前意义下的 0
for(int i = 0; i < m; i++)
{
if(f[0][i] == 0x3f3f3f3f) continue;
for(int j = f[0][i] + 4; j <= n; j += 3)
{
if((Axorpre[j] ^ Axorpre[f[0][i]]) == b[i + 1])
{
f[0][i + 1] = j;
prest[0][i + 1] = tran_t{0, i};
break;
} // 再往后一定不优
// f[0][i] + 1 ~ j 这一段异或和 == b[i + 1] 可以转移 注意这里转移不能越过 a 起始分界点
}
} // 对于每一个 i 枚举一次
// [0] 阶段内部转移完毕
for(int i = 0; i <= m; i++)
{
while(pq.size() && pq.top().third < i) pq.pop(); // 靠补上来加成
if(pq.size())
{
air qtop = pq.top();
int znxt = qtop.first - qtop.second + i, ztype = (znxt == n ? 2 : 1); // 是端点就直接跳过转移下一段
if(f[ztype][i] > znxt) f[ztype][i] = znxt, prest[ztype][i] = tran_t{-1, qtop.second}; // -1 表示转移时没有操作
}
if(f[0][i] != 0x3f3f3f3f && i <= m) pq.emplace(f[0][i], i, i + g[f[0][i] + 1][i + 1]);
// first 是为了对齐 second 存储转移来源 third 判合法性
} // [0] ~ [1]
for(int i = 0; i < m; i++)
{
if(f[1][i] == 0x3f3f3f3f) continue;
for(int j = f[1][i] + 4; j < n && j < f[1][i + 1]; j += 3)
{
if((Axorpre[j] ^ Axorpre[f[1][i]]) == b[i + 1])
{
f[1][i + 1] = j;
prest[1][i + 1] = tran_t{1, i};
break;
}
} // 保证最优
} // [1] ~ [1]
for(int i = 0; i < m; i++)
{
if(f[1][i] == 0x3f3f3f3f) continue;
if(f[1][i] + 4 <= n &&
(n - f[1][i]) % 3 == 1 && // n - (f[1][i] + 1) + 1 \equiv 1 \pmod 3
(Axorpre[n] ^ Axorpre[f[1][i]]) == b[i + 1] &&
n < f[2][i + 1]) f[2][i + 1] = n, prest[2][i + 1] = tran_t{1, i};
// f[1][i] + 1 ~ n
// 再往后
// l_{beg} \equiv l_{end} + 2
int len = (n - f[1][i] + 2) % 3;
int needMod = (n + len) % 3;
int needXor = b[i + 1] ^ Axorpre[f[1][i]];
if(!mp[needMod].count(needXor)) continue;
int j = mp[needMod][needXor]; // XOR {f[1][i] + 1 ~ ?} = b[i + 1]
if(j < f[2][i + 1]) f[2][i + 1] = j, prest[2][i + 1] = tran_t{1, i}; // 存在且更优
} // [1] ~ [2]
int A_lst = A_beg + n - 1;
for(int i = 0; i < m; i++)
{
if(f[2][i] == 0x3f3f3f3f) continue;
for(int j = f[2][i] + 2; j <= A_lst && j < f[2][i + 1]; j += 3)
{
if((Axorpre[j] ^ Axorpre[f[2][i]]) == b[i + 1])
{
f[2][i + 1] = j;
prest[2][i + 1] = tran_t{2, i};
break;
}
}
}
return f[2][m] == A_lst; // 能不能,,,笑到最后??!
}
void down(int len, int op)
{
while(len > 4)
{
ans.emplace_back(op);
ans.emplace_back(op);
ans.emplace_back(3 - op); // 1 <=> 2
len -= 3;
} // 四个缩成一个,最后最多留 4 个
}
void makeAnswer(bool rev)
{
ans.clear();
seg.clear();
int type = 2, id = m;
while(id) // 不是起始状态
{
auto [type2, id2] = prest[type][id];
if(type2 == -1)
{
type = 0, id = id2; // 神秘转移
continue;
}
int pref = f[type2][id2];
seg.emplace_back(pref + 1, f[type][id], type2);
type = type2, id = id2;
} // [0] ~ [1] ~ [2]
reverse(seg.begin(), seg.end());
id = (int)seg.size() - 1;
while(id >= 0 && seg[id].tp != 1) --id; // [1]
// 因为 DP 的时候实际上这一段是算倒着来的所以这里要倒着找 1
if(id >= 0)
{
int oldid = id;
if(seg[id].l <= n && seg[id].r > n) // 跨过分界线
{
int len = seg[id].r - seg[id].l + 1;
int lc = (n - seg[id].l + 2) / 2;
// lc == l_end / 2 向上取整
if((n - seg[id].l + 1) & 1)
{
if(seg[id].r == n + 1)
{
ans.emplace_back(2);
--len; // 2 相当于往左一位
ans.emplace_back(1);
--len; // 1 相当于往右一位
--lc;
}
else
{
ans.emplace_back(1);
--len;
}
// 正确调整 l ~ n 奇偶性
// 到了最左边
// 正好 l_end /= 2
// mod 3 意义下
// 2 -> 1 1 -> 2 3 -> 0
// l_end - l_beg == 1 (mod 3)
// 2 + 1 = 0 -> 1 + 1 = 2
// 0 + 2 = 2 -> 2
// 1 + 0 = 1 -> 2 + 0 = 2
// 精妙的构造这一块
// 总共肯定是一开始的 lc 次
}
for(int k = 1; k <= lc; k++)
{
ans.emplace_back(2);
--len;
}
down(len, 1); // 把右边的吞成一段
--id;
}
while(id >= 0 && seg[id].tp == 1)
{
int len = seg[id].r - seg[id].l + 1;
down(len, 2); // 一直收拢
ans.emplace_back(2);
ans.emplace_back(2);
--id;
} // l mod 3 == 1 一直回收左边的,顺便往左边跳
for(int k = id + 1; k <= oldid; k++) ans.emplace_back(1); // 往右边一直跳
// 这里的最开始跨 n 那段因为精密的构造所以最后一定是一个 _ _,往右跳一次可以正好跨过.
}
// 接下来是 [2]
id = (int)seg.size();
while(id >= 1 && seg[id - 1].tp == 2) --id;
for(int k = id; k < (int)seg.size(); k++)
{
int len = seg[k].r - seg[k].l + 1;
down(len, 1);
ans.emplace_back(1);
// l mod 3 == 2
// 最后一个 1 跳过去
}
// 即单边部分
id = -1;
while (id + 1 < (int)seg.size() && seg[id + 1].tp == 0) ++id;
// 因为是倒着所以只能用这种猎奇做法
// 但是正过来的话 dp 部分就会变成倒着的,更不好写
for (int k = 0; k <= id; k++)
{
int len = seg[k].r - seg[k].l + 1;
down(len, 1);
ans.emplace_back(1);
ans.emplace_back(1);
// l mod 3 == 1
// 最后剩一个 l = 4
}
for (int k = 0; k <= id; k++) ans.emplace_back(2);
// [0]
if(rev) for(int &wer : ans) wer = 3 - wer;
cout << "Yes" << endl;
for (int i = 0; i < (int)ans.size(); ++i)
{
if (i) cout << ' ';
cout << ans[i];
}
cout << endl;
}
int ATRI_solving()
{
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
a[i + n] = a[i];
}
for(int i = 1; i <= m; i++) cin >> b[i];
init();
for(int i = 1; i <= n + 1; i++)
{
A_beg = i;
if(solve())
{
makeAnswer(0);
return 0;
}
}
reverse(a + 1, a + 1 + n + n);
reverse(b + 1, b + m + 1);
init();
for(int i = 1; i <= n + 1; i++)
{
A_beg = i;
if(solve())
{
makeAnswer(1);
return 0;
}
}
cout << "No" << endl;
return 1;
} // 高性能
int main()
{
cin.tie(nullptr)->sync_with_stdio(false);
cin >> c >> t;
while(t--) ATRI_solving();
return 0;
}
特别鸣谢
- 在线画图网站
- night,@larsr
- 题解:P15651 [省选联考 2026] 夜空 / night,@tiger2005
- 题解:P15651 [省选联考 2026] 夜空 / night,@chenbs
- P15651 题解,@lmh_qwq
- P15651 题解,@zjy2008
创作声明
本文遵循 CC BY-NC-SA 4.0 协议。
保证本文未使用任何 AI 工具辅助创作。
转载例如下:
[原文](https://www.luogu.com.cn/article/bk4ydbo0)作者为 [clx201022](https://www.luogu.com.cn/user/552688),转载人保证会遵循 [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans) 协议:以适当的方式署名,不以盈利为目的进行转载,并以相同方式共享此文。
浙公网安备 33010602011771号