题解:P15651 [省选联考 2026] 夜空 / night

· · 题解

本文深受 这份题解 启发,拜谢 zjy2008!

观察操作的本质,不难发现两种操作都是将相邻两个元素合并起来,然后在环上将起始位置移动一位。因此,目标序列中每个元素对应原序列在环上的一个区间(下面称为块)。

接下来,不妨从特殊性质 B 入手考虑问题。

::::info[特殊性质 B]{open} 此时原序列和目标序列的最左侧都恰好包含了 3 个 1,而剩余的数字都是偶数。不难发现,除了两侧的 1 可以和 0 合并之外,其余情况下 1 不应该参与到合并流程中。接下来,从 1 所在的位置将序列断成两半,左侧可以看作一个空的栈,而右侧可以看作一个包含所有偶数的栈。

在这个背景下,考虑对偶数进行操作,考虑过程中每个元素对应的块,可以注意到如下事实:在左侧栈的元素对应的块大小总是模 3 余 2,而在右侧栈的元素对应的块大小总是模 3 余 1。因此,最终的目标就是将所有偶数断成若干个长度模 3 余 1 的段,并且需要处理块长度为 1 的特殊情况(因为它不能被操作)。

在构造时,若需要将一整个块将右侧放到左侧,则可以先通过 2 2 1 操作将最右边的四个值合并为一个,直到块长度被缩减到 4 时,通过 2 2 操作将其移动到左侧;若需要将一整个块从左侧放到右侧,考虑到此时块长度恰好为 2,通过 1 操作即可实现。在这一构造下,所有大块都只需要恰好两次整体移动就能正确合并。 ::::

上面的讨论实际上具有一定的启发意义:几乎所有的块都可以被分成三类:

接下来继续考虑特殊性质 C。

::::info[特殊性质 C]{open} 此时原序列和目标序列中都出现了连续 3 个 1,不妨假设它们都出现在序列的中间,而不是左右两侧。此时依然可以使用两个栈的思路考虑,但会出现两个问题:

假设所有块被通过如下方式编号:

| k | k+1 | ... | m | "1 1 1" | 1 | 2 | ... | k' |

下面使用 (k, k') 表示这个状态,这象征了左侧栈顶元素属于第 k 个块,而右侧栈顶元素属于第 k' 个块。此时可以观察到,几乎所有情况下都有 k' = k - 1,而只有初始的跨栈块以及整体移动的中间状态(可忽略)会导致 k' = k。下面为方便起见,称 k' = k - 1 时状态 (k, k') 为状态 k

接下来考虑第 k 个块:

因此,每个块的整体移动次数需求就被转化为状态 k 和状态 k+1 相互切换的次数。如果块分配方式确定,那么根据长度就可以知道相邻两个状态之间需要相互切换多少次,根据原序列和目标序列的结构还可以知道初始状态和结束状态。从抽象的角度看,将状态 k 看成点,移动次数需求看成无向边,初始状态和结束状态看成起点和终点,那么问题就转化为了如下信息:

使用动态规划进行分块,在考虑第 k 个块时,其除了会将状态 k 和状态 k+1 相连之外,还必须考虑状态 k 是否满足上述要求,所以需要维护以下内容:

如果第 k 个块是跨栈块,则还需要额外考虑初始状态对应的是状态 k 还是状态 k+1(可以通过贪心的方案尽快将所有块元素集中在左侧栈或者右侧栈)。如果是状态 k,则当前检查时需要切换奇偶性,否则在计算下一个块时需要切换奇偶性。关于如何处理跨栈块,可以参考最终的代码。

另外,考虑到欧拉回路判定只和度数奇偶性与连通性相关,所以移动 2x + 1 次的块可以等效为移动 1 次,而移动 2 x + 2 次的块可以等效为移动 2 次。构造时通过动态规划逆推出分块方式,在建图之后跑一条欧拉路径即可构造方案。

上述动态规划在没有任何优化的情况下时间复杂度是大常数 \mathcal{O} (n^3 m),可能无法直接通过特殊性质 C 的后两个点。关于更详细的状态设计,以及进一步优化动态转移,可以参考本文剩余的部分。 ::::

在解决了特殊性质 C 之后,回到原问题,可以发现所需的大多数性质已经在特殊性质 C 部分讨论过了。状态设计是类似的,只是在原问题中,双栈的概念已经不复存在了,因此状态的定义需要稍加修改:

2026/3/11 补充:下面的状态设计较为复杂,文末给出了一个更简单的设计,但最后依然需要使用“只取最短长度”的方式降低复杂度。

接下来是精细化的状态设计。状态设计可以表示为一个六元组 (l, r, k, \text{link}, \text{access}, \text{start}),表示:

状态空间为 [1, n] \times [1, 2 n] \times [1, m] \times [0, 2] \times \{0, 1\} \times \{0, 1\},所以总的状态数是大常数 \mathcal{O} (n^2 m) 的。另外可以发现,在动态规划时,我们只关注这个状态是否可以到达,因此状态是布尔的,而在上一个块到下一个块的转移中,只有参数 l 是不发生变化的,因此可以将参数 l 放在 bitset 中。

接下来考虑状态的转移。对于 (\circ, r', k - 1, \circ, \circ, \circ)(\circ, r, k, \circ, \circ, \circ) 的转移,需要枚举第 k 个块的范围,也就是 [r' + 1, r]。在枚举完毕之后,可以确认第 k 个块需要整体移动的次数,也就是状态 k 和状态 k+1 之间的边数。在确定“状态 k 要么可以到达状态 1,要么可以通过新的边到达状态 k+1”以及“在包含了起始状态的考虑下,状态 k 对应的度数奇偶性正确”这两个条件后,就可以计算出每个旧状态对应的新状态。

如果在枚举时考虑的是所有区间异或和等于 b_k 的块,则每个状态可能会有 \mathcal{O} (n) 个后继,整体的时间复杂度会膨胀到 \mathcal{O} (\frac{n^3 m} w),无法接受。此时需要注意到一个事实:除了第一个块之外,剩下的所有块总是可以选取同类型下最短的一个,而类型是通过“模剩余系”与“块长度是否等于 1”确定的(模数在大多数情况下是 3,而在跨环块的某一个情况下需要额外判断新的块长度的奇偶性,故模数变成 6),因此对于每个块端点而言,类型个数就是常数的,后继的个数就是 \mathcal{O} (1) 的。上述方案正确性的简单论证如下:

因此,第一个块对应的转移是 \mathcal{O}(\frac {n^3} w) 的,而剩下的 m-1 的块中每个块对应的转移都是 \mathcal{O} (\frac {n^2} w) 的,故转移的总复杂度就是 \mathcal{O} (\frac {n^3} w) 的。另外,在利用 bitset 加速转移的同时,还需要额外记录每个除去 l 的状态对应的前驱,以便后续从后往前搜索出一种分块方式。在构造时,依然需要先将图建出来,随后根据欧拉路径进行状态转移。至于最终的方案构造,只需要先利用 1 1 22 2 1 将块长度压缩到 1、2 和 4 之一,此时再进行整体移动就很方便了。

最终的时间复杂度就是 \mathcal{O}(\sum \frac {n^3} w),需要额外注意 m = 1m = 2 的情况。

::::info[代码]

// https://qoj.ac/submission/2107883

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <vector>
#include <bitset>
#include <set>

using namespace std;

int t, n, m;
int a[510], b[260];
int pa[510];

int que(int l, int r) {
  return pa[r] ^ pa[l - 1];
}

/*
  (u, v) 表示中间状态中最左侧值属于 b[u],而最右侧值属于 b[v]
  第 k 块可以切换状态 (k, k-1) 和 (k+1, k)
  最开始可能位于 (u, u) 或 (u, u - 1),需要经过变换到达 (1, m)
  下面将状态 (i, i - 1) 简称为状态 i。

  以下状态设计的结果是布尔的(也就是只关心能不能到达这个状态)

  l - 第 1 块的起始位置(使用 bitset 表示)
  k - 当前枚举的块号
  r - 第 k 块的结束位置
  lnk - 当前的图结构中,状态 k+1 包含的边数
  acc - 当前的图结构中,状态 1 能否到达状态 k+1
        这个状态用于处理中间断开的连通块
  odd - 奇偶性是否受到过干预
        为了将欧拉回路和欧拉路径一起处理,
        可以将起始状态和最终状态之间连边
        在加上这条边之后,所有状态必须是偶度数的。

        在代码中,我们并不在于状态 1 的奇偶性,其他状态是偶度数即可。
        在处理第 k (k > 1) 块时,会处理状态 k 的奇偶性。

        如果第 k 个块的起始位置恰好是 a[1],
        那么状态 k 一定是起始状态,
        因此它立刻受到这条边的干预,度数需求逆转
        否则,如果第 k 个块同时包含 a[n] 和 a[1]:
        - 如果其转移到状态 k,那么需要立马实行干预;
        - 如果其转移到状态 k+1,则干预会在下一个块执行,
          下一个块的触发条件是:
          “已经绕了一周(l > n),而没有受过干预(odd = false)”
*/
struct State {
  int r;      // [1, 2 * n]
  int k;      // [1, m]
  int lnk;    // [0, 2]
  bool acc;   // [0, 1]
  bool odd;   // [0, 1]
  // 状态空间为 24 n m

  int id() const {
    int res = k - 1;
    res = res * (2 * n) + r - 1;
    res = res * 3 + lnk;
    res = res * 2 + acc;
    res = res * 2 + odd;
    return res;
  }
};

bitset<251> dp[1500000];
vector<State> from[1500000];

void from_to(State dest, State src) {
  dp[dest.id()] |= dp[src.id()];
  from[dest.id()].push_back(src);
}

/*
  状态的更新函数
  k 是当前块的编号
  nl 是第 k 块的开始位置
  nr 是第 k 块的结束位置
  lnk 表示受到第 k 块影响,从状态 k 到状态 k+1 的边数
  start 表示状态 k 是不是起始状态
*/
void update(int k, int nl, int nr, int lnk, bool start) {
  if (k == 1) {
    dp[State(nr, k, lnk, lnk != 0, start).id()][nl] = 1;
    return;
  }

  int pr = nl - 1;
  for (int plnk : {0, 1, 2})
    for (bool pacc : {false, true})
      for (bool podd : {false, true}) {
        // 形成了不连通的部分
        if (!pacc && plnk != 0 && lnk == 0) continue;
        // 状态 k 是否受到干预
        bool infected = start || (nl > n && !podd);
        // 在干预下,状态 k 的奇偶性是否正确
        if ((infected + plnk + lnk) & 1) continue;
        State pre(pr, k - 1, plnk, pacc, podd);
        State nxt(
          nr, k, lnk,
          pacc && (lnk != 0),
          podd || infected
        );
        from_to(nxt, pre);
      }
}

int target_l;
int st_state;
pair<int, int> blocks[260];

int len[260];
multiset<int> g[260];

void dfs(State st) {
  int u = st.k, v = st.k % m + 1;
  for (int _ = 0; _ < st.lnk; _ ++) {
    g[u].insert(v);
    g[v].insert(u);
  }

  if (st.k == 1) {
    if (st_state == 0)
      st_state = 1;
    blocks[1] = {target_l, st.r};
    return;
  }

  for (auto pre : from[st.id()]) if (dp[pre.id()][target_l]) {
    blocks[st.k] = {pre.r + 1, st.r};
    if (st.odd && !pre.odd) st_state = st.k;
    dfs(pre);
    return;
  }
}

int main() {
  int _;
  scanf("%d%d", &_, &t);
  while (t --) {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++)
      scanf("%d", &a[i]), a[i + n] = a[i];
    for (int i = 1; i <= 2 * n; i ++)
      pa[i] = pa[i - 1] ^ a[i];
    for (int i = 1; i <= m; i ++)
      scanf("%d", &b[i]);

    if (m == 1) {
      if (pa[n] != b[1]) {
        puts("No");
      } else {
        puts("Yes");
        for (int i = 1; i < n; i ++)
          printf("1 ");
        puts("");
      }
      continue;
    }

    for (int i = 0; i < 24 * n * m; i ++)
      dp[i].reset(), from[i].clear();

    for (int k = 1; k <= m; k ++) {
      for (int bl = k; bl <= (k == 1 ? n : 2 * n); bl ++) {
        // 需要注意,在 k > 1 的时候,只需要选取同情况下最短的区间
        // 如果有多个区间,则长区间会在最后形成
        // 长度为 3 的倍数,而异或和为 0 的“气泡”
        // 气泡可以传递到下一个块,过程不断持续到气泡消失或者回到第一个块
        // 回到第一个块的情况通过在 k = 1 时枚举所有可能的区间处理

        // 第一部分:不同时包含 a[n] 和 a[1]
        int targ = (bl > n) ? 2 * n : n;
        for (int dlen : {0, 1}) {
          for (int br = bl + dlen; br <= targ; br += 3) {
            if (que(bl, br) != b[k]) continue;
            if (bl == br)
              update(k, bl, br, 0, bl == 1 || bl == n + 1);
            else if (dlen == 0) {
              update(k, bl, br, 2, bl == 1 || bl == n + 1);
              if (k != 1) break;
            } else {
              update(k, bl, br, 1, bl == 1 || bl == n + 1);
              if (k != 1) break;
            }
          }
        }

        if (bl <= n) {
          // 第二部分:同时包含 a[n] 和 a[1],并选择先放在左边
          // 这会导致起始状态为状态 k
          // 这一部分的长度最多为 n - m + 1
          // 另外,这里不需要考虑 br 的奇偶性,所以枚举 mod 3 即可
          for (int dlen : {0, 1, 2}) {
            for (int br = n + dlen + 1; br <= bl + n - m; br += 3) {
              if (que(bl, br) != b[k]) continue;
              int lhs = br - n, rhs = n - bl + 1;
              if (rhs & 1) rhs ++, lhs -= 2;
              if (lhs < 0) continue;
              lhs += rhs / 2;
              if (lhs == 1) {
                update(k, bl, br, 0, true);
              } else if (lhs % 3 == 1) {
                update(k, bl, br, 2, true);
                if (k != 1) break;
              } else if (lhs % 3 == 2) {
                update(k, bl, br, 1, true);
                if (k != 1) break;
              }
            }
          }

          // 第三部分:同时包含 a[n] 和 a[1],并选择先放在右边
          // 这会导致起始状态为状态 k + 1
          // 另外,这里需要考虑 br 的奇偶性,所以需要枚举 mod 6
          for (int dlen : {0, 1, 2, 3, 4, 5}) {
            for (int br = n + dlen + 1; br <= bl + n - m; br += 6) {
              if (que(bl, br) != b[k]) continue;
              int lhs = br - n, rhs = n - bl + 1;
              if (lhs & 1) lhs ++, rhs -= 2;
              if (rhs < 0) continue;
              rhs += lhs / 2;
              if (rhs == 1) {
                update(k, bl, br, 0, false);
              } else if (rhs % 3 == 1) {
                update(k, bl, br, 2, false);
                if (k != 1) break;
              } else if (rhs % 3 == 2) {
                update(k, bl, br, 1, false);
                if (k != 1) break;
              }
            }
          }
        }
      }
    }

    st_state = 0;
    for (int i = 1; i <= m; i ++)
      g[i].clear();

    bool flg = false;
    for (int l = 1; l <= n; l ++) {
      for (int lnk : {0, 1, 2}) if (!flg)
        for (bool acc : {false, true}) if (!flg)
          for (bool odd : {false, true}) if (!flg) {
            State p(l + n - 1, m, lnk, acc, odd);
            if (dp[p.id()][l]) {
              flg = true;
              target_l = l;
              dfs(p);
            }
          }

    }

    if (!flg) {
      puts("No");
      continue;
    }
    puts("Yes");

    for (int i = 1; i <= m; i ++)
      len[i] = blocks[i].second - blocks[i].first + 1;

    auto comb_left = [&] () {
      printf("1 1 2 ");
    };
    auto comb_right = [&] () {
      printf("2 2 1 ");
    };

    // 进入起始状态
    for (int i = 1; i <= m; i ++) if (blocks[i].second > n && blocks[i].first <= n) {
      int lhs = blocks[i].second - n;
      int rhs = n - blocks[i].first + 1;
      if (st_state == i) {
        if (rhs & 1)
          rhs ++, lhs -= 2, printf("1 ");
        while (rhs)
          lhs ++, rhs -= 2, printf("2 ");
        len[i] = lhs;
      } else {
        if (lhs & 1)
          lhs ++, rhs -= 2, printf("2 ");
        while (lhs)
          rhs ++, lhs -= 2, printf("1 ");
        len[i] = rhs;
      }
    }

    // 搜索欧拉路径
    int cur_state = st_state;
    bool first = true;

    auto node = [&] (int nw) -> void {
      if (first) {
        first = false;
        return;
      }
      // 这里对 m = 2 的情况进行了隐式的特殊处理
      if (nw == cur_state % m + 1 && len[cur_state] > 1) {
        int x = cur_state;
        // 从左到右
        while (len[x] > 4)
          len[x] -= 3, comb_left();
        if (len[x] == 4)
          len[x] -= 2, printf("1 1 ");
        else
          len[x] --, printf("1 ");
      } else {
        int x = cur_state == 1 ? m : cur_state - 1;
        // 从右到左
        while (len[x] > 4)
          len[x] -= 3, comb_right();
        if (len[x] == 4)
          len[x] -= 2, printf("2 2 ");
        else
          len[x] --, printf("2 ");
      }
      cur_state = nw;
    };

    auto dfs = [&] (auto self, int x) -> void {
      while (!g[x].empty()) {
        int v = *g[x].begin();
        g[x].extract(v);
        g[v].extract(x);
        self(self, v);
      }
      node(x);
    };

    dfs(dfs, 1);
    puts("");
  }
  return 0;
}

::::

2026/3/11 更新

实际上,考虑到这张图上相邻两个点之间的连边不超过两条,为了保证图中存在欧拉回路,不难发现只会出现类似“双边 - 单边 - 双边 - 无边”的类型。在这一情况下,钦定终止状态在靠左的“双边 - 单边”边界,而初始状态在靠右的“单边 - 双边”边界,将序列反转即可处理另一半可能性。

在这一设计上,状态转移的辅助参数只需要一个范围在 \{0, 1, 2, 3\} 内的参数,表示目前所在的段,从起点开始分别为:

转移则需要根据下一个块的长度确定是否需要跳转到下一个段,以及中间的段是否出现退化情况等。这个写法的常数会比上一个写法小一些,而对于构造,在已知终止状态方位的情况下可以进行贪心构造,具体方式请查看下面的代码。

::::info[更新后的代码]

// https://qoj.ac/submission/2121975

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <vector>
#include <bitset>
#include <set>
#include <deque>

using namespace std;

int t, n, m;
int a[510], b[260];
int pa[510];

int que(int l, int r) {
  return pa[r] ^ pa[l - 1];
}

/*
  状态:
  l: 第 1 个块的左端点(bitset)

  i:考虑的块数量
  j: 第 i 个块的右端点
  k: 目前所在的状态
    - 0 = 单边状态,允许的长度有 3k - 1
          这里经过了环上 n 到 1 的部分
    - 1 = 前双边状态,允许的长度有 3k + 1
    - 2 = 无边状态,允许的长度只有 1
    - 3 = 后双边状态,允许的长度有 3k + 1
*/

bitset<260> dp[260][510][4];
vector<pair<int, int>> pre[260][510][4];

int move_l(int l, int r) {
  int lhs = n - l + 1, rhs = r - n;
  // 无法放在一侧
  if (rhs % 3 == lhs % 3) return -1;
  return lhs + rhs / 2 - rhs % 2;
}

int move_r(int l, int r) {
  int lhs = n - l + 1, rhs = r - n;
  // 无法放在一侧
  if (rhs % 3 == lhs % 3) return -1;
  return rhs + lhs / 2 - lhs % 2;
}

inline void transfer(int i, int pj, int pk, int nj, int nk) {
  if (i == 1) {
    dp[i][nj][nk][pj] = 1;
    return;
  }
  dp[i][nj][nk] |= dp[i - 1][pj - 1][pk];
  pre[i][nj][nk].push_back({pj, pk});
}

vector<int> solve() {
  for (int i = 1; i <= n; i ++)
    a[i + n] = a[i];
  for (int i = 1; i <= 2 * n; i ++)
    pa[i] = pa[i - 1] ^ a[i];

  for (int i = 1; i <= m; i ++)
    for (int j = 1; j <= 2 * n; j ++)
      for (int k : {0, 1, 2, 3})
        dp[i][j][k].reset(), pre[i][j][k].clear();

  for (int i = 1; i <= m; i ++) {
    for (int l = 1 + (i != 1); l <= (i == 1 ? n + 1 : 2 * n); l ++) {
      for (int d = 0; d < 3; d ++) {
        for (int r = l + d; r <= (l <= n ? n : 2 * n); r += 3) {
          if (que(l, r) != b[i]) continue;
          int len = r - l + 1;
          if (l <= n) {
            if (len % 3 == 2) {
              transfer(i, l, 0, r, (r == n));
              if (i != 1) break;
            } else if (len > 1)
              break;
          } else {
            if (len == 1) {
              transfer(i, l, 1, r, 2);
              transfer(i, l, 2, r, 2);
            }
            else if (len % 3 == 1) {
              transfer(i, l, 1, r, 1);
              transfer(i, l, 2, r, 3);
              transfer(i, l, 3, r, 3);
              if (i != 1) break;
            } else
              break;
          }
        }
      }

      if (l > n)
        continue;
      for (int d = 0; d < 6; d ++) {
        for (int r = n + 1 + d; r <= l + n - m; r += 6) {
          if (que(l, r) != b[i]) continue;
          int bl = move_l(l, r);
          if (bl == -1)
            break;
          if (bl % 3 == 2) {
            transfer(i, l, 0, r, 1);
            if (i != 1) break;
          } else
            break;
        }
      }

      for (int d = 0; d < 6; d ++) {
        for (int r = n + 1 + d; r <= l + n - m; r += 6) {
          if (que(l, r) != b[i]) continue;
          int br = move_r(l, r);
          if (br == -1)
            break;
          if (br == 1)
            transfer(i, l, 0, r, 2);
          else if (br % 3 == 1) {
            transfer(i, l, 0, r, 1);
            if (i != 1) break;
          } else
            break;
        }
      }
    }
  }

  bool flg = false;
  vector<pair<int, int>> segs;
  for (int l = 1; l <= n + 1 && !flg; l ++) {
    for (int p : {0, 1, 2, 3})
      if (dp[m][l + n - 1][p][l]) {
        flg = true;
        int cur = l + n - 1, cp = p;
        for (int it = m; it >= 2; it --) {
          for (auto [pj, pk] : pre[it][cur][cp]) if (dp[it - 1][pj - 1][pk][l]) {
            segs.push_back({pj, cur});
            cp = pk; cur = pj - 1;
            break;
          }
        }
        segs.push_back({l, cur});
        break;
      }
  }

  if (!flg)
    return {-1};

  reverse(segs.begin(), segs.end());
  vector<int> res;
  vector<int> pre, suf;
  deque<int> dq;
  for (int i = 1; i <= m; i ++) {
    int l = segs[i - 1].first, r = segs[i - 1].second;
    if (r <= n)
      suf.push_back(r - l + 1);
    else if (l > n)
      pre.push_back(r - l + 1);
    else {
      int lhs = n - l + 1, rhs = r - n;
      if (move_r(l, r) >= move_l(l, r)) {
        while (lhs >= 2)
          lhs -= 2, ++ rhs, res.push_back(2);
        if (lhs == 1)
          -- rhs, res.push_back(1), res.push_back(2);
        pre.push_back(rhs);
      } else {
        while (rhs >= 2)
          rhs -= 2, ++ lhs, res.push_back(1);
        if (rhs == 1)
          -- lhs, res.push_back(2), res.push_back(1);
        suf.push_back(lhs);
      }
    }
  }

  for (int ele : pre) dq.push_back(ele);
  for (int ele : suf) dq.push_back(ele);

  auto LEFT = [&] () {
    while (dq.front() != 1) {
      int x = dq.front();
      dq.pop_front();
      while (x > 4)
        x -= 3, res.push_back(1), res.push_back(1), res.push_back(2);
      if (x == 4)
        x = 2, res.push_back(1), res.push_back(1);
      else
        x = 1, res.push_back(1);
      dq.push_back(x);
    }
  };

  auto RIGHT = [&] () {
    while (dq.back() != 1) {
      int x = dq.back();
      dq.pop_back();
      while (x > 4)
        x -= 3, res.push_back(2), res.push_back(2), res.push_back(1);
      if (x == 4)
        x = 2, res.push_back(2), res.push_back(2);
      else
        x = 1, res.push_back(2);
      dq.push_front(x);
    }
  };

  LEFT(); RIGHT(); LEFT(); RIGHT();
  return res;
}

int main() {
  int _;
  scanf("%d%d", &_, &t);
  while (t --) {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++)
      scanf("%d", &a[i]);
    for (int i = 1; i <= m; i ++)
      scanf("%d", &b[i]);

    auto vec = solve();
    if (vec != vector<int>{-1}) {
      puts("Yes");
      for (int ele : vec) printf("%d ", ele);
      puts("");
      continue;
    }

    reverse(a + 1, a + n + 1);
    reverse(b + 1, b + m + 1);
    vec = solve();
    if (vec != vector<int>{-1}) {
      puts("Yes");
      for (int ele : vec) printf("%d ", 3 - ele);
      puts("");
      continue;
    }

    puts("No");
  }
  return 0;
}

::::