概率 DP

注意事项

关于概率和期望的基本定义,不再赘述
参见这个题单的前面(我们今天上课的课件)
讲的还是很好的
本篇主要是做题笔记
如想了解一些基本概念请移步其他博客
本篇主要都是以上题单里的题

前置知识

这一部分很恶心
因为概率 DP 总会涉及到一些其他很难的算法和数据结构
但也只是一部分
后面我们讲到再说

Problem Set

P1850 换教室

这题是一个偏模板的题
首先我们需要用 \(Floyd\) 跑一遍全源最短路
当然本题没有负权直接每个点一轮 \(Dijkstra\) 也可以
然后我们考虑设一个 \(DP[i][j][0/1]\)
表示枚举到了第 \(i\) 个时间段
还剩下 \(j\) 次申请机会
然后这一次是否进行了申请最终所需的最小代价期望

期望基本性质 - $E(X+Y)=E(X)+E(Y)$

\(X,Y\) 当作两个 \(rand()\)
他们期望的和就等于他们和的期望

  • \(E(kX)=kE(x)\)
  • \(E(X*Y)=E(X)E(Y)\)

这个性质要求 \(X\)\(Y\) 之间是没有关系的

有了基本性质
我们就容易进行转移了

\[DP[i][j][0]=\min(DP[i-1][j][0]+OwO(i,i-1),DP[i-1][j][1]+OwQ(i,i-1)) \]

\[DP[i][j][1]=\min(DP[i-1][j+1][0]+OwQ(i-1,i),DP[i-1][j+1][1]+QwQ(i,i-1)) \]

\[OwO(i,j)=Dis[c_i][c_j] \]

\[OwQ(i,j)=Dis[c_i][c_j]*(1-p_j)+Dis[c_i][d_j]*p_j \]

\[QwQ(i,j)=Dis[c_i][c_j](1-p_i)(1-p_j)+Dis[c_i][d_j](1-p_i)p_j+Dis[d_i][c_j]p_i(1-p_j)+Dis[d_i][d_j]p_ip_j \]

式子看起来很癫
其中的 \(OwO\) 就是两个端点都没申请
其余同理,看看就行
至于为什么要取 \(\min\) 而不是简单相加
因为你肯定不能既申请又不申请,而是要取最优的选择
然后就没有了

#include<bits/stdc++.h>
using namespace std;
typedef double db;
const int N=2009,K=303,inf=1000000009;
int n,m,v,e,c[N],d[N],Dis[K][K];
db shit[N],dp[N][N][2];
inline db OwO(int l,int r){return (db)Dis[c[l]][c[r]];}
inline db OwQ(int l,int r){return (db)Dis[c[l]][d[r]]*shit[r]+(db)Dis[c[l]][c[r]]*(1.0-shit[r]);}
inline db QwQ(int l,int r){return (db)Dis[d[l]][d[r]]*shit[l]*shit[r]+(db)Dis[c[l]][d[r]]*shit[r]*(1.0-shit[l])+(db)Dis[d[l]][c[r]]*shit[l]*(1.0-shit[r])+(db)Dis[c[l]][c[r]]*(1.0-shit[l])*(1.0-shit[r]);}
int main(){
	int l,r,w;
	scanf("%d%d%d%d",&n,&m,&v,&e);
//Memset
	for(int i=1;i<=v;i++)for(int j=1;j<=v;j++)Dis[i][j]=inf;
	for(int i=0;i<=n;i++)for(int j=0;j<=m;j++)dp[i][j][0]=dp[i][j][1]=1e18;
	for(int i=1;i<=v;i++)Dis[i][i]=0;
//Scan
	for(int i=1;i<=n;i++)scanf("%d",&c[i]);
	for(int i=1;i<=n;i++)scanf("%d",&d[i]);
	for(int i=1;i<=n;i++)scanf("%lf",&shit[i]);
	for(int i=1;i<=e;i++)scanf("%d%d%d",&l,&r,&w),Dis[l][r]=Dis[r][l]=min(Dis[l][r],w);
//Floyd
	for(int tmp=1;tmp<=v;tmp++)for(int i=1;i<=v;i++)for(int j=1;j<=v;j++)Dis[i][j]=min(Dis[i][j],Dis[i][tmp]+Dis[tmp][j]);
//Dp	
	dp[1][m][0]=0;
	if(m>0)dp[1][m-1][1]=0;
	for(int i=2;i<=n;i++)for(int j=max(m-i,0);j<=m;j++){
		dp[i][j][0]=min(dp[i-1][j][0]+OwO(i,i-1),dp[i-1][j][1]+OwQ(i,i-1));
		if(j<m)dp[i][j][1]=min(dp[i-1][j+1][0]+OwQ(i-1,i),dp[i-1][j+1][1]+QwQ(i,i-1));
	}
//Get Ans
	double ans=1e18;
	for(int i=0;i<=m;i++)ans=min(min(ans,dp[n][i][0]),dp[n][i][1]);
	printf("%.2lf\n",ans);
	return 0;
}

我们发现其中 \(dp\) 的第一维可以压缩掉优化时间复杂度

#include<bits/stdc++.h>
using namespace std;
typedef double db;
const int N=2009,K=303,inf=1000000009;
int n,m,v,e,c[N],d[N],Dis[K][K];
db shit[N],dp[N][2];
inline db OwO(int l,int r){return (db)Dis[c[l]][c[r]];}
inline db OwQ(int l,int r){return (db)Dis[c[l]][d[r]]*shit[r]+(db)Dis[c[l]][c[r]]*(1.0-shit[r]);}
inline db QwQ(int l,int r){return (db)Dis[d[l]][d[r]]*shit[l]*shit[r]+(db)Dis[c[l]][d[r]]*shit[r]*(1.0-shit[l])+(db)Dis[d[l]][c[r]]*shit[l]*(1.0-shit[r])+(db)Dis[c[l]][c[r]]*(1.0-shit[l])*(1.0-shit[r]);}
int main(){
	int l,r,w;
	scanf("%d%d%d%d",&n,&m,&v,&e);
	for(int i=1;i<=v;i++)for(int j=1;j<=v;j++)Dis[i][j]=inf;
	for(int i=0;i<=m;i++)dp[i][0]=dp[i][1]=1e18;
	for(int i=1;i<=v;i++)Dis[i][i]=0;
	for(int i=1;i<=n;i++)scanf("%d",&c[i]);
	for(int i=1;i<=n;i++)scanf("%d",&d[i]);
	for(int i=1;i<=n;i++)scanf("%lf",&shit[i]);
	for(int i=1;i<=e;i++)scanf("%d%d%d",&l,&r,&w),Dis[l][r]=Dis[r][l]=min(Dis[l][r],w);
	for(int tmp=1;tmp<=v;tmp++)for(int i=1;i<=v;i++)for(int j=1;j<=v;j++)Dis[i][j]=min(Dis[i][j],Dis[i][tmp]+Dis[tmp][j]);	
	dp[m][0]=0;
	if(m>0)dp[m-1][1]=0;
	for(int i=2;i<=n;i++)for(int j=max(m-i,0);j<=m;j++){
		dp[j][0]=min(dp[j][0]+OwO(i,i-1),dp[j][1]+OwQ(i,i-1));
		if(j<m)dp[j][1]=min(dp[j+1][0]+OwQ(i-1,i),dp[j+1][1]+QwQ(i,i-1));
	}
	double ans=1e18;
	for(int i=0;i<=m;i++)ans=min(min(ans,dp[i][0]),dp[i][1]);
	printf("%.2lf\n",ans);
	return 0;
}

轻松跑到最优解第二页

P2473 奖励关

一眼状压+期望
DP 就很明显啦
\(DP[i][status]=max(DP[i-1][status],DP[i-1][status]+value,DP[i-1][status-current]+value)\)
但这要是个正经状压是对的
可是它是个期望
所以我们对于每一种情况取最优
如果没有就默认不取
然后还需要除以 n
然后你可能发现你调了很久却始终过不了样例
此处省略作者惨痛经历
然后你翻了几篇 \(TJ\)
发现 dalao 们一个倒推然后就过了
只留下一个蒟蒻作者在风中凌乱
先放个 AC 代码

#include<bits/stdc++.h>
using namespace std;
int n,k,need[17],val[17];
double dp[153][32780];
int main(){
	int tmp;
	scanf("%d%d",&k,&n);
	for(int i=1;i<=n;i++){
		scanf("%d%d",&val[i],&tmp);
		while(tmp)need[i]|=(1<<(tmp-1)),scanf("%d",&tmp);
	}
	for(int i=k;i>=1;i--)for(int st=0;st<(1<<n);st++){
		for(int j=1;j<=n;j++){
			if((need[j]&st)!=need[j])dp[i][st]+=dp[i+1][st];
			else dp[i][st]+=max(dp[i+1][st],dp[i+1][st|(1<<(j-1))]+val[j]);
		}
		dp[i][st]=dp[i][st]/n;
		//printf("dp[%d][%d]=%.6lf\n",i,st,dp[i][st]);
	}
	printf("%.6lf",dp[1][0]);
	return 0;
}

但是当你 A 掉它之后
你就会有一个疑问:Why?
是的
你会觉得这个代码不就是把东西倒着算了一遍,有啥区别呢?
然而并不是
因为你可能会发现朴素顺推它并不能排除一些奇葩情况
然后你就会加很多奇奇怪怪的局部补偿修正却始终无法过大样例
或者摆明了说
由于奖品之间恶心的相关性质
我们无法保证这一轮能到达对应的状态
是的!无论你用 \(\_\_builtin\_popcount\) 还是什么类似的检测手段
你都会因为一些恶心的问题而无法通过!
但为什么倒推能过呢
因为我们最终输出了 \(dp[1][0]\)
如果非法的情况存在那他就无法推到 \(dp[1][0]\)
也就无法影响答案
或者其实作者也只是口胡一下
也可以把她当一个 \(trick\)
引用一位洛谷大佬的话

转移通常分两种,一种是从上一个状态转移得(填表法),另一种是转移向下一个状态(刷表法)。
一般来说,初始状态确定时可用顺推,终止状态确定时可用逆推。
如果你实在过不了,请使用方法:咋推过了就咋推

P4316 绿豆蛙的归宿

本题还是比较简单的
但这道题很好
经典永流传
首先这道题有很多很好的性质
起点为 \(1\), 终点为 \(n\), 还是 \(DAG\) 。。。
一眼拓扑排序
然后我们考虑顺推刷表法
我们发现这题有个很不一样的东西
就是他需要维护一个概率
我们考虑拓扑两次
第一次搞他的概率
第二次再搞每个节点到一号结点的期望
用定义去算就好了

#include<bits/stdc++.h>
using namespace std;
const int N=100006;
int n,m,In[N],cnt[N];
double G[N],Dis[N];
struct node{int i,d;};
vector<node> k[N];
void DFS1(int u){
	for(node i:k[u]){
		G[i.i]+=G[u]/(double)k[u].size();
		if(++cnt[i.i]==In[i.i])DFS1(i.i);
	}
}
void DFS2(int u){
	for(node i:k[u]){
		Dis[i.i]+=(Dis[u]+i.d)*G[u]/k[u].size()/G[i.i];
		if(++cnt[i.i]==In[i.i])DFS2(i.i);
	}
}
int main(){
	int l,r,w;
	scanf("%d%d",&n,&m),G[1]=1,Dis[1]=0;
	for(int i=1;i<=m;i++)scanf("%d%d%d",&l,&r,&w),k[l].push_back({r,w}),++In[r];
	DFS1(1);
	for(int i=1;i<=n;i++)cnt[i]=0;
	DFS2(1);
	printf("%.2lf",Dis[n]);
	return 0;
}

P3232 [HNOI2013] 游走

劝君更进一杯酒,西出阳关无故人。

从此开始,我们要开始上强度了。
要搞这个,你首先需要会一个高斯消元。
我们先不管那些没有用的
看看这题咋做
首先这题有一个显而易见的贪心
我们把每条边的经过次数期望搞出来
然后从大到小排序,从小到大标号即可
然后有一个 \(\text{trick}\)
首先我们发现边很多
要是搞 \(\text{DP}\) 的话可能会炸
然后我们就转而处理点的经过次数期望
因为一条边只有两个端点
所以很容易就可以处理
接下来我们搞点就好了
我们每个点的转移是什么呢?
我们如果在搞一遍概率一边期望会发现这样很麻烦
能不能只搞一个期望呢
我们润题解发现有一个玄妙的式子

\[f(i)=\frac{\sum_{j \in To(i)}f(j)}{Deg(i)}+[i==1] \]

这个东西其实可以理解成从 \(i\) 号点把他周围的东西 "抓" 过来
但有一点恶心的是
这个东西是有后效性的
我们先把 \(Deg[i]\) 乘过来

\[Deg(i)f(i)-\sum_{j \in To(i)}f(j)-[i==1]=0 \]

注意所有的 \(f(j)\) 中都不会统计 \(f(n)\)
因为此时他就不会往回走了
我们用高消把 \(f(1\to n-1)\) 求出来
然后我们就可以算出 \(f(n)\)
接下来

\[g(i,j)=\frac{f_i}{deg_i}\frac{f_j}{deg_j} \]

sort 一下然后就没了
至于高斯消元。。。

高斯削元

模板题1
模板题2
我们用最快的速度搞完他
首先我们把一个方程组的系数给他搞出来
然后我们枚举每一条方程式
将他之中绝对值最大的系数搞出来
如果他里头没有非零系数那么分成两种情况

  1. 他的常数项不为零,即 \(0\neq 0\) 此时直接返回无解
  2. 他的常数项为零,此时 如果其他方程没有无解那么就无限解

我忘了这高斯消元是啥时候学的了
只觉得当时的老师 YYDS

\(\text{P3389 AC Code}\)

#include<bits/stdc++.h>
using namespace std;
typedef double db;
const double eps=1e-3;
int n;
db k[502][502],ans[502];
int main(){
	int tmp;
	double f;
	scanf("%d",&n);
	for(int i=1;i<=n;i++) for(int j=1;j<=n+1;j++) scanf("%d",&tmp),k[i][j]=tmp;
	for(int i=1;i<=n;i++){
		tmp=0;
		for(int j=1;j<=n;j++)if(fabs(k[i][j])-fabs(k[i][tmp])>eps)tmp=j;
		if(tmp==0)puts("No Solution"),exit(0);
		for(int j=1;j<=n;j++){
			if(i==j)continue;
			f=k[j][tmp]/k[i][tmp];
			for(int c=1;c<=n+1;c++)k[j][c]-=f*k[i][c];
		}
	}
	for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) if(fabs(k[i][j])>eps) {ans[j]=k[i][n+1]/k[i][j];break;}
	for(int i=1;i<=n;i++) printf("%.2lf\n",ans[i]);
	return 0;
}

\(\text{P2455 AC Code}\)


posted @ 2025-03-30 18:34  2025ing  阅读(37)  评论(0)    收藏  举报