Noip 2016 换教室 - 期望DP

本文探讨了随机变量期望的概念及其在动态规划中的应用。通过具体案例,详细介绍了如何使用动态规划解决涉及随机事件的问题,并给出了实现代码。

期望反映了一个随机变量的平均结果,是所有可能结果的概率乘上结果的和。例如对于一个随机变量x,1/3几率变为1,1/3几率变为2,1/3几率变为3,则x的期望值为

131+132+133=213∗1+13∗2+13∗3=2

期望具有线性性质,我们可以根据加法原理和乘法原理来对期望进行计算

可以发现,最终方案是某种决策集合,而决策是没有概率的,所以每个状态应该取一个最优决策的期望消费,而不是各种决策搞一下期望和什么的,期望体力是确定的,能使期望体力不同是决策不同的原因
就是。。。一个状态只能由前面阶段的一个状态转移过来,因为一个人不能同时处于两种状态,最终方案是每个决断取了一个决策构成的集合

f(i,j,0/1)f(i,j,0/1)表示到了第i节课,申请j次,第i节课是否申请的最小体力期望(申请需要一次提交)
那么转移就要在多重情况中取最优的,分别为:

i不申请,i-1申请成功
i不申请,i-1申请失败
i申请成功,i-1申请成功
i申请成功,i-1申请失败
i申请失败,i-1申请成功
i申请失败,i-1申请失败
不提交任何申请

具体的转移方程看代码吧。。。

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
#define debug(x) cerr << #x << "=" << x << endl; 
const int MAXN = 2000 + 10;
int n,m,v,e,c[MAXN],d[MAXN];
double g[310][310],k[MAXN],f[MAXN][MAXN][2],ans = 1e30;
void floyed() {
    for(int k=1; k<=v; k++)
        for(int i=1; i<=v; i++)
            for(int j=1; j<=v; j++)
                g[i][j] = min(g[i][j], g[i][k] + g[k][j]);
}
void solve() {
    for(int i=2; i<=n; i++) 
        for(int j=0; j<=m; j++)
            if(j==0)
                f[i][j][0] = f[i-1][j][0] + g[c[i-1]][c[i]];            
            else {
                f[i][j][0] = min( f[i-1][j][0] + g[c[i-1]][c[i]], f[i-1][j][1] + g[d[i-1]][c[i]] * k[i-1] + g[c[i-1]][c[i]] * (1-k[i-1]) );
                f[i][j][1] = min( f[i-1][j-1][0] + g[c[i-1]][d[i]] * k[i] + g[c[i-1]][c[i]] * (1-k[i]),
                f[i-1][j-1][1] 
                + g[d[i-1]][d[i]] * k[i-1] * k[i]
                + g[d[i-1]][c[i]] * k[i-1] *(1-k[i])
                + g[c[i-1]][d[i]] * (1-k[i-1]) * k[i]
                + g[c[i-1]][c[i]] * (1-k[i-1]) * (1-k[i]) );  
            }
}
int main() {
    cin >> n >> m >> v >> e;
    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++) 
            g[i][j] = 1e30;
        g[i][i] = 0;
    }
    for(int i=0; i<=n; i++)
        for(int j=0; j<=m; j++)
            f[i][j][0] = f[i][j][1] = 1e30;
    for(int i=1; i<=e; i++) {
        int a, b;
        double z;
        cin >> a >> b >> z;
        g[a][b] = g[b][a] = min(g[a][b], z);
    }
    floyed();
    f[1][0][0] = 0;
    f[1][1][1] = 0;
    solve();
    ans = 1e30;
    for(int i=0; i<=m; i++)
        ans = min(ans, min(f[n][i][0], f[n][i][1]));
    printf("%.2lf",ans);
    return 0;
}
<think>嗯,用户问的是NOIP2016提高组第三题“海港”的解题思路和算法分析。首先,我得回忆一下这个题目的具体要求。根据引用里的信息,题目大概是关于处理到达海港的船只,每艘船有乘客的国籍数据,需要实时统计最近24小时内的不同国籍数量。用户可能想要一个高效的算法,因为直接遍历所有乘客肯定会超时。 首先,题目中的时间窗口是24小时,也就是86400秒。每次新船到达时,需要移除所有超过24小时的旧数据,然后统计剩下的国籍数。直接每次检查所有记录显然不行,时间复杂度会很高,特别是当数据量大时。 那如何处理时间窗口呢?滑动窗口或者队列结构可能有用。因为每次新船到达时,只需要移除时间超过当前时间-86400的记录,这样可以用队列来维护乘客记录,保证队列里的时间都在窗口内。这样可以避免每次全量检查,只需从队头开始移除过期的。 然后统计国籍数量。这里的问题是如何高效地维护国籍的出现次数。每添加一个乘客,对应国籍的计数加1;当移除过期乘客时,对应国籍的计数减1,当减到0时,总的国家数减1。这样需要维护一个哈希表或字典来记录每个国籍的数量。 但乘客是按船到达的,而每艘船可能有多个乘客。所以可能需要将每个乘客的信息分开存储,或者按批次处理。比如,队列中的每个元素是一批乘客(同一艘船的),并记录他们的到达时间和国籍列表。当处理新船时,先检查队列头部的船是否超出时间窗口,如果是,就依次将这些乘客的国籍计数减少,直到所有过期的船都被移除。 这样,每次处理新船时,需要先处理队列中过期的船,然后添加当前船的乘客,并更新国家计数。这样时间复杂度应该是线性的,因为每个乘客只会被加入和移除一次队列。 可能的难点是,如何处理同一时间多艘船的情况,或者时间跨度较大的情况。比如,如果有很多船都在时间窗口内,那么队列中的元素可能很多,但每次处理时只需要移除超过时间窗口的部分,而不是全部。 举个例子,假设当前时间是ti,那么队列中保存的是所有到达时间大于ti-86400的船。当新船到达时间ti时,我们需要将队列中所有时间<=ti-86400的船移除,并更新国籍计数。然后,将当前船的乘客加入队列,并更新计数。这样就能保证队列里的数据都在窗口内,统计国家数的时候只需查看当前哈希表中计数大于0的国家数量。 这样,主要的数据结构是队列,每个元素包含时间和该船的所有乘客国籍,以及一个哈希表记录国籍的出现次数,还有一个变量记录当前的国家数量。 另外,需要考虑时间ti的范围。题目中ti可能很大,但每次只需要比较相对时间差是否超过86400,所以不需要担心时间溢出问题。 可能的优化点:队列中的每个元素可以存储该船的时间和国籍列表,这样处理过期船时,逐个出队,并减少对应国籍的计数。当国籍计数减到0时,总的国家数减1。 现在,总结一下步骤: 1. 初始化一个队列,用来保存每艘船的到达时间和乘客国籍列表。 2. 维护一个哈希表(字典)记录每个国籍的出现次数。 3. 维护一个变量count记录当前的有效国家数。 4. 对于每艘新到达的船: a. 检查队列头部的船是否超过当前时间ti-86400。如果是,依次出队,并减少对应国籍的计数。如果某个国籍的计数减到0,则count减1。 b. 将当前船的乘客国籍加入哈希表,如果该国籍之前计数为0,则count加1。 c. 将当前船的信息入队。 d. 输出当前的count值。 这样,每个乘客的国籍被处理两次(入队和出队),时间复杂度是O(K),其中K是所有乘客的总数,应该可以通过题目中的时间限制。 需要注意的点: - 处理时间时,要确保比较的是船的时间是否在窗口内。例如,当前船的时间是ti,那么窗口的有效范围是ti-86400+1到ti?或者ti-86400到ti?需要看题目具体要求,比如,是否包括刚好24小时前的船。比如,题目中的描述可能是“在24小时内的”,也就是ti - t <= 86400,或者严格小于? 根据引用3中的样例输入,比如第一艘船时间1,第二艘船时间4,第三艘时间10。假设样例输出是3、4、4。这说明当第三艘船时间为10时,前两艘船是否在窗口内取决于窗口是否包含10-86400到10。但样例中的时间可能较小,所以需要明确比较条件是 ti - prev_t >=86400吗? 比如,假设当前处理的时间是ti,那么队列中保留的是那些船的时间 > ti -86400。当处理新的ti时,将所有队列中时间<=ti -86400的船移除。 例如,ti=86400,那么移除所有时间<=0的船。ti=86401,则移除时间<=1的船。这样,每个船在队列中的时间必须严格大于ti-86400,否则会被移除。 所以在代码中,当处理新船时,先循环检查队列头部的船的时间是否<=当前ti -86400,如果是,就出队,并减少该船所有乘客的国籍计数,如果国籍计数减到0,则总count减1。然后处理当前船,将乘客国籍加入哈希表,若加入前计数为0,则count加1,然后将该船的信息入队。 这样,每个乘客被处理两次,时间复杂度是线性的。 所以,这样的算法应该可以满足题目的要求,避免超时。 现在,用户可能还需要具体的代码实现,或者更详细的步骤解释。比如,如何处理队列中的每个船的数据结构,如何记录每个船的乘客国籍列表,以及如何维护哈希表和count变量。 例如,在Python中,可以用一个deque作为队列,每个元素是(时间,国籍列表)。哈希表可以用字典。然后遍历每个新船,先处理过期数据,再处理当前数据。 可能的代码结构: from collections import deque q = deque() nationality_count = {} current_count = 0 n = int(input()) for _ in range(n): ti, ki, *nationalities = map(int, input().split()) # 处理过期船只 while q and q[0][0] <= ti - 86400: old_t, old_nations = q.popleft() for nation in old_nations: nationality_count[nation] -= 1 if nationality_count[nation] == 0: current_count -= 1 # 处理当前船的乘客 new_nations = nationalities[:ki] for nation in new_nations: if nationality_count.get(nation, 0) == 0: current_count += 1 nationality_count[nation] = nationality_count.get(nation, 0) + 1 q.append( (ti, new_nations) ) print(current_count) 不过,这里需要注意,当处理旧的船只时,可能需要遍历该船的所有国籍,并逐个减少计数。例如,假设一个船有多个相同国籍的乘客,那么每个乘客都需要处理,不能合并,因为每个乘客的国籍都会影响计数。 但是,如果同一艘船上有多个相同国籍的乘客,那么在出队时,每个都要减少一次计数。比如,如果一艘船有3个国籍为2的乘客,那么在移除该船时,国籍2的计数减3。当减到0时,current_count减1。 但原题中的输入中,每艘船的ki个乘客可能有重复的国籍。例如,引用3中的样例输入: 3 1 4 4 1 2 2 2 2 2 3 10 1 3 其中第二艘船有ki=2,两个乘客国籍都是3。 那么在处理这艘船时,加入哈希表中国籍3的计数是加2。当该船被移除时,国籍3的计数减2,如果原来计数是2,减后变为0,则current_count减1。 所以,在队列中的每个元素需要保存该船的所有乘客国籍的列表,包括重复的国籍。例如,第一艘船的国籍列表是[4,1,2,2],这样出队时,每个国籍都要处理一次。这样,如果同一艘船中有多个相同国籍的乘客,在出队时会逐个减少计数,可能导致多次减到0的情况,但实际上只有当最后一个减少到0时才需要调整current_count。 比如,假设国籍2的当前计数是3,在出队时减去2次,变成1,那么current_count不需要变化。但如果减到0的话,就需要减少current_count。 所以,代码中需要确保每个乘客的国籍都被处理,这样哈希表的计数正确,而current_count的增减只在计数从0到1或1到0的时候触发。 所以,上述代码中的处理是正确的,因为当处理旧船时,遍历该船的所有国籍,逐个减少计数,如果某个nation的计数减到0,则current_count减1。 但是在加入新船时,可能多个相同国籍的乘客被加入,此时,只有当该国籍的计数从0变为1时,current_count才加1。其他情况下(比如原本已经有该国籍的计数),current_count不变。 这样,current_count始终维护的是当前不同国籍的数量。 这个逻辑是正确的。 综上,解题思路是使用队列维护时间窗口内的船只,哈希表维护国籍的出现次数,并在处理每艘新船时,先移除过期的船,再添加当前船的乘客国籍,最后输出当前的国籍数量。</think>### NOIP2016提高组第三题'海港'解题思路 #### 问题分析 题目要求统计每艘船到达后,过去24小时内所有乘客的不同国籍数量[^3]。核心挑战在于高效处理时间窗口内的数据,避免暴力遍历导致超时。 #### 算法设计 1. **滑动窗口**:使用队列维护时间窗口内的船只信息。每次新船到达时,移除所有超过24小时的旧数据。 2. **哈希表计数**:记录每个国籍的乘客数量。当某国籍的计数从0变为1时,总国家数加1;当计数减到0时,总国家数减1。 #### 具体步骤 1. **初始化队列和哈希表**: - 队列存储船只的到达时间和乘客国籍列表。 - 哈希表`nationality_count`记录国籍出现次数。 - 变量`current_count`表示当前有效国家数。 2. **处理每艘船**: - **移除过期数据**:检查队列头部船只的时间是否超过当前时间$t_i - 86400$,若过期则出队,并减少对应国籍计数。若某国籍计数归零,则`current_count`减1。 - **添加新数据**:将当前船的乘客国籍加入哈希表。若国籍计数从0变为1,则`current_count`加1。 - **输出结果**:每处理完一艘船,输出`current_count`。 #### 代码示例 ```python from collections import deque q = deque() nationality_count = {} current_count = 0 n = int(input()) for _ in range(n): data = list(map(int, input().split())) ti, ki = data[0], data[1] nations = data[2: 2 + ki] # 移除过期船只 while q and q[0][0] <= ti - 86400: old_t, old_nations = q.popleft() for nation in old_nations: nationality_count[nation] -= 1 if nationality_count[nation] == 0: current_count -= 1 # 处理当前船只 for nation in nations: if nationality_count.get(nation, 0) == 0: current_count += 1 nationality_count[nation] = nationality_count.get(nation, 0) + 1 q.append((ti, nations)) print(current_count) ``` #### 复杂度分析 - 时间复杂度:$O(K)$,其中$K$为总乘客数,每个乘客仅被加入和移除队列一次。 - 空间复杂度:$O(K)$,队列和哈希表存储所有乘客信息。 #### 优化点 1. **合并重复国籍**:若同一艘船的乘客国籍重复,可合并计数减少操作次数。 2. **延迟删除**:在极端时间跨度下,队列可能存储大量过期数据,但实际操作中队列长度受窗口限制,无需额外优化[^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值