概率期望基础
在概率论和统计学中,数学期望(mathematic expectation )(或均值,亦简称期望)是试验中每次可能结果的概率乘以其结果的总和,是最基本的数学特征之一。它反映随机变量平均取值的大小。
期望具有线性性质,所以我们可以很方便的求解。
P4316 绿豆蛙的归宿
这题就是教你求期望,在 DAG 上,容易想到拓扑排序后跑 DP。
设 \(f_i\) 表示从起点到 \(i\) 点的期望,\(p_i\) 表示从起点到 \(i\) 点的概率。显然有状态转移方程
#include<bits/stdc++.h>
inline int read(){
char ch=getchar();int x=0,f=1;
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
const int N=1e5+10;
int n,m,tot,head[N],out[N],in[N];
struct edge{int v,next,w;}e[N<<1];
inline void add(int w,int v,int u){e[++tot]={v,head[u],w};head[u]=tot;out[u]++;in[v]++;}
double ans,f[N],p[N];
inline void topo(){
std::queue<int> q;
for(int i=1;i<=n;++i)if(!in[i])q.push(i);
p[1]=1;
while(!q.empty()){
int u=q.front();q.pop();
for(int i=head[u];i;i=e[i].next){
int v=e[i].v;
f[v]+=(f[u]+e[i].w*p[u])/out[u];
p[v]+=p[u]/out[u];
if(!--in[v])q.push(v);
}
}
}
int main(){
// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
n=read(),m=read();
for(int i=1;i<=m;++i){
add(read(),read(),read());
}
topo();
printf("%.2lf\n",f[n]);
}
P4206 [NOI2005] 聪聪与可可
提前处理好各个位置的猫的一二步走法,然后直接设 \(f_{i,j}\) 表示猫在 \(i\),老鼠在 \(j\) 的期望,有 \(f_{i,j}=\sum \frac{f_{x,y}}{du_j+1}+1\),\(y\) 表示老鼠的可能位置,\(x\) 表示老鼠在 \(y\) 时猫将到达的位置。
当猫能一步或两步到达时 \(f_{i,j}=1\),同点时 \(f_{i,j}=0\)。
#include <bits/stdc++.h>//没写这道题,he的。
using namespace std;
const int N=2010;
const double eps=1e-8;
struct edge{
int u,v;
}road[N];
int n,m,st,en;
int tot,first[N],nex[N],out[N];
int deep[N],vis[N],step[N][N];
double f[N][N];
queue <int> q;
void Add(int x,int y)
{
nex[++tot]=first[x];
first[x]=tot;
road[tot].u=x;
road[tot].v=y;
}
double DFS(int x,int y) //记忆化搜索即可
{
if(x==y) return 0.0; //已经抓到,期望为0
if(step[x][y]==y||step[step[x][y]][y]==y) return 1.0; //下一步即可捉到,期望为1
if(!(fabs(f[x][y])<eps)) return f[x][y]; //已经算过
double sum=DFS(step[step[x][y]][y],y); //原地不动
for(int i=first[y];i!=-1;i=nex[i])
sum+=DFS(step[step[x][y]][y],road[i].v); //枚举选择
return f[x][y]=sum/(out[y]+1.0)+1.0; //可可可以不动,最后加1与扑克同理可证
}
void Get_Step(int point) //step[i][j]表示当可可在j位置聪聪在i位置时聪聪的选择
{
memset(vis,0,sizeof(vis));
memset(deep,127,sizeof(deep));
q.push(point);
vis[point]=1;
deep[point]=0;
while(!q.empty()){
int x=q.front();
vis[x]=0;
for(int i=first[x];i!=-1;i=nex[i]){
int to=road[i].v;
if(!vis[to]&&deep[to]>deep[x]+1){
deep[to]=deep[x]+1; //更新深度
step[to][point]=x; //记录目标点
q.push(to); //由于深度发生变化,重新入对
vis[to]=1; //标记为已入队
}
else{
if(deep[to]==deep[x]+1)
if(x<step[to][point]) step[to][point]=x; //更新为下标更小的点
}
}
q.pop();
}
}
int main()
{
memset(first,-1,sizeof(first));
scanf("%d%d",&n,&m);
scanf("%d%d",&st,&en);
int x,y;
for(int i=1;i<=m;i++){
scanf("%d%d",&x,&y);
Add(x,y); Add(y,x);
out[x]++; out[y]++; //统计出度
}
for(int i=1;i<=n;i++) //枚举每个点,当成可可的位置进行BFS,求解step数组
Get_Step(i);
printf("%.3lf",DFS(st,en)); //记忆化搜索答案易得
return 0;
}
P1654 OSU!
设 \(f_i\) 表示到第 \(i\) 个位置时的期望,经过思考后发现因为取立方的缘故,不是很好转移。考虑将式子展开。
\((x+1)^3=x^3+3\cdot x^2+3\cdot x+1\) 不难发现只多了 \(3\cdot x^3+3\cdot x+1\) ,考虑将它们分别维护出来。
设 \(x1_i\) 表示 \(x\),设 \(x2_i\) 表示 \(x^2\),有
$x1_i=(x1_{i-1}+1)\cdot p_i $
\(x2_i=(x2_{i-1}+2\cdot x1_{i-1}+1)\cdot p_i\)
\(f_i=f_{i-1}+[3\cdot(x1_{i-1}+x2_{i-1})+1]\cdot p_i\)
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 111111
#define int long long
using namespace std;
int n;
double p[maxn];
double x1[maxn],x2[maxn],ans[maxn];
signed main()
{
scanf("%lld",&n);
for(int i=1;i<=n;i++)
scanf("%lf",&p[i]);
for(int i=1;i<=n;i++){
x1[i]=(x1[i-1]+1)*p[i];
x2[i]=(x2[i-1]+2*x1[i-1]+1)*p[i];
ans[i]=ans[i-1]+(3*(x1[i-1]+x2[i-1])+1)*p[i];
}
printf("%.1lf",ans[n]);
return 0;
}
时间复杂度 \(O(n)\),观察上面的式子,发现它还可以拿矩阵快速幂来优化,故时间复杂度可以降到 \(O(n\log n)\)
本人的做法通过打表找到了一定的规律,发现每次贡献都会多一个系数为 \(6\) 的贡献,实际上原理一样,故只贴代码,不多赘述了。
#include<bits/stdc++.h>
inline int read(){
char ch=getchar();int x=0,f=1;
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
const int N=1e5+10;
int n;
double p[N],f[N],zc,last,y;
int main(){
// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
scanf("%d",&n);
for(int i=1;i<=n;++i)scanf("%lf",p+i);
f[1]=1*p[1];last=1;zc=0;y=0;
for(int i=2;i<=n;++i){
last*=p[i-1];y*=p[i-1];y+=6*p[i-1]*(1-p[i-2]);
zc*=p[i-1],zc+=y;
f[i]=f[i-1]*(1-p[i])+p[i]*(f[i-1]+(1-p[i-1])+zc+last);
last=(1-p[i-1])+zc+last;
}
printf("%.1lf",f[n]);
}
关于此题将 \(3\) 次方推广到 \(k\) 次方,要用到一些多项式的知识,感兴趣可以去看这里的一些讨论,此题还有一些多倍经验,如P1365 WJMZBMR打osu! / Easy 和Let's Play Osu!,做法大同小异,不多赘述。
BZOJ1419 Red is good
注意这里的最优策略指的是不知道牌的情况下平均全局最优策略,并不是每种情况拿最多。
设 \(f_{i,j}\) 表示要选 \(i\) 个红,\(j\) 个黑的时的最优期望,
显然有 \(f_{i,j}=\max \{0,(f_{i-1,j}+1)\cdot \frac{i}{i+j}+(f_{i,j-1}-1)\cdot \frac{j}{i+j}\}\)
当期望小于 \(0\) 时及时停止。
#include<bits/stdc++.h>
inline int read(){
char ch=getchar();int x=0,f=1;
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
const int N=5e3+10;
int a,b;
double f[N][N];
int main(){
// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
a=read(),b=read();
int now=0;
for(int i=0;i<=a;++i,now^=1,f[now][0]=i)
for(int j=1;j<=b;++j)
f[now][j]=std::max(0*1.0,(f[now^1][j]+1)*i/(i+j)+(f[now][j-1]-1)*j/(i+j));
now^=1;
printf("%.6lf\n",f[now][b]-5.0/1e7);
}
BZOJ3029 守卫者的挑战
数据看起来是跑不了 \(O(n^3)\) 的算法的,但是稍微观察不难发现对于所有的 \(k\geq n\) 都属于同一类数。
设 \(f_{i,j,k}\) 表示到了第 \(i\) 个挑战,赢了 \(j\) 次,当前背包容量为 \(k\) 时的概率。有
直接刷表就行了。
#include<bits/stdc++.h>
inline int read(){
char ch=getchar();int x=0,f=1;
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
const int N=205;
int n,l,K,a[N];
double p[N],f[N][N][N*2];
int main(){
// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
n=read();l=read();K=std::min(read(),n);
for(int i=1;i<=n;++i)p[i]=read()/100.0;
for(int i=1;i<=n;++i)a[i]=read();
f[0][0][K+n]=1;
for(int i=0;i<n;++i){
for(int j=0;j<=i;++j){
for(int k=0;k<=n*2;++k){
if(!f[i][j][k])continue;
if(k+a[i+1]>=n*2){
f[i+1][j+1][n*2]+=f[i][j][k]*p[i+1];
f[i+1][j][k]+=f[i][j][k]*(1-p[i+1]);
}else{
f[i+1][j+1][k+a[i+1]]+=f[i][j][k]*p[i+1];
f[i+1][j][k]+=f[i][j][k]*(1-p[i+1]);
}
}
}
}
double ans=0;
for(int i=l;i<=n;++i){
for(int j=n;j<=2*n;++j)ans+=f[n][i][j];
}
printf("%.6lf\n",ans);
}
BZOJ2720 列队春游
不要定式思维,不要一眼 DP,考虑数学,然后比较平凡。
直接考虑每个人对答案的贡献,枚举人,枚举这个人看到的视野,然后做完了。时间复杂度 \(O(n^3)\),然后稍微处理一下一些东西可以 \(O(n^2)\),推会式子可以做到 \(O(n)\),懒得写了。
#include<bits/stdc++.h>
inline int read(){
char ch=getchar();int x=0,f=1;
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
const int N=305;
int n,a[N],temp[N],_len,big[N],sm[N];
double ans=0;
int main(){
// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
n=read();
for(int i=1;i<=n;++i)a[i]=temp[i]=read();
std::stable_sort(temp+1,temp+n+1);_len=std::unique(temp+1,temp+n+1)-temp-1;
for(int i=1;i<=n;++i)a[i]=std::lower_bound(temp+1,temp+_len+1,a[i])-temp;
for(int i=1;i<=n;++i){
for(int j=1;j<=n;++j){
if(a[j]>=a[i])big[i]++;else sm[i]++;
}
big[i]--;
}
for(int i=1;i<=n;++i){
for(int j=1;j<=n;++j){
double p=1;int zc=sm[i];
for(int k=1;k<=j-1;++k){
ans+=k*p*big[i]/(n-k);
p*=zc*1.0/(n-k);
if(zc)zc--;
}
ans+=j*p;
}
}
printf("%.2lf\n",ans/n);
}
BZOJ2969 矩形粉刷
跟上面一样吧,直接考虑每个格子的贡献,找出有多少种方案它会有贡献,画画图发现就是一个简单的容斥,平凡题。
#include<bits/stdc++.h>
#define int long long
typedef long double Ld;
inline int read(){
char ch=getchar();int x=0,f=1;
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
const int N=1e3+10;
int k,w,h,tot,a[N][N];
double ans=0;
signed main(){
// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
k=read(),w=read(),h=read();
for(int i=1;i<=w;++i){
for(int j=1;j<=h;++j){
a[i][j]=i*j*(w-i+1)*(h-j+1)+i*(h-j+1)*j*(w-i+1)-i*(w-i+1)-j*(h-j+1)+1;
a[i][j]=a[i][j]*2-1;
a[i][j]=w*h*w*h-a[i][j];
}
}
for(int i=1;i<=w;++i){
for(int j=1;j<=h;++j){
double p=1;
for(int it=1;it<=k;++it){
p*=1.0*a[i][j]/w/w/h/h;
}
ans+=1.0*(1-p);
}
}
printf("%.0lf\n",ans);
}
P2059 [JLOI2013] 卡牌游戏
看着就是一个记忆化搜索(没写聪聪可可还是要在这题写记搜),直接设 \(f(tot,s)\) 表示省 \(tot\) 个人,第 \(s\) 个人为庄家时,这 \(tot\) 个人中没人获胜的概率。
显然有 \(f(tot,s)=\sum_{k=1}^m \frac{f(tot-1,new_s)}{m}\),比较平凡,注意然后按顺时针从庄家位置数第X个人将被处决即退出游戏,庄家算数的第一个。
#include<bits/stdc++.h>
inline int read(){
char ch=getchar();int x=0,f=1;
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
const int N=55;
int n,m,x[N];
double f[N][N][N];
bool vis[N][N];
inline void search(int tot,int s){
vis[tot][s]=1;
if(tot==1){f[tot][s][s]=1.0;return;}
for(int i=1;i<=m;++i){
int a=tot-1,c=((s+x[i]-1)%tot==0?tot:(s+x[i]-1)%tot);
int _new=(c%a==0?a:c%a);
if(!vis[a][_new]){search(a,_new);}
for(int i=1;i<c;++i){f[tot][s][i]+=1.0/m*f[a][_new][i];}
for(int i=c+1;i<=tot;++i){f[tot][s][i]+=1.0/m*f[a][_new][i-1];}
}
}
int main(){
std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
n=read(),m=read();
for(int i=1;i<=m;++i)x[i]=read();
search(n,1);
for(int i=1;i<=n-1;++i)printf("%.2lf%% ",f[n][1][i]*100);
printf("%.2lf%%",f[n][1][n]*100);
}
P1850 [NOIP2016 提高组] 换教室
先拿 Floyd 处理好全源最短路,然后直接设 \(f_{i,j,0/1}\) 表示到第 \(i\) 节课,申请了 \(j\) 次,这一次申请与否的最小代价期望。分类讨论上一次在哪,显然有
直接转移即可。
#include<bits/stdc++.h>
inline int read(){
char ch=getchar();int x=0,f=1;
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
const int N=2e3+10;
int n,m,v,E,c[N],d[N],e[305][305];
double k[N],f[N][N][2];
inline void floyd(){
for(int i=1;i<=v;++i)e[i][i]=0;
for(int k=1;k<=v;++k)
for(int i=1;i<=v;++i)
for(int j=1;j<=v;++j)
e[i][j]=std::min(e[i][j],e[i][k]+e[k][j]);
}
inline void work(){
memset(f,0x43,sizeof(f));
f[1][0][0]=f[1][1][1]=0;
for(int i=2;i<=n;++i){
for(int j=0;j<=m&&j<=i;++j){
if(j<=i-1){
f[i][j][0]=std::min(f[i][j][0],f[i-1][j][0]+e[c[i-1]][c[i]]);
f[i][j][0]=std::min(f[i][j][0],(f[i-1][j][1]+e[c[i-1]][c[i]])*(1.0-k[i-1])+(f[i-1][j][1]+e[d[i-1]][c[i]])*k[i-1]);
}
if(!j)continue;
double nw=(f[i-1][j-1][0]+e[c[i-1]][c[i]])*(1.0-k[i])+(f[i-1][j-1][0]+e[c[i-1]][d[i]])*k[i];
f[i][j][1]=std::min(f[i][j][1],nw);
nw=(f[i-1][j-1][1]+e[c[i-1]][c[i]])*(1.0-k[i])*(1.0-k[i-1])+
(f[i-1][j-1][1]+e[c[i-1]][d[i]])*(1.0-k[i-1])*(k[i])+
(f[i-1][j-1][1]+e[d[i-1]][c[i]])*(1.0-k[i])*(k[i-1])+
(f[i-1][j-1][1]+e[d[i-1]][d[i]])*k[i]*k[i-1];
f[i][j][1]=std::min(f[i][j][1],nw);
}
}
}
int main(){
// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
scanf("%d%d%d%d",&n,&m,&v,&E);
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",k+i);
memset(e,0x3f,sizeof(e));
for(int i=1;i<=E;++i){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
e[u][v]=e[v][u]=std::min(e[u][v],w);
}
floyd();work();
double ans=1e9;
for(int i=0;i<=m;++i)ans=std::min({ans,f[n][i][0],f[n][i][1]});
printf("%.2lf\n",ans);
}
P2473 [SCOI2008] 奖励关
male,从这道题开始才应该写题解,前面全是水题,好像就 red is good 值得写写,那我补了个几把的博客。
看到数据范围,考虑状压,但是我们不知道当前的物品是否应该拿(它对后面有影响),导致从这个思路出发很难转移,考虑逆推,设 \(f_{i,s}\) 表示选完了 \(i\) 到 \(k\) 这些次,且 \(i\) 次之前的物品状态为 \(s\) 时的期望。
当宝物能取时,有 \(f_{i,s}+=\max(f_{i+1,s},f_{i+1,s|(1<<k-1)}+p_k)\cdot \frac{1}{n}\)
不能取就直接从不去转移即可。
关于此题的逆推,如果正推的话,我们甚至都不知道每次的状态能否到达,并且还需要处理概率问题,比较棘手。
因为一个状态到后面状态的可能性是均分的,但是由从其他状态导入后一个状态时,由于题目条件的约束,其他状态的各个概率并不是均分的。而且这题的条件约束是个乱七八糟的集合,毫无规律,所以正推不了
但如果是逆推的话,保证了转移时的概率和为 \(1\),还可以做到最有决策。遇到期望DP,往往考虑逆推是一个不错的选择。
#include<bits/stdc++.h>
inline int read(){
char ch=getchar();int x=0,f=1;
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
const int N=1<<16;
int n,k,p[N],st[20],maxn,_p[20];
double f[105][N];
int main(){
// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
k=read(),n=read();
maxn=(1<<n)-1;
for(int i=1;i<=n;++i){
p[i]=read();int zc=read();
while(zc){st[i]|=(1<<zc-1);zc=read();}
}
for(int i=k;i;--i){
for(int s=0;s<=maxn;++s){
for(int k=1;k<=n;++k){
if((s&st[k])==st[k]){
f[i][s]+=std::max(f[i+1][s],f[i+1][s|(1<<k-1)]+p[k])*1.0/n;
}else f[i][s]+=f[i+1][s]*1.0/n;
}
}
}
printf("%.6lf\n",f[1][0]);
}
P4284 [SHOI2014] 概率充电器
很难想到的换根DP神仙题(其实是一个树形DP up and down 的trick)。
考虑每一个点怎样来电
- 自己来电了
- 儿子给它电了
- 父亲给它电了
因为儿子父亲都可以给电,很那难来转移,首先考虑暴力,设 \(f_i\) 表示一个点在前两个情况下来电的概率,不难发现如果我们钦定一个根的话,这个根没有父亲,所以只根据前两种情况我们可以求出根的正确答案,设初始状态 \(f_i=q_i\) 有状态转移方程 \(f_i+=f_i+f_{son}\cdot p_e-f_i\cdot f_{son}\cdot p_e\),(有一个小容斥),时间复杂度 \(O(n^2)\),只能拿到30分,考虑如何通过 \(f_i\) 处理出来真正的答案。
一个点真正的答案为三个情况来电的概率和,对于一个儿子,我们可以处理出来它的父亲不从这个儿子来电的概率为 \(pa\),设 \(zc=f_{son}\cdot p_e\),观察上面的式子,有 \(f_{fa}=pa+zc-pa\cdot zc\),所以 \(pa=\frac{f_{fa}-w}{1-w}\),所以儿子的真正答案为 \(f_{son}+pa\cdot p_e-f_{son}\cdot pa\cdot p_e\)。跑一遍换根即可。
大体来讲就是我们需要处理出来父亲不需要这个儿子时来电的概率,然后依靠这个来处理出父亲给这个儿子供电的概率,从而处理出来儿子的真正答案。
#include<bits/stdc++.h>
inline int read(){
char ch=getchar();int x=0,f=1;
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
const int N=5e5+10;
int n,tot,head[N];
double p[N],f[N];
struct EDGE{int v,next;double p;}e[N<<1];
inline void add(int u,int v,double p){e[++tot]={v,head[u],p/100},head[u]=tot;}
inline void dfs1(int x,int fa){
for(int i=head[x];i;i=e[i].next){
int y=e[i].v;
if(y!=fa){
dfs1(y,x);
double zc=f[y]*e[i].p;
f[x]=f[x]+zc-f[x]*zc;
}
}
}
inline void dfs2(int x,int fa){
for(int i=head[x];i;i=e[i].next){
int y=e[i].v;
if(y!=fa){
double zc=f[y]*e[i].p;
if(zc!=1){
double pa=e[i].p*(f[x]-zc)/(1-zc);
f[y]=f[y]+pa-f[y]*pa;
}
dfs2(y,x);
}
}
}
int main(){
// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
n=read();
for(int i=1;i<n;++i){
int u,v;double _p;
u=read(),v=read(),_p=read();
add(u,v,_p);add(v,u,_p);
}
for(int i=1;i<=n;++i)p[i]=read(),p[i]/=100,f[i]=p[i];
double ans=0;
dfs1(1,0);dfs2(1,0);
for(int i=1;i<=n;++i)ans+=f[i];
printf("%.6lf\n",ans);
}
感觉这题的转化确实比较牛。
P3232 [HNOI2013] 游走
边数太多,不好求,所以求点,设每个点的的期望经过次数为 \(f_i\),显然有 \(f_i=\sum_{k\in i} \frac{f_k}{d_k}(k\in i)\),特别的,对于节点 \(1\),有 \(f_1-1=\sum_{k\in i} \frac{f_k}{d_k}\),这个式子显然有后效性,直接拿高斯消元解就行,解完之后对于每条边就是 \(e_{i\to j}=\frac{f_i}{d_i}+\frac{f_j}{d_j}\),排完序后就完了,时间复杂度 \(O(n^3)\)。
#include<bits/stdc++.h>
inline int read(){
char ch=getchar();int x=0,f=1;
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
const int N=505;
int n,m,out[N];
double a[N][N],x[N],v[N*N];
std::vector<int> e[N];
std::pair<int,int> p[N*N];
inline double Abs(double x){return x<0?-x:x;}
inline bool ep(double x,double y){return Abs(x-y)<1e-9;}
inline void GUASS(){
int line=1;
for(int k=1;k<n;++k){
int maxi=line;
for(int i=line+1;i<n;++i)if(Abs(a[i][k])>Abs(a[maxi][k]))maxi=i;
if(ep(a[maxi][k],0))continue;
for(int i=1;i<=n;++i)std::swap(a[line][i],a[maxi][i]);
for(int i=1;i<n;++i){
if(i==line)continue;
double mul=a[i][k]/a[line][k];
for(int j=k;j<=n;++j)a[i][j]-=a[line][j]*mul;
}
++line;
}
for(int i=1;i<n;++i)x[i]=a[i][n]/a[i][i];
}
int main(){
// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
n=read(),m=read();
for(int i=1;i<=m;++i){
int u=read(),v=read();
out[u]++,out[v]++;
p[i]={u,v};
e[u].push_back(v),e[v].push_back(u);
}
for(int i=1;i<n;++i){
if(i==1)a[i][n]=-1;
a[i][i]=-1;
for(int x:e[i]){
if(x!=n)a[i][x]=1.0/out[x];
}
}
GUASS();
for(int i=1;i<=m;++i){
int u=p[i].first,w=p[i].second;
if(u!=n)v[i]+=x[u]/out[u];
if(w!=n)v[i]+=x[w]/out[w];
}
std::stable_sort(v+1,v+m+1);
double ans=0;
for(int i=1;i<=m;++i){
ans+=v[i]*(m-i+1);
}
printf("%.3lf\n",ans);
}
P3211 [HNOI2011] XOR和路径
这题跟上一题虽然比较像,但不可定式思维。
直接考虑的话发现无法进行异或计算,不如直接考虑最终答案的每一位为 \(1\) 的概率,按位考虑每个节点到终点的概率 \(f_i\),设边权的这一位为 \(w\),分为两种情况
随便导下式子然后直接拿高斯消元做就行,最后答案就是每位是 \(1\) 的贡献乘上概率就行。
#include<bits/stdc++.h>
inline int read(){
char ch=getchar();int x=0,f=1;
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
const int N=105;
int n,m,head[N],tot,out[N];
double a[N][N],x[N],w[N*N*2];
struct EDGE{int u,v,next,w;}e[N*N*2];
inline void add(int u,int v,int w){e[++tot]={u,v,head[u],w};head[u]=tot;}
namespace GUASS{
inline double Abs(double x){return x<0?-x:x;}
inline bool ep(double x,double y){return Abs(x-y)<1e-9;}
inline void sol(){
int line=1;
for(int k=1;k<n;++k){
int maxi=line;
for(int i=line+1;i<n;++i)if(Abs(a[i][k])>Abs(a[maxi][k]))maxi=i;
if(ep(a[maxi][k],0))continue;
for(int i=1;i<=n;++i)std::swap(a[line][i],a[maxi][i]);
for(int i=1;i<n;++i){
if(i==line)continue;
double mul=a[i][k]/a[line][k];
for(int j=k;j<=n;++j)a[i][j]-=a[line][j]*mul;
}
++line;
}
for(int i=1;i<n;++i)x[i]=a[i][n]/a[i][i];
}
}
inline void clear(){
for(int i=1;i<=n;++i){
x[i]=0;
for(int j=1;j<=n;++j)a[i][j]=0;
}
}
int main(){
// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
n=read(),m=read();double ans=0;
for(int i=1;i<=m;++i){
int u=read(),v=read(),w=read();
add(u,v,w);if(u!=v)add(v,u,w);
out[u]++;if(u!=v)out[v]++;
}
for(int j=1;j<=30;++j){
clear();
for(int i=1;i<n;++i){
for(int it=head[i];it;it=e[it].next){
int v=e[it].v;
if(e[it].w&(1<<j-1)){
if(v!=n){
a[i][v]+=-1.0/out[i];
a[i][n]+=-1.0/out[i];
}else a[i][n]+=-1.0/out[i];
}
else{
if(v!=n)a[i][v]+=1.0/out[i];
}
}
a[i][i]+=-1;
}
GUASS::sol();
ans+=pow(2,j-1)*x[1];
}
printf("%.3lf\n",ans);
}
P3750 [六省联考 2017] 分手是祝愿
这篇题解比较详细,想解释如何去想到这样做。
首先考虑暴力,一个状态为一个节点,将他们都连接起来,直接拿高斯消元做,时间复杂度 \(O(2^{3n})\),这是不能接受的,不难发现这些节点只有 \(n\) 类,第 \(i\) 类表示需要 \(i\) 次操作可以全部关灯,第 \(i\) 类进行一次操作有 \(\frac{i}{n}\) 的概率向更少操作次数的状态转移,同理,有 \(\frac{n-i}{n}\) 的概率向更多次数的状态转移。对于 \(f_i\) 显然有 \(f_i=f_{i-1}\cdot \frac{n-i+1}{n}+f_{i+1}\cdot \frac{i+1}{n}\),此时进行高斯消元,时间复杂度为 \(O(n^3)\)。
仔细观察这个式子,发现对于每个 \(f_i\) 的方程,只有 \(f_{i-1},f_i,f_{i+1}\) 的系数不为 \(0\),考虑这样一个矩阵。
最后的 \(-1\) 根据需要插在起点处,将这些式子加起来显然有 \(f_1\cdot\frac{1}{n}=-1\),然后逐一代入解就行了。
对于 \(k\),直接删去前 \(k\) 行就行,同理。
显然最后有 \(ans=\sum_{i=k}^n f_i\)。
#include<bits/stdc++.h>
#define int long long
inline int read(){
char ch=getchar();int x=0,f=1;
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
const int N=1e5+10,mod=1e5+3;;
bool a[N];
int n,x,k,ans=0,jc[N],e[N],v[N];
struct EQ{int x1,x2,x3,k1,k2,k3,c;}eq[N];
inline int qpow(int a,int b){
if(a==0)return 0;
int zc=a;
int res=1;
for(;b;b>>=1,a=a*a%mod)if(b&1)res=res*a%mod;
return res;
}
inline int ny(int x){return qpow(x,mod-2);}
inline int C(int x,int y){return jc[x]*ny(jc[y])%mod*ny(jc[x-y])%mod;}
inline int mo(int x){if(x>=0&&x<mod)return x;return (x%mod+mod)%mod;}
inline void work(){
e[1]=n;
for(int i=2;i<=n;++i){
e[i]=mo(C(n,i-1)*n-e[i-1]);
}
for(int i=k+2;i<=n;++i){
if(i>k+1)eq[i]={i-1,i,i+1,mo(e[i]*ny(e[i-1]+e[i])),mo(-1),mo(e[i+1]*ny(e[i+1]+e[i+2]))};
}
int s=k+1;
eq[x].c=mo(-1);ans=k;
eq[s]={0,s,s+1,0,mo(-1),mo(e[s+1]*ny(e[s+1]+e[s+2]))};
v[s]=mo(-1*ny(eq[s].k2+eq[s+1].k1));ans=mo(ans+v[s]);
for(int i=s;i<n;++i){
eq[i].c=mo(eq[i].c-eq[i].k1*v[i-1]-eq[i].k2*v[i]);
v[i+1]=mo(eq[i].c*ny(eq[i].k3));
ans=mo(ans+v[i+1]);
}
}
signed main(){
// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
n=read(),k=read();jc[0]=1;
for(int i=1;i<=n;++i)a[i]=read(),jc[i]=jc[i-1]*i%mod;
for(int i=n;i;--i){
if(a[i]){
x++;
for(int j=1;j*j<=i;++j){
if(i%j==0){
a[j]^=1;
if(j*j!=i)a[i/j]^=1;
}
}
}
}
if(x<=k)ans=x;else work();
std::cout<<ans*jc[n]%mod<<'\n';
}
P3706 [SDOI2017] 硬币游戏
神仙题,不会。
有一个显然的暴力,直接建出AC自动机跑随机游走,时间复杂度 \(O(n^3m^3)\),能拿 50 分。
显然每个同学的串出现概率是一样的,为什么获胜概率不一样呢,有这样的情况,在接上这个同学的串的时候,有一位同学把他截胡了。
具体来说,我没把一个没有人获胜的串(任意长度)看做 S,它出现的概率和为 \(w\),每个人获胜的概率为 \(f_i\),看起来有 \(f_i=\frac{w}{2^m}\),但是这个式子并不成立,原因在于会有别的同学将它截胡,就是非法串的后缀加上第 \(i\) 个同学的串的前缀组成了另一个同学的串。有 \(f_i+\sum_{j=1}^n\sum_{k=1\&pre_{i,m-k}=st_{j,k}}^{m-1}f_j\cdot\frac{1}{2^{m-k}}=\frac{w}{2^m}\),自己也可能将自己截胡,\(f_j\cdot\frac{1}{2^{m-k}}\) 表示截胡的同学获胜后继续接上 \(i\) 同学前 \(m-k\) 个数的概率,随便整一个字符串匹配,然后直接上高斯消元就行。
#include<bits/stdc++.h>
#define int long long
inline int read(){
char ch=getchar();int x=0,f=1;
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
const int N=305,base=131,mod1=998244353,mod2=1e9+7;
int n,m,P1[N],P2[N];
double f[N],a[N][N],_p[N];
std::pair<int,int>has[N][N];
char s[N];
inline double Abs(double x){return x<0?-x:x;}
inline void hs(int i,int j){has[i][j]={((has[i][j-1].first*base+s[j]-'A')%mod1+mod1)%mod1,((has[i][j-1].second*base+s[j]-'A')%mod2+mod2)%mod2};}
inline std::pair<int,int> Hs(int i,int l,int r){return {((has[i][r].first-has[i][l-1].first*P1[r-l+1])%mod1+mod1)%mod1,((has[i][r].second-has[i][l-1].second*P2[r-l+1])%mod2+mod2)%mod2};}
inline void GUASS(){
int line=1;
for(int k=1;k<=n;++k){
int maxi=line;
for(int i=line+1;i<=n;++i)if(Abs(a[i][k])>Abs(a[maxi][k]))maxi=i;
for(int i=1;i<=n+1;++i)std::swap(a[line][i],a[maxi][i]);
for(int i=1;i<=n;++i){
if(i==line)continue;
double mul=a[i][k]/a[line][k];
for(int j=k;j<=n+1;++j)a[i][j]-=a[line][j]*mul;
}
line++;
}
for(int i=1;i<=n;++i)f[i]=a[i][n+1]/a[i][i];
}
signed main(){
// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
std::cin>>n>>m;
P1[0]=1;for(int i=1;i<=m;++i)P1[i]=P1[i-1]*base%mod1;
P2[0]=1;for(int i=1;i<=m;++i)P2[i]=P2[i-1]*base%mod2;
_p[0]=1;for(int i=1;i<=m;++i)_p[i]=_p[i-1]*1.0/2.0;
for(int i=1;i<=n;++i){
std::cin>>s+1;
for(int j=1;j<=m;++j)hs(i,j);
}
for(int i=1;i<=n;++i){
a[i][1]=-_p[m];
for(int j=1;j<=n;++j){
for(int k=1;k<m;++k){
if(Hs(i,1,k)==Hs(j,m-k+1,m)){
a[i][j+1]+=_p[m-k];
}
}
}
}
n++;
for(int i=1;i<=n;++i)a[i][i+1]+=1;
for(int i=2;i<=n;++i)a[n][i]=1;a[n][n+1]=1;
GUASS();
for(int i=2;i<=n;++i)printf("%.10lf\n",f[i]);
}
树
钦定一个根后,对于从 \(i\) 到 \(j\) 的期望步数,有 \(f_{i\to j}=\frac{1}{d_i}+\sum_{k\in i\& k!=j}\frac{1+f_{k\to i}+f_{i\to j}}{d_i}\),化简一下有 \(f_{i\to j}=d_i+\sum_{k\in i\&k!=j}f_{k\to i}\),显然一个叶子到它的父亲的期望步数为 \(1\),所以进行一次 dfs 可以从下往上算出每个点去父亲的期望步数,同理,再次依据上面的式子进行一遍 dfs 可以求出每个点到每个与它相邻的点的期望步数,最后处理一下树上前缀和直接做就行了。
感觉这题没啥精度,全是整数。
#include<bits/stdc++.h>
#define int long long
inline int read(){
char ch=getchar();int x=0,f=1;
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=(x<<3)+(x<<1)+(ch^48);
return x*f;
}
const int N=1e5+10,mod=1e9+7;
int n,m,f[N],d[N],up[N],down[N],st[N][20],dfn[N],tot;
std::vector<int> e[N],v[N];
inline int mo(int x){if(x>=0&&x<mod)return x;if(x<0)return (x%mod+mod)%mod;return x%mod;}
inline int qpow(int a,int b){
int res=1;
for(;b;b>>=1,a=mo(a*a))if(b&1)res=mo(res*a);
return res;
}
inline void dfs(int x,int fa){
dfn[x]=++tot;st[tot][0]=fa;
for(int y:e[x]){
if(y!=fa){dfs(y,x);f[x]=mo(f[x]+f[y]);}
}
f[x]=mo(f[x]+d[x]);
}
inline void dfs1(int x,int fa,int last){
int sum=d[x];
for(int y:e[x]){
if(y!=fa)sum=mo(sum+f[y]);
else sum=mo(sum+v[fa][last]);
}
for(int w=0;w<e[x].size();++w){
int y=e[x][w];
if(y==fa){v[x].push_back(f[x]);}
else v[x].push_back(mo(sum-f[y])),dfs1(y,x,w);
}
}
inline void dfs2(int x,int fa){
for(int w=0;w<e[x].size();++w){
int y=e[x][w],val=v[x][w];
if(y!=fa){
up[y]=f[y]+up[x];
down[y]=val+down[x];
dfs2(y,x);
}
}
}
inline int get(int x,int y){return dfn[x]<dfn[y]?x:y;}
inline int lca(int u,int v){
if(u==v)return u;
if((u=dfn[u])>(v=dfn[v]))std::swap(u,v);
int k=std::__lg(v-u++);
return get(st[u][k],st[v-(1<<k)+1][k]);
}
inline int ask(int u,int v){
int LCA=lca(u,v);
return up[u]-up[LCA]+down[v]-down[LCA];
}
signed main(){
// freopen("in.in","r",stdin),freopen("out.out","w",stdout);
std::ios::sync_with_stdio(false);std::cin.tie(0),std::cout.tie(0);
n=read();m=read();
for(int i=1;i<n;++i){
int u=read(),v=read();e[u].push_back(v),e[v].push_back(u);
d[u]++,d[v]++;
}
dfs(1,0);dfs1(1,0,0);dfs2(1,0);
for(int k=1;k<=std::__lg(n);++k)
for(int i=1;i+(1<<k)-1<=n;++i)
st[i][k]=get(st[i][k-1],st[i+(1<<k-1)][k-1]);
for(int i=1;i<=m;++i){
int u=read(),v=read();
std::cout<<mo(ask(u,v))<<'\n';
}
}

浙公网安备 33010602011771号