概率 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\) 之间是没有关系的
有了基本性质
我们就容易进行转移了
式子看起来很癫
其中的 \(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}\) 的话可能会炸
然后我们就转而处理点的经过次数期望
因为一条边只有两个端点
所以很容易就可以处理
接下来我们搞点就好了
我们每个点的转移是什么呢?
我们如果在搞一遍概率一边期望会发现这样很麻烦
能不能只搞一个期望呢
我们润题解发现有一个玄妙的式子
这个东西其实可以理解成从 \(i\) 号点把他周围的东西 "抓" 过来
但有一点恶心的是
这个东西是有后效性的
我们先把 \(Deg[i]\) 乘过来
注意所有的 \(f(j)\) 中都不会统计 \(f(n)\)
因为此时他就不会往回走了
我们用高消把 \(f(1\to n-1)\) 求出来
然后我们就可以算出 \(f(n)\) 了
接下来
sort 一下然后就没了
至于高斯消元。。。
高斯削元
模板题1
模板题2
我们用最快的速度搞完他
首先我们把一个方程组的系数给他搞出来
然后我们枚举每一条方程式
将他之中绝对值最大的系数搞出来
如果他里头没有非零系数那么分成两种情况
- 他的常数项不为零,即 \(0\neq 0\) 此时直接返回无解
- 他的常数项为零,此时 如果其他方程没有无解那么就无限解
我忘了这高斯消元是啥时候学的了
只觉得当时的老师 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}\)