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+11 的位置为 p,它前面有 z0。最终序列的前 z 位一定是 0;其余位置等于原序列整体左移 k 位,右端补 1.

这个结论可以从一轮操作看出:第一轮会把第一个 1 推到它能到达的最右侧,相当于删掉这个 1、后缀左移一位、最后补一个 1。重复 k 次即可.

这个发现这个形式时即为重要的,有了这样简单的形式,可以支持我们使用数据结构维护.

第一类询问

pos[x]x 的初始位置。如果 pos[x] 不在 [l,r] 中,答案就是 pos[x]

否则答案可以拆成两部分:

  1. 原来在 x 左边,最终仍在 x 前面的数。
  2. 原来在 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 了).

事实上可以优化这个过程:

  1. \(x\ge r-k+1\),则最后 \(k\) 个位置应当依次是区间第 \(k\) 大.
  2. 否则,在二分时,\(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;
}
posted @ 2026-07-08 09:52  zhoumengxuan  阅读(4)  评论(0)    收藏  举报