2026.7.7 模拟赛
https://www.hszxoj.com/d/hztg/contest/6a4b5431860c260215922ed2
这场是 衡中 上打的,T1、T2 为签到.
T3 按照我的能力应当是能想出正解的,但是却只拿了 52 分,应当反思.
T4 本来已经想到冒泡排序的规律,但是未想到题解中的 “二值化引理”,因此不知道如何维护,看来关于数值的算法要经常想到拆位.
树
在场上想的是:题目中的条件相当于,每次在两个相邻的点中选择一个删去,重复这个过程,求有多少种方案(序列与操作过程形成双射关系)?
事实上如果转化为上面的形式前途就没了,只能使用 \(O(2^nn)\) 的模拟,在随机数据下可以通过 \(n\le 100\) 的数据.
之后打表可以发现:菊花图时答案恒为 \(n\),链时答案为 \(F_n\)(斐波那契数列),拼一下可以通过 52 分的数据.
下面讲解正解:
应当使用树形 DP,不然 \(n\le 10^6\) 的数据你用什么算法?
可以发现,对于一个节点,其被删除的情况至于子节点的相对顺序有关,因此可以进行树形 DP.
设计如下状态:
设节点 \(u\),其父边为 \(p\).
dp0[u]表示 \(u\) 在处理 \(p\) 之前已经染黑.dp1[u]表示 \(u\) 在处理 \(p\) 时仍然为白色.dp2[u]表示 \(u\) 在处理 \(p\) 时变成黑色.
考虑子树的转移:
设子节点 \(v\):
若 \(v\) 时 \(u\) 仍为白色,则产生 dp0[v]+dp2[v] 的贡献.
若 \(v\) 时 \(u\) 已经变成黑色,则产生 dp0[v]+dp1[v] 的贡献.
若 \(v\) 时 \(u\) 恰好变成黑色,则产生 dp1[v] 的贡献.
于是在 DP 转移时,可以记录子节点的贡献的前缀积与后缀积辅助转移.
最终答案即为 dp0[1]+dp1[1].
这样就是 \(O(n)\) 的复杂度.
#include<iostream>
#include<vector>
#define int long long
using namespace std;
constexpr int N=1e6+5,p=998244353;
int n,dp0[N],dp1[N],dp2[N];
vector<int> v[N];
int a[N],b[N];
void dfs(int x,int f){
if(x!=1&&v[x].size()<2){
dp0[x]=0,dp1[x]=1,dp2[x]=1;
return;
}
for(int u:v[x])if(u!=f)dfs(u,x);
a[0]=1,b[v[x].size()]=1;
for(int i=0;i<v[x].size();i++){
int u=v[x][i]; a[i+1]=a[i];
if(u==f)continue;
a[i+1]=a[i+1]*(dp0[u]+dp2[u])%p;
}
for(int i=v[x].size()-1;i>=0;i--){
int u=v[x][i]; b[i]=b[i+1];
if(u==f)continue;
b[i]=b[i]*(dp0[u]+dp1[u])%p;
}
bool flag=0;
for(int i=0;i<v[x].size();i++){
int u=v[x][i];
if(u==f){
dp2[x]=a[i]*b[i+1]%p,flag=1;
}else if(flag){
dp1[x]=(dp1[x]+a[i]*dp1[u]%p*b[i+1]%p)%p;
}else {
dp0[x]=(dp0[x]+a[i]*dp1[u]%p*b[i+1]%p)%p;
}
}
dp1[x]=(dp1[x]+a[v[x].size()])%p;
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n;
for(int i=1,x,y;i<n;i++)cin>>x>>y,
v[x].push_back(y),v[y].push_back(x);
dfs(1,0);
int ans=(dp0[1]+dp1[1])%p;
cout<<ans<<'\n';
return 0;
}
冒泡排序二合一
这里引用一下题解的二值化引理:
二值化引理
给定一个阈值 T,把区间内的数变成二进制序列:
1表示 \(a_i \ge T\);0表示 \(a_i < T\).
冒泡操作在这个二进制序列中等价于从左到右把相邻的 10 变成 01.
考虑这个二进制序列做 k 轮后的形态:
- 若
k\(\ge\) 1 的个数,所有1都会被推到右端,最终是若干0后接若干1. - 否则,设原序列中第
k+1个1的位置为p,它前面有z个0。最终序列的前z位一定是0;其余位置等于原序列整体左移k位,右端补1.
这个结论可以从一轮操作看出:第一轮会把第一个 1 推到它能到达的最右侧,相当于删掉这个 1、后缀左移一位、最后补一个 1。重复 k 次即可.
这个发现这个形式时即为重要的,有了这样简单的形式,可以支持我们使用数据结构维护.
第一类询问
设 pos[x] 是 x 的初始位置。如果 pos[x] 不在 [l,r] 中,答案就是 pos[x]。
否则答案可以拆成两部分:
- 原来在
x左边,最终仍在x前面的数。 - 原来在
x右边,但经过冒泡后跑到x前面的小于x的数。
对左侧:
- 原来在
[l,pos[x])中小于x的数,不会被x向左越过,始终在x前面。 - 原来在
[l,pos[x])中大于x的数,每轮最多有一个能越过x到右侧。因此若共有cntGreater个,k轮后还剩max(cntGreater-k,0)个在x前面。
对右侧:
把区间按 >=x 和 <x 二值化。令 lim 为从 l 开始第 k+1 个 >=x 的位置;若不存在则视作 n+1。只有原来位于 (pos[x], min(lim,r+1)) 中的小于 x 的数会跑到 x 前面。
所以可以按值离线,用树状数组分别维护:
- 值
>x的位置,统计左侧较大值个数; - 值
<x的位置,统计右侧会越过的小值个数; - 值
>=x的位置,求第k+1个位置lim。
这样就可以做到 \(O((n+ q)\log n)\),使用 主席树+二分 可以做到 \(O(n\log n+q\log^2 n)\).
第二类询问
考虑二分,讲序列按照 >mid 二值化,使用 “二值化引理” 进行模拟,从而得出 \(a_x\) 上的数值是否大于 \(mid\),可以使用整体二分做到 \(O((n+q)\log^2 n)\)(必须使用树状数组维护,线段树会比树状数组慢 1 倍,然后就 T 了).
事实上可以优化这个过程:
- 若 \(x\ge r-k+1\),则最后 \(k\) 个位置应当依次是区间第 \(k\) 大.
- 否则,在二分时,\(a_x\) 取决于 \(a_{x_k}\) 的值:
当 \(x+k\) 前有 \(≥k\) 个 \(1\) 时,它会向前移动 \(k\) 位.
当 \(x+k\) 前 \(1\) 的个数 \(<k\) 时,此时 \(x+k\) 之前的 \(1\) 全部会跑到序列最后,但是 \(x\) 处的值会取到原本在 \(<x+k\) 处的值,那么必定是 \(0\).
那么只需考虑 \(x+k\) 前有 \(≥k\) 个 \(1\) 的情况,求出 \(<x+k\) 的第 \(k\) 大值 \(s\),在 \(lim≤s\) 时都是满足这种情况的,只要再考虑 \(a_{x+k}\) 即可,答案即为 \(\min(a_{x+k},s)\).
这样就可以做到 \(O((n+q)\log n)\).
#include<iostream>
using namespace std;
constexpr int N=6e5+5,M=1.5e7+5;
int n,q,o,a[N];
namespace Solve1{
struct Segment_Tree{
int lch[M],rch[M],sum[M],tot;
int copy(int x){
int id=++tot;
lch[id]=lch[x],rch[id]=rch[x];
sum[id]=sum[x]; return id;
}
void add(int &x,int id,int L,int R){
x=copy(x),sum[x]++;
if(L==R)return; int mid=(L+R)>>1;
if(id<=mid)add(lch[x],id,L,mid);
else add(rch[x],id,mid+1,R);
}
int query(int x,int l,int r,int L,int R){
if(l<=L&&R<=r)return sum[x];
int mid=(L+R)>>1,res=0;
if(l<=mid)res+=query(lch[x],l,r,L,mid);
if(mid<r) res+=query(rch[x],l,r,mid+1,R);
return res;
}
}tr;
int pos[N],rt[N];
int count(int L,int R,int x){
return tr.query(rt[R],x,n,1,n)
-tr.query(rt[L-1],x,n,1,n);
}
int find(int L,int R,int k,int x){
if(count(L,R,x)<k)return R+1;
int l=L,r=R,mid,s=tr.query(rt[L-1],x,n,1,n);
while(l+1<r){
mid=(l+r)>>1;
if(tr.query(rt[mid],x,n,1,n)<k+s)l=mid;
else r=mid;
}return r;
}
void work(){
for(int i=1;i<=n;i++)pos[a[i]]=i;
for(int i=1;i<=n;i++)
rt[i]=rt[i-1],tr.add(rt[i],a[i],1,n);
for(int i=1,l,r,k,x;i<=q;i++){
cin>>l>>r>>k>>x;
if(pos[x]<l||r<pos[x])cout<<pos[x]<<'\n';
else {
int p=pos[x];
p-=min(count(l,pos[x]-1,x),k);
int z=find(l,r,k+1,x)-1;
if(z>pos[x])p+=z-pos[x]-count(pos[x]+1,z,x);
cout<<p<<'\n';
}
}exit(0);
}
}
namespace Solve2{
struct BIT{
#define lowbit(x) (x&(-x))
int sum[N],topbit;
void build(int len){
topbit=1;
while((topbit<<1)<=len)topbit<<=1;
}
void add(int x,int val){
for(int i=x;i<=n;i+=lowbit(i))sum[i]+=val;
}
int query(int x){
int ans=0;
for(int i=x;i;i-=lowbit(i))ans+=sum[i];
return ans;
}
int query(int L,int R){
return query(R)-query(L-1);
}
int kth(int k){
int pos=0;
for(int i=topbit;i;i>>=1){
int nex=pos+i;
if(nex<=n&&sum[nex]<k)
pos=nex,k-=sum[pos];
}return pos+1;
}
}tr;
struct Que{ int op,l,r,k,x; }b[N<<1],c[N<<1],d[N<<1];
int out[N],pos[N]; bool vis[N];
void BinarySearch(int be,int ed,int L,int R){
if(L==R){
for(int i=be;i<=ed;i++)
if(b[i].x<b[i].l||b[i].r<b[i].x)
out[b[i].op]=a[b[i].x];
else out[b[i].op]=L;
tr.add(pos[L],1),vis[pos[L]]=1;
return;
}
int mid=(L+R)>>1,pos1=0,pos2=0;
for(int i=R;i>mid;i--)
tr.add(pos[i],1),vis[pos[i]]=1;
for(int i=be;i<=ed;i++){
int s=tr.query(b[i].l,b[i].r);
int k=b[i].k;
if(s<=k){
if(b[i].x<=b[i].r-s)c[++pos1]=b[i];
else d[++pos2]=b[i];
}else {
s=k+1;
int p=min(tr.kth(tr.query(b[i].l-1)+s),b[i].r+1);
int z=p-k-1;
if(b[i].x<=z)c[++pos1]=b[i];
else {
k+=b[i].x;
if(k>b[i].r)d[++pos2]=b[i];
else if(vis[k])d[++pos2]=b[i];
else c[++pos1]=b[i];
}
}
}
for(int i=1;i<=pos1;i++)b[be+i-1]=c[i];
for(int i=1;i<=pos2;i++)b[be+pos1+i-1]=d[i];
for(int i=R;i>mid;i--)
tr.add(pos[i],-1),vis[pos[i]]=0;
BinarySearch(be+pos1,ed,mid+1,R);
BinarySearch(be,be+pos1-1,L,mid);
}
void work(){
tr.build(n);
for(int i=1;i<=n;i++)pos[a[i]]=i;
for(int i=1;i<=q;i++)b[i].op=i,
cin>>b[i].l>>b[i].r>>b[i].k>>b[i].x;
BinarySearch(1,q,1,n);
for(int i=1;i<=q;i++)cout<<out[i]<<'\n';
}
}
namespace Solve3{
struct Segment_Tree{
int lch[M],rch[M],sum[M],tot;
int copy(int x){
int id=++tot;
lch[id]=lch[x],rch[id]=rch[x];
sum[id]=sum[x]; return id;
}
void add(int &x,int id,int L,int R){
x=copy(x),sum[x]++;
if(L==R)return; int mid=(L+R)>>1;
if(id<=mid)add(lch[x],id,L,mid);
else add(rch[x],id,mid+1,R);
}
int find(int x,int y,int k,int L,int R){
while(L!=R){
int mid=(L+R)>>1,s=sum[rch[y]]-sum[rch[x]];
if(k<=s)x=rch[x],y=rch[y],L=mid+1;
else k-=s,x=lch[x],y=lch[y],R=mid;
}return L;
}
}tr;
int pos[N],rt[N];
void work(){
for(int i=1;i<=n;i++)
rt[i]=rt[i-1],tr.add(rt[i],a[i],1,n);
for(int i=1;i<=q;i++){
int l,r,k,x;
cin>>l>>r>>k>>x;
if(x<l||r<x||!k)cout<<a[x]<<'\n';
else if(x>r-k)cout<<tr.find(rt[l-1],rt[r],r-x+1,1,n)<<'\n';
else cout<<min(tr.find(rt[l-1],rt[x+k-1],k,1,n),a[x+k])<<'\n';
}
exit(0);
}
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>q>>o;
for(int i=1;i<=n;i++)cin>>a[i];
if(o==1)Solve1::work();//Case 1
else Solve2::work();//Case 2
else Solve3::work();//Case 2
return 0;
}

浙公网安备 33010602011771号