P2973 [USACO10HOL] Driving Out the Piggies G 解题报告
P2973 [USACO10HOL] Driving Out the Piggies G 解题报告
1. 题目大意
想象一下,我们有一张由 \(N\) 个城市和 \(M\) 条双向道路组成的地图。我们在城市 1 放置了一颗“随机臭弹”。
这颗臭弹的行为模式如下:
- 每一小时,当臭弹位于某个城市时,它有 \(\frac{P}{Q}\) 的概率原地爆炸,污染当前所在的城市。
- 如果它没有爆炸(概率为 \(1 - \frac{P}{Q}\)),它会从当前城市的所有道路中,随机选择一条,移动到下一个城市。每条道路被选中的概率是均等的。
我们的任务是,计算出对于每一个城市 \(i\)(从 1 到 \(N\)),这颗臭弹最终在该城市爆炸的总概率是多少。
2. 思路分析:从无限到有限
核心难点: 臭弹的移动和爆炸过程可以无限进行下去。比如,它可能在城市1和城市2之间来来回回走很多次才爆炸。我们不可能通过模拟每一种路径来计算概率,因为路径有无限多条。
解决思路: 当我们遇到一个可能无限进行的过程时,一个常见的解决策略是建立方程。我们不关心过程具体走了多少步,只关心最终的“稳态”结果。
让我们借鉴一下题解中那个有趣的小例子:
三个人玩“手心手背”,如果两个人出的手势一样,他们就胜出。如果三个人手势都不一样(比如一人心、一人背、一人侧),就重来。问小明和小红一起胜出的概率是多少?
设小明和小红胜出的概率为 \(x\)。
在任意一轮中:
- 他们直接胜出的概率是 \(\frac{1}{4}\)。
- 游戏重开的概率也是 \(\frac{1}{4}\)。如果游戏重开,那么未来小明和小红胜出的概率依然是 \(x\)。
所以,我们可以列出方程:
解这个简单的方程,就能得到 \(x = \frac{1}{3}\)。
应用到本题: 我们可以用同样的方式来思考臭弹问题。我们设 \(Prob_i\) 为臭弹最终在城市 \(i\) 爆炸的总概率。这个 \(Prob_i\) 就是我们要求的答案。
现在,我们来分析 \(Prob_i\) 是如何构成的。臭弹要在城市 \(i\) 爆炸,它必须先到达城市 \(i\)。
让我们从“概率流”的角度来思考。
- 初始状态:在游戏开始时,有 \(100\%\) 的概率(也就是概率值为 1)在城市 1。
- 概率的流动:在每个城市,一部分概率会因为爆炸而“固化”下来,成为最终结果。另一部分概率会因为移动而“流向”相邻的城市。
我们来为每个城市建立一个关于概率的平衡方程。
对于城市 \(i\)(\(i \neq 1\)):
臭弹要在这里爆炸,它必须是从某个相邻的城市 \(j\) 移动过来的。
- 考虑一个与城市 \(i\) 相邻的城市 \(j\)。臭弹在城市 \(j\) 的整个过程中,总共贡献了 \(Prob_j\) 的爆炸概率。
- 这意味着,在到达城市 \(j\) 后,有 \(1 - \frac{P}{Q}\) 的概率没有爆炸,而是选择移动。
- 城市 \(j\) 有 \(d_j\) 条路(\(d_j\)是城市 \(j\) 的度),所以选择去城市 \(i\) 的概率是 \(\frac{1}{d_j}\)。
- 因此,从城市 \(j\) "流向" 城市 \(i\) 并最终在 \(i\) 爆炸的这部分概率,可以表示为:\(Prob_j \times (1-\frac{P}{Q}) \times \frac{1}{d_j}\)。
- 把所有与 \(i\) 相邻的城市 \(j\) 的贡献加起来,就是 \(Prob_i\) 的全部来源。
所以,对于任意一个非起始点的城市 \(i\)(\(i \neq 1\)),我们有:
其中 \(E\) 表示所有道路的集合,\((i,j) \in E\) 表示城市 \(i\) 和 \(j\) 之间有路。
对于城市 1:
城市 1 比较特殊,因为它是一切的起点。
- 来源一(初始爆炸):臭弹一开始就在城市 1,它可能第一小时就原地爆炸。这个概率是 \(\frac{P}{Q}\)。
- 来源二(从别处回来):和上面的分析一样,臭弹也可能从相邻的城市 \(j\) 移动回来,再在城市 1 爆炸。
所以,对于起始点城市 1,我们有:
3. 建立方程组与求解
现在我们有了 \(N\) 个未知数 (\(Prob_1, Prob_2, ..., Prob_N\)) 和 \(N\) 个方程。这是一个标准的N元一次方程组。我们的任务就是解这个方程组。
为了方便计算机求解,我们把所有未知项移到等式左边,常数项移到右边:
-
对于城市 1 的方程:
\[Prob_1 - \sum_{(1,j) \in E} \left( (1 - \frac{P}{Q}) \frac{1}{d_j} \right) Prob_j = \frac{P}{Q} \] -
对于城市 \(i \neq 1\) 的方程:
\[Prob_i - \sum_{(i,j) \in E} \left( (1 - \frac{P}{Q}) \frac{1}{d_j} \right) Prob_j = 0 \]
这是一个形如 \(A\vec{x} = \vec{b}\) 的线性方程组,其中 \(\vec{x}\) 是我们要求的概率向量 \((Prob_1, ..., Prob_N)^T\)。解决这种问题的经典算法就是高斯消元法(Gaussian Elimination)。
4. 代码解析
我们来看一下题解提供的 C++ 代码是如何实现这个过程的。
#include<bits/stdc++.h>
#define maxn 310
#define eps (1e-6)
using namespace std;
int n,m,p,q,x,y;
int f[310][310]; // 邻接矩阵,f[i][j]=1 表示i和j有路
int in[310]; // 存储每个点的度
double a[maxn][maxn]; // 增广矩阵,用于高斯消元
// 高斯-若尔当消元法
void gauss_jordan(){
// ... (标准的高斯消元模板)
}
int main(){
// 1. 输入数据并建图
scanf("%d%d%d%d",&n,&m,&p,&q);
for(int i=1;i<=m;i++){
scanf("%d%d",&x,&y);
f[x][y]=f[y][x]=true; // 标记x和y之间有路
in[x]++;in[y]++; // 对应度数加1
}
// 2. 构建增广矩阵 a
// 我们的方程是 Prob_i - sum(c_j * Prob_j) = b_i
// 矩阵 a[i][j] 存 Prob_j 在第 i 个方程里的系数
// a[i][n+1] 存第 i 个方程的常数项 b_i
// 设置方程1的常数项
a[1][n+1] = 1.0 * p / q;
// 其他方程的常数项默认为0,不需要显式设置
for(int i=1;i<=n;i++){
// Prob_i 的系数是 1
a[i][i] = 1;
// 遍历所有点j,看它是不是i的邻居
for(int j=1;j<=n;j++)
if(f[i][j]) { // 如果 j 是 i 的邻居
// Prob_j 的系数是 -(1 - P/Q) / d_j
a[i][j] = -(1 - (double)p/q) / in[j];
}
}
// 3. 调用高斯消元求解
gauss_jordan();
// 4. 输出结果
// 消元后,a[i][n+1] 中存储的就是 Prob_i 的解
for(int i=1;i<=n;i++)
printf("%.9lf\n",a[i][n+1]);
return 0;
}
代码核心逻辑剖析:
- 建图:使用邻接矩阵
f
和度数数组in
来存储图的信息。 - 构建方程组(增广矩阵
a
):a[i][i] = 1;
对应方程左边的Prob_i
项,其系数为1。a[i][j] = -(1 - (double)p/q) / in[j];
对应方程左边的- \sum (...)
部分。对于第i
个方程,如果j
是i
的邻居,那么Prob_j
会出现在求和项里,其系数就是(1-P/Q) * (1/d_j)
。移到等式左边后,系数变为负。a[1][n+1] = 1.0 * p / q;
单独设置了第一个方程右边的常数项。其他方程的常数项为0,这是数组初始化的默认值。
- 求解:
gauss_jordan()
函数是一个实现高斯消元的模板,它直接对矩阵a
进行操作,把解算出来。 - 输出:经过高斯消元后,矩阵
a
会变成一个对角矩阵(或者上三角矩阵再回代),解就存在了最后一列a[i][n+1]
中。直接输出即可。
5. 总结
本题是一个典型的概率与图论结合的问题,并利用数学期望/概率方程来求解。
解题步骤可以归纳为:
- 识别问题模型:发现这是一个涉及无限步骤的随机过程,常规的搜索或模拟无法解决。
- 建立数学模型:为每个城市的最终爆炸概率
Prob_i
设立未知数。 - 列出方程:根据概率的流动和平衡关系,为每个
Prob_i
列出一个线性方程,形成一个 N 元一次方程组。 - 求解方程:利用高斯消元法这一标准工具来求解该线性方程组,得到每个未知数的具体值。
- 编码实现:将上述过程转化为代码,主要是正确地构建增广矩阵和应用高斯消元模板。
这种“设未知数,列方程”的思想是解决许多复杂概率、期望问题的利器。