WC2022 Solution Set
\(~~~~\) 蹭到了题单,所以随便挑点喜欢的题写写。
Day1 直觉与证明
\(~~~~\) 所以我也不知道从第三部分的几何开始它讲的与Topic有什么关系
\(~~~~\) 所以说白了这个部分就是杂题选讲套了个直觉的壳子是吧
Game Relics
\(~~~~\) 有 \(n\) 种物品,定向购买 \(i\) 的价格是 \(c_i\),也可以花 \(x\) 块钱从所有 \(n\) 种物品种随机一个。如果随机到一个 新物品,则直接得到;如果随机到一个已经有的 物品,则返还 \(\frac{x}{2}\) 块钱。
\(~~~~\) 求最优策略下集齐所有物品至少要多少钱。\(~~~~\) \(1\leq n\leq 100,1\leq x\leq c_i\leq 10^4,\sum c_i\leq 10^4\).
\(~~~~\) 从直觉来看,如果某一刻抽卡是最优策略,那么抽卡将会一直持续到抽出新东西为止。因为没抽出新东西时你并没有改变当前已有的东西,那么继续抽一定还是最优策略。抽卡停不下来的原因找到了!
\(~~~~\) 因此,我们记当前有 \(i\) 个物品,那么期望还要 \(\dfrac{n}{n-i}\) 次抽出新东西,故抽卡抽出新东西的价格为 \((\dfrac{n}{n-i}-1)\times \dfrac{x}{2}+x=(\dfrac{n}{n-i}+1)\times \dfrac{x}{2}\)。
\(~~~~\) 注意到这个东西是随 \(i\) 单调递增的,直觉来看也就是越到后面越可能定向购买,所以最优策略是抽卡,然后开始买剩下的。
\(~~~~\) 所以记 \(dp_{i,j}\) 表示,现在剩 \(i\) 张牌,其价格之和为 \(j\) 的概率,这可以通过组合数的递推转移。然后对每种情况,计算买来和抽来的平均价格更小者作为贡献即可。
查看代码
#include <cstdio>
#include <algorithm>
using namespace std;
#define db double
db f[105][10005];
int C[105],Sum;
int main() {
int n;db x;
scanf("%d %lf",&n,&x);x/=2;
for(int i=1;i<=n;i++) scanf("%d",&C[i]),Sum+=C[i];
f[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=i;j>=1;j--)
for(int k=C[i];k<=Sum;k++) f[j][k]=f[j][k]+f[j-1][k-C[i]]*j/(n-j+1);
db Ans=0;
for(int i=1;i<=n;i++)
{
for(int j=0;j<=Sum;j++) Ans+=f[i][j]*min((1.0*n/i+1)*x,1.0*j/i);
}
printf("%.12f",Ans);
return 0;
}
Swap Space
\(~~~~\) 有若干个装满的容器 \(a\),每个容器需要进行升级改造,之后会获得新的容量 \(b\)(不一定会变大),为了使所有容器都被升级且所有东西都能被装下(不一定装回原容器),至少需要多少的额外容量。
\(~~~~\) 直觉上应该先升级那些会变大的容器,那么这些容器内部的升级顺序呢?不难发现先升级 \(a\) 较小的,这样升级后多出来的空间说不定能顺便装下下一个容器里面的东西,不需要额外容量。同理,对于升级后会变小的容器,应该先升级 \(b\) 较大的,这样才能尽可能缓解先升级的容器对已经有的额外空间的占用。
\(~~~~\) Sum-Up:关键在于对物品性质进行分类。
查看代码
#include <cstdio>
#include <vector>
#include <algorithm>
#define ll long long
using namespace std;
struct Drive{
int a,b;
Drive(){}
Drive(int A,int B){a=A,b=B;}
}D1[1000005],D2[1000005];
bool cmp1(Drive x,Drive y){return x.a<y.a;}
bool cmp2(Drive x,Drive y){return x.b>y.b;}
int main() {
int n,cnt1=0,cnt2=0;
scanf("%d",&n);
for(int i=1,a,b;i<=n;i++)
{
scanf("%d %d",&a,&b);
if(a<=b) D1[++cnt1]=Drive(a,b);
else D2[++cnt2]=Drive(a,b);
}
sort(D1+1,D1+1+cnt1,cmp1);
sort(D2+1,D2+1+cnt2,cmp2);
ll Ans=0,Rest=0;
for(int i=1;i<=cnt1;i++)
{
Rest-=D1[i].a;
if(Rest<0) Ans-=Rest,Rest=0;
Rest+=D1[i].b;
}
for(int i=1;i<=cnt2;i++)
{
Rest-=D2[i].a;
if(Rest<0) Ans-=Rest,Rest=0;
Rest+=D2[i].b;
}
printf("%lld",Ans);
return 0;
}
Is It Rated?
\(~~~~\) 做判断题,你只知道每个人这道题的答案并被要求作答,作答完成后告诉你这道题的答案,要求错误次数不超过错得最少的人的 \(1.3\) 倍加 \(100\) 道。
\(~~~~\) 直觉题 × 乱搞题 √
\(~~~~\) 直觉上跟票当前错得最少的人即可。但如果有两个人都错得最少,且一道对一道错,你就会跟票导致50%的概率对一道。不太妙。
\(~~~~\) 那我们加个权,对于错了 \(x\) 道题的人给他这次投的票加个权,考虑尽可能让错得少的人的权值更高,不妨用 \(a^x\) (\(0<a\leq 1\)),然后再把权值对应成概率随机即可。
\(~~~~\) 至于为什么它是对的,我!不!知!道!
查看代码
#include <bits/stdc++.h>
using namespace std;
double qpow(double a,int b)
{
double ret=1;
while(b)
{
if(b&1) ret=ret*a;
b>>=1;a=a*a;
}
return ret;
}
int Wrong[100005];
char Ans[5],Out[1005];
int main() {
srand(time(NULL));
default_random_engine e(rand());
uniform_real_distribution<double> range(0,1);
int n,m;
scanf("%d %d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%s",Out+1);
double One=0,Zero=0;
for(int j=1;j<=n;j++)
{
if(Out[j]=='1') One+=qpow(0.7,Wrong[j]);
if(Out[j]=='0') Zero+=qpow(0.7,Wrong[j]);
}
printf("%d\n",range(e)<=One/(One+Zero));fflush(stdout);
scanf("%s",Ans+1);
for(int j=1;j<=n;j++) if(Out[j]!=Ans[1]) Wrong[j]++;
}
return 0;
}
Date Pickup
\(~~~~\) \(n\) 个点 \(m\) 条边的有向图,初始时在 \(1\) 号点,每一刻必须在图上走。在 \(a\sim b\) 分钟时会给出信号要求往 \(n\) 号结点走,求在任何时候从给出信号到到达 \(n\) 号结点的最小时间。
\(~~~~\) 它与直觉和证明有什么关系呢?小编也不知道。
\(~~~~\) 求出 \(1\) 号结点到所有点的最短路和所有结点到 \(n\) 号结点的最短路。
\(~~~~\) 显然直接求解不好求,故考虑转化为判定问题:在 \(k\) 分钟内能不能到。
\(~~~~\) 那么所有给信号的时候可以在的点 \(u\) 一定有 \(dis_{u,n}\leq k\)
\(~~~~\) 然后,所有第一时间可以去的点应有 \(dis_{1,u}+dis_{u,n}\leq a+k\) ,同时,如果从这样的点向旁边走一个点仍能赶到,也就是 \(len_{u,v}+dis_{v,n}\leq k\) ,那么这样的边是可以走的,以此类推,我们可以得到一张可以游走的子图。
\(~~~~\) 最后,我们只需要判断这张子图能不能让你撑到 \(b\) 时刻即可。具体来说,如果有环就在环里面绕,否则 \(\texttt{dp}\) 求最长路找到能不能撑这么久即可。
查看代码
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
#define ll long long
#define PII pair<ll,ll>
#define mp(a,b) make_pair(a,b)
using namespace std;
struct cmp{
bool operator()(const PII x,const PII y){return x.second>y.second;}
};
priority_queue<PII,vector<PII>,cmp>Q;
ll a,b,n,m;
bool vis[100005];
ll dis[3][100005];
vector < PII > G[3][100005];
void Dij(ll op)
{
op==1?dis[op][1]=0:dis[op][n]=0; memset(vis,0,sizeof(vis));
if(op==1) Q.push(mp(1,dis[op][1]));
else Q.push(mp(n,dis[op][n]));
while(!Q.empty())
{
ll u=Q.top().first;Q.pop();
if(vis[u]) continue; vis[u]=true;
for(ll i=0;i<G[op][u].size();i++)
{
ll v=G[op][u][i].first,w=G[op][u][i].second;
if(dis[op][v]>dis[op][u]+w)
{
dis[op][v]=dis[op][u]+w;
Q.push(mp(v,dis[op][v]));
}
}
}
}
vector < PII > Sub[100005];
bool InSub[100005];
ll deg[100005];
void BFS(ll Limit)
{
queue<ll>q;
memset(deg,0,sizeof(deg)); memset(vis,0,sizeof(vis));
for(ll i=1;i<=n;i++)
{
Sub[i].clear();
if(InSub[i]&&dis[1][i]+dis[2][i]<=a+Limit) q.push(i),vis[i]=true;
}
while(!q.empty())
{
ll u=q.front();q.pop();
for(ll i=0;i<G[1][u].size();i++)
{
ll v=G[1][u][i].first,w=G[1][u][i].second;
if(w+dis[2][v]<=Limit)
{
if(!vis[v]) q.push(v),vis[v]=true;
Sub[u].push_back(G[1][u][i]);
deg[v]++;
}
}
}
}
ll dp[100005];
bool Check(ll Limit)
{
memset(dp,0xbf,sizeof(dp));
queue<ll>q;
for(ll i=1;i<=n;i++) if(vis[i]&&!deg[i]) q.push(i);
while(!q.empty())
{
ll u=q.front();q.pop();
if(dis[1][u]+dis[2][u]<=a+Limit) dp[u]=max(dp[u],a+Limit-dis[2][u]);
if(dp[u]>=b) return true;
for(ll i=0;i<Sub[u].size();i++)
{
ll v=Sub[u][i].first,w=Sub[u][i].second;
dp[v]=max(dp[v],dp[u]+w);
if(!(--deg[v])) q.push(v);
}
}
for(ll i=1;i<=n;i++) if(vis[i]&°[i]) return true;
return false;
}
bool check(ll k)
{
memset(InSub,0,sizeof(InSub));
for(ll i=1;i<=n;i++) InSub[i]=(dis[2][i]<=k);
if(InSub[1]) return true;
BFS(k);
return Check(k);
}
int main() {
memset(dis,127,sizeof(dis));
scanf("%lld %lld %lld %lld",&a,&b,&n,&m);
for(ll i=1,u,v,w;i<=m;i++)
{
scanf("%lld %lld %lld",&u,&v,&w);
G[1][u].push_back(mp(v,w));
G[2][v].push_back(mp(u,w));
}
Dij(1); Dij(2);
ll l=0,r=dis[1][n],mid,Ans;
while(l<=r)
{
mid=(l+r)>>1;
if(check(mid)) r=mid-1,Ans=mid;
else l=mid+1;
}
printf("%lld",Ans);
return 0;
}
Day2 数据结构题目选讲
新年的聚会
\(~~~~\) 交互题。 有一张 \(n\) 个点 \(m\) 条边的图,你可以给定一个点集,询问该点集内的点之间有没有边。 你需要猜出这张图。
\(~~~~\) \(1 \leq n \leq 1000, 1 \leq m \leq 2000\)。询问次数在 \(50000\) 次以内,点集大小和在 \(10^6\) 以内。
\(~~~~\) 一个很神奇的的结论:对于有 \(m\) 条边的图,可以将它分为 \(\mathcal{O(\sqrt m)}\) 个独立集,至于为什么,我!不!知!道!
\(~~~~\) 所以我们先问 \(\mathcal{O(n\sqrt{m})}\) 次找到所有独立集,然后对于两个独立集之间如果有边,就把较大的独立集裂开成一半分治找边。
查看代码
#include <cstdio>
#include <vector>
#include <algorithm>
#include "meeting.h"
#define PII pair<int,int>
#define mp(a,b) make_pair(a,b)
using namespace std;
bool meeting(vector<int> set);
bool Check1(int p,vector <int> S)
{
S.push_back(p);
return !meeting(S);
}
bool Check2(vector <int> S1,vector <int> S2)
{
for(int i=0;i<S1.size();i++) S2.push_back(S1[i]);
return !meeting(S2);
}
int cnt;
vector <PII> Ans;
vector <int> S[1005];
void Solve(vector <int> A,vector <int> B)
{
if(A.size()==1&&B.size()==1){Ans.push_back(mp(A[0],B[0]));return;}
if(A.size()<B.size()) swap(A,B);
vector <int> C,D;
for(int i=0;i<A.size()/2;i++) C.push_back(A[i]);
for(int i=A.size()/2;i<A.size();i++) D.push_back(A[i]);
if(Check2(C,B)) Solve(C,B);
if(Check2(D,B)) Solve(D,B);
}
vector<PII> solve(int n)
{
for(int i=0;i<n;i++)
{
int Belong=0;
for(int j=1;j<=cnt;j++)
{
if(!Check1(i,S[j])) {Belong=j;break;}
}
if(!Belong) S[++cnt].push_back(i);
else S[Belong].push_back(i);
}
for(int i=1;i<=cnt;i++)
{
for(int j=i+1;j<=cnt;j++)
{
if(Check2(S[i],S[j])) Solve(S[i],S[j]);
}
}
return Ans;
}
赶路
\(~~~~\) 给平面上 \(n\) 个点,没有三点共线。 指定起点终点,找到一条不自交的路径。
\(~~~~\) \(1\leq n\leq 500\).
\(~~~~\) ¥老师一句话题解太难懂了吧。
\(~~~~\) 定义 \(solve(s,t,S)\) 表示走集合 \(S\) 内的点,以 \(s\) 为起点,以 \(t\) 为终点。
\(~~~~\) 不妨设 \(u\) 为一个中转点,那么相对 \(s\) 到 \(u\) 来说在同一侧的点一定在 \(u\) 之前走,否则在 \(v\) 之后走。
\(~~~~\) 于是,你做完了……
\(~~~~\) 说不定跟¥老师的一样难懂
查看代码
#include <bits/stdc++.h>
using namespace std;
#define db double
#define ll long long
struct node{
int x,y;
}P[505];
vector<int> Ans;
int check(int a,int b,int c)
{
ll K=1ll*(P[b].x-P[a].x)*(P[c].y-P[a].y)-1ll*(P[c].x-P[a].x)*(P[b].y-P[a].y);
if(K>0) return 1;return 0;
}
void Solve(int s,int t,vector <int> V)
{
if(V.empty()) return;
vector<int> a[2];
for(int i=1;i<V.size();i++) a[check(s,V[0],V[i])].push_back(V[i]);
int K=check(s,V[0],t);
Solve(s,V[0],a[K^1]),printf("%d ",V[0]),Solve(V[0],t,a[K]);
}
int main() {
int T,n;scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d %d",&P[i].x,&P[i].y);
Ans.clear();printf("1 ");
for(int i=2;i<n;i++) Ans.push_back(i);
Solve(1,n,Ans);printf("%d\n",n);
}
return 0;
}
Day 3杂题选讲
\(~~~~\) 邓老师选择了弹幕最多的讲法。
Od deski do deski
\(~~~~\) Link
\(~~~~\) 有 \(n\) 棵树,每棵树有可能是 \(m\) 种之一。 小 C 每天可以选择连续的一段树砍掉:要求这一段的长度至少是 \(2\),并且第一棵与最后一棵种类相同。 问有多少种初始局面,使得存在一种方法通过若干天将树砍光。 对 \(10^9+7\) 取模。
\(~~~~\) \(n \leq 3000, m \leq 10^9\).
\(~~~~\) 考虑我们会不会判断一个序列有没有解,那肯定有 \(n^2\) 的dp:\(dp_i\) 表示前 \(i\) 个数是否合法,那么找一个位置 \(j\) ,其颜色和当前位置 \(i\) 一样,且 \(dp_{j-1}\) 为 \(\text{true}.\)
\(~~~~\) 换到这道题,那我们需要记一下当前位置是否合法,然后意识到转移与当前可放元素的大小有关,于是把大小放进dp里面,那就是正常的dp了。
查看代码
#include <cstdio>
#include <algorithm>
using namespace std;
int f[3005][2];
const int MOD=1e9+7;
int main() {
int n,m;
scanf("%d %d",&n,&m);
f[0][0]=1;
for(int i=1;i<=n;i++)
{
int Up=min(i,m);
for(int j=Up;j>=0;j--)
{
f[j+1][1]=(1ll*f[j+1][1]+1ll*f[j][0]*(m-j)%MOD)%MOD;
f[j][0]=(1ll*(1ll*f[j][0]+f[j][1])%MOD)*j%MOD;
f[j][1]=1ll*f[j][1]*(m-j)%MOD;
}
}
int Ans=0;
for(int i=0;i<=n;i++) Ans=(1ll*Ans+f[i][0])%MOD;
printf("%d",Ans);
return 0;
}
Robbery
\(~~~~\) 有 \(n\) 种物品,第 \(i\) 种质量为 \(i\),价格为 \(a_i\),每种物品的数量无限。 给定 \(k, w\),选择 \(k\) 个物品,满足质量总和为 \(w\),价格之和最大。
\(~~~~\) \(n \leq 1000, k \leq 10^6, k \leq w \leq kn, 1 \leq a_i \leq 10^9\)
\(~~~~\) 神仙题好吧。
\(~~~~\) 记 \(f(k,w)\) 表示选 \(k\) 件物品,质量为 \(w\) 时的最大花费,如果按普通转移显然会超,所以:
\(~~~~\) 当 \(k\) 为偶数时:\(f(k,w)=\max_{i=1}^n f(k-1,w-i)+a_i\) 。
\(~~~~\) 否则:\(f(k,w)=\max_{|i|\leq \lfloor n/2 \rfloor } f(k/2,\lfloor w/2\rfloor-i)+f(k/2,\lceil w/2 \rceil+i)\)。
\(~~~~\) 至于为什么它是对的可以看官方题解:Link 。
\(~~~~\) 这样做完过后复杂度降为 \(\mathcal{O(n^2\log n)}\) ,可以通过本题。
查看代码
#include <cstdio>
#include <cstring>
#include <algorithm>
#define ll long long
using namespace std;
ll dp[1005][5000];
int n,Num[1000005],val[100005],cnt,fix[1000005];
void Divide(int k)
{
if(k==0) return;
Num[k]=++cnt;
if(k&1) Divide(k-1);
else Divide(k/2);
}
ll Solve(ll k,ll w)
{
if(w<0||(k&&w==0)) return -1e18;
if(!k)
{
if(w) return -1e18;
else return 0;
}
if(~dp[Num[k]][w-fix[k]+2500]) return dp[Num[k]][w-fix[k]+2500];
ll ret=-1e18;
if(k&1)
{
fix[k-1]=fix[k];
for(int i=1;i<=n;i++) ret=max(ret,Solve(k-1,w-i)+val[i]);
}
else
{
fix[k/2]=fix[k]/2;
for(int i=-(n/2);i<=n/2;i++) ret=max(ret,Solve(k/2,(w/2)-i)+Solve(k/2,(w+1)/2+i));
}
return dp[Num[k]][w-fix[k]+2500]=ret;
}
int main() {
memset(dp,-1,sizeof(dp));
int k,w;
scanf("%d %d %d",&n,&k,&w);
Divide(k);
for(int i=1;i<=n;i++) scanf("%d",&val[i]);
fix[k]=w;
printf("%lld",Solve(k,w));
return 0;
}
Random Pawn
\(~~~~\) 圆上有 \(N\) 个点,你初始随机出生在一个点。第 \(i\) 号点有属性 \(A_i, B_i\)。如果你当前处在 \(i\) 号点,你可以选择结 束游戏并获得 \(A_i\) 元,或者花费 \(B_i\) 元将自己随机移动到左右两点中的一个。求最优策略的期望收益。收益为得到的钱减付出的钱。
\(~~~~\) \(1\leq N\leq 2\times 10^5\)
\(~~~~\) 考虑这题的弱化版 Balance Beam P ,即没有 \(B\) 的限制的情况,此时可以将最大值作为端点破环成链,放在两端。
\(~~~~\) 这个时候可以发现值得结算的点一定是凸包上的点,对于不在凸包上的点则用其两边第一个凸包上的点来计算即可。
\(~~~~\) 问题在于现在有了 \(b\) 的限制,我们可以发现 \(f_i=\max{A_i,\dfrac{1}{2}(f_{i-1}+f_{i+1})-B_i}\) ,那我们消去 \(B\) 的影响,令 \(g_i=f_i-C_i\) ,满足若不在凸包上的点(即不取自己作为收益的点) \(g_i=\dfrac{1}{2}(g_{i-1}+g_{i+1})\) ,把这个式子拆开,可以推出 \(g_i=\dfrac{1}{2}(f_{i-1}+f_{i+1})-B_i-C_i\) ,继续化:\(2(B_i+C_i)=f_{i-1}+f_{i+1}-2(f_i-C_i)=2C_i\) ,也就是说令 \(C_1=0,C_2=0\) ,那么现在就优势在我,套用前面那道弱化版即可。
\(~~~~\) 但是需要注意单独算每个点的答案再加起来精度会爆炸,所以直接对凸包上的两个点算围成梯形面积,最后单独做除法。
查看代码
#include <cstdio>
#include <algorithm>
#define ll long long
using namespace std;
int n;
int L[200005],R[200005];
ll TmpA[200005],TmpB[200005],A[200005],B[200005],C[200005],Top,hep[200005];
int main() {
scanf("%d",&n);
ll Maxn=0,pos=0;
for(int i=1;i<=n;i++)
{
scanf("%lld",&TmpA[i]);
Maxn=max(Maxn,TmpA[i]);
if(Maxn==TmpA[i]) pos=i;
}
for(int i=1;i<=n;i++) scanf("%lld",&TmpB[i]);
int cnt=0;
for(int i=pos;i<=n;i++) A[cnt]=TmpA[i],B[cnt]=TmpB[i],cnt++;
for(int i=1;i<=pos;i++) A[cnt]=TmpA[i],B[cnt]=TmpB[i],cnt++;
ll Ans=0;
for(int i=2;i<=n;i++) C[i]=(C[i-1]+B[i-1]<<1)-C[i-2],A[i]-=C[i],Ans+=(i==n)?0:(C[i]<<1);
for(int i=1;i<=n;i++)
{
while(Top&&(A[i]-A[hep[Top]])*(hep[Top]-hep[Top-1])>=(A[hep[Top]]-A[hep[Top-1]])*(i-hep[Top])) Top--;
hep[++Top]=i;
}
for(int i=0;i<Top;i++) Ans+=(hep[i+1]-hep[i]+1)*(A[hep[i]]+A[hep[i+1]])-(A[hep[i+1]]<<1);
printf("%.12f",0.5*Ans/n);
return 0;
}
南京的构造
\(~~~~\) Link
\(~~~~\) 有 \(n\) 盏灯排在圆周上。每次你可以选一盏没亮的灯,反转它和相邻两盏的状态。你需要在 \(2n\) 步之内点亮所有灯或者输出无解。
\(~~~~\) 枚举第一个位置和第二个位置点了奇数次或者偶数次,可以顺次确定其他点(因为每个点只受三个点的影响)然后考虑如果一个位置要点,并且灯也是灭的,那么直接点这个点即可。
\(~~~~\) 否则那就一定是这个灯是灭的,且这个点也不点,那它右边那个点必定要点,并且灯也应该是亮的(否则上一轮就被干掉了),所以再右边那个点也应该是要点且亮的。且最右边应该是暗的。
0110
\(~~~~\) 这种情况只需要依次点 \(1,2,1,3\) 四个灯即可,用四步解决了两个灯,所以最后最多 \(2n\) 步
查看代码
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
char S[100005];
int n,A[100005],B[100005];
vector <int> Ans;
bool Solve()
{
for(int i=3;i<=n;i++) A[i]=B[i-1]^A[i-2]^A[i-1];
for(int i=1;i<=n;i++)
{
if(i==1&&((A[n]^A[1]^A[2])!=B[1])) return false;
else if(i==n&&((A[n]^A[n-1]^A[1])!=B[n])) return false;
else if(1<i&&i<n&&(A[i-1]^A[i]^A[i+1])!=B[i]) return false;
}
Ans.clear();
for(int i=1;i<=n;i++) B[i]=1-B[i];
bool flag=true;
while(flag)
{
flag=false;
for(int i=1;i<=n;i++)
{
if(B[i]==0&&A[i]==1)
{
flag=true;
Ans.push_back(i),A[i]=0;
B[i]^=1; B[(i-1-1+n)%n+1]^=1; B[(i+1-1+n)%n+1]^=1;
}
}
}
flag=true;
while(flag)
{
flag=false;
for(int i=1;i<=n;i++)
{
int j=i%n+1,k=(i+1)%n+1;
if(A[i]==0&&A[j]==1&&A[k]==1&&B[i]==0&&B[j]==1&&B[k]==1)
{
flag=true;
Ans.push_back(i); Ans.push_back(j); Ans.push_back(i); Ans.push_back(k);
B[j]^=1; B[k]^=1; B[(k+1-1)%n+1]^=1;
A[j]^=1; A[k]^=1;
}
}
}
printf("%d\n",(int)Ans.size());
for(int i=0;i<Ans.size();i++) printf("%d ",Ans[i]);
puts(""); return true;
}
int main() {
int T;
scanf("%d",&T);
while(T--)
{
scanf("%d %s",&n,S+1);
for(int i=1;i<=n;i++) B[i]=1-(S[i]-'0');
bool flag=false;
if(!flag) memset(A,0,sizeof(A)),A[1]=0,A[2]=0,flag=Solve();
if(!flag) memset(A,0,sizeof(A)),A[1]=1,A[2]=0,flag=Solve();
if(!flag) memset(A,0,sizeof(A)),A[1]=0,A[2]=1,flag=Solve();
if(!flag) memset(A,0,sizeof(A)),A[1]=1,A[2]=1,flag=Solve();
if(!flag) puts("0");
}
return 0;
}