P14223 [ICPC 2024 Kunming I] 乐观向上
我们考虑如何从左往右的进行填数操作。当我们填到位置 \(i\) 时,\(p_{i}\) 这个数能被填进去需要满足的条件就是 \(p_{0} \oplus p_{1} \oplus \cdots \oplus p_{i-1} \neq p_{i}\)。
先看看无解的情况。
我们经过一番思考后,发现无解的情况一定是填到最后一个数时填不进去,也就是 \(p_{0} \oplus p_{1} \oplus \cdots \oplus p_{n-2} = p_{n-1}\),即 \(p_{0} \oplus p_{1} \oplus \cdots \oplus p_{n-2} \oplus p_{n-1} = 0\)。
这个也很好理解,因为如果从左往右填数时,只要不是第 \(n-1\) 位,其他位的备选项都有两个及以上,一个数如果填不进去总是会有另一个合法的数的。
然后就是有解的情况了。一种很直接的贪心思路就是,当填到第 \(i\) 位时,把那个最小的合法的数填进去。
但是这样就有一个问题:如果当前这个位置局部贪心填进去,是否会导致后续填数的时候无解?
当然是不会存在这种情况的,因为正如我们证明无解的情况一样,当你后续的备选项不止一个时,一个数不合适又会有另一个;当你后面只剩一个数时,也就是到了最后一位,直接填进去就好,因为这是有解的情况,最后一个数填进去一定合法。
而我们要求的是字典序最小,所以保障当前这一位最小是最重要的。综上,该种贪心策略是正确的。
然后就到了我们的乱搞时间
既然思路以及有了,那剩下的关键点就在于怎么找当前最小的合法的数了。
假设我们有一个数组 \(a\),下标为 \([1,n]\),我们让 \(a_{i}\) 初始等于 \(i-1\)。
我们建一棵权值线段树,区间 \([l,r]\) 维护的是 \(a\) 数组对应区间的最小值。
若当前这个最小值合法,那么直接选走就行。
而当我们选择的最小数非法时,我们就去选次小值。具体来说,假设这个非法最小值是 \(num\),我们直接选 \(a\) 数组里 \([num+2,n]\) 这个区间的最小值。这个最小值一定是合法的。
(因为 \(num\) 这个数存在下标 \(num+1\) 处,现在我们不能选这个数了,所以下标还得加一)
每当我们选择一个数以后,就把 \(a\) 数组里这个数原本的位置置为 \(inf\),表示这个数被选走了。
这样我们的时间复杂度就是 \(O(n \log n)\) 的,虽然不是很优,但是它无脑啊。
代码:
#include<bits/stdc++.h>
#define int long long
#define ls (id<<1)
#define rs (id<<1|1)
using namespace std;
inline int read(){
int x=0,f=1;char c=getchar();
while(c<48){
if(c=='-') f=-1;
c=getchar();
}
while(c>47) x=(x<<1)+(x<<3)+(c^48),c=getchar();
return x*f;
}
inline void write(int x){
if(x<0) putchar('-'),x=-x;
if(x<10) putchar(x+'0');
else write(x/10),putchar(x%10+'0');
}
const int N=1e6+6;
const int inf=1e16;
int T,n,ans[N];
struct sw{
int id,l,r,mn;
}tr[4*N];
inline void build(int id,int l,int r){
tr[id].l=l,tr[id].r=r;
if(l==r){
tr[id].mn=l-1;
return ;
}
int mid=(l+r)>>1;
build(ls,l,mid);build(rs,mid+1,r);
tr[id].mn=min(tr[ls].mn,tr[rs].mn);
}
inline void update(int id,int pos,int k){
if(tr[id].l==pos&&tr[id].r==pos){
tr[id].mn=k;
return ;
}
int mid=(tr[id].l+tr[id].r)>>1;
if(pos<=mid){
update(ls,pos,k);
}
else{
update(rs,pos,k);
}
tr[id].mn=min(tr[ls].mn,tr[rs].mn);
}
inline int query(int id,int l,int r){
if(l<=tr[id].l&&tr[id].r<=r){
return tr[id].mn;
}
int mid=(tr[id].l+tr[id].r)>>1,res=inf;
if(l<=mid) res=min(res,query(ls,l,r));
if(r>mid) res=min(res,query(rs,l,r));
return res;
}
signed main(){
T=read();
while(T--){
n=read();
int dq=0;
//dq:前面填过的所有数的区间异或值
build(1,1,n);
int chk=0;
//chk:0~n-1的连续异或值
for(int i=0;i<n;i++){
chk^=i;
}
if(chk==0){//判断无解
printf("impossible\n");
continue;
}
for(int i=0;i<n;i++){//填第i位
int num=query(1,1,n);//找最小值
if(num==dq){//当前最小值不合法,找次小值
int num2=query(1,num+2,n);
dq^=num2;
ans[i]=num2;
update(1,num2+1,inf);
}
else{//直接填最小值
dq^=num;
ans[i]=num;
update(1,num+1,inf);
}
}
for(int i=0;i<n;i++){
printf("%lld ",ans[i]);
}
printf("\n");
}
return 0;
}