0131题解

~ 2026-1-31 11:34:34

T1 聪明的质检员

就是,我们考虑这样一个dpdp

dpl,rdp_{l,r}表示当前我知道不合格品在[l,r][l,r]区间内,我有多大的概率算出来答案。

那么dp1,ndp_{1,n}就是我要的结果了。

那么,对于[l,r][l,r]来说,首先我有1rl+1\frac{1}{r-l+1}的概率直接找到答案。

其次,有$lef=\frac{mid-l+1}{r-l+1},rig=1-lef-\frac{1}{r-l+1}$的概率不合格品在左边/右边。

而后续找到的前提条件是,我们得到了正确的提示:

也就是:P=i=k+12kCkipi(1p)ki P=\sum_{i=\frac{k+1}{2}}^kC_k^ip^i(1-p)^{k-i} 的概率得到正确的提示。

所以答案就是:

$dp_{l,r}=\frac{1}{r-l+1}+lef\times P\times dp_{l,mid}+rig\times P\times dp_{mid+1,r}$。

很显然地:我们只关心rlr-l的大小,所以直接用dplendp_{len}来算即可。

由于当lenlen是奇数的时候只会递归到两个一样的dplen2dp_{\frac{len}{2}}lenlen是偶数的时候会递归到一个dplen2dp_{\frac{len}{2}}和一个dplen21dp_{\frac{len}{2}-1}。容易发现有用的状态只有O(logn)O(logn)种。

#include <bits/stdc++.h>
#define ll long long
#define maxn 200005
#define eps -1e-8
using namespace std;
map<ll,double> f;
double p,dui;
ll n,k;
ll C[15][15];
double dfs(ll n)
{
	if (n<=2) return f[n]=1.0;
	if (f.count(n)) return f[n];
	ll lef,rig;
	if (n&1) lef=rig=n/2; else lef=n/2-1,rig=n/2;
	double zuo=1.0/n*lef,you=1.0/n*rig; //在左边的概率,在右边的概率
	f[n]+=1.0/n;
	f[n]+=dui*dfs(lef)*zuo+dui*dfs(rig)*you;
	return f[n];
}
int main()
{
	C[0][0]=1;
	for (int i=1;i<=11;i++)
	for (int j=0;j<=i;j++)
	{
		if (j==0 || i==1) C[i][j]=1; else C[i][j]=C[i-1][j]+C[i-1][j-1];
	}
	cin>>n>>p>>k;
	for (int i=k/2+1;i<=k;i++)
	{
		double gai=1;
		for (int j=1;j<=i;j++) gai*=p;
		for (int j=1;j<=k-i;j++) gai*=(1-p);
		gai*=C[k][i];
		dui+=gai;
	}
	dfs(n);
	printf("%.4lf\n",f[n]);
}

T2 乒乓球

source:某个CF题改编的简单版本。

30分的小数据模拟起来还是很简单的,不多说。

对于一条链,是在提示正解的:

我们考虑开个线段树维护一下区间内从0:0,1:0,0:1,1:1分别开始模拟一轮,一轮以后比分差是多少,小分是多少。

比如

0:0开始模拟一轮以后,比分差是11,小分是1:1

1:0开始模拟一轮以后,比分差是1-1,小分是0:1

0:1开始模拟一轮以后,比分差是00,小分是1:1

1:1开始模拟一轮以后,比分差是11,小分是1:0

那么,从大的层面上来说,每一轮就会陷入:0:0->1:1->1:0->0:1->1:1->...的循环,在这个循环内的比分是1+1+0=01+-1+0=0,所以是平局。

那么正解就很简单了,我直接上树剖或者树上倍增,就可以维护好这个东西。

纯粹就是个小模拟,复杂度O(nlog2n)O(nlog^2n)

#include<bits/stdc++.h>
#define maxn 200005
#define ll long long
using namespace std;
//0    1   2   3
//0:0 1:0 0:1 1:1
struct nod{
	int sta[4],score[4];
};
nod val[3];
nod init(int f)
{
	nod ret;	
	if (f==1) 
	{
		ret.sta[0]=1; ret.sta[1]=0; ret.sta[2]=3; ret.sta[3]=0;
		ret.score[0]=0; ret.score[1]=1; ret.score[2]=0; ret.score[3]=1;
	}
	else if (f==0)
	{
		ret.sta[0]=2; ret.sta[1]=3; ret.sta[2]=0; ret.sta[3]=0;
		ret.score[0]=0; ret.score[1]=0; ret.score[2]=-1; ret.score[3]=-1;
	}
	else
	{
		for (int i=0;i<=3;i++) ret.sta[i]=i,ret.score[i]=0;
	}
	return ret;
}
nod merge(nod x,nod y)
{
	nod ret;
	for (int fir=0;fir<=3;fir++)
	{
		int score=0;
		int now=fir;
		score+=x.score[now]; now=x.sta[now];
		score+=y.score[now]; now=y.sta[now];
		ret.score[fir]=score; ret.sta[fir]=now;
	}
	return ret;
}

int n,Q;
int u,v,w;
basic_string<int> edge[maxn];
int fa[maxn][21],dep[maxn];
//F表示从你向上,G表示从父亲到你 
nod F[maxn][21],G[maxn][21];
void dfs(int x,int father,int v)
{
	dep[x]=dep[father]+1; 
	fa[x][0]=father; for (int i=1;i<=19;i++) fa[x][i]=fa[fa[x][i-1]][i-1];
	F[x][0]=val[v]; G[x][0]=val[v];
	for (int i=1;i<=19;i++)
	{
		F[x][i]=merge(F[x][i-1],F[fa[x][i-1]][i-1]);
		G[x][i]=merge(G[fa[x][i-1]][i-1],G[x][i-1]);
	}
	for (int y:edge[x])
	{
		int id=abs(y),tag=(y<0?1:0);
		if (id!=father) dfs(id,x,tag);
	}
}

void out(nod x)
{
	for (int i=0;i<=3;i++) cout<<x.sta[i]<<" "<<x.score[i]<<endl;	
}
int mem[4];
void check(nod x)
{
	mem[0]=0; mem[1]=mem[2]=mem[3]=1<<29;
	int now=0,score=0;
	while (true)
	{
		score+=x.score[now]; now=x.sta[now];
		if (mem[now]!=(1<<29))
		{
			int cha=score-mem[now];
			if (cha>0) cout<<"Win\n";
			if (cha==0) cout<<"Draw\n";
			if (cha<0) cout<<"Lose\n";
			return;
		}
		mem[now]=score;
	}
	return;
}

void work(int u,int v)
{
	nod a=val[2],b=val[2];
	int cha=dep[u]-dep[v];
	if (cha>0)
	{
		for (int i=19;i>=0;i--)
		if ((cha>>i)&1)
		{
			a=merge(a,F[u][i]);
			u=fa[u][i];
		}
	}
	else if (cha<0)
	{
		cha=-cha;
		for (int i=19;i>=0;i--)
		if ((cha>>i)&1)
		{
			b=merge(G[v][i],b);
			v=fa[v][i];
		}
	}
	if (u==v) {check(merge(a,b)); return;}
	for (int i=19;i>=0;i--)
	if (fa[u][i]!=fa[v][i])
	{
		a=merge(a,F[u][i]); u=fa[u][i];
		b=merge(G[v][i],b); v=fa[v][i];
	}
	a=merge(a,F[u][0]); u=fa[u][0];
	b=merge(G[v][0],b); v=fa[v][0];
	check(merge(a,b));
	return;
}

int main()
{
	ios::sync_with_stdio(false); cin.tie(0);
	val[0]=init(0); val[1]=init(1); val[2]=init(2);
	cin>>n;
	for (int i=1;i<n;i++) 
	{
		cin>>u>>v>>w; 
		if (w==0) edge[u]+=v,edge[v]+=u;
		else edge[u]+=-v,edge[v]+=-u;
	}
	dfs(1,1,2);
	cin>>Q;
	while (Q--)
	{
		cin>>u>>v;
		work(u,v);
	}
}

T3 田忌赛马

bonus:有一个神秘的不依赖随机数据的莫队做法,见WYR的题解。

source:瞎编的。

首先,我们可以把星粉剂想象成,每次可以给一个数字+1+1

45分:

我们考虑一下询问的区间a,ba,b里面分别有多少个1,2,3,...,n1,2,3,...,n

假设5\leq 5aia_ixx个,4\leq 4bib_iyy个,那么显然:有xyx-y5\leq 5的数字要变得5\geq 5,假设=5=5bib_izz个,那么:我们需要从xyx-y个数字中,挑选xyzx-y-z个变得6\geq 6

我们设AiA_i表示aa数组中有多少个i\leq i的数字,BiB_i表示bb数组中有多少个i\leq i的数字,那么:

i=1mxCAiBi1AiBi\prod _{i=1}^{mx}C_{A_i-B_{i-1}}^{A_{i}-B_{i}}

就是答案了。

那么对于mx1000mx\leq 1000的小数据,我们直接暴力求出A,BA,B数组即可。注意一个细节是如果你数组开了2×2e5×10002\times 2e5\times 1000,会喜提MLE

100分:

这个好容易想到的其实。

这是随机数据诶,随机数据性质多多。

我们对于每次li,ril_i,r_i,从ali,bria_{l_i},b_{r_i}分别向右/向左暴力,一旦遇到一个位置算出来的系数跟Ai,BiA_i,B_i一样,就说明中间这一段都一样,直接用之前的答案就好了。

至于维护区间里有多少个范围内的数字,直接用主席树即可(实际上这个过程可以只在一开始主席树查一遍,之后的询问直接在这个基础上指针跑一跑就可以线性了)。

时间复杂度在随机数据下是:O(nlogn+kQlogn)O(nlogn+kQlogn)

可以通过上述优化把复杂度变成:O(nlogn+kQ+Qlogn)O(nlogn+kQ+Qlogn),这样就能卡掉莫队了。

#include<bits/stdc++.h>
#define maxn 200005
#define ls lson[now]
#define rs rson[now]
#define mid ((l+r)>>1) 
#define ll long long 
#define mod 998244353
using namespace std;
ll pw(ll x,ll mi)
{
	ll ret=1;
	while (mi)
	{
		if (mi&1) ret=ret*x%mod;
		mi=mi>>1; x=x*x%mod;
	}
	return ret;
}
ll fac[maxn],inv[maxn],ni[maxn];
ll cc(int n,int m){return fac[n]*inv[m]%mod*inv[n-m]%mod;}
void init2(int zd)
{
	ni[0]=ni[1]=1;
	for (int i=2;i<=zd;i++) ni[i]=(mod-(mod/i)*ni[mod%i]%mod);
	fac[0]=fac[1]=inv[0]=1;
	for (int i=2;i<=zd;i++)	fac[i]=fac[i-1]*i%mod;
	inv[zd]=pw(fac[zd],mod-2);
	for (int i=zd-1;i>=1;i--) inv[i]=inv[i+1]*(i+1)%mod;
	return;
}
int rt1[maxn],rt2[maxn],tot=0;
int sum[maxn<<6],lson[maxn<<6],rson[maxn<<6];
ll a[maxn],b[maxn];
ll ans[maxn]; 
ll x[maxn],y[maxn];
int n;

int tx[maxn],ty[maxn];

void ins(int &now,int pre,int l,int r,int id)
{
	if (!now) now=++tot;
	if (l==r) {sum[now]=sum[pre]+1; return;}
	if (id<=mid) {ins(lson[now],lson[pre],l,mid,id); rson[now]=rson[pre];}
	else {ins(rson[now],rson[pre],mid+1,r,id); lson[now]=lson[pre];}
	sum[now]=sum[lson[now]]+sum[rson[now]];
}
int query(int now,int l,int r,int L,int R)
{
	if (!now || L>R) return 0;
	if (L<=l && r<=R) return sum[now];
	int ret=0;
	if (L<=mid) ret+=query(lson[now],l,mid,L,R);
	if (mid+1<=R) ret+=query(rson[now],mid+1,r,L,R);
	return ret;
} 

void out(int l,int r,int *a)
{
	for (int i=l;i<=r;i++) cout<<a[i]<<" "; cout<<endl;
}
void work(int l,int r)
{
	ll ret=1,L=-1,R=-1;
	for (int i=a[l];i<=b[r];i++)
	{
		int shang=query(rt1[r],1,n,1,i)-query(rt1[l-1],1,n,1,i);
		int xia=query(rt2[r],1,n,1,i-1)-query(rt2[l-1],1,n,1,i-1);
		int xia2=query(rt2[r],1,n,1,i)-query(rt2[l-1],1,n,1,i);
		tx[i]=shang-xia; //一共还剩这么多数字
		ty[i]=shang-xia2; //要搬这么多数字走	
		ret=ret*cc(tx[i],ty[i])%mod;
		if (tx[i]==x[i] && ty[i]==y[i]) {L=i; break;}
	}
	if (L==-1) {cout<<ret<<"\n"; return;} //两端碰撞了 
	for (int i=b[r];i>L;i--)
	{
		int shang=query(rt1[r],1,n,1,i)-query(rt1[l-1],1,n,1,i);
		int xia=query(rt2[r],1,n,1,i-1)-query(rt2[l-1],1,n,1,i-1);
		int xia2=query(rt2[r],1,n,1,i)-query(rt2[l-1],1,n,1,i);
		tx[i]=shang-xia; //一共还剩这么多数字
		ty[i]=shang-xia2; //要搬这么多数字走	
		ret=ret*cc(tx[i],ty[i])%mod;
		if (tx[i]==x[i] && ty[i]==y[i]) {R=i; break;}
	}
	if (R==-1 || R==L+1) {cout<<ret<<"\n"; return;} //两端碰撞了 
	//还要乘上L+1~R-1
	ret=ret*pw(ans[L],mod-2)%mod*ans[R-1]%mod; 
	cout<<ret<<"\n";
}

int main()
{
	ios::sync_with_stdio(false); cin.tie(0);
	init2(200000);
	cin>>n;
	for (int i=1;i<=n;i++) cin>>a[i];
	for (int i=1;i<=n;i++) cin>>b[i];
	for (int i=1;i<=n;i++) 
	{
		ins(rt1[i],rt1[i-1],1,n,a[i]);
		ins(rt2[i],rt2[i-1],1,n,b[i]);
	}
	//搞定初始的答案
	for (int i=1;i<=n;i++)
	{
		int shang=query(rt1[n],1,n,1,i);
		int xia=query(rt2[n],1,n,1,i-1),xia2=query(rt2[n],1,n,1,i);
		x[i]=shang-xia; //一共还剩这么多数字
		y[i]=shang-xia2; //要搬这么多数字走	
	}
	
	ans[0]=1;
	for (int i=1;i<=n;i++) ans[i]=ans[i-1]*cc(x[i],y[i])%mod;
	int Q; cin>>Q;
//	for (int i=1;i<=b[n];i++) cout<<x[i]<<" "; cout<<endl;
//	for (int i=1;i<=b[n];i++) cout<<y[i]<<" "; cout<<endl;
	int l,r;
	while (Q--)
	{
		cin>>l>>r;
		work(l,r);
	}
}

这是一个不依赖数据随机的 O(n+q)O(n+q) 做法。

考虑先求出两个数组匹配的方案数,满足任意一组匹配 (x,y)(x,y) 都有 axbya_x\le b_y

容易发现求出这个数就能求得询问的答案(唯一的区别是题目中相同的 bb 是无标号的,而我们求的是有标号的),并且这里的转化可以通过类似预处理前缀积的形式做到 O(n)O(1)O(n)-O(1)

先考虑能不能单组询问 O(n)O(n) 求出答案。下文令 pip_i 表示最大的下标 xx 使得 bxib_x\le i

考虑从右向左寻找每一个 aia_i 的匹配,对于 i=ri=r 来说,它可以选的是匹配是 bb 中区间内所有 ar\ge a_r 的数,也就是说这一位的方案数是 rmax(l1,par1)r-\max(l-1,p_{a_r-1})

再来考虑 [l,r1][l,r-1] 的区间内的匹配,容易发现这一部分的方案数恰好与直接询问 [l,r1][l,r-1] 的匹配数相等。

所以我们可以知道,对于一组询问,匹配数应当是 i=lrimax(l1,pai1)\prod\limits_{i=l}^ri-\max(l-1,p_{a_i-1})

这样我们就可以单组询问 O(n)O(n) 求出匹配数。

这个时候我们发现,由于数组 a,ba,b 都有序,那么一定存在一个位置 w(l1wr)w(l-1\le w\le r) 满足对于所有 i[l,w]i\in[l,w],都有上述式子的 max\max 取到 l1l-1,对于所有 i(w,r]i\in(w,r] 都有上述式子的 max\max 取到 pai1p_{a_i-1},并且 ww 容易 O(1)O(1) 求出。

那么对于 i[l,w]i\in[l,w] 的部分,贡献是一个阶乘;对于剩下的部分,可以直接前缀积求。

#include<bits/stdc++.h>
#define inf 0x3f3f3f3f3f3f3f3fll
#define debug(x) cerr<<#x<<"="<<x<<endl
using namespace std;
using ll=long long;
using ld=long double;
using pli=pair<ll,int>;
using pi=pair<int,int>;
template<typename A>
using vc=vector<A>;
inline int read()
{
    int s=0,w=1;char ch;
    while((ch=getchar())>'9'||ch<'0') if(ch=='-') w=-1;
    while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
    return s*w;
}
inline ll lread()
{
    ll s=0,w=1;char ch;
    while((ch=getchar())>'9'||ch<'0') if(ch=='-') w=-1;
    while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
    return s*w;
}
const int mod=998244353;
ll p[200001],ip[200001];
ll L[200001],iL[200001];
ll jc[200001];
ll ijc[200001];
ll inv[200001];
int ar[200001];
int br[200001];
int a[200001];
int b[200001];
int n,m;
inline ll qow(ll a,ll b)
{
    ll ans=1;
    while(b)
    {
        if(b&1) ans=ans*a%mod;
        a=a*a%mod;
        b>>=1;
    }
    return ans;
}
inline void init()
{
    jc[0]=1;
    for(int i=1;i<=n;i++) jc[i]=jc[i-1]*i%mod;
    ijc[n]=qow(jc[n],mod-2);
    for(int i=n;i;i--)
    {
        ijc[i-1]=ijc[i]*i%mod;
        inv[i]=ijc[i]*jc[i-1]%mod;
    }

    for(int i=1;i<=n;i++)
    {
        ar[i]=ar[i-1],br[i]=br[i-1];
        while(ar[i]<n&&a[ar[i]+1]<=i) ar[i]++;
        while(br[i]<n&&b[br[i]+1]<=i) br[i]++;
    }
    p[0]=ip[0]=1;
    for(int i=1;i<=n;i++)
    {
        int th=i-br[a[i]-1];
        p[i]=p[i-1]*th%mod;
        ip[i]=ip[i-1]*inv[th]%mod;
    }

    int cnt=0;L[0]=iL[0]=1;
    for(int i=1;i<=n;i++)
    {
        if(b[i]!=b[i-1]) cnt=0;
        cnt++;
        L[i]=L[i-1]*inv[cnt]%mod;
        iL[i]=iL[i-1]*cnt%mod;
    }
}
inline ll get(int l,int r)
{
    if(b[l]==b[r]) return ijc[r-l+1];
    int w1=br[b[l]],w2=br[b[r]-1]+1;
    return ijc[w1-l+1]*ijc[r-w2+1]%mod*L[w2-1]%mod*iL[w1]%mod;
}
int main()
{
    n=read();

    for(int i=1;i<=n;i++) a[i]=read();
    for(int i=1;i<=n;i++) b[i]=read();
    init();

    m=read();
    for(int i=1;i<=m;i++)
    {
        int l=read(),r=read();ll val;
        if(a[r]<=b[l]) val=jc[r-l+1];
        else
        {
            int w=ar[b[l]];
            val=jc[w-l+1]*ip[w]%mod*p[r]%mod;
        }
        printf("%lld\n",val*get(l,r)%mod);
    }
    return 0;
}

T4 云顶之弈

考虑 n=1n=1 的时候怎么做,令 fpi,0fp_{i,0} 表示前 ii 个牌的权值总和,fpi,15fp_{i,1\sim 5} 表示前 ii 个牌,最后要留下 151\sim 5A 的权值总和(不算留的几张),fpi,610fp_{i,6\sim 10} 表示前 ii 个牌,最后留下 151\sim 5B 的权值总和(不算留的几张)。

同理使用 dpi,010dp_{i,0\sim 10} 表示方案数。

考虑如何转移,以第 ii 张牌是 Afpfp 的转移为例。

首先,第 ii 张牌可以丢掉,有 fpi,jfpi1,jfp_{i,j}\gets fp_{i-1,j}

ii 张牌还可以和之前的一些 A 一起留着,对于 j<5j<5fpi,jfpi1,j1fp_{i,j}\gets fp_{i-1,j-1}

ii 张牌还可以和之前的一些 A 一起合成一张大牌,有:$fp_{i,0}\gets fp_{i-1,0}+dp_{i-1,0}A+fp_{i-1,2}+dp_{i-1,2}B+fp_{i-1,5}+dp_{i-1,5}C$。

dpdp 的转移差不多,第 ii 张牌是 a 的转移也类似。

这样就解决了 n=1n=1 的问题。

n1n\ne 1 的时候,瓶颈在于操作 33。考虑使用链表维护序列,使用矩阵维护上述操作。

操作 33 就是矩阵乘法然后合并链表,操作 11 就是矩阵乘法然后链表插入一个元素。

操作 22 就是链表删除一个元素,然后乘一个转移矩阵的逆。

这样做显然是对的,但是由于要同时维护方案数和权值和,你会发现矩阵是 22×2222\times 22 的,这很孬。

考虑把 dpdpfpfp 分开维护,你会发现这俩拆成两个矩阵也是可以维护的,这样就只需要维护两个 11×1111\times 11 的矩阵。

时间复杂度 O((Q+ki)L3)O((Q+\sum k_i)L^3),其中 L=11L=11 为矩阵大小。



我们会审查剪贴板内容,并对发布不合适内容的同学进行相应的处理