【题解】 UVA10529 Dumb Bones 骨牌
题意
你试图把一些多米诺骨牌排成直线,然后推倒它们。但是如果你在放骨牌的时候不小心把刚放的骨牌碰倒了,它就会把相临的一串骨牌全都碰倒,而你的工作也被部分的破坏了。
比如你已经把骨牌摆成了 DD__DxDDD_D 的形状(侧视图)(其中 D 代表骨牌,下划线和 x 均代表还没有放骨牌的空位),而想要在 x 这个位置再放一块骨牌。它可能会把左边的一块骨牌或右边的三块骨牌碰倒,而你将不得不重新摆放这些骨牌。
给出你要摆放的骨牌数目,以及放骨牌时它向左和向右倒的概率(每一块骨牌都一样)。
为了使期望摆放骨牌的次数最少,你可以采用某种特定的摆放顺序。求这个最少的期望摆放骨牌次数。
算法 1
概率 + 最优解,可以考虑用区间 DP
考虑一个区间。枚举这个区间里最后摆放的一块骨牌的位置,把原区间分割成左右两个子区间。
设原区间期望最少摆放次数为 \(E\) ,左区间为 \(E_1\) ,右区间 \(E_2\) 。此时最后一块骨牌还没有放上去,那么设这个空位的位置是 \(x\)
已知每块骨牌向左倒的概率是 \(P_l\) ,向右是 \(P_r\)
单独看 \(x\) 这一个骨牌,它不倒的概率是 \(1-P_l-P_r\) ,那么使它不倒的期望摆放次数是 \(\frac1{1-P_l-P_r}\)
注意这个次数里:最后一次是不倒的,之前的有些向左有些向右。因为向左的概率是 \(P_l\) ,所以向左倒的次数就是 \(\frac1{1-P_l-P_r}\times P_l\) 。由于每向左倒一次,左区间的就要全部重新摆放。左区间放好一次需要 \(E_1\) 个摆放,所以一共左区间要摆 \(\frac{P_l}{1-P_l-P_r}\times E_1\) 次。同理右区间一共要放置 \(\frac{P_r}{1-P_l-P_r}\times E_2\) 次。
还有就是放 \(x\) 之前需要先把左右区间都摆好一次,共 \(E_1+E_2\) 个摆放。
最后状态转移方程就是
于是我们轻松的写出了下面这一坨
int n;
double pl, pr;//向左、向右倒的概率
double f[N][N];//dp 数组(记忆化搜索)
double dfs(int l, int r) {//区间左右端点
double &E = f[l][r];//使用引用方便又快捷
if (E > eps) return E;//记忆化
if (l > r) return E = 0.0;//边界
if (l == r) return E = 1.0/(1.0-pl-pr);//只放一个
E = inf;//先来一个极大值
for (int i=l; i<=r; ++i) {//枚举最后一个摆放的位置 i
double E1 = dfs(l, i-1);//左区间摆放次数
double E2 = dfs(i+1, r);//右区间
E = std::min(
E,
E1 + E2 + 1.0/(1.0-pl-pr) + pl*E1/(1.0-pl-pr) + pr*E2/(1.0-pl-pr)//刚才的方程
);//通过 min 获得最优解
}
return E;
}
然后光荣地 T 飞爆零了
算法 2
上面的算法是 \(O(n^3)\) 的,当然过不了,我们考虑降维
因为每块牌都一模一样,显然对于所有长度相同的区间,它们的 \(E\) 都是相同的
把状态重新设置为区间长度,就只有不同长度的区间才需要转移,状态转移方程不变
获得正解
#include <cstdio>
#include <iostream>
using namespace std;
#define N 1030
const double eps = 1e-8;
const double inf = 1e20;
double pl, pr;//向左、向右倒的概率
double f[N];
double dfs(int n) {//长度为 n 的区间
double &E = f[n], E1, E2;//还是引用
if (E > -0.5) return E;//让 f[0]=0.0 和 f[1]=1.0/(1.0-pl-pr) 都可以自然转移
E = inf;
for (int i=1; i<=n; ++i) {
E1 = dfs(i-1), E2 = dfs(n-i);//左、右区间的答案
E = min(
E,
E1 + E2 + f[1] + pl*E1*f[1] + pr*E2*f[1]
);
}
return E;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);//关闭流同步
while (true)
{
int n;
cin >> n;
if (!n) break;
cin >> pl >> pr;
for (int i=2; i<=n; ++i) f[i] = -1.0;//注意细节
f[1] = 1.0 / (1.0 - pl - pr);//同时 f[0]=0.0
printf("%.2f\n", dfs(n));
}
return 0;
}