[冲刺国赛2022] 模拟赛11

星图

题目描述

n 个点的序列,每个点在权值在 [li,ri) 中均匀随机(权值是实数),问对这个序列构建笛卡尔树,期望深度之和是多少,其中深度定义为到根的距离,答案对 998244353 取模。

n300

解法

好浓的一股原题味道,但是确实是飘飘蛋的原创题

使用贡献法,如果 u,v(u<v) 满足 maxi=u+1vai<au,那么有 1 的贡献(u>v 同理)

考虑将连续型概率问题转化为离散型概率问题,即我们先对原序列离散化,然后枚举 au 的取值在 [bw,bw+1) 中。对于其他数,要么小于 bw,要么在 [bw,bw+1] 中。设在 [bw,bw+1) 一共有 k 个,那么 au 作为最大值的概率是 1k

可以转化为背包问题,设 dp[i] 表示在 [bw,bw+1) 中的数有 i 个的概率,暴力转移 O(n4),实现形如:

for(int i=1;i<=n;i++)
{
    int np=in(i);
    memset(dp,0,sizeof dp);dp[0]=1;
    for(int r=i+1;r<=n;r++)
    {
    	add(r);
        for(int j=0;j<=r-i;j++)
        	add(ans,dp[j]*inv[j+1]%MOD*np);
    }
}

考虑整体 dp,即只做一次 dp,在所有左端点处初始化,在右端点处统计贡献,时间复杂度 O(n3)

我真的特别喜欢整体 dp 这个技巧,但是这次怎么没反应过来啊

#pragma GCC optimize("Ofast")
#pragma GCC target("avx", "sse")
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 605;
const int MOD = 998244353;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,w,ans,l[M],r[M],b[M],dp[M];
int inv[M],z[M];
void add(int &x,int y) {x=(x+y)%MOD;}
int qkpow(int a,int b)
{
	int r=1;
	while(b>0)
	{
		if(b&1) r=r*a%MOD;
		a=a*a%MOD;
		b>>=1;
	}
	return r;
}
int p(int i,int br)
{
	if(br<=l[i]) return 1;
	if(br>=r[i]) return 0;
	return (r[i]-br)*z[i]%MOD;// div (r-l)
}
int in(int i)
{
	return (p(i,b[w])-p(i,b[w+1])+MOD)%MOD;
}
void add(int x)
{
	for(int i=n-1;i>=0;i--) if(dp[i])
	{
		add(dp[i+1],dp[i]*in(x));
		dp[i]=dp[i]*(MOD+1-p(x,b[w]))%MOD;
	}
}
void work()
{
	memset(dp,0,sizeof dp);
	for(int i=1;i<n;i++)
	{
		add(dp[0],in(i));add(i+1);
        for(int j=0;j<=n;j++)
            add(ans,dp[j]*inv[j+1]);
	}
	memset(dp,0,sizeof dp);
	for(int i=n;i>1;i--)
	{
		add(dp[0],in(i));add(i-1);
        for(int j=0;j<=n;j++)
            add(ans,dp[j]*inv[j+1]);
	}
}
signed main()
{
        freopen("starmap.in","r",stdin);
	freopen("starmap.out","w",stdout);
	n=read();inv[0]=1;
	for(int i=1;i<=n;i++)
	{
		b[++m]=l[i]=read();
		b[++m]=r[i]=read();
		z[i]=qkpow(r[i]-l[i],MOD-2);
		inv[i]=qkpow(i,MOD-2);
	}
	sort(b+1,b+1+m);
	m=unique(b+1,b+1+m)-b-1;
	for(w=1;w<m;w++) work();
	printf("%lld\n",(ans+MOD)%MOD);
}

无损加密

题目描述

n×m 的矩形,进行 q 次变换,设 k 次变换后的矩阵是 Ak,初始 A0 是单位矩阵。每次变换给定 (lk,rk,ck,dk)

(Ak)ij={dk(Ak1)ijj[lk,rk]dkt=lkj(ck)jt(Ak1)itj[lk,rk]

f(Aq),其中 f(B) 定义为:

f(B)=(S{1,2...m},|S|=ndetBS)mod(109+7)

其中 BS 表示矩阵 B 的所有行与 S 中元素对应的列,交叉处生成的一个 n×|S| 的矩阵。

数据范围保证对于所有的 1im,都有:

i=1q[i[lk,rk]]s

2nm2105,1s8,qsm

解法

真理:永远不要丝门弄数。

这题没有什么好的突破口,考虑从答案的角度入手,我们如何理解答案中出现的行列式?

谈到行列式,丝告诉我们,常见的模型有:BinetCauthy 公式、矩阵树定理、LGV 引理。最和本题贴合的是 LGV 引理,我们把矩阵理解成 LGV 引理中两点间路径的方案数,考虑构造一张分层图

解释一下上面的构造:第一层有 n 个红点代表起点;最后一层有 m 个绿点代表终点。根据 LGV 引理,我们只需要求出 n 条点不相交的路径,从红点开始在绿点结束的方案数。路径的权值是边权的乘积,方案的权值是路径权值的乘积。

再解释一下 BS 这个东西,这对应了方案中的任意选择终点,所以被考虑进去了。

每一次变换相当于添加了 m 个点,和上一层的对应点连一条边权为 d 的边。对于在 [li,ri) 中的点,和下一个点连一条边权为 c 的边,这样构造正好能对应变换规则。


那么问题转化成在上图中求 n 条不交路径的方案数,直接 dp 很困难。

利用题目中 s 的条件,考虑切换 dp 主体,即我们不再按行 dp 而是按列 dp。那么每一列最多存在 9 个路径暂时的终点,可以暴力状压下来,设 dp[i][s] 表示考虑到第 i 列,集合 s 中的点作为终点的方案数(全集是这一列所有存在入边的点)

一个暴力的转移方法是,我们枚举这一层每个点对应走到下一层的哪个点,由于不交的限制,复杂度大概是 O(q22s)

再深入的分析,其实每个点都对应着下一层的一个范围,这个范围中的点是只有这个点能走到的。所以可以先传递给下一层离得最近的一个点,在考虑到下一层的时候考虑层间的转移(类似高维前缀和),复杂度大概是 O(q2s)

对于前 in 列,转移时需要加入起点;如果终点在 s 中,转移到下一层时需要把终点删除(代表这条路径结束)

还是不太好讲清楚,可以结合代码理解具体怎么 dp 的。

总结

这种逆向思考的题目通常是难以下手的,但是出题人造出这道题一定是正向出发的

所以正确的打开方式是,抓住题目中有特征的关键东西,然后考虑和这东西有关的理论,尝试套用一些经典模型。

#pragma GCC optimize("Ofast")
#pragma GCC target("avx", "sse")
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 200005;
#define int long long
#define pb push_back
#define z(x) (1<<(x))
const int MOD = 1e9+7;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,k,po,dp[2][1<<9];vector<int> v[M],W[M];
int qkpow(int a,int b)
{
	int r=1;
	while(b>0)
	{
		if(b&1) r=r*a%MOD;
		a=a*a%MOD;
		b>>=1;
	}
	return r;
}
void add(int &x,int y) {x=(x+y)%MOD;}
signed main()
{
	freopen("encode.in","r",stdin);
	freopen("encode.out","w",stdout);
	n=read();m=read();k=read();po=1;
	for(int cnt=0;k--;)
	{
		int l=read(),r=read(),c=read(),h=read();
		po=po*h%MOD;
		for(int i=l;i<r;i++)
			v[i].pb(++cnt),W[i].pb(c);
	}
	dp[0][0]=1;
	for(int t=1,w=0;t<=m+1;t++,w^=1)
	{
		memset(dp[w^1],0,sizeof dp[w^1]);
		static int a[11]={},b[11]={},d[11]={};
		int n1=0,n2=0;
		for(auto x:v[t-1]) a[n1++]=x;
		for(auto x:v[t]) b[n2++]=x;
		for(int i=0,k=0;i<n1;i++)
		{
			while(k<n2 && b[k]<a[i]) k++;
			d[i]=k;
		}
		//down
		for(int i=0;i<n1;i++)
			for(int s=0;s<z(n1+1);s++)
				if((s>>i&1) && !(s>>(i+1)&1))
					add(dp[w][s^z(i)^z(i+1)],dp[w][s]);
		//delete the last one
		for(int s=0;s<z(n1);s++)
			add(dp[w][s],dp[w][s|z(n1)]);
		if(t>m) break;
		//to next
		for(int s=0;s<z(n1);s++) if(dp[w][s])
		{
			int ts=0,c=0,x=dp[w][s];
			if(t<=n) ts=1,c++;
			for(int i=0;i<n1;i++) if(s>>i&1)
				ts|=z(d[i]),c++,x=x*W[t-1][i]%MOD;
			if(__builtin_popcount(ts)!=c) continue;
			add(dp[w^1][ts],x);
		}
	}
	printf("%lld\n",dp[m&1][0]*qkpow(po,n)%MOD);
}
posted @   C202044zxy  阅读(351)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
历史上的今天:
2021-07-10 CF1392H ZS Shuffles Cards
点击右上角即可分享
微信分享提示