概率与期望
概率期望
P4316 绿豆蛙的归宿
非常基础的概率期望,设 \(f_i\) 表示点i到点n的期望总长度,由于要求起点期望,所以反向连边,倒序更新:\(f_i = \sum_{j \in son_i} \frac{f_j+val_{i,j}}{d_i}\) ,代码如下:
#include<bits/stdc++.h>
using namespace std;
const long long MAXN=1e5+5;
long long n,m,cd[MAXN],rd[MAXN];
long double dp[MAXN];
struct node{
long long next,to,dis;
}e[MAXN<<1];
long long head[MAXN],cnt;
void add(long long a,long long b,long long c){
e[++cnt].to=b;
e[cnt].next=head[a];
e[cnt].dis=c;
head[a]=cnt;
}
queue<long long> q;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>n>>m;
if(n==1){
cout<<"0.00";
return 0;
}
for(long long i=1;i<=m;i++){
long long x,y,z;
cin>>x>>y>>z;
add(y,x,z);
cd[x]++,rd[x]++;
}
dp[n]=0.0;q.push(n);
while(!q.empty()){
long long x=q.front();q.pop();
for(long long i=head[x];i;i=e[i].next){
long long y=e[i].to;
if(cd[y]) dp[y]+=(dp[x]+e[i].dis)/(1.0*cd[y]);
rd[y]--;
if(!rd[y]) q.push(y);
}
}
cout<<fixed<<setprecision(2)<<dp[1];
return 0;
}
P1654 OSU!
设 \(f_i\) 表示长度为i的串期望得到的值,则答案为 \(f_n\) :
考虑转移方程,对于长度为 \(x\) 的串,其贡献为 \(x^3\) ,对于长度为 \(x+1\) 的串,其贡献为 $(x+1)^3 = x^3 + 3x^2 + 3x + 1 $,则增加的贡献为 \(3x^2 + 3x + 1\);
那么我们就可以维护增加的贡献值,设 \(x1c\) 表示式子中的一次期望 ,\(x2c\) 表示式子中的二次期望,逐个更新答案即可;
代码如下:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int MAXN=1e5;
ll n;
long double p[MAXN],x1c[MAXN],x2c[MAXN],ans[MAXN];
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>p[i];
for(int i=1;i<=n;i++){
x1c[i]=(x1c[i-1]+1)*p[i];
x2c[i]=(x2c[i-1]+2*x1c[i-1]+1)*p[i];
ans[i]=ans[i-1]+(3*x2c[i-1]+3*x1c[i-1]+1)*p[i];
}
cout<<fixed<<setprecision(1)<<ans[n];
return 0;
}
二倍经验:P1365 WJMZBMR打osu! / Easy,对于不确定的问号,对于连续串长度再取一个期望即可;
P4206 [NOI2005] 聪聪与可可
很有意思的一道期望DP;
容易想到需要先预处理出聪聪的行走逻辑 \(nxt_{i,j}\) ,即对于任意一种两者的位置关系,聪聪下一步会选择的行走节点,即是在聪聪所在点的所有相连点中,与可可所在点距离最短,且编号最小的点(若\(i==j\),则 \(nxt_{i,j}=j\));
注意到 \(n<=2000\) ,floyd会超时;但同时 \(m<=2000\),对于稀疏图dijkstra的时间复杂度是优于floyd的,我们可以跑n遍堆优化dijkstra来处理处任意两点的距离;
设 \(dp_{u,v}\) 表示聪聪在点 \(u\) ,可可在点 \(v\) 时聪聪抓到可可的期望步数,设 \(nxm=nxt_{nxt_{u,v},v}\) 表示聪聪下一次到达的点, 可得状态转移方程:
1、\(dp_{u,v}=0 (nxm=v,u=v)\)
2、\(dp_{u,v}=1 (nxm=v,u \neq v)\)
3、\(dp_{u,v}= \frac{1}{d_v +1} * dp_{nxm,v} + \sum_{w \in son_v} \frac{1}{d_v +1} * dp_{nxm,w} (nxm \neq v)\)
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5;
int n,E,c,m,nxt[1005][1005],dis[1005][1005],d[1005];
int flag[1005][1005],vis[1005][1005];
double dp[1005][1005],ans;
struct edge{
int next,to;
}e[MAXN<<1];
int head[MAXN],cnt;
void add(int a,int b){
e[++cnt].to=b;
e[cnt].next=head[a];
head[a]=cnt;
}
struct node{
int d,pos;
bool operator < (const node &x)const{
return x.d<d;
}
};
double dfs(int u,int v){
if(flag[u][v]) return dp[u][v];
if(u==v) return 0;
int nxm=nxt[nxt[u][v]][v];
if(nxm==v) return 1;
dp[u][v]=1;
for(int i=head[v];i;i=e[i].next){
int w=e[i].to;
dp[u][v]+=dfs(nxm,w)/(d[v]+1);
}
dp[u][v]+=dfs(nxm,v)/(d[v]+1);
flag[u][v]=1;
return dp[u][v];
}
void dijkstra(int s){
dis[s][s]=0;
priority_queue<node> q;
q.push((node){0,s});
while(!q.empty()){
node tmp=q.top();
q.pop();
int x=tmp.pos;
if(vis[s][x]) continue;
vis[s][x]=1;
for(int i=head[x];i;i=e[i].next){
int y=e[i].to;
if(dis[s][y]>dis[s][x]+1){
dis[s][y]=dis[s][x]+1;
if(!vis[s][y]) q.push((node){dis[s][y],y});
}
}
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>n>>E>>c>>m;
dp[m][c]=0.0;
memset(dis,0x3f,sizeof dis);
memset(vis,0,sizeof vis);
for(int i=1;i<=n;i++) dis[i][i]=0;
for(int i=1;i<=E;i++){
int x,y;cin>>x>>y;
add(x,y);add(y,x);
d[x]++,d[y]++;
}
for(int i=1;i<=n;i++)
dijkstra(i);
for(int x=1;x<=n;x++){
for(int p=1;p<=n;p++){
if(x==p){
nxt[x][p]=p;
continue;
}
int minl=1e8,mind;
for(int i=head[x];i;i=e[i].next){
int y=e[i].to;
if(dis[y][p]<minl) minl=dis[y][p],mind=y;
else if(dis[y][p]==minl&&y<mind) mind=y;
}
nxt[x][p]=mind;
}
}
cout<<fixed<<setprecision(3)<<dfs(c,m);
return 0;
}
树
简单朴实的名字总让人感到不安
原题找不到了,就用这个代替吧
树上操作+多次询问,容易联想到lca,事实的确如此。我们预处理出 \(f_i\) 表示点 \(i\) 转移到 其父亲的期望步数,\(g_i\) 表示点 \(i\) 从其父亲转移来的期望步数,用lca+前缀和优化就可以方便的求出每次询问的答案;
为啥这么少,因为真的懒得打了 代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+10;
const int mod=1e9+7;
int n,q,head[MAXN],cnt,fa[MAXN],c[MAXN],dep[MAXN];
int par[MAXN][25];
int f[MAXN],g[MAXN];//f_i 表示从i转移到fa_i的期望步数; g_i 表示从fa_i转移到i的期望步数
struct edge{
int next,to;
}e[MAXN<<1];
struct node{
int u,v;
}query[MAXN];
void add(int a,int b){
e[++cnt].to=b;
e[cnt].next=head[a];
head[a]=cnt;
}
void dfs_getf(int x,int ff){
par[x][0]=ff,dep[x]=dep[ff]+1;
fa[x]=ff,f[x]=0;
for(int i=head[x];i;i=e[i].next){
int y=e[i].to;
if(y==ff) continue;
dfs_getf(y,x);
f[x]=(f[x]+f[y]+1)%mod;
}
f[x]=(f[x]+1)%mod;
return ;
}
void dfs_getg(int x){
if(x!=1){
g[x]+=g[fa[x]],g[x]%=mod;
for(int i=head[fa[x]];i;i=e[i].next){
int y=e[i].to;
if(y!=x&&y!=fa[fa[x]]) g[x]=(g[x]+f[y])%mod;
g[x]=(g[x]+1)%mod;
}
}
for(int i=head[x];i;i=e[i].next){
int y=e[i].to;
if(y==fa[x]) continue;
dfs_getg(y);
}
return ;
}
void dfs(int x){
f[x]=(f[x]+f[fa[x]])%mod;
g[x]=(g[x]+g[fa[x]])%mod;
for(int i=head[x];i;i=e[i].next){
int y=e[i].to;
if(y==fa[x]) continue;
dfs(y);
}
}
void getp(){
for(int j=1;(1<<j)<=n;j++)
for(int i=1;i<=n;i++)
par[i][j]=par[par[i][j-1]][j-1];
return ;
}
int lca(int u,int v){
if(dep[u]>dep[v]) swap(u,v);
for(int i=22;i>=0;i--){
if(dep[par[v][i]]>=dep[u]){
v=par[v][i];
}
}
if(u==v) return u;
for(int i=22;i>=0;i--){
if(par[u][i]!=par[v][i]){
u=par[u][i],v=par[v][i];
}
}
return par[u][0];
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>n>>q;
for(int i=1,x,y;i<n;i++){
cin>>x>>y;
add(x,y);add(y,x);
c[x]++,c[y]++;
}
dfs_getf(1,0);
f[1]=g[1]=0;
dfs_getg(1);
dfs(1);
getp();
for(int i=1,u,v;i<=q;i++){
cin>>u>>v;
int LCA=lca(u,v);
cout<<(long long)(((f[u]-f[LCA])%mod+(g[v]-g[LCA])%mod)+mod)%mod<<"\n";
}
// for(int i=1;i<=n;i++)
// cout<<f[i]<<" "<<g[i]<<"\n";
return 0;
}
概率期望&高斯消元
DP序混乱,往往是指存在两个状态x,y,x可以转移到y,y也可以转移到x;
在非概率/期望DP中不可能存在这种情况,因为这会导致状态值不确定;
然而在概率/期望DP中这种情况却可以存在,一般是x的p/q可以转移到y,y的a/b可以转移到x(即一个无向图);
有一个显然的想法是经过多次转移后逼近精确值,但这样的时间复杂度显然过大,精度也非常容易被卡;
所以我们可以列出多个状态的转移式(这其中必然有循环转移的情况),变式使等式右边成为一个可求值,接下来就可以用高斯消元解出方程组了。
例题:
P3232 [HNOI2013] 游走
题意:给出一个无向图,从1号点到n号点,给每条边编号(从1到m),经过代价为该边编号,求最低期望价值
容易想到求出每条边的期望经过次数然后排序,次数最多的边编号最小,以此类推;
设 \(E(u,v)\) 表示双端节点为 \(u,v\) 的期望经过次数, \(f_i\) 表示点 \(i\) 的期望经过次数 , \(siz_i\) 表示点 \(i\) 的度;
容易得到: \(E(u,v)= \frac{f_u}{siz_u} + \frac{f_v}{siz_v}\)
因为点的度是已知的,所以重点就是求点的期望经过次数;
对于点n,因为走到它时就会停止,所以 \(f_n=0\) ,在后续计算中也不考虑它;
对于点1,因为是起点,所以必定至少经过1次,那么 \(f_1= \sum \frac{f_v}{siz_v} +1 ( v 与 1 相邻)\)
对于其他点,\(f_u= \sum \frac{f_v}{siz_v} ( v 与 u 相邻)\)
为了方便高斯消元,以上式子等价于
\(-f_i + \sum \frac{f_v}{siz_v} = -1 (i=1)\)
\(-f_u + \sum \frac{f_v}{siz_v} = 0 (1<i<n)\)
这样就非常好写了
#include<bits/stdc++.h>
using namespace std;
const int N=5e2+10,M=13e4+10;
const double eps=1e-6;
int n,m;
vector<int> q[N];
double zg[N][N],f[N],siz[N],ans;
struct edge{
int u,v;
double val;
}E[M];
bool cmp(edge a,edge b){
return a.val>b.val;
}
bool gauss(){
for(int i=1;i<=n;i++){
int r=i;
for(int k=i;k<=n;k++)
if(fabs(zg[k][i])>eps){
r=k;break;
}
if(r!=i) swap(zg[r],zg[i]);
if(fabs(zg[i][i])<eps) return 0;
for(int j=n+1;j>=i;j--)
zg[i][j]/=zg[i][i];
for(int k=i+1;k<=n;k++)
for(int j=n+1;j>=i;j--)
zg[k][j]-=zg[k][i]*zg[i][j];
}
for(int i=n-1;i>=1;i--)
for(int j=i+1;j<=n;j++)
zg[i][n+1]-=zg[i][j]*zg[j][n+1];
return 1;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>E[i].u>>E[i].v;
q[E[i].u].push_back(E[i].v);
q[E[i].v].push_back(E[i].u);
siz[E[i].u]++,siz[E[i].v]++;
}
n--;
zg[1][n+1]=-1.0;
for(int i=1;i<=n;i++){
zg[i][i]=-1.0;
for(int j=0;j<q[i].size();j++){
if(q[i][j]==n+1) continue;
zg[i][q[i][j]]=(1.0/siz[q[i][j]]);
}
}
gauss();
for(int i=1;i<=n;i++) f[i]=zg[i][n+1];
for(int i=1;i<=m;i++) E[i].val=f[E[i].v]/siz[E[i].v]+f[E[i].u]/siz[E[i].u];
sort(E+1,E+m+1,cmp);
for(int i=1;i<=m;i++){
ans+=E[i].val*i;
}
cout<<fixed<<setprecision(3)<<ans;
return 0;
}
P3211 [HNOI2011] XOR和路径
期望值异或恐怖如斯
状态其实是非常好想的,设 \(f_i\) 表示从点 \(i\) 出发到达点 \(n\) 的期望值,\(val_{i,j}\) 表示边 \(i,j\) 的值,\(d_i\) 表示点 \(i\) 的度,理所当然的:
$f_i= \sum_{j \in son_i} \frac{val_{i,j} \oplus f_j}{d_i} $
即:$d_i*f_i= \sum_{j \in son_i} val_{i,j} \oplus f_j $
但是考虑到对期望值求异或和完全不可能非常困难,所以需要做一点优化;
因为每一位上的异或和结果是相互不影响的,所以我们可以对式子进行按位拆分,枚举答案的二进制位单独计算,此时\(f_i\)就表示答案当前位是1的概率,对于边的权值也按当前数位计算;
然后根据异或的性质,可以将式子拆成两种情况,即:
\(d_i*f_i= \sum_{val_{i,j}=0} f_j + \sum_{val_{i,j}=1} (1-f_j)\) ,因为对于异或运算 \(x \oplus y\),如果x=0(即此处 \(val_{i,j}=0)\),答案就是y的值(\(0 \oplus 1=1, 0 \oplus0=0\)),同理,如果x是1,答案就是(1-y)的值;
化简得:$d_i*f_i - \sum_{val_{i,j}=0} f_j+ \sum_{val_{i,j}=1} f_j = \sum_{val_{i,j}=1} 1 $
可以发现符合线性方程组的性质,接下来就可以用高斯消元解了
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e2+10,MAXM=1e4+10;
const double eps=1e-6;
int n,m,d[MAXN];
struct edge{
int next,to,dis;
}e[MAXM<<1];
int head[MAXN],cnt;
double f[MAXN],zg[MAXN][MAXN],ans;
void add(int a,int b,int c){
e[++cnt].to=b;
e[cnt].next=head[a];
e[cnt].dis=c;
head[a]=cnt;
}
void gauss(){
for(int i=1;i<=n;i++){
int r=i;
for(int k=i;k<=n;k++){
if(fabs(zg[k][i])>eps){
r=k;break;
}
}
if(r!=i) swap(zg[r],zg[i]);
for(int j=n+1;j>=i;j--)
zg[i][j]/=zg[i][i];
for(int k=i+1;k<=n;k++)
for(int j=n+1;j>=i;j--)
zg[k][j]-=zg[k][i]*zg[i][j];
}
for(int i=n-1;i>=1;i--)
for(int j=i+1;j<=n;j++)
zg[i][n+1]-=zg[i][j]*zg[j][n+1];
return ;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>n>>m;
for(int i=1,u,v,w;i<=m;i++){
cin>>u>>v>>w;
add(u,v,w);if(u!=v) add(v,u,w);
d[u]++;if(u!=v) d[v]++;
}
for(int k=1;k<=30;k++){
memset(zg,0,sizeof zg);
for(int x=1;x<n;x++){
int sum=0;
zg[x][x]=d[x];
for(int i=head[x];i;i=e[i].next){
int y=e[i].to;
if(e[i].dis&(1<<(k-1))) zg[x][y]+=1,zg[x][n+1]++;
else zg[x][y]-=1;
}
}
zg[n][n]=-1;
gauss();
ans+=zg[1][n+1]*(1<<(k-1));
}
cout<<fixed<<setprecision(3)<<ans;
return 0;
}
本文来自博客园,作者:zhangch_qwq,转载请注明原文链接:https://www.cnblogs.com/zhangchenhua-awa/p/18905969

浙公网安备 33010602011771号