[noip2016-提高] 换教室

GitHub跳转原题关系图返回列表

通过 Floyd 预处理任意两教室间的最短路,并使用动态规划求出在至多更换 $m$ 节课程限制下,移动消耗体力的期望最小值。

OJ: roj

题目 ID: 3640

难度:提高+/省选-

标签:动态规划概率期望最短路

创建: 2026-10-10 09:20

更新: 2026-10-10 09:20

形式化题目

有 nn 节课程依次在 nn 个时间段进行,每节课程 ii 有两个可能的上课地点:原本安排的教室 cic_i 和申请更换后的教室 did_i。申请成功的概率为 kik_i,不同课程的申请结果互相独立。

你至多可以对 mm 节课程提交更换教室的申请,申请一旦提交便不能撤回且不可根据前序结果更改后序决定。两节课程之间如果地点不同,需要移动,教室构成一个包含 vv 个顶点、ee 条无向带权边的图。移动消耗体力值为两教室间的最短路径长度。

求在最优的申请策略下,这 nn 节课程期间移动所消耗体力的期望值的最小值。

正解

思路

首先由于每次下课后从一间教室走到下一间教室走的必然是最短路径,而且教室数 v≤300v \le 300 较小,可以使用 Floyd 算法在 O(v3)O(v^3) 的时间内求出任意两间教室间的最短路。对于图中的重边保留权值最小的即可。

接下来是一个比较典型的线性动态规划。 在所有的 nn 节课中依次做出决策(不申请或申请),并且限制了总共最多申请 mm 次。 由于期望的线性性质,可以按阶段进行转移。我们用 f[i][j][0/1]f[i][j][0/1] 表示前 ii 节课,共申请了 jj 次,第 ii 节课 不申请(0) / 申请(1) 时,前 ii 节课产生的总移动体力期望的最小值。

可以利用滚动数组优化掉第一维 ii,或者直接每一轮只从上一轮的状态进行转移。 设前一节课的原教室为 cprevc_{prev},更换后的教室为 dprevd_{prev},当前课程原教室为 ccurrc_{curr},更换后教室为 dcurrd_{curr}。 上一节课的成功概率为 kprevk_{prev},当前课概率为 kcurrk_{curr}。

若当前课不申请(00),那么必然在 ccurrc_{curr} 上课:

  • 若上一节课未申请:体力增加为 dis[cprev][ccurr]dis[c_{prev}][c_{curr}]
  • 若上一节课申请了:有 kprevk_{prev} 概率在 dprevd_{prev} 上课,有 (1−kprev)(1 - k_{prev}) 概率仍在 cprevc_{prev} 上课。 体力期望增加为 kprev×dis[dprev][ccurr]+(1−kprev)×dis[cprev][ccurr]k_{prev} \times dis[d_{prev}][c_{curr}] + (1 - k_{prev}) \times dis[c_{prev}][c_{curr}]

若当前课申请(11),需要消耗一次申请机会,那么有 kcurrk_{curr} 概率在 dcurrd_{curr},有 1−kcurr1 - k_{curr} 概率仍在 ccurrc_{curr}:

  • 若上一节未申请: 体力期望增加为 kcurr×dis[cprev][dcurr]+(1−kcurr)×dis[cprev][ccurr]k_{curr} \times dis[c_{prev}][d_{curr}] + (1 - k_{curr}) \times dis[c_{prev}][c_{curr}]
  • 若上一节申请了:有 4 种组合情况,分别为都成功、前成功后失败、前失败后成功、都失败。 体力期望增加为: kprevkcurr×dis[dprev][dcurr]+kprev(1−kcurr)×dis[dprev][ccurr]+(1−kprev)kcurr×dis[cprev][dcurr]+(1−kprev)(1−kcurr)×dis[cprev][ccurr]k_{prev} k_{curr} \times dis[d_{prev}][d_{curr}] + k_{prev} (1 - k_{curr}) \times dis[d_{prev}][c_{curr}] + (1 - k_{prev}) k_{curr} \times dis[c_{prev}][d_{curr}] + (1 - k_{prev}) (1 - k_{curr}) \times dis[c_{prev}][c_{curr}]

每一轮迭代后保留期望值的最小值即可,由于转移的合法性,初始状态仅 f[1][0][0]=0,f[1][1][1]=0f[1][0][0] = 0, f[1][1][1] = 0(若 m≥1m \ge 1)合法,其余均为无穷大。最后结果即为 min⁡(f[n][j][0],f[n][j][1])\min(f[n][j][0], f[n][j][1]) 对 0≤j≤m0 \le j \le m 取极小值。

代码

python
import sys

def main():
    input = sys.stdin.read
    data = input().split()
    if not data:
        return
    
    n, m, v, e = map(int, data[:4])
    idx = 4
    
    c = [0] + [int(x) for x in data[idx:idx+n]]
    idx += n
    d = [0] + [int(x) for x in data[idx:idx+n]]
    idx += n
    k = [0.0] + [float(x) for x in data[idx:idx+n]]
    idx += n
    
    dis = [[float('inf')] * (v + 1) for _ in range(v + 1)]
    for i in range(1, v + 1):
        dis[i][i] = 0
        
    for _ in range(e):
        u = int(data[idx])
        v_node = int(data[idx+1])
        w = int(data[idx+2])
        idx += 3
        if w < dis[u][v_node]:
            dis[u][v_node] = dis[v_node][u] = w
            
    for p in range(1, v + 1):
        for i in range(1, v + 1):
            if dis[i][p] != float('inf'):
                for j in range(1, v + 1):
                    if dis[i][p] + dis[p][j] < dis[i][j]:
                        dis[i][j] = dis[i][p] + dis[p][j]
                        
    f = [[float('inf')] * 2 for _ in range(m + 1)]
    f[0][0] = 0.0
    if m >= 1:
        f[1][1] = 0.0
        
    for i in range(2, n + 1):
        nf = [[float('inf')] * 2 for _ in range(m + 1)]
        c_prev, d_prev, k_prev = c[i-1], d[i-1], k[i-1]
        c_curr, d_curr, k_curr = c[i], d[i], k[i]
        
        for j in range(min(i, m) + 1):
            v1 = f[j][0] + dis[c_prev][c_curr]
            v2 = f[j][1] + k_prev * dis[d_prev][c_curr] + (1.0 - k_prev) * dis[c_prev][c_curr]
            nf[j][0] = min(v1, v2)
            
            if j > 0:
                v3 = f[j-1][0] + k_curr * dis[c_prev][d_curr] + (1.0 - k_curr) * dis[c_prev][c_curr]
                v4 = f[j-1][1] + \
                     k_prev * k_curr * dis[d_prev][d_curr] + \
                     k_prev * (1.0 - k_curr) * dis[d_prev][c_curr] + \
                     (1.0 - k_prev) * k_curr * dis[c_prev][d_curr] + \
                     (1.0 - k_prev) * (1.0 - k_curr) * dis[c_prev][c_curr]
                nf[j][1] = min(v3, v4)
        f = nf
        
    ans = min(min(f[j][0], f[j][1]) for j in range(m + 1))
    print(f"{ans:.2f}")

if __name__ == '__main__':
    main()
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <iomanip>

using namespace std;

typedef long long ll;

const int MAXN = 2005;
const int MAXV = 305;
const double INF = 1e18;

int n, m, v, e;
int c[MAXN], d[MAXN];
double k[MAXN];
int dis[MAXV][MAXV];

double f[MAXN][2];
double nf[MAXN][2];

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    if (!(cin >> n >> m >> v >> e)) return 0;

    for (int i = 1; i <= n; ++i) {
        cin >> c[i];
    }
    for (int i = 1; i <= n; ++i) {
        cin >> d[i];
    }
    for (int i = 1; i <= n; ++i) {
        cin >> k[i];
    }

    for (int i = 1; i <= v; ++i) {
        for (int j = 1; j <= v; ++j) {
            if (i == j) dis[i][j] = 0;
            else dis[i][j] = 1e8;
        }
    }

    for (int i = 0; i < e; ++i) {
        int u, v_node, w;
        cin >> u >> v_node >> w;
        if (w < dis[u][v_node]) {
            dis[u][v_node] = w;
            dis[v_node][u] = w;
        }
    }

    for (int p = 1; p <= v; ++p) {
        for (int i = 1; i <= v; ++i) {
            for (int j = 1; j <= v; ++j) {
                if (dis[i][p] + dis[p][j] < dis[i][j]) {
                    dis[i][j] = dis[i][p] + dis[p][j];
                }
            }
        }
    }

    for (int j = 0; j <= m; ++j) {
        f[j][0] = f[j][1] = INF;
    }
    f[0][0] = 0.0;
    if (m >= 1) {
        f[1][1] = 0.0;
    }

    for (int i = 2; i <= n; ++i) {
        for (int j = 0; j <= m; ++j) {
            nf[j][0] = nf[j][1] = INF;
        }
        for (int j = 0; j <= min(i, m); ++j) {
            // 第 i 节课不申请
            double v1 = f[j][0] + dis[c[i-1]][c[i]];
            double v2 = f[j][1] + k[i-1] * dis[d[i-1]][c[i]] + (1.0 - k[i-1]) * dis[c[i-1]][c[i]];
            nf[j][0] = min(v1, v2);
            
            // 第 i 节课申请
            if (j > 0) {
                double v3 = f[j-1][0] + k[i] * dis[c[i-1]][d[i]] + (1.0 - k[i]) * dis[c[i-1]][c[i]];
                double v4 = f[j-1][1] + 
                            k[i-1] * k[i] * dis[d[i-1]][d[i]] + 
                            k[i-1] * (1.0 - k[i]) * dis[d[i-1]][c[i]] + 
                            (1.0 - k[i-1]) * k[i] * dis[c[i-1]][d[i]] + 
                            (1.0 - k[i-1]) * (1.0 - k[i]) * dis[c[i-1]][c[i]];
                nf[j][1] = min(v3, v4);
            }
        }
        for (int j = 0; j <= m; ++j) {
            f[j][0] = nf[j][0];
            f[j][1] = nf[j][1];
        }
    }

    double ans = INF;
    for (int j = 0; j <= m; ++j) {
        ans = min(ans, f[j][0]);
        ans = min(ans, f[j][1]);
    }

    cout << fixed << setprecision(2) << ans << "\n";

    return 0;
}

复杂度

  • 时间复杂度:计算最短路 Floyd O(v3)O(v^3)。动态规划状态数为 O(nm)O(nm),每次转移常数时间 O(1)O(1)。故总时间复杂度为 O(v3+nm)O(v^3 + nm)。其中 v≤300,n,m≤2000v \le 300, n,m \le 2000,能够毫无压力地在时限内跑完。
  • 空间复杂度:存储地图最短路 O(v2)O(v^2),DP 使用了滚动数组空间复杂度为 O(m)O(m) 或不滚动 O(nm)O(nm),都非常优秀。总空间复杂度为 O(v2+m)O(v^2 + m)。

总结

  • 图中会有重边、自环,需要在建图时对同样两点的距离取最小值。
  • 初始化地图最短距离为无穷大,注意不要使用太大的无穷大造成溢出。DP 的初值同样应为一个较大的数。
  • 概率转移一定要推导仔细,所有概率系数相加恒等于 1,保证期望和正确。输出要注意四舍五入保留两位小数。

★★ 数据目录污染记录(T78,数据缺陷第 24 类)

new_ROJ/problems/3640/data/ 原本含 两套互不相同的数据:

序列 数量 首行形态 示例答案 闸门
classroom1..25.in/.ans 25 对 1 1 300 34973(4 整数 + 多行浮点) 0.00 PASS 25/25
★ problem1..20.in/.ans 20 对 1 2 / 1 1(图的边) 0 FAIL 20/20

机械判据(决定性)

原闸门结果 恰好是 PASS=25, FAIL=20:

text
= 「classroom 序列【全过】」+ 「problem 序列【全败】」
⇒ 这不是「算法在 20 个点上错了」,而是【数据分两套】的指纹

★ 判据模式:当失败数恰好等于某个文件序列的长度时, 应怀疑「目录里有多个序列」,而不是「算法部分正确」。

处置

  1. ★ 不做「让 20 个异题点通过」的努力(那是 T62:不该反解)
  2. 异题文件已移出 data/,备份在 new_ROJ/problems/_foreign-3640/(40 个文件)
  3. 移出后闸门 PASS=25/25(main.cpp 与 main.py 双通过)⇒ 证实主解对本題序列完全正确

与 T41 的关系

编号 缺陷
T41 题面与数据是两道不同的题(整个数据都是异题的)
T78 ★ 数据目录【混入】异题数据(部分污染)

⇒ T78 更隐蔽:一部分点会通过,看起来像「算法不够好」。