概率与期望经典题三道
题目一:分手是祝愿
思路:
其实题面的部分分是有提示作用的:对于一半的数据 \(n=k\) 。这其实也就提醒我们直接去思考对于一个给定的灯泡的局面如何得到最优的操作方法。其实这是可以贪心的:如果我们想要最小化操作次数,那么必然每一个灯泡至多被操作一次。
这是显然的,因为发现对于一个灯泡操作超过两次和没操作没有任何区别。又因为一个灯泡只可能影响到他自己与他前面的灯泡,所以,我们考虑从后往前扫,遇到亮的灯就熄灭他,那么一定最后的方案是最优的。现在这是一种可行解,那么是否是唯一的一种方案呢(需要注意的是,如果一个操作序列同时出现了两个相同的灯泡操作,我们认为其与没操作没有区别)。可以证明对于每一个不同的局面,这个解法都是唯一的,证明也很好证,由一开始的“每个灯泡至多被操作一次”的基本事实,结合反证法与映射关系就可以证明。
所以这个问题的关键就完全转化为了最小操作次数,这是可以用 \(O(n\ln n)\) 预处理的。因为现在的解法唯一且最优,所以就意味着,我们随机选一个灯泡的时候,要么局面所需的最少操作次数减一,要么加一,所以我们就可以将局面所需操作数作为状态进行dp而不必关心具体局面。
设 \(f_i\) 表示最少还需 \(i\) 次的局面转移到最少还需 \(i-1\) 次的局面的期望操作次数。那么有转移:
先解释一下方程的由来,还需 \(i\) 次就意味着有 \(i\) 个有效的操作的灯泡,选中的概率就为 \(\frac{i}{n}\) 而如果没有选中有效的灯泡就需要回到最少还需 \(i+1\) 次的局面,括号内加 \(1\) 其实是因为操作错的那一下的操作次数。
化简一下就得到无后效性的递推式:
Code:
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#define ll long long
const int N = 1e5 + 10;
const int MOD = 100003;
int n,a[N],k,now;
ll f[N],inv[N],ans;
inline int read(){
char c=getchar();bool f=0;int x=0;
while(c > '9' || c < '0') f|=c=='-',c=getchar();
while(c >= '0'&&c <= '9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
if(f) x=-x;return x;
}
int main() {
n = read(),k = read();
for(int i = 1;i <= n;++i) a[i] = read();
inv[1] = 1;
for(ll i = 2;i <= n;++i) inv[i] = ((MOD - MOD / i) * inv[MOD % i]) % MOD;
for (int i = n;i >= 1;--i) {
for (int j = i << 1;j <= n;j += i) a[i] ^= a[j];
now += a[i];
}
ll fc = 1;
for(int i = 1;i <= n;++i) fc = fc * i % MOD;
if(now <= k)
{
printf("%d",now * fc % MOD);
return 0;
}
f[n] = 1;
for(int i = n - 1;i > k;--i)
f[i] = 1ll * (n - i) * (f[i + 1] + 1) % MOD * inv[i] % MOD + 1;
for(int i = k + 1;i <= now;++i) ans = (ans + f[i]) % MOD;
printf("%d",(ans + k) * fc % MOD);
return 0;
}
题目二:扫雷机器人
思路:
考虑形式化地推导这个问题,显然无论是连锁反应还是直接引爆的炸弹,都一定会存在先后顺序,可以证明的是,总情况数就是 \(n!\) 。显然每种情况都是独立的,所以任意情况的出现概率都相同,为 \(\frac{1}{n!}\) 由期望的定义可知,如果我么定义每一种情况的直接引爆的地雷的数量为 \(num_i\) 所求答案即为:
考虑优化这个算法,实际上,这个算法的复杂度之所以高,通过暴力也可以看出,是对于一些重复的排列的子情况重复计算,这理应是可以优化的,但是类似记忆化的思想似乎并不好处理。所以我们换种思考方式,考虑引爆与连锁反应的本质:是一种约束关系,所以可以考虑建出关系图,从引爆向间接引爆连边。
建出这个图之后,发现对于一个点,他若没有对答案造成贡献,那么一定可以从图上找到被引爆的一个点,找到至少一条路径到达他,所以我们考虑找出所有这样的点(特别的,包括他自己,也就是 \(i\) 号店),记这个点集叫做 \(V_i\),你发现,如果 \(i\) 号点造成一点贡献,当且仅当他在一个方案中出现的位置在这个点集中的最前面。所以对于所有方案,第 \(i\) 个点的总贡献即为:
现在问题转化为对于每一个点 \(i\) ,求出其对应的 \(|V_i|\) 。如果对于每一个点都 BFS ,显然 \(O(n^3)\) 是过不去的。但是你发现在这个图里面,应当会存在一些强连通分量,所以缩点之后形成了一个 DAG 。那我们就可以考虑用类似拓扑 dp 的方式去求解这个问题了,你发现强连通分量中的元素数目是可以贡献给其可以到达的下一个强连通分量的,所以我们用 bitset 去维护这个拓扑过程,最后统计每一个 \(i\) 的 bitset 的大小就 OK 了,看起来很复杂,但是可以跑的很快。
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cmath>
#include<bitset>
#include<vector>
#include<queue>
inline int read() {
int x=0,f=1;char ch=getchar();
while(ch > '9' || ch < '0'){if(ch == '-'){f = -1;}ch = getchar();}
while(ch >= '0'&&ch <= '9'){x = x * 10 + ch - 48; ch = getchar();}
return x * f;
}
const int N = 4000 + 5;
int n,dfn[N],low[N],sccno[N],nw_index,num,indegree[N],siz[N],a[N],d[N];
double ans = 0.0;
std::vector<int> G[N],nG[N];
std::bitset<N> f[N];
bool link[N][N];
int st[N],top;
void tarjan(int u)
{
dfn[u] = low[u] = ++num;
st[top++] = u;
for(auto v : G[u])
{
if(!dfn[v])
{
tarjan(v);
low[u] = std::min(low[u],low[v]);
}else if(!sccno[v]){
low[u] = std::min(low[u],dfn[v]);
}
}
if(dfn[u] == low[u])
{
++nw_index;
int v;
do{
v = st[--top];
sccno[v] = nw_index;
f[nw_index].set(v);
++siz[nw_index];
}while(v != u);
}
}
int main()
{
n = read();
for(int i = 1;i <= n;++i)
{
a[i] = read(),d[i] = read();
}
for(int i = 1;i <= n;++i)
{
for(int j = 1;j <= n;++j)
{
if(i == j) continue;
if(abs(a[i] - a[j]) <= d[i])
G[i].push_back(j);
}
}
for(int i = 1;i <= n;++i)
{
if(!dfn[i])
tarjan(i);
}
for(int u = 1;u <= n;++u)
{
for(auto v : G[u])
{
if(sccno[u] != sccno[v] && !link[sccno[u]][sccno[v]])
{
link[sccno[u]][sccno[v]] = true;
nG[sccno[u]].push_back(sccno[v]);
++indegree[sccno[v]];
}
}
}
std::queue<int> q;
for(int i = 1;i <= nw_index;++i)
{
if(!indegree[i]) q.push(i);
}
while(!q.empty())
{
int u = q.front();
q.pop();
for(auto v : nG[u])
{
f[v] |= f[u];
if(!(--indegree[v])) q.push(v);
}
}
for(int i = 1;i <= n;++i) ans += 1.0 / f[sccno[i]].count();
printf("%.4lf",ans);
return 0;
}
思路二:
嘿嘿,杀了个回马枪,其实这道题建立图论模型后可以通过观察省去建图。注意到这道题的传递性是非常强烈的,所以我们将所有位置排序之后,就可以找到对于某一个点,其分别可以到达的左右两边最远的点,观察建立的图后,可以证明的是,在左右两个点之间的点,一定可以对答案造成贡献(用反证法可以证明)。
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<vector>
inline int read() {
int x=0,f=1;char ch=getchar();
while(ch > '9' || ch < '0'){if(ch == '-'){f = -1;}ch = getchar();}
while(ch >= '0'&&ch <= '9'){x = x * 10 + ch - 48; ch = getchar();}
return x * f;
}
const int N = 4e3 + 5;
int n,ML[N],MR[N],id[N];
double ans = 0;
struct node
{
int a,d;
}bomb[N];
bool cmp(int x,int y)
{
return bomb[x].a < bomb[y].a;
}
int main()
{
n = read();
for(int i = 1;i <= n;++i)
{
bomb[i].a = read(),bomb[i].d = read();
id[i] = i;
}
std::sort(id + 1,id + n + 1,cmp);
for(int j = 1;j <= n;++j)
{
int i = id[j];
ML[i] = bomb[i].a - bomb[i].d,MR[i] = bomb[i].a + bomb[i].d;
int L = j,R = j;
while(true)
{
int l = id[L - 1],r = id[R + 1];
if(l && ML[i] <= bomb[l].a){
--L;
ML[i] = std::min(ML[i],bomb[l].a - bomb[l].d);
MR[i] = std::max(MR[i],bomb[l].a + bomb[l].d);
continue;
}
if(r && MR[i] >= bomb[r].a){
++R;
ML[i] = std::min(ML[i],bomb[r].a - bomb[r].d);
MR[i] = std::max(MR[i],bomb[r].a + bomb[r].d);
continue;
}
break;
}
}
for(int i = 1;i <= n;++i){
int now = 0;
for(int j = 1;j <= n;++j){
if(ML[j] <= bomb[i].a && bomb[i].a <= MR[j]) ++now;
}
ans += 1.0 / now;
}
printf("%.4lf",ans);
return 0;
}
题目三:概率充电器
思路:
题目结构显然构成一棵树,考虑树形dp。首先可以明确的一点是,每一个节点的贡献都是 \(1\) ,所以由期望的定义,我们最终需要求得的即为所有点的概率之和。我们很容易想到 \(O(n^2)\) 做法,也就是对于每一个点,我们都当作根节点来进行树形dp,这是很容易的。其实到这一步就比较容易往换根dp去做了,但是我们还需要详细的理论分析。
还是先结合题目寻找重要性质。一个关键点在与,一个点会成功充电仅来源与以下三种情况:
1.自己充电成功。
2.由子树内导电到自己
3.由子树外导电到自己(不包含自己)。
由于平常的习惯,前两个情况是十分容易处理的,自己充电成功就直接把这个点的充电成功概率贡献给答案就好了,那么对于由子树内导电到自己,因为子树有电自己也直接充电成功的概率我们已经考虑在情况1中了,所以我们接下来的考虑都基于若此节点还没有被导电,但是在这一阶段可以被导电的概率。有转移:
其中 \(P(e)\) 的表示边 \(e\) 上的导电成功概率。
显然在求解过程中我们无论如何都需要钦定一个点为根节点,由暴力方法可知,这个节点的信息是完全的,所以我们可以考虑由此作为突破口。我们首先通过一次的dfs求得目前不完善的 \(f_u\) 。然后以 \(f_1\) 作为突破口再做一次dfs,比较显然的想法就是对于每一个节点我们将其父节点的的贡献值以上述方程转移到现在这个节点中。但是仔细分析就会发现这是错误的,因为你自己这一部分的贡献给了 \(father\) 节点,而现在 \(father\) 节点又贡献了回来,这就产生了重复,我们希望 \(father\) 节点不包含现在这个节点的贡献。考虑变形最后的 \(f_{fa}\) 以此来产生贡献,由上述转移方程可以发现,遍历子节点的顺序是无关紧要的,所以我们可以把当前节点当作 \(father\) 节点最后遍历到的儿子。我们需要遍历到这个节点之前的信息,定义其为 \(f'_{fa}\),所以先列出上述转移。
化简为关于 \(f'_{fa}\) 的计算等式就为:
带入 \(f'_{fa}\) 到 \(u\) 的转移方程就可以了,注意在计算时要判断分母为 \(0\) 的情况。
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<vector>
inline int read() {
int x=0,f=1;char ch=getchar();
while(ch > '9' || ch < '0'){if(ch == '-'){f = -1;}ch = getchar();}
while(ch >= '0'&&ch <= '9'){x = x * 10 + ch - 48; ch = getchar();}
return x * f;
}
const int N = 5e5 + 10;
const double EPS = 1e-7;
int n,head[N],cnt;
double ans = 0.0,f[N];
struct edge {
int v,last;
double w;
} e[N << 1];
void add(int u,int v,double w)
{
e[++cnt].v = v;
e[cnt].w = w;
e[cnt].last = head[u];
head[u] = cnt;
}
void dfs1(int u,int fa)
{
for(int i = head[u]; i;i = e[i].last)
{
int v = e[i].v;
double p = e[i].w;
if(v == fa) continue;
dfs1(v,u);
f[u] += (1 - f[u]) * f[v] * p;
}
}
void dfs2(int u,int fa)
{
for(int i = head[u]; i;i = e[i].last)
{
int v = e[i].v;
double p = e[i].w;
if(v == fa) continue;
if(fabs(1 - f[v] * p) <= EPS)
{
dfs2(v,u);
continue;
}
f[v] += (1 - f[v]) * (f[u] - f[v] * p) / (1 - f[v] * p) * p;
dfs2(v,u);
}
}
int main()
{
n = read();
for(int i = 1;i <= n - 1;++i)
{
int u,v,w;
u = read(),v = read(),w = read();
add(u,v,0.01 * w);
add(v,u,0.01 * w);
}
for(int i = 1;i <= n;++i) f[i] = 0.01 * read();
dfs1(1,0);
dfs2(1,0);
for(int i = 1;i <= n;++i) ans += f[i];
printf("%.6lf",ans);
return 0;
}

浙公网安备 33010602011771号