期望概率DP

发布时间 2023-07-12 00:02:10作者: Zeoy_kkk

期望的线性性

image-20230711134127015

image-20230711140132449

例1·CodeForces - 453A

image-20230711205954315

题解

  • 我们定义随机变量\(X=x\) 代表独立投掷\(n\)次的最大点数为\(x\)

  • 对于离散变量的概率,具有类似前缀和的性质:

  • \[P(X=k) =P(X \leq k) - P(X \leq k-1)\\ =(\frac{k}{m})^n - (\frac{k-1}{m})^n \]

  • \[\]

\[ * 所以我们直接$O(m)$枚举点数即可 \]

const int N = 2e5 + 10, M = 4e5 + 10;

int n, m;

void solve()
{
    cin >> m >> n;
    double ans = 0.0;
    for (int i = 1; i <= m; ++i)
    {
        ans += (pow(1.0 * i / m, n) - pow(1.0 * (i - 1) / m, n)) * i;
    }
    cout << fixed << setprecision(12) << ans << endl;
}

例2·CF1612E Messages

image-20230711133922657

题解

  • 我们定义随机变量\(X = x\) 代表读到自己对应消息的学生的数量为\(x\)\(Y_i = 0 / 1\) 代表第\(i\)个人有没有读到自己对应的消息

  • 利用随机变量的可加性得:\(X = Y_1 + Y_2+...+Y_n\),对于\(Y_i\)的取值只有\(0\)\(1\)两种情况,\(1\)代表第\(i\)个人读到了自己对应的消息,\(0\)代表第\(i\)个人没有读到了自己对应的消息

  • 根据期望的线性性可知,读到自己对应消息的学生数量的期望\(E[X] = E[Y_1]+E[Y_2]+...+E[Y_n]\)

  • 我们考虑每个人的\(E[Y_i]\),因为只有两种情况,所以

  • \[E[Y_i] = 0 \times P(Y_i = 0)+1 \times P(Y_i = 1)\\ E[Y_i] = P(Y_i = 1) \]

  • 所以每个人的\(E[Y_i]\)就等于每个人读到自己对应消息\(m_i\)的概率

  • 所以我们把题目转化为求每个人读到自己对应消息\(m_i\)的概率之和的最大值

  • 接下来我们考虑如何计算每个人读到自己对应消息\(m_i\)的概率

  • 假设我们已经确定了\(t\)则消息,分别为\(c_1,c_2...c_t\)

  • 如果这\(t\)则消息中不包含\(m_i\),那么读到消息的概率一定为\(0\)

  • 如果这\(t\)则消息中包含\(m_i\),且\(t \leq k_i\),那么读到消息的概率一定为\(1\)

  • 如果这\(t\)则消息中包含\(m_i\),且\(t > k_i\),那么读到消息的概率为\(\frac{C_{t-1}^{k_i-1}}{C_{t}^{k_i}}=\frac{k_i}{t}\)

  • 假设我们确定了\(t\),那么每个人的\(E[Y_i]\)期望都可以通过上面的式子\(O(1)\)得到,所以我们\(O(n)\)枚举每个人求出选每一个\(m_i\)能够产生的贡献,那么贡献之和就是\(E[X]\),所以现在的关键是怎样枚举\(t\)

  • 观察到题目中\(k_i<=20\)的情况,对于\(t <= 20\)的时候,我们不妨直接暴力枚举\(t\),贪心取前\(t\)个最大的消息对答案产生的贡献

  • 对于\(t \geq 21\)的时候,我们没有必要枚举,因为只会出现第\(3\)种情况,所以随着\(t\)的变大答案只会越来越小,最优值一定会出现在\(t \leq 20\)的情况中

  • 时间复杂度\(O(20 \times mlog\ m)\)

const int N = 2e5 + 10, M = 4e5 + 10;

int n;
int m[N], k[N];
vector<int> ans;

void solve()
{
    cin >> n;
    for (int i = 1; i <= n; ++i)
        cin >> m[i] >> k[i];
    double mx = 0;
    for (int t = 1; t <= 20; ++t)
    {
        vector<pii> vec(N); // 记录选择每个消息能够产生的贡献
        double sum = 0;
        for (int i = 1; i <= n; ++i)
        {
            vec[m[i]].first += min(t, k[i]);
            vec[m[i]].second = m[i];
        }
        sort(all(vec), greater<pii>()); // 将贡献排序
        vector<int> tmp;
        for (int i = 0; i < t; ++i) // 贪心选择前t个消息
        {
            sum += vec[i].first;
            if (vec[i].second)
                tmp.push_back(vec[i].second);
        }
        sum /= t; // 不要忘记除以t
        if (sum > mx)
        {
            mx = sum;
            ans = tmp;
        }
    }
    cout << ans.size() << endl;
    for (auto it : ans)
        cout << it << " ";
    cout << endl;
}

期望DP

  • 上面的题目基本都是直接基于期望的性质或者定义来进行计算,但很多题目是无法直接计算的

  • 期望DP一般通过记忆化搜索的方式进行DP

DP模型

给出一个有向无环图(\(DAG\)),每条有向边有一个权值\(w_u\),一个人从节点\(x\)出发,假设当前处于节点\(u\),则:

  • 如果节点\(u\)不存在连向其他任何点的边,则过程停止
  • 如果节点\(u\)存在连向其他点的边,人从\(u\)随机的走向它的邻点\(v\)的概率为\(P_{u,v}\),其中\(\sum_{v邻接于u}P_{u,v} = 1\)\(u\)\(v\)是有边相邻的点,且两者之间路程(边权)为\(w_{u,v}\)

求人从\(x\)出发直到停止走过的总路程的期望

  • 我们定义随机变量\(D_u=d\) 代表从\(x\)出发到\(u\)停止时走过的总路程为\(d\),则:
  • \(E[D_u] = \sum_d d \times P(D_u = d)\)
  • 我们再定义随机变量\(Next_u = v\) 代表\(v\)是与\(u\)相连的点,则:

\[D_u = D_{Next_u} + W_{Next_u,u} \]

\[\begin{flalign} E[D_u] =&\sum_d d \times P(D_u = d)\\ =&\sum_d d \times \sum_v P(u,v)\times P(D_v = d - w_{u,v})\\ =&\sum_v P(u,v)\times \sum_d d \times P(D_v = d - w_{u,v})\\ =&\sum_v P(u,v)\times \sum_{d'} (d' + w_{u,v}) \times P(D_v = d')\ \ (令d'=d-w_{u,v})\\ =&\sum_v P(u,v)\times \{\sum_{d'} d' \times P(D_v = d') + \sum_{d'}w_{u,v} \times P(D_v = d') \}\\ =&\sum_v P(u,v) \times (E[D_v] + w_{u,v})&\\ \end{flalign} \]

  • \[\]

\[ * 实际上对于**有向有环图**来说,该公式也是**有效**的,只不过无法进行期望DP罢了 \]

例1·AtCoder - abc184_d

image-20230711210857804

题解

  • 由于按照期望的性质和定义比较麻烦得到答案,我们考虑期望DP

  • \(f[i][j][k]\)代表金币状态为\((i,j,k)\)时操作次数的期望

  • 我们考虑DP的状态转移:

  • \[\]

\[\]

const int N = 1e2 + 10, M = 4e5 + 10;

int A, B, C;
double f[N][N][N];

double dfs(int a, int b, int c)
{
    if (a >= 100 || b >= 100 || c >= 100)
        return 0;
    if (f[a][b][c] != 0)
        return f[a][b][c];
    double ans = 0;
    double p1 = (1.0 * a) / (a + b + c), p2 = (1.0 * b) / (a + b + c), p3 = (1.0 * c) / (a + b + c);
    ans = p1 * (dfs(a + 1, b, c) + 1) + p2 * (dfs(a, b + 1, c) + 1) + p3 * (dfs(a, b, c + 1) + 1);
    return f[a][b][c] = ans;
}

void solve()
{
    cin >> A >> B >> C;
    memset(f, 0, sizeof f);
    f[A][B][C] = dfs(A, B, C);
    cout << fixed << setprecision(8) << f[A][B][C] << endl;
}

例2·Timaeus

image-20230711192835560

题解

  • 我们考虑期望DP

  • 我们定义\(f[x]\)为有\(x\)个材料最多能够合成材料数量的期望

  • 我们考虑状态的转移,两个人分为两种情况:

  • 如果我们选择第一个人,有\(p\)的概率获得双倍合成材料,有\(1-p\)的概率不获得双倍合成材料

  • \[f[x] = \frac{p}{100} \times(f[x - B] + 2) + (1 - \frac{p}{100}) \times (f[x - B] + 1) \]

  • 如果我们选择第二个人,有\(q\)的概率返还一个材料,有\(1-q\)的概率不返还一个材料

  • \[f[x] = \frac{q}{100}\times(f[x - B + 1] + 1) + (1 - \frac{q}{100}) \times (f[x - B] + 1) \]

  • 但是我们发现当\(B = 1\)的时候,会从\(DAG\)变成有自环的有向图,即使刚刚推导的公式可以使用,但是我们无法DP

  • 但是如果我们将\(B = 1\)代入第二个状态转移的式子后,容易得到:

  • \[f[x] = dp[x - 1] + \frac{100}{100 - q}\ \ (B = 1) \]

  • 所以当\(B = 1\)时按照上述式子转移即可

const int N = 1e6 + 10, M = 4e5 + 10;

// p / 100 * (f[x - B] + 2) + (1 - p / 100) * (f[x - B] + 1)
// q / 100 * (f[x - B + 1] + 1) + (1 - q / 100) * (f[x - B] + 1)

int a, b, p, q;
double f[N];

double dfs(int x, int b, int p, int q)
{
    if (x < b)
        return 0;
    if (f[x] != 0)
        return f[x];
    double ans = 1.0 * p / 100 * (dfs(x - b, b, p, q) + 2) + (1 - 1.0 * p / 100) * (dfs(x - b, b, p, q) + 1);
    if (b == 1)
    {
        ans = max(ans, dfs(x - 1, b, p, q) + (100.0 / (100 - q)));
    }
    else
    {
        ans = max(ans, 1.0 * q / 100 * (dfs(x - b + 1, b, p, q) + 1) + (1 - 1.0 * q / 100) * (dfs(x - b, b, p, q) + 1));
    }
    return f[x] = ans;
}

void solve()
{
    cin >> a >> b >> p >> q;
    cout << fixed << setprecision(10) << dfs(a, b, p, q) << endl;
}

例3·Fork in the Road

image-20230711233626181

题解

  • 我们考虑期望DP
  • 我们定义\(f[u]\)代表从\(u\)\(N\)号房间要走的通道数的期望
  • 题目要求我们堵塞一条路,我们先考虑\(O(m)\)枚举所有边,然后将这条边删去后\(O(m)\)再做一遍期望DP,所以时间复杂度为\(O(m^2)\),显然不可行
  • 但是容易发现如果我们枚举每个节点,对其最大的\(dp\)贡献值的边删去后,再做一遍期望DP,时间复杂度为\(O(m\times n)\)
const int N = 6e2 + 10, M = 4e5 + 10;

int n, m;
vector<int> g[N];
int deg[N];
double f[N];
double dp[N];
bool vis[N][N];
bool st[N];

double dfs(int u)
{
    if (u == n)
        return 0;
    if (f[u])
        return f[u];
    double ans = 0;
    for (auto v : g[u])
    {
        ans += (1.0 / deg[u]) * (dfs(v) + 1);
    }
    return f[u] = ans;
}

double DFS(int u)
{
    if (u == n)
        return 0;
    if (dp[u])
        return dp[u];
    double ans = 0;
    for (auto v : g[u])
    {
        if (vis[u][v])
            continue;
        if (st[u])
            ans += (1.0 / (deg[u] - 1)) * (DFS(v) + 1);
        else
            ans += (1.0 / deg[u]) * (DFS(v) + 1);
    }
    return dp[u] = ans;
}

void solve()
{
    cin >> n >> m;
    for (int i = 1; i <= m; ++i)
    {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        ++deg[u];
    }
    double ans = INF;
    ans = min(ans, dfs(1));
    for (int i = 1; i <= n; ++i)
    {
        int u = i;
        if (deg[u] == 1)
            continue;
        double mx = 0;
        int p = -1;
        for (auto v : g[u])
        {
            if (mx < (f[v] + 1) * (1.0 / deg[u]))
            {
                mx = (f[v] + 1) * (1.0 / deg[u]);
                p = v;
            }
        }
        memset(dp, 0, sizeof dp);
        vis[u][p] = true;
        st[u] = true;
        ans = min(ans, DFS(1));
        vis[u][p] = false;
        st[u] = false;
    }
    cout << fixed << setprecision(10) << ans << endl;
}