2026.3~2026.6 文化课期间复建 OI 记录
前言:高一的 noip2025 倒闭了,然后原地退役。但是平常还是想稍微开开脑子愉悦一下身心,遂偶尔做题,记录于此。
由于题解是写给自己看的,所以不太建议阅读,如果真的想读,请配合 Gemini 食用。
Luogu P15652. [省选联考 2026] 排列游戏 / perm *2000
注意到 \(\mathop{\text{mex}}\limits_{i\in[l,r]}\{a_i\}=\min\limits_{i\in[0,l)\cup(r,n)}\{a_i\}\),因此我们相当于只能确定一些前后缀区间并的 \(\min\),因此只要确定出这些那么就一定够我们构造一个合法的排列了。
由于我们只能问前后缀,所以不妨问出所有前、后缀 \(\min\)。由于问到 \(0\) 之后后面询问的结果都是 \(0\),因此可以问前缀问到 \(0\) 后停止,然后直接从后缀开始问到 \(0\) 分界后的位置。这样最坏的询问的次数就是 \(n\)。
确定出前、后缀 \(\min\) 后,考虑填剩下的数。我们一定确定的数排列起来,大小关系形如 \(x_1>x_2>\cdots>x_p>0<y_q<\cdots<y_2<y_1\) 的谷形,因此数字越大、限制越松。因此我们可以考虑从 \(0\) 开始向两端从小到大填数,先填限制紧的,即最开始先填 \(\min(x_p,y_q)\) 对应的区间,然后向两端扩展,以此类推。
时间复杂度 \(O(n)\)。
#include<bits/stdc++.h>
#include "perm.h"
#define N 30005
#define ll long long
#define mod
using namespace std;
// void init(int,int);
// vector<int>perm(int);
int query(int,int);
int n,a[N],pre[N],nxt[N];
bool fl[N];
struct node{
int l,r,d;
};
bool operator<(node x,node y){return max(a[x.l],a[x.r])>max(a[y.l],a[y.r]);}
void init(int c,int t){}
vector<int>perm(int n0)
{
n=n0;
fill(a,a+n,-1);
fill(pre,pre+n,-1);
fill(nxt,nxt+n,-1);
fill(fl,fl+n,0);
int p=0,lst=-1;
while(p<n-1)
{
int x=query(p+1,n-1);
if(!p||lst!=x) a[p]=x,fl[x]=1;
if(!x) break;
lst=x;
p++;
}
int q=n-1;
lst=-1;
while(p<q)
{
int x=query(0,q-1);
if(!x) break;
if(!q||lst!=x) a[q]=x,fl[x]=1;
lst=x;
q--;
}
for(int i=0;i<n;i++)
{
if(a[i]) continue;
goto lab;
}
a[n-1]=0;
fl[0]=1;
lab:;
// for(int i=0;i<n;i++) cerr<<a[i]<<' ';cerr<<'\n';
lst=0;
for(int i=0;i<n;i++)
{
if(a[i]<0) continue;
lst=i;
break;
}
for(int i=1;i<n;i++)
{
if(a[i]<0) continue;
pre[i]=lst;
nxt[lst]=i;
lst=i;
}
priority_queue<node>que;
for(int i=0;i<n;i++)
{
if(a[i]) continue;
que.push({pre[i],i,0});
que.push({i,nxt[i],1});
}
int num=0;
while(!que.empty())
{
auto [l,r,d]=que.top();
que.pop();
// cerr<<"["<<l<<','<<r<<"] "<<d<<'\n';
for(int i=l+1;i<r;i++)
{
while(fl[num]) num++;
a[i]=num;
fl[num]=1;
}
if(!d&&l>0) que.push({pre[l],l,0});//,cerr<<"ins ["<<pre[l]<<','<<l<<"]\n";
else if(d&&~r&&r<n-1) que.push({r,nxt[r],1});//,cerr<<"ins ["<<r<<','<<nxt[r]<<"]\n";
// for(int i=0;i<n;i++) cerr<<a[i]<<' ';cerr<<'\n';
}
vector<int>res;
for(int i=0;i<n;i++) res.push_back(a[i]);
return res;
}
Luogu P10767. 「CROI · R2」在相思树下 II *2200
dp,设当前节点为 \(i\),左、右儿子为 \(x,y\)。
那么设 \(l_i,r_i\) 分别表示能够在 \(i\) 号节点获胜的选手至少有多少个比这个节点获胜的值 \(d\) 小/大。
这句话有点绕,但是我不太会表述,洛谷题解也全都是一坨,建议配合 Gemini 食用。
考虑这一轮为 \(\max\) 的情况。对于下界,由于都比 \(d\) 小,所以易得 \(l_i=l_x+l_y+1\);对于上界,只有胜出的是可以被计入的,最坏情况即 \(r_i=\min(r_x,r_y)+1\)。\(\min\) 的情况同理。
于是某一层的范围就是这一层所有的节点上、下界的最小值。设第 \(i\) 层下、上界为 \(f_i,g_i\) 的话,那么询问即要求 \(a\in(f_b,2^n-g_b+1)\)。
时间复杂度 \(O(2^n+m)\)。
#include<bits/stdc++.h>
#define N 1000005
#define M 25
#define ll long long
#define lc (i<<1)
#define rc (i<<1|1)
#define mid (l+r>>1)
#define mod
using namespace std;
int n,m,a[N<<1],fl[N<<1],fr[N<<1],f[M],g[M];
void dp(int i,int l,int r)
{
if(l>=r) return;
int j=log2(r-l+1);
dp(lc,l,mid);
dp(rc,mid+1,r);
if(a[i]==1)
{
fl[i]=fl[lc]+fl[rc]+1;
fr[i]=min(fr[lc],fr[rc]);
}
else
{
fl[i]=min(fl[lc],fl[rc]);
fr[i]=fr[lc]+fr[rc]+1;
}
f[j]=min(f[j],fl[i]);
g[j]=min(g[j],fr[i]);
}
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i=1;i<(1<<n);i++) cin>>a[i];
fill(f+1,f+n+1,1e9);
fill(g+1,g+n+1,1e9);
dp(1,1,1<<n);
while(m--)
{
int x,y;
cin>>x>>y;
y--;
cout<<(f[y]<x&&g[y]+x-1<(1<<n)?"Yes":"No")<<'\n';
}
return 0;
}
Luogu P12336. 第三心脏 *2800
首先题目的式子等价于:
那么有 \(a\oplus b\oplus c\oplus d<d\)。
考虑令 \(a\oplus b\oplus c\oplus d=d+1\),则有:
这个 \(d\) 一定大于 \(a\),因此不论我们怎么构造 \(b,c\) 都可以只需将 \(b,c,d\) 升序重排得到最终的构造。
与此同时,如果要 \(a\oplus b\oplus c\oplus d=d+1\) 成立,那么显然一定得有 \(a\oplus b\oplus c=1\) 且 \(2\mid d\)。\(2\mid d\) 等价于 \(2\nmid(a^2+b^2+c^2)\)。
因此现在的问题就变成了:给定 \(a\),构造 \(b,c\) 使得 \(a\oplus b\oplus c=1\) 且 \(a<b<c\) 且 \(2\nmid(a^2+b^2+c^2)\)。
为了让 \(b\oplus c=a\oplus 1\) 的同时 \(b,c>a\),如果我们令 \(k=\min\{k\mid 2^k>a\}\),就可以:
- \(2\mid a\) 时,令 \(b=2^k+a\),\(c=b\oplus a\oplus 1\);
- \(2\nmid a\) 时,令 \(b=2^k\),\(c=b+(a\oplus 1)\)。
容易验证它们都是对的。然后就可以求得 \(d\)。
然后发现 \(a=1\) 时上述构造不合法。它可以通过暴力枚举找到一组解。
于是就做完了。
#include<bits/stdc++.h>
#define N
#define ll long long
#define i128 __int128
#define mod
using namespace std;
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
ll a;
cin>>a;
if(a==1)
{
cout<<"4 28 40";
return 0;
}
int k=0;
while((1ll<<k)<=a) k++;
ll b,c;
if(a&1) b=1ll<<k,c=b+(a^1);
else b=a+(1ll<<k),c=b^a^1;
if(b>c) swap(b,c);
ll d=((i128)a*a+b*b+c*c-1)/2;
cout<<b<<' '<<c<<' '<<d;
//cerr<<'\n'<<((i128)a*a+(i128)b*b+(i128)c*c+(i128)d*d==(i128)(a^b^c^d)*(a^b^c^d));
return 0;
}
Luogu P16258. [DSTOI Round 0] 万分之一的光 *2200
首先忽略 \(a\) 开头的 \(0+n\)。设 \(f_n\) 表示过程 \(n\) 对应的答案。易得转移式子:
其中 \(f_1=0,f_2=3\)。然后尝试进行一些推导:
也就是说,如果令 \(g(n)=n\oplus (n-3)\oplus (n-6)\oplus\cdots\),则 \(f_n=g(n-1)\oplus g(n-2)\oplus 2g(n-1)\oplus 2g(n-2)\)。现在需要计算 \(g(n)\)。
公差是 \(3\) 在二进制上感觉是没有什么比较好的规律的,那么考虑拆位。设 \(f_{i,j}=\bigoplus\limits_{0\le x<2^i\land x\bmod 3=j}x\),转移从 \(i-1\) 过来,即有:
其中 \([d]\) 表示对应的区间内长度是否为奇数,显然只有奇数的长度时高位才会有新的贡献。
然后可以利用 \(f\) 逐位进行计算即可。
最终答案也可以求出来,有一些比较特殊的情况需要小心,详见代码。
#include<bits/stdc++.h>
#define V 60
#define M 64
#define ll long long
#define mod
using namespace std;
ll f[M][3];
void init()
{
for(int i=1;i<=V;i++)
{
for(int j=0;j<3;j++)
{
f[i][j]=f[i-1][j]^f[i-1][(j-(1ll<<i-1)%3+3)%3];
if((((1ll<<i)+2-j)/3-((1ll<<i-1)+2-j)/3)&1) f[i][j]^=1ll<<i-1;
// cerr<<"f["<<i<<"]["<<j<<"] = "<<f[i][j]<<'\n';
}
}
}
ll cal(ll n)
{
ll res=n,j=n%3,s=0;
for(int i=V;~i;i--)
{
if(n>>i&1^1) continue;
res^=f[i][j];
if(((1ll<<i)+2-j)/3&1) res^=s;
j=(j-(1ll<<i)%3+3)%3;
s+=1ll<<i;
// cerr<<i<<' '<<j<<' '<<res<<'\n';
}
return res;
}
void sol()
{
ll n;
cin>>n;
if(n<3)
{
cout<<"1\n";
return;
}
if(n==3)
{
cout<<"7\n";
return;
}
ll x=cal(n-1),y=cal(n-2);
cout<<(n^x^(x<<1)^y^(y<<1)^(n%3==0))<<'\n';
}
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int t;
cin>>t;
init();
while(t--) sol();
return 0;
}
Luogu P16952. 「LAOI-18」口約束
果然退役的脑子只够给构造题了。
首先显然有一个必要条件是 \(n\equiv 2\pmod 3\) 且 \(n\ge 8\)。猜测它是充分的并尝试构造。
考虑 \(\text{mex}=0\) 的部分。这一部分只能是全 \(1\),为了让 \(1\) 尽可能少,我们让其挨在一起。那么剩下的就不会再出现连续三个 \(1\) 了。
然后是 \(\text{mex}=1\) 的部分。这一部分只能是全 \(0\),我们不妨直接放在全 \(1\) 的一侧。
那么另一侧只能是 \(\text{mex}=2\)。现在可以确定的前缀是 \(00\cdots0011\cdots110\),前面已经出现三个 \(\text{mex}=2\) 的区间。后面接续下去的话,两个 \(0\)、一个 \(1\) 自然是比两个 \(1\)、一个 \(0\) 更优的,于是可以构造答案串:\(00\cdots0011\cdots11001001001\cdots\),于是就做完了。
#include<bits/stdc++.h>
#define N
#define ll long long
#define mod
using namespace std;
void sol()
{
int n;
cin>>n;
if(n%3!=2||n<8)
{
cout<<"-1\n";
return;
}
int k=(n-2)/3+2;
for(int i=1;i<=k;i++) cout<<"0 ";
for(int i=1;i<=k;i++) cout<<"1 ";
for(int i=k*2+1,j=0;i<=n;i++,(++j)%=3) cout<<(j==2)<<' ';
cout<<'\n';
}
int main()
{
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int t;
cin>>t;
while(t--) sol();
return 0;
}

浙公网安备 33010602011771号