B. 细节博弈

知识点:博弈论(对称博弈)、贪心、字符串

预估难度:1200

题解

解题思路

不难发现,如果Alice无法一次将所有的的元素变成A, 那么无论接下来怎么操作,Bob都可以镜像防守,将Alice的操作还原回去,使得Alice操作无效化。

过程实现

  1. 我们可以 遍历一下字符串,找到最左边的字符 B 和最右边的字符 B,记录为
  2. 计算两个 B 的最远距离是否超过 ,如果没超过,说明 Alice 能够一次把字符串变成全部由 A 组成的字符串,Alice 胜利;否则 Bob 胜利。

参考代码

参考代码
#include <bits/stdc++.h>
using namespace std;

int main() {
    int t = 1;
    cin >> t;
    while (t--) {
        int n, k;
        cin >> n >> k;
        string a;
        cin >> a;
        int l = 0, r = 0;
        for (int i = 0; i < a.size(); i++) {
            if (a[i] == 'B') {
                l = i;
                break;
            }
        }
        for (int i = (int)(a.size()) - 1; i >= 0; i--) {
            if (a[i] == 'B') {
                r = i;
                break;
            }
        }
        cout << (r - l + 1 <= k ? "Alice" : "Bob") << '\n';
    }
    return 0;
}

C. 超级蜗牛

知识点:二分查找、模拟

预估难度:1400

题解

题目大意

有一只蜗牛他会按照奇数天数上爬偶数天数下滑的模式在无限高的墙上进行活动,你需要对 次询问进行回答,每次询问给出一个高度 回答需要爬到这个高度的最小天数。

思路

观察题目发现,蜗牛进行的周期性攀爬,所以我们可以用前缀和预处理一个周期,对于每一天记录这一天结束时的所在高度,并额外记录这个周期蜗牛每次刷新最高高度的位置以及此时的天数。接下来对每次询问进行分类讨论:

第一种情况为询问值 不超过一次周期的最高值(注意,最高值不是周期结束时的高度),说明在第一个周期即可达到这个高度 ,我们只需要在上述额外记录状态中简单二分高度对应的最小天数即可。

第二种情况为询问值 大于一次周期的最高值,并且一次周期结束的位置变化量为正数(即一次周期后的高度大于周期前的高度),我们需要先简单算出需要多少个完整周期,设需要完整周期 周期,那么 是满足式子 周期位置变化量 的最大值,简单算出 之后按照第一种情况二分剩余高度所需天数即可。

第三种情况为询问值 大于一次周期的最高值,并且一次周期结束的位置变化量为正数,此时一个周期过后蜗牛高度无法上升即永远我无法达到询问高度,输出

算法即时间复杂度解析

前缀和、二分查找、分类讨论、预处理算法

时间复杂度:前缀和预处理为 时间复杂度,单次查找需要 时间复杂度,整体时间复杂度为

参考代码

参考代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int T;
    cin>>T;
    while (T--){
        int n,m;
        cin>>n>>m;
        vector<ll> a(2*n+1);
        vector<pair<ll,ll>> b;
        for (int i=1;i<=2*n;i++) {
            cin>>a[i];
            a[i]=a[i-1]+a[i]*(i%2==1?1:-1);
            if (b.empty()||b.back().first<a[i]) b.push_back({a[i],i});
        }
        while(m--){
            ll x;
            cin>>x;
            ll y=x-b.back().first;
            ll z=0;
            if (y>0){
                if (a[2*n]<=0){
                    cout<<-1<<'\n';
                    continue;
                }
                ll xx=(y/a[2*n]+(y%a[2*n]!=0));
                x-=xx*a[2*n];
                z+=xx*2*n;
            }
            ll l=0,r=b.size()-1,mid;
            while(l!=r){
                mid=l+r>>1;
                if (b[mid].first>=x) r=mid;
                else l=mid+1;
            }
            cout<<b[l].second+z<<'\n';
        }
    }
}

D. 遥控器

知识点:dfs,预处理,前缀和,组合数学

预估难度:1600

题解

题意

这道题的核心是计算在给定操作下能够产生的不同排列的总数:统计有多少个排列 ,满足其支撑集 构成树上的一条链。

思路

首先,分析操作的本质。在一条祖先-后代路径P上,选择偶数个点并依次进行交换,这个操作等价于对路径P上的点的对应值施加一个由次对换构成的排列。因为我们可以任意选择点(允许重复),并且可以构造任意长度的偶数序列,所以我们可以实现任意次数的对换。这等价于我们可以在路径P上的点集上施加任意的排列(对称群)。

接下来,我们需要计算所有可能的排列总数。一个操作由一对祖先-后代节点确定一条路径。所有可生成的排列,是作用在所有这些路径上的排列的集合的并集。

观察路径的结构:对于任意一个节点和它的一个祖先,路径上的点集是根到的路径上的点集的子集。因此,在上的任意排列都可以看作是上的一个排列(它保持中的元素不变)。这意味着,对于一个固定的节点,所有以为后代节点的路径所能生成的排列集合,它们的并集就等于在最长路径上进行排列所能生成的集合。

因此,问题转化为计算,其中表示在点集上的所有排列构成的集合。

直接计算这个并集的大小很困难,可以转换思路。一个排列是可生成的,当且仅当它所移动的点的集合是某个根路径的子集。一个集合是某个的子集,当且仅当中所有点都在同一条从根出发的路径上,即中任意两点都具有祖先-后代关系。

我们统计所有满足这个条件的排列。总数等于 (不操作) 加上所有非恒等排列的数量。

一个非恒等排列由其移动的点的集合唯一地确定了其贡献(个排列,即个元素的错排数)。

我们可以根据一个集合中深度最大的节点来对所有合法的进行分类计数。每个合法的都有唯一一个深度最大的节点。

对于一个节点,其深度为,它作为最深节点的合法集合必须满足。若,则和从个真祖先中选出的个节点构成。这样的集合有个。

因此,节点的贡献是

。通过一系列组合恒等式和错排的递推公式,可以推导出的递推式:。解得

最终答案为

时间复杂度:预计算阶乘,每组数据建图和计算深度和累加答案。总复杂度为

参考代码

参考代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=1e9+7;
ll A[200005];
int main() {
    int T;
    cin>>T;
    A[0]=1;
    for (int i=1;i<=200000;i++) A[i]=A[i-1]*i%mod;
    while(T--) {
        int n;
        cin>>n;
        vector<vector<ll>> e(n+1);
        vector<ll> h(n+1);
        for (int i=1;i<n;i++){
            ll x,y;
            cin>>x>>y;
            e[x].push_back(y);
            e[y].push_back(x);
        }
        h[1]=1;
        ll ans=0;
        auto dfs = [&](auto dfs,ll x,ll fa,ll y)->void{
            if (h[x]>=2){
                y=(y+A[h[x]]-2*A[h[x]-1]+A[h[x]-2]+3*mod)%mod;
                ans=(ans+y)%mod;
                
            }
            for (auto u:e[x]){
                if (u==fa) continue;
                h[u]=h[x]+1;
                dfs(dfs,u,x,y);
            }
        };
        dfs(dfs,1,0,0);
        cout<<(ans+1)%mod<<(char)(10);
    }
}
</details>

E. 超大无向连通图-1

知识点:单调栈,构造,思维,贪心

预估难度:1800

题解

题目大意

题目需要你在数组 的限制内构造个数组 使得由这两个数组按照:对于任意一对下标 ,若同时满足 ,则在节点 与节点 之间连一条无向边,且这条边的长度为 ,这样的连边方式使得 个结点构成的图是无向连通图并最大化所有边之和。

思路

观察题目,发现边长与数组 有直接关系,而 的取值范围为 ,容易看出我们需要尽可能多利用 的情况,尽可能不利用 的情况。

因为每对下标 都独立影响一条边,所以我们容易想到当 时,将 与所有的 都进行建边来最大程度的利用 做出的贡献即只需要

接下来考虑 的情况,如果存在一个下标 并且 ,此时按照上面的思路就会链接 此时就保证连通性问题了,即这种情况不需要在额外加边;反之情况,因需要保证连通性问题,这些下标一定要至少建立一个边,为了最小化消耗损耗,对于点 去寻找 满足 的最大 比较 选择最大值进行连接即可,另外注意全为负数时根据算法不同可能需要额外讨论。

因题目没有限制 的的大小所以我们只需要考虑上述情况并不需要考虑具体 的值, 的构造方案有无数种。

算法及时间复杂度

贪心,思维,构造,前缀和(单调栈)。在 的时间复杂度完成(即题解思路)。

One More Thing

by the way,本题还有一种神秘写法(李超线段树优化dp),简单说一下。

题目中节点 与左侧 个节点连边,等价于覆盖区间 。图连通的充要条件是:所有节点的覆盖区间并集必须完全覆盖 。我们可以将连通性转化为区间覆盖问题。

为覆盖后缀区间 所需的最小代价。当前起点是 ,我们必须选择一个右侧的节点 (),让它向左延伸覆盖到 。为了覆盖 ,节点 需要延伸长度 ,产生的代价为 ,剩下的后缀 则由子问题 解决。

由于这是一个经典的 动态规划问题,且转移方程符合直线形式,我们可以使用 李超线段树 (Li Chao Tree) 来维护这些直线,并在 时间内查询最小 值。

参考代码

<details style="border: 1px solid #ddd; border-radius: 8px; padding: 5px 12px; margin: 3px 0; width: 100%; box-sizing: border-box;">
参考代码
#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
int main() {
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        vector<ll> a(n+1);
        int f=0;
        for (int i=1;i<=n;i++){
            cin>>a[i];
            if (a[i]>=0) f=i;
        }
        ll ans=0;
        for (int i=1;i<=n;i++){
            if (a[i]>=0) ans+=a[i]*(i-1);
        }
        vector<ll> stk;
        for (int i=f+1;i<=n;i++){
            while(!stk.empty()&&a[stk.back()]<=a[i]) stk.pop_back();
            stk.push_back(i);
        }
        for (int i=1;i<stk.size();i++){
            ans+=a[stk[i]]*(stk[i]-stk[i-1]);
        }
        if (!stk.empty())ans+=a[stk[0]]*(stk[0]-(f+(f==0)));
        cout<<ans<<'\n';
    }
}

F. 小s的逆序对

知识点:线段树,扫描线,推公式

预估难度:2200

题解

题意

给定数组 ,你最多进行一次操作:选一个位置 ,把 取出后插入到任意位置(可到首尾),其余元素相对顺序不变;定义逆序对为满足 的有序对数量(相等不计),求最小可能的逆序对数量。

思路

记原逆序对为 。移动某个位置 的元素时,除该元素外,其余元素相对顺序不变,所以它们之间的逆序对不变;变化只来自该元素与其它元素的配对。

为该元素在原数组中参与的逆序对数,即“左边比它大的个数”加“右边比它小的个数”,则把它移走会让答案变为 ,其中 是它插入到某个位置后与其它元素产生的新逆序对数;因此只要对每个 求最小 ,取最小增量 ,答案为

用坐标压缩后两次树状数组即可在 求出所有 以及

关键是如何对某个值 快速求“插入任意位置的最小新贡献”。若把该元素插入在前缀长度为 的位置之后,则 。 把右边那项改写为全局小于 的数量 减去前缀中小于 的数量,可得 , 其中 。 所以

代码按 从小到大扫描,用线段树维护整条 (对后缀区间做加法、查询全局最小)。从 变到 时,只需要把值为 的出现位置对应的后缀区间各减 1,即可把 更新成 ,从而得到每个 ,进而得到每个值的最优

最后枚举位置 ,取 的最小值即可。

复杂度

坐标压缩 ,两次树状数组 ,线段树按值扫描总计 ,总体

参考代码

参考代码
#include <bits/stdc++.h>
using namespace std;

int a[200000];
map<int, int> mp;
vector<int> v[200000];
int tre[200001], N;

int lowbit(int x) {
    return x - (x & (x - 1));
}
void add(int p, int vl) {
    for (; p <= N; p += lowbit(p)) tre[p] += vl;
}
int qsum(int p) {
    int vl = 0;
    for (; p > 0; p -= lowbit(p)) vl += tre[p];
    return vl;
}
void clr() {
    int i;
    for (i = 0; i <= N; i++) tre[i] = 0;
}
int vmx[1 << 19], lazy[1 << 19];
void gen(int t, int dep) {
    vmx[t] = 0;
    lazy[t] = 0;
    if (dep == 0) return;
    gen(t * 2 + 1, dep - 1);
    gen(t * 2 + 2, dep - 1);
}
void pushdown(int t) {
    int l = t * 2 + 1, r = t * 2 + 2;
    vmx[l] += lazy[t];
    lazy[l] += lazy[t];
    vmx[r] += lazy[t];
    lazy[r] += lazy[t];
    lazy[t] = 0;
}
void add(int t, int dep, int l, int r, int vl) {
    if (l == 0 && r == (1 << dep)) {
        vmx[t] += vl;
        lazy[t] += vl;
        return;
    }
    pushdown(t);
    int x = (1 << (dep - 1));
    if (l < x) {
        if (r <= x)
            add(t * 2 + 1, dep - 1, l, r, vl);
        else {
            add(t * 2 + 1, dep - 1, l, x, vl);
            add(t * 2 + 2, dep - 1, 0, r - x, vl);
        }
    } else
        add(t * 2 + 2, dep - 1, l - x, r - x, vl);
    vmx[t] = max(vmx[t * 2 + 1], vmx[t * 2 + 2]);
}
int qmax(int t, int dep, int l, int r) {
    if (l == 0 && r == (1 << dep)) return vmx[t];
    pushdown(t);
    int x = (1 << (dep - 1));
    if (l < x) {
        if (r <= x) return qmax(t * 2 + 1, dep - 1, l, r);
        return max(qmax(t * 2 + 1, dep - 1, l, x), qmax(t * 2 + 2, dep - 1, 0, r - x));
    }
    return qmax(t * 2 + 2, dep - 1, l - x, r - x);
}
int main() {
    ios::sync_with_stdio(false), cin.tie(0);
    int T, n, i, j, c, tdep, ans1;
    map<int, int>::iterator it;
    long long ans0;
    for (cin >> T; T > 0; T--) {
        cin >> n;
        for (i = 0; i < n; i++) {
            cin >> a[i];
            mp[a[i]] = 0;
        }
        c = 0;
        for (it = mp.begin(); it != mp.end(); it++) {
            (*it).second = c;
            c++;
        }
        for (i = 0; i < n; i++) a[i] = mp[a[i]];
        mp.clear();
        N = c;
        ans0 = 0;
        for (i = n - 1; i > -1; i--) {
            ans0 += qsum(a[i]);
            add(a[i] + 1, 1);
        }
        clr();
        for (tdep = 0; (1 << tdep) < n + 1; tdep++);
        gen(0, tdep);
        for (i = 0; i < n; i++) add(0, tdep, i + 1, n + 1, -1);
        for (i = 0; i < n; i++) v[a[i]].push_back(i);
        ans1 = 0;
        for (i = 0; i < c; i++) {
            for (j = 0; j < v[i].size(); j++) add(0, tdep, v[i][j] + 1, n + 1, 1);
            for (j = 0; j < v[i].size(); j++) {
                ans1 = max(ans1, qmax(0, tdep, v[i][j] + 1, n + 1) - qmax(0, tdep, v[i][j] + 1, v[i][j] + 2));
                ans1 = max(ans1, qmax(0, tdep, 0, v[i][j] + 1) - qmax(0, tdep, v[i][j], v[i][j] + 1));
            }
            for (j = 0; j < v[i].size(); j++) add(0, tdep, v[i][j] + 1, n + 1, 1);
        }
        cout << ans0 - ans1 << '\n';
        for (i = 0; i < c; i++) v[i].clear();
    }
    return 0;
}

G. Block Game2.0

知识点:构造、贪心、模拟

预估难度:2500

题解

题意

给定 列的矩阵,和一个万能数字 ,每次操作选择一行,把 插入这一行的头,同时所有数字后移,最后一列的数字被挤出来成为新的

构造一个不超过 的操作序列,最大化操作结束后矩阵第一列上两个数字 的值。(不必最小化操作次数。)

思路

贪心地,我们发现因为 次操作之内就能让一行矩阵的所有数字都充当一次头部元素,而两行矩阵也不过 次操作,题目给了 次操作。因此我们不妨大胆假设:这些操作必然能使得 个数字中的前三大位于我们要求的位置。

因此接下来我们尝试来构造这样一种方案。

首先考虑,整个过程需要从头插入元素和从尾部删除元素,因此我们可以用两个 来维护这个矩阵的两行。

方便起见,我们不妨先将这前三大标记为 ,其余数字均标记为 。这样一来,问题转化成了构造这个操作序列,使得最终矩阵第一列的两个数字都是 ,且

接下来,下文中的矩阵变成了 这两个 维护的两个 序列。其中 分别代表第一行和第二行。

接下来,为了方便分类讨论,我们不妨令这三个 都在矩阵中,也就是令 ,即如果 是前三大中的值,我们就预先操作几次让 不在前三大中。(这一步,在下面的代码中直接先操作令 成为了所有数字中的最小值,这样在 矩阵的视角下,就必然满足 了。)

进行完这一步后,我们就可以按第一行中的 分类讨论,但在此之前我们为了简化代码,可以先钦定 中的 1 比 更多,这里只需要在 更少时将 进行交换,同时把标记是否交换过的 置为

那么此时 中的 个数必然不小于 。(因为此时 是所有数的 ,因此必然 ):

接下来,我们先不着急分类讨论,先考虑这样一件事:

我们对一行考虑,什么时候一次操作能使得操作结束后,这一行的第一个数字和 都等于 ,我们会发现实际上当且仅当在这行数字中有两个相邻的

因为实际上只考虑一行数字和 的话,实际上操作相当于将这个长度为 的序列**( 本质上就是第 个数字)**循环右移,而 和第一个数字在这个循环中是相邻的位置关系。

有了这个发现,我们就可以考虑无脑构造,使得在某行中存在两个相邻的 ,这样一来,后面对这一行做不超过 次操作,就能使得这一行满足条件。

而此时我们发现,我们已经钦定了 中的 更多,而显然 中的元素我们并不方便去修改顺序,因此我们可以暴力地对 操作,碰到 就塞入 ,而碰到 就塞入 ,直至给 中塞入两个 ,则此时这两个 必然位于 的前两个位置。

则此时,就只有三种比较简单的情况了:

三个 全在 中。

两个 中,而第三个 上。

两个 中,而第三个 中。

此时我们会发现,无论是哪种情况,我们总能转化成第二种情况。

如果全在 中,则我们就无脑对 操作,直到碰到最靠后的那个 ,此时这个 就被放在了 中。

如果有一个 中,则我们就无脑对 操作,直到碰到那个 ,此时这个 也被放入了

(不难发现,上面这一步不超过 次操作就能使得 ,同时另外两个 恰好位于 中的某两个相邻的位置。)

接下来,局面已经变成了:,且 中存在两个相邻的

那么问题几乎已经解决了,我们直接把这个 塞入 中,此时 就符合条件了(仅操作一次。)

接着再对 进行无脑地操作,直至 ,此时由于原先 中的两个 处于相邻的位置,因此第二个 必然位于 的末尾,因此再进行一次操作,就可以直接让: 的头部元素为 ,同时

到此我们就构造了一个合法的解,我们来计算一下操作次数。

首先我们调整了 ,使得 取到了全局的 。(这一步不超过 次。)

接着我们将 中的两个 塞入了 中相邻的位置。(这一步不超过 次。)

接着我们进行了分类讨论,这里最坏的一种类是,我们需要把 中剩下的那个 调整到 里,(这一步最多 次操作。)

最后,我们首先进行了一次操作把 摁进了 中,让 合法(这一步 次。)

接着我们无脑地对 操作,将 中的那两个相邻的 调整成合法的位置。(这一步不超过 次。)

因此我们只需要最多 次操作,必然可以达到这个最优的局面,而题目给了 次,显然是够用的。

Bonus:实际上,极限的操作次数在 次。

细节见代码:(当然,最后别忘记判断 ,要把 交换回去,相当于把最后那一部分的操作数的 互换。

参考代码

参考代码
#include <bits/stdc++.h>
using namespace std;

struct Node {
    int val, r, c;

    bool operator>(const Node& n) const {
        return val > n.val;
    }
};

void solve() {
    int n, k;
    cin >> n >> k;
    deque<int> A, B;
    int mx = -1e9;
    int mn1 = 1e9, mn2 = 1e9;
    int c1 = 0, c2 = 0;
    for (int i = 1, x; i <= n; i++) {
        cin >> x;
        mx = max(mx, x);
        if (x <= mn1) {
            mn1 = x;
            c1 = n - i + 1;
        }
        A.emplace_back(x);
    }
    for (int i = 1, x; i <= n; i++) {
        cin >> x;
        mx = max(mx, x);
        if (x <= mn2) {
            mn2 = x;
            c2 = n - i + 1;
        }
        B.emplace_back(x);
    }
    if (n == 1) {
        cout << 0 << endl;
        return ;
    }

    string pre; // 把 k 调到最小值
    if (mn1 < mn2) { // A 的 min 更小
        while (c1--) {
            pre += '1';
            A.emplace_front(k);
            k = A.back();
            A.pop_back();
        }
    } else {
        while (c2--) {
            pre += '2';
            B.emplace_front(k);
            k = B.back();
            B.pop_back();
        }
    }

    vector<Node> v;
    for (int i = 0; i < n; i++) v.push_back({A[i], 1, i});
    for (int i = 0; i < n; i++) v.push_back({B[i], 2, i});
    sort(v.begin(), v.end(), greater<>()); // 前三大标记成1,其余0
    k = 0;
    for (int i = 0; i < 3; i++) {
        auto [val, r, c] = v[i];
        if (r == 1) {
            A[c] = 1;
        } else {
            B[c] = 1;
        }
    }
    for (int i = 3; i < n * 2; i++) {
        auto [val, r, c] = v[i];
        if (r == 1) {
            A[c] = 0;
        } else {
            B[c] = 0;
        }
    }
    int mx_cnt1 = (v[0].r == 1) + (v[1].r == 1) + (v[2].r ==
                  1); // 第一行的最大值个数

    bool change = 0;
    if (mx_cnt1 <= 1) { // 让A的更多
        swap(A, B);
        change = 1;
    }

    // 把A的两个大的扔到B里
    string ans;
    int cnt1 = 0;
    while (cnt1 < 2) {
        if (k == 1) {
            B.emplace_front(k);
            k = B.back();
            B.pop_back();
            ans += '2';
            cnt1++;
        } else {
            A.emplace_front(k);
            k = A.back();
            A.pop_back();
            ans += '1';
        }
    }
    int haveA = -1;
    if (k == 1) {
        haveA = 1;
    }
    for (int i = 0; i < n; i++) {
        if (A[i] == 1) {
            haveA = n - i + 1;
            break;
        }
    }
    if (haveA == -1) { // 0 3
        while (k != 1) { // 把B里多的那最后一个1拿出来给k
            B.emplace_front(k);
            k = B.back();
            B.pop_back();
            ans += '2';
        }
        haveA = 1;
    }
    while (haveA--) {
        A.emplace_front(k);
        k = A.back();
        A.pop_back();
        ans += '1';
    }
    while (!(B.front() == 1 && k == 1)) {
        B.emplace_front(k);
        k = B.back();
        B.pop_back();
        ans += '2';
    }

    if (change) {
        for (auto& c : ans) {
            if (c == '1') {
                c++;
            } else {
                c--;
            }
        }
    }
    pre += ans;
    cout << pre.size() << '\n' << pre << '\n';
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int _ = 1;
    cin >> _;
    while (_ --) {
        solve();
    }
    return 0;
}#include <bits/stdc++.h>
using namespace std;

struct Node {
    int val, r, c;

    bool operator>(const Node& n) const {
        return val > n.val;
    }
};

void solve() {
    int n, k;
    cin >> n >> k;
    deque<int> A, B;
    int mx = -1e9;
    int mn1 = 1e9, mn2 = 1e9;
    int c1 = 0, c2 = 0;
    for (int i = 1, x; i <= n; i++) {
        cin >> x;
        mx = max(mx, x);
        if (x <= mn1) {
            mn1 = x;
            c1 = n - i + 1;
        }
        A.emplace_back(x);
    }
    for (int i = 1, x; i <= n; i++) {
        cin >> x;
        mx = max(mx, x);
        if (x <= mn2) {
            mn2 = x;
            c2 = n - i + 1;
        }
        B.emplace_back(x);
    }
    if (n == 1) {
        cout << 0 << endl;
        return ;
    }

    string pre; // 把 k 调到最小值
    if (mn1 < mn2) { // A 的 min 更小
        while (c1--) {
            pre += '1';
            A.emplace_front(k);
            k = A.back();
            A.pop_back();
        }
    } else {
        while (c2--) {
            pre += '2';
            B.emplace_front(k);
            k = B.back();
            B.pop_back();
        }
    }

    vector<Node> v;
    for (int i = 0; i < n; i++) v.push_back({A[i], 1, i});
    for (int i = 0; i < n; i++) v.push_back({B[i], 2, i});
    sort(v.begin(), v.end(), greater<>()); // 前三大标记成1,其余0
    k = 0;
    for (int i = 0; i < 3; i++) {
        auto [val, r, c] = v[i];
        if (r == 1) {
            A[c] = 1;
        } else {
            B[c] = 1;
        }
    }
    for (int i = 3; i < n * 2; i++) {
        auto [val, r, c] = v[i];
        if (r == 1) {
            A[c] = 0;
        } else {
            B[c] = 0;
        }
    }
    int mx_cnt1 = (v[0].r == 1) + (v[1].r == 1) + (v[2].r ==
                  1); // 第一行的最大值个数

    bool change = 0;
    if (mx_cnt1 <= 1) { // 让A的更多
        swap(A, B);
        change = 1;
    }

    // 把A的两个大的扔到B里
    string ans;
    int cnt1 = 0;
    while (cnt1 < 2) {
        if (k == 1) {
            B.emplace_front(k);
            k = B.back();
            B.pop_back();
            ans += '2';
            cnt1++;
        } else {
            A.emplace_front(k);
            k = A.back();
            A.pop_back();
            ans += '1';
        }
    }
    int haveA = -1;
    if (k == 1) {
        haveA = 1;
    }
    for (int i = 0; i < n; i++) {
        if (A[i] == 1) {
            haveA = n - i + 1;
            break;
        }
    }
    if (haveA == -1) { // 0 3
        while (k != 1) { // 把B里多的那最后一个1拿出来给k
            B.emplace_front(k);
            k = B.back();
            B.pop_back();
            ans += '2';
        }
        haveA = 1;
    }
    while (haveA--) {
        A.emplace_front(k);
        k = A.back();
        A.pop_back();
        ans += '1';
    }
    while (!(B.front() == 1 && k == 1)) {
        B.emplace_front(k);
        k = B.back();
        B.pop_back();
        ans += '2';
    }

    if (change) {
        for (auto& c : ans) {
            if (c == '1') {
                c++;
            } else {
                c--;
            }
        }
    }
    pre += ans;
    cout << pre.size() << '\n' << pre << '\n';
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int _ = 1;
    cin >> _;
    while (_ --) {
        solve();
    }
    return 0;
}