概率期望DP做题记录-Part2
概率期望DP做题记录-Part2
Part1#
CF804D Expected diameter of a tree#
题意#
给定一片森林。
做法#
首先,期望的式子是很好推的。
设
分母是好求的,考虑如何求出分子。
设
在继续往下写之前,先考虑一下如何求出
。 有一个结论:在一棵树上,任选一条直径,到任意一个点最远的一个点一定可以是这条直径的两个端点之一。
这个东西的证明可以去康康hegm的博客。在里面
Ctrl+F
搜索区间树上最远点对
即可。所以,对于每个联通块,仅需进行三遍dfs即可求出它的
,复杂度 。
如果只有第一项或者只有最后两项,这个式子是非常好算的。
这是一个取
既然如此,我们对每棵树开一棵值域线段树,然后枚举
显然,现在我们可以在
观察题解,考虑使用根号分治。
- 当
时,直接按照上面的暴力计算即可。显然这里的复杂度是 的。 - 容易发现,
的 不会超过 个,这样的询问不会超过 个。 - 于是我们预处理出这些答案,开一个map存起来。这样预处理的复杂度是
的。 - 我们枚举这样较大的树对,然后枚举其中一棵树中的所有点,对另一棵树的值域线段树进行查询即可。
考虑枚举这些集合的过程。
这个过程就相当于,对于每个在较大集合中的点,都枚举一遍所有的较大的集合。
显然所有点加起来不会超过
,较大的集合数量不会超过 ,单次查询是 的。 所以,这里的复杂度就是
的。
综上,这样根号分治的复杂度就是
当然,作者在写的时候脑子抽了,这个是可以不用值域线段树实现的,只需要开一个vector存即可。这样的复杂度是
具体细节见代码。
代码#
写得恶臭,长度约3.87KB。
#include<bits/stdc++.h>
using namespace std;
inline int read(){//快读
int ans=0;bool op=0;char ch=getchar();
while(ch<'0'||'9'<ch){if(ch=='-')op=1;ch=getchar();}
while('0'<=ch&&ch<='9'){ans=(ans<<1)+(ans<<3)+(ch^48);ch=getchar();}
if(op)return -ans;
return ans;
}
const int maxn=1e5+10;
struct node{
int l,r,cnt,val;
}tr[maxn<<5];//动态开点线段树
int n,m,q;
int BLOCK_SIZ;//根号分治的块长
int cnt=0;
int root[maxn];//最多n棵动态开点线段树,记录n个根
vector<int>g[maxn];
int dis[maxn];//如题
int fa[maxn];//并查集
int clr[maxn],col;
int fst[maxn],lst[maxn];//直径的起点和终点
int dep[maxn];
vector<int>s[maxn];//记录一个联通块中有哪些点
vector<int>big_tr;//记录较大的树都有哪些
map<int,map<int,double>>anss;//预处理的ans
void newnode(int &p){
tr[++cnt]=(node){0,0,0,0};
p=cnt;
}
void pushup(int p){
tr[p].cnt=tr[tr[p].l].cnt+tr[tr[p].r].cnt;
tr[p].val=tr[tr[p].l].val+tr[tr[p].r].val;
}
void add(int pos,int s,int t,int &p,int val){//单点加的实现
if(!p)newnode(p);
if(s==pos&&pos==t){
++tr[p].cnt;
tr[p].val+=pos;
return;
}
int mid=(s+t)>>1;
if(pos<=mid)add(pos,s,mid,tr[p].l,val);
else add(pos,mid+1,t,tr[p].r,val);
pushup(p);
}
void add(int pos,int val,int _){//单点加
add(pos,0,n,root[_],val);
}
pair<int,int> query(int l,int r,int s,int t,int p){//区间查询的实现
if(!p)return make_pair(0,0);
if(l<=s&&t<=r)return make_pair(tr[p].cnt,tr[p].val);
int mid=(s+t)>>1;
pair<int,int>ans=make_pair(0,0);
if(l<=mid){
pair<int,int>tmp=query(l,r,s,mid,tr[p].l);
ans=tmp;
}
if(mid<r){
pair<int,int>tmp=query(l,r,mid+1,t,tr[p].r);
ans.first+=tmp.first;
ans.second+=tmp.second;
}
return ans;
}
pair<int,int> query(int l,int r,int _){//区间查询
return query(l,r,0,n,root[_]);
}
/*到这里是线段树 到这里是线段树 到这里是线段树 到这里是线段树 到这里是线段树 到这里是线段树 到这里是线段树 到这里是线段树*/
int find(int now){//并查集查询
if(fa[now]==now)return now;
return fa[now]=find(fa[now]);
}
void dfs1(int now,int fa){//第一遍dfs
clr[now]=col;
s[col].push_back(now);
dep[now]=dep[fa]+1;
if(dep[now]>dep[fst[col]])fst[col]=now;
for(int nxt:g[now]){
if(nxt==fa)continue;
dfs1(nxt,now);
}
}
void dfs2(int now,int fa){//第二遍dfs
dep[now]=dep[fa]+1;
dis[now]=dep[now]-1;
if(dep[now]>dep[lst[col]])lst[col]=now;
for(int nxt:g[now]){
if(nxt==fa)continue;
dfs2(nxt,now);
}
}
void dfs3(int now,int fa){//第三遍dfs
dep[now]=dep[fa]+1;
dis[now]=max(dis[now],dep[now]-1);
add(dis[now],1,clr[now]);
for(int nxt:g[now]){
if(nxt==fa)continue;
dfs3(nxt,now);
}
}
signed main(){
n=read(),m=read(),q=read();
BLOCK_SIZ=sqrt(n)+1;//设置块长
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=m;i++){
int u=read(),v=read();
fa[find(u)]=find(v);
g[u].push_back(v),g[v].push_back(u);
}
for(int i=1;i<=n;i++){//处理联通快
if(!clr[i]){
++col;
dfs1(i,0);
dfs2(fst[col],0);
dfs3(lst[col],0);
}
}
for(int i=1;i<=col;i++){//记录哪些树是大树
if(query(0,n,i).first>=BLOCK_SIZ)big_tr.push_back(i);
}
for(int i:big_tr){//枚举所有大树,预处理答案
int di=dis[fst[i]];
int si=query(0,n,i).first;
for(int j:big_tr){
if(i==j)continue;
int dj=dis[fst[j]];
int sj=query(0,n,j).first;
double ans=0;
int cnt=0;
for(int k:s[i]){
pair<int,int>tmp=query(max(max(di,dj)-dis[k]-1,0),n,j);
ans+=1.0*tmp.second+tmp.first*(dis[k]+1);
cnt+=tmp.first;
}
ans+=1.0*(si*sj-cnt)*max(di,dj);
ans/=1.0*si*sj;
anss[find(fst[i])][find(fst[j])]=ans;
anss[find(fst[j])][find(fst[i])]=ans;
}
}
for(int i=1;i<=q;i++){//q次询问
int u=find(read()),v=find(read());
if(u==v){
puts("-1");
continue;
}
int su=query(0,n,clr[u]).first,sv=query(0,n,clr[v]).first;
if(su>sv)swap(u,v),swap(su,sv);
if(su>=BLOCK_SIZ)cout<<fixed<<setprecision(10)<<anss[u][v]<<'\n';//大树,输出预处理的答案
else{//小树,暴力查询
int du=dis[fst[clr[u]]];
int dv=dis[fst[clr[v]]];
double ans=0;
int cnt=0;
for(int k:s[clr[u]]){
pair<int,int>tmp=query(max(max(du,dv)-dis[k]-1,0),n,clr[v]);
ans+=1.0*tmp.second+tmp.first*(dis[k]+1);
cnt+=tmp.first;
}
ans+=1.0*(su*sv-cnt)*max(du,dv);
ans/=1.0*su*sv;
cout<<fixed<<setprecision(10)<<ans<<'\n';
}
}
return 0;
}
P4562 [JXOI2018]游戏#
题意#
九条可怜负责管理
员工会偷懒,但是当她检查某个办公室时,这个办公室的员工会开始认真工作,并且通知编号是它倍数的其他办公室的员工也开始认真工作。
对于每种顺序
现在需要求出所有
做法#
考虑对于一个排列
观察题解发现,
- 是
中某个数的倍数的。 - 不是
中某个数的倍数的。
根据定义,当且仅当第二类数都出现过时,
考虑求出
那我们仅需求出
那么:
然后直接计算即可。
需要注意的是,我们可以在做乘法的时候特判掉
代码#
signed main(){
l=read(),r=read();
n=r-l+1;
for(int i=l;i<=r;i++){
if(!vis[i]){
++m;
for(int j=2;i*j<=r;j++)vis[i*j]=1;
}
}
ans=m;
for(int i=1;i<=n+1;i++)if(i!=m+1)ans=(ans*i)%mod;
cout<<ans;
return 0;
}
P6835 [Cnoi2020]线形生物#
题意#
给定一个从
现在加入
你在这个图上随机游走,问走到
做法#
观察题解,我们获得了一个全新的套路。
一般地,对于一个随机游走的问题,我们可以设
表示从 走到 的期望步数。 利用期望的线性性质,从
走到 的步数期望 。 然后只需要考虑如何求出
即可。
考虑如何求出
这个东西看起来有后效性,我们观察题解,得到解决方法。
先写出朴素的转移式子(
利用期望的线性性质,可以得到:
设
然后按照式子转移即可。
代码#
signed main(){
id=read(),n=read(),m=read();
for(int i=1;i<=n;i++){
g[i].push_back(i+1);
}
for(int i=1;i<=m;i++){
int u=read(),v=read();
g[u].push_back(v);
}
for(int i=1;i<=n;i++){
f[i]=g[i].size();
for(int j:g[i]){
if(j==i+1)continue;
f[i]=(f[i]+sum[i-1]-sum[j-1]+mod)%mod;
}
sum[i]=(sum[i-1]+f[i])%mod;
}
for(int i=1;i<=n;i++)ans=(ans+f[i])%mod;
cout<<ans<<endl;
return 0;
}
P6834 [Cnoi2020]梦原#
题意#
给定一个
节点
做法#
首先,设
考虑加入一个点会产生的贡献。分两种情况讨论(设当前点编号为
- 如果
,那么在取 的时候可以顺便把 取完,所以不会产生额外贡献。 - 如果
,那么在取完 时, 还会剩下 ,这是额外产生的贡献。
然后我们就有了一个愉快的转移式子:
容易发现,这样直接转移的复杂度是
观察讨论发现,这里可以直接用值域线段树扫过去,只是空间会炸,离散化即可。
这样的时间复杂度是
代码#
恶臭代码,2.13KB。
const int maxn=1e6+10;
const int mod=998244353;
int n,m;
int a[maxn];
int dp[maxn];
vector<int>mp;
struct node{
int l,r,cnt,sum;
}tr[maxn<<1];
int root,cnt;
void newnode(int &p){
p=++cnt;
tr[p]=(node){0,0,0,0};
}
void add(int pos,int s,int t,int &p,int num){//单点加入一个元素
if(!p)newnode(p);
if(s==pos&&pos==t){
tr[p].cnt+=num;
tr[p].sum+=mp[pos]*num;
return;
}
int mid=(s+t)>>1;
if(pos<=mid)add(pos,s,mid,tr[p].l,num);
else add(pos,mid+1,t,tr[p].r,num);
tr[p].cnt=(tr[tr[p].l].cnt+tr[tr[p].r].cnt)%mod;
tr[p].sum=(tr[tr[p].l].sum+tr[tr[p].r].sum)%mod;
}
int get_cnt(int l,int r,int s,int t,int p){//查询一个值域内的元素数量
if(!p)return 0;
if(l<=s&&t<=r)return tr[p].cnt;
int mid=(s+t)>>1,ans=0;
if(l<=mid)ans+=get_cnt(l,r,s,mid,tr[p].l);
if(mid<r)ans+=get_cnt(l,r,mid+1,t,tr[p].r);
return ans;
}
int get_sum(int l,int r,int s,int t,int p){//查询一个值域内的元素的值的和
if(!p)return 0;
if(l<=s&&t<=r)return tr[p].sum;
int mid=(s+t)>>1,ans=0;
if(l<=mid)ans+=get_sum(l,r,s,mid,tr[p].l);
if(mid<r)ans+=get_sum(l,r,mid+1,t,tr[p].r);
return ans;
}
void add(int pos,int num){//偷懒
add(pos,0,n,root,num);
}
int getcnt(int l,int r){
return get_cnt(l,r,0,n,root);
}
int getsum(int l,int r){
return get_sum(l,r,0,n,root);
}
int inv(int x){
int ans=1;
for(int i=mod-2;i;i>>=1){
if(i&1)ans=(ans*x)%mod;
x=(x*x)%mod;
}
return ans;
}
signed main(){
n=read(),m=read();
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<=n;i++)mp.push_back(a[i]);
sort(mp.begin(),mp.end());//离散化
mp.erase(unique(mp.begin(),mp.end()),mp.end());//离散化
for(int i=1;i<=n;i++)a[i]=lower_bound(mp.begin(),mp.end(),a[i])-mp.begin();//离散化
dp[1]=mp[a[1]];//特殊处理第一项
add(a[1],1);
for(int i=2;i<=n;i++){
int lc=getcnt(0,a[i]-1),ls=getsum(0,a[i]-1);
int rc=getcnt(a[i],mod);
dp[i]=(dp[i-1]+((lc*mp[a[i]]-ls+mod)%mod)*inv(lc+rc))%mod;//转移
add(a[i],1);//加入当前点的贡献
if(i-m>0)add(a[i-m],-1);//删除多于的贡献
}
cout<<dp[n];//输出
return 0;
}
P2059 [JLOI2013] 卡牌游戏#
题意#
有
现在已经知道了每张卡牌上的数字,求每个人获胜的概率。
说句闲话#
本人在做的时候看错题了,以为每张牌抽出来之后就不放回去了。
然后就想到了一个肥肠美妙的性质:每个抽排方式出现的概率是相等的。
然后就在这个小小的性质里面挖呀挖呀挖,考虑求出每个人获胜的可能得排列数。
然后就寄了。
做法#
考虑设一个正序的状态。
然后发现这样的状态不好转移,需要记录存在的人什么的,直接和多项式说再见。
那么我们考虑反向转移。设
显然,
考虑转移。肯定要枚举
不难发现,这次会删掉的是第
- 如果
, 直接出局,无缘胜利。 - 如果
, 前面有 ,所以 会在 。 - 如果
, 前面有 加上 ,所以 会在 。
现在,我们知道了从哪转移,然后直接转移即可。
代码#
signed main(){
n=read(),m=read();
for(int i=1;i<=m;i++)a[i]=read();
dp[1][1]=1;
for(int i=2;i<=n;i++){
for(int j=1;j<=i;j++){
for(int k=1;k<=m;k++){
if(a[k]%i==j)continue;
if(a[k]%i<j)dp[i][j]+=dp[i-1][j-a[k]%i]/m;
else dp[i][j]+=dp[i-1][j+i-a[k]%i]/m;
}
}
}
for(int i=1;i<=n;i++)cout<<fixed<<setprecision(2)<<dp[n][i]*100<<"% ";//百分比输出
return 0;
}
UVA11021 Tribles#
题意#
一开始有
这种鸟只能活
问
做法#
设
为什么是一只鸟呢?因为鸟与鸟之间互不影响,所以概率可以直接相乘。所以
然后就有了转移方程:
直接转移即可。
代码#
void real_main(){
n=read(),k=read(),m=read();
for(int i=0;i<n;i++)cin>>p[i];
for(int i=1;i<=m;i++)dp[i]=0;
dp[1]=p[0];
for(int i=2;i<=m;i++){
double base=1;
for(int j=0;j<n;j++){
dp[i]+=p[j]*base;
base*=dp[i-1];
}
}
printf("Case #%d: %.7lf\n",id,ksm(dp[m],k));
}
signed main(){
T=read();
for(id=1;id<=T;id++)real_main();
return 0;
}
UVA1639 糖果 Candy#
题意#
有两个盒子各有
每天随机选一个盒子(概率分别为
输入
不知道对不对的做法#
设
不难发现这个东西是
做法#
考虑每种取法的概率。
设当前左边空了,右边还剩
同理,右边空了,左边剩
现在有了概率,计算期望即可。
那么期望就是:
需要注意的是,这里计算组合数会溢出。观察题解发现,这里取个
代码#
#define ld long double
void real_main(){
ans=0;
_p=log(1-p);
p=log(p);
for(int i=0;i<=n;i++){
ld l=fact[2*n-i]-fact[n]-fact[n-i]+(n+1)*p+(n-i)*_p;
ld r=fact[2*n-i]-fact[n]-fact[n-i]+(n+1)*_p+(n-i)*p;
ans+=1.0*i*(exp(l)+exp(r));
}
printf("Case %d: %.6lf\n",id,ans);
}
signed main(){
for(int i=1;i<=4e5+5;i++)fact[i]=fact[i-1]+log(i);
while(cin>>n>>p){
++id;
real_main();//多测
}
return 0;
}
P3239 [HNOI2015]亚瑟王#
题意#
你在玩一个卡牌游戏。你有
在每个回合开始时会进行以下操作:
从第一张牌开始考虑。
- 如果这张卡牌在这一局游戏中已经发动过技能,则
1. 如果这张卡牌不是最后一张,则跳过之(考虑下一张卡牌); 否则(是最后一张),结束回合。- 否则(这张卡牌在这一局游戏中没有发动过技能),设这张卡牌为第
张
1. 将其以的概率发动技能。
2. 如果技能发动,则对敌方造成点伤害,并结束回合。
3. 如果这张卡牌已经是最后一张(即等于 ),则结束回合;否则,考虑下一张卡牌。
求你能造成的总伤害的期望。
美妙的假做法#
设
设
那么
附上代码:
for(int i=1;i<=m;i++){
f[i][0]=1;
for(int j=1;j<=n;j++){
double now=(1-dp[i-1][j])*f[i][j-1]*p[j];
dp[i][j]=dp[i-1][j]+now;
f[i][j]=f[i][j-1]*(dp[i-1][j]+(1-p[j])-dp[i-1][j]*(1-p[j]));
ans+=now*a[j];
}
}
还发了一篇帖子/kk
后来一翻,这篇题解提到了这个做法,但还是不造咋错的。
这里画个图大致解释一下。
这时,这两个值就会在绿色部分产生一些重叠,这样计算的贡献就是错误的。所以这个做法是错误的。
那为什么不特殊处理一下绿色部分呢?当然是因为我不会啦
正解#
观察题解,得到一个没有重叠部分的dp。
设
考虑如何求出
设
为了方便转移,设
显然这
然后分两种情况讨论:
- 第
张牌并没有被选。根据定义,考虑从 转移。在此前提下, 在可释放技能的回合中没有释放过技能。那么 就应该加上 。 - 第
张牌被选了。根据定义,考虑从 转移。在此前提下, 在可释放回合中释放过技能。那么就应该加上 。
根据定义,我们得到:
直接计算即可。
代码#
void real_main(){
ans=0;
memset(dp,0,sizeof(dp));//double可以用memset赋全0,别的没试
memset(f,0,sizeof(f));
memset(g,0,sizeof(g));
n=read(),m=read();
for(int i=1;i<=n;i++)cin>>p[i]>>d[i];
for(int i=1;i<=n;i++){
g[i][0]=1;
for(int j=1;j<=m;j++)g[i][j]=g[i][j-1]*(1-p[i]);
}
f[0][0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
if(j)f[i][j]+=(1-g[i][m-j+1])*f[i-1][j-1];
f[i][j]+=g[i][m-j]*f[i-1][j];
}
}
for(int i=1;i<=n;i++){
for(int j=0;j<=min(i,m);j++){
dp[i]+=f[i-1][j]*(1.0-g[i][m-j]);
}
}
for(int i=1;i<=n;i++)ans+=dp[i]*d[i];
cout<<fixed<<setprecision(10)<<ans<<'\n';
}
signed main(){
int T=read();
while(T--)real_main();//多测
return 0;
}
P3412 仓鼠找sugar II#
题意#
给定一棵树,随机选择一个起点和一个终点,然后从起点开始随机游走,问走过边数量的期望。
麻烦的做法#
考虑到起点和终点一共有
首先定
然后发现不能直接把起点到
所以,设
然后发现,这样做每次都要求一遍
可以用一些东西来优化成 但是我懒。
正解#
开始之前,先考虑一下如何求出
不妨设
这是非常简单的。然后考虑求出
其实这个还可以再化简。不难发现,
现在我们求出了
考虑一条边会对多少个点对产生贡献。方便起见,我们枚举点
然后分类讨论:
- 当起点在
的子树中而终点不在时,路径经过 。这时会产生 的贡献。 - 当终点在
的子树中而起点不在时,路径经过 。这时会产生 的贡献。 - 其他情况下,路径不经过这条边,不产生贡献。
容易发现,对于前两种情况,都会产生
直接计算即可,时间复杂度
代码#
void dfs1(int now,int fa){
f[now]=d[now];
siz[now]=1;
for(int nxt:tr[now]){
if(nxt==fa)continue;
dfs1(nxt,now);
f[now]+=f[nxt];
siz[now]+=siz[nxt];
}
f[now]%=mod;
}
void dfs2(int now,int fa){
if(now!=1)g[now]=(g[fa]+f[fa]-f[now]+mod)%mod;
else g[now]=0;
for(int nxt:tr[now]){
if(nxt==fa)continue;
dfs2(nxt,now);
}
}
signed main(){
n=read();
for(int i=1;i<n;i++){
int u=read(),v=read();
tr[u].push_back(v);
tr[v].push_back(u);
++d[u],++d[v];
}
dfs1(1,0);
dfs2(1,0);
for(int i=2;i<=n;i++){
ans=(ans+siz[i]*(siz[1]-siz[i])%mod*(f[i]+g[i])%mod)%mod;
}
cout<<ans*inv(n*n%mod)%mod;
return 0;
}
P3750 [六省联考 2017] 分手是祝愿#
什么题目名称
题意#
给定
你的目标是使所有灯都灭掉。
每次你会等概率随机操作一个开关,直到所有灯都灭掉。
B 君想知道按照这个策略(也就是先随机操作,最后最小操作次数小于等于
求这个期望乘以
做法#
先考虑已知这些路灯的亮灭状态,如何求出最小操作次数。
不难发现,从
不妨假设当前位置为
- 当
时,只能把 按灭掉。 - 当
时,假设 都灭了。首先,更小的不能让 灭掉。如果试图用更大的把 按掉,那么就会一直需要更大的把按亮的按回去,直到没有更大的能把它按回去,这时候还是要一个一个按回去,不如直接把 按掉。
这样感性理解,就说明了从大到小按掉是最优的做法之一。
对于一个序列,根据刚才的过程,容易发现,需要按一下的点是固定的。因为这个操作是异或,所以顺序没有影响。
现在问题就转化成了,给定一个
然后就好做了。容易发现期望步数只与
不妨设
显然转移为:
直接转移即可。
那么步数的期望就是
直接计算即可,记得要乘上
代码#
void update(int x){
for(int i=1;i*i<=x;i++){
if(x%i)continue;
a[i]^=1;
if(i*i!=x)a[x/i]^=1;
}
}
signed main(){
n=read(),m=read();
for(int i=1;i<=n;i++)a[i]=read();
for(int i=n;i;i--){
if(a[i]){
update(i);
++cnt;
}
}
m=min(cnt,m);
for(int i=n;i;i--)dp[i]=(n+(n-i)*dp[i+1])%mod*inv(i)%mod;
for(int i=m+1;i<=cnt;i++)ans=(ans+dp[i])%mod;
ans+=m;
for(int i=1;i<=n;i++)ans=ans*i%mod;
cout<<ans;
return 0;
}
emmm#
这篇也有点长了,本地都开始卡了,后面会写个Part3,然后把链接放这。
Update 6.12:去搞具体数学了,Part3就给鸽了。这里的最后一题是原本打算放在Part3的,但是Part3被我鸽了,就搬过来了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通