【Codeforces553E_CF553E】Kyoya and Train(概率_CDQ分治_FFT)

题目

Codeforces 553E

我为什么要写这道题?因为说到 553 ,你有没有想到 ……

翻译

这个 Kyoya Ootori 怎么看都像是日语名字但是我是真查不出来对应的汉字是什么(好像是什么京屋鳳之类的),方便起见直接认为主人公叫张三。

题目名称:张三和火车

描述

张三想坐火车去学校。有 \(n\) 个火车站和在不同车站间开行的 \(m\) 条单向火车线路。张三现在在 \(1\) 号火车站,学校在 \(n\) 号火车站。他必须买票才能坐火车,并且坐火车也需要花费时间。然而,由于火车存在缺陷,所以到达目的地所需的时间是随机的。如果张三到达学校的时间严格大于 \(t\) ,他需要支付 \(x\) 元的罚款。

对每条火车线路会给出票价和花费时间的概率分布。更加形式化地,第 \(i\) 条线路的票价是 \(c_i\) ;对于 \(1\leq k\leq t\) ,概率分布 \(p_{i,k}\) 表示这条线路需要花费 \(k\) 单位时间的概率。张三搭乘火车所需的时间是互相独立的随机变量(此外,如果张三搭乘一条火车线路超过一次,这条线路可能花费不同的时间,这些值也互相独立)。

张三想让去学校的花费的期望最小(包括票价和迟到的罚款)。当然,张三会采取最优方案去学校,并且每当他到达一个火车站,他可以根据目前已经花费的时间重新计算他的最优方案。如果张三采取最优方案,他去学校的花费的期望是多少?

输入

第一行包含四个整数 \(n,m,t,x(2\leq n\leq 50,1\leq m\leq 100,1\leq t\leq 20000,0\leq x\leq 10^6)\) 。

接下来 \(2m\) 行描述 \(m\) 条火车线路。

第 \(2i\) 行包含三个整数 \(a_i,b_i,c_i\) ,表示第 \(i\) 条单向线路从 \(a_i\) 到 \(b_i\) ,票价为 \(c_i(1\leq a_i,b_i\leq n,a_i\neq b_i,0\leq c_i\leq 10^6)\) 。

第 \((2i+1)\) 行包含 \(t\) 个整数 \(p_{i,1},p_{i,2},\dots,p_{i,t}\) ,\(\frac{p_{i,k}}{100000}\) 是这条线路花费 \(k\) 单位时间的概率。 \((\) 对于 \(1\leq k\leq t,0\leq p_{i,k}\leq 100000,\sum_{k=1}^t p_{i,k}=100000)\) 。

保证任意一对车站之间每个方向只有不超过一条线路。

输出

输出一个实数,表示到学校花费的最小期望。和标准答案的绝对或相对误差不超过 \(10^6\) 即为正确。

分析

其实思路挺显然的。

很明显,如果已经迟到了,因为迟到一分钟和迟到一天没区别,那么就不需要管时间了,直接沿着花钱最少的路线走到学校就可以了。因此只需要考虑还没有迟到时,即时间不超过 \(t\) 的决策。因为时间 \(a\) 可以转移到时间 \(a+t\) ,所以为了方便,实际上要考虑的范围是 \([0,2t]\) 。

看到 \(n\cdot t\) 这么小,考虑用 \(f_{i,j}\) 表示当前在 \(i\) ,已经过去了 \(j\) 单位时间。然后就有一个很显然的转移方程:

\[f_{u,i}=\begin{cases}d_u+x(i>t)\\ \min(\{a|a=c_e+\sum_{k=1}^{t}f_{v,i+k}\cdot p_{e,k},\mathrm{from}_e=u,\mathrm{to}_e=v\})\ \mathrm{otherwise}\end{cases}\]

(其中边 \(e\) 起点为 \(u\) ,终点为 \(v\) )

虽然图不是 DAG ,但只会从只会从时间晚的状态转移到时间早的状态,所以转移没有环。直接暴力做的复杂度是 \(O(mt^2)\) 。

仔细观察一下, \(\sum_{k=1}^tf_{v,i+k}\cdot p_{e,k}\) 是不是长得一脸多项式卷积的样子(什么?你说 \(i+k\) 和 \(k\) 是差为定值不是和为定值?你把其中一个翻转一下不就是和为定值了吗)。但很不幸的一点是,这个式子里用到了 \(f\) ,也就是卷积的结果。这是一个经典问题,详见 【洛谷4721】【模板】分治FFT(CDQ分治_NTT)

具体到这道题来说。因为 \(f\) 不是和式,而是对一堆和式取 min ,所以要设 \(g_{e,i}\) 表示 \(e\) 这条边对应的 \(\sum_{k=1}^tf_{v,i+k}\cdot p_{e,k}\) 。

对于每个分治的区间,先向下分治 \((mid,r]\) ,再枚举每一条边,用 FFT 计算 \((mid,r]\) 里的 \(f\) 对 \([l,mid]\) 里的 \(g\) 的贡献,最后向下分治 \([l,mid]\) 。因此,当分治到 \([l,l]\) 时,对 \(l\) 有贡献的区间(即 \((l,+\infin)\) )已经全部算完了,所以直接用 \(g_{e,l}\) 更新 \(f_{u,l}\) 。

用 FFT 计算 \((mid,r]\) 对 \([l,mid]\) 的贡献时,因为先分治了右区间,所以已经知道了 \((mid,r]\) 的 \(f\) 。把整个区间翻转,这样就变成了传统的已知左边计算右边的模型,同时也把上面式子里的 \(i+k\) 变成了 \(i-k\) 。

我感觉我说了一团浆糊。其实思路是比较好理解的,上面两段没说清楚的话直接看代码吧。

分治的每一层的多项式长度之和是 \(O(T)\) 的,因此时间复杂度是 \(O(mT\log^2T)\) 。

代码

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cctype>
#include <vector>
#include <functional>
#include <queue>
#include <cmath>
using namespace std;

namespace zyt
{
    template<typename T>
    inline bool read(T &x)
    {
        char c;
        bool f = false;
        x = 0;
        do
            c = getchar();
        while (c != EOF && c != '-' && !isdigit(c));
        if (c == EOF)
            return false;
        if (c == '-')
            f = true, c = getchar();
        do
            x = x * 10 + c - '0', c = getchar();
        while (isdigit(c));
        if (f)
            x = -x;
        return true;
    }
    inline bool read(double &x)
    {
        return ~scanf("%lf", &x);
    }
    template<typename T>
    inline void write(T x)
    {
        char buf[20];
        char *pos = buf;
        if (x < 0)
            putchar('-'), x = -x;
        do
            *pos++ = x % 10 + '0';
        while (x /= 10);
        while (pos > buf)
            putchar(*--pos);
    }
    inline void write(const double x, const int fixed = 9)
    {
        printf("%.*f", fixed, x);
    }
    typedef pair<int, int> pii;
    const int N = 60, M = 110, T = 2e4 + 10, B = 20, INF = 0x3f3f3f3f;
    int n, m, t, x, dis[N], head[N], ecnt;
    struct edge
    {
        int from, to, w, next;
    }e[M];
    double p[M][T], f[N][T << 1], g[M][T << 1];
    void add(const int a, const int b, const int c)
    {
        e[ecnt] = (edge){a, b, c, head[a]}, head[a] = ecnt++;
    }
    void Dijkstra()
    {
        static bool vis[N];
        priority_queue<pii, vector<pii>, greater<pii> > q;
        memset(dis, INF, sizeof(int[n + 1]));
        memset(vis, 0, sizeof(bool[n + 1]));
        dis[n] = 0;
        q.push(pii(0, n));
        while (!q.empty())
        {
            int u = q.top().second;
            q.pop();
            if (vis[u])
                continue;
            vis[u] = true;
            for (int i = head[u]; ~i; i = e[i].next)
            {
                int v = e[i].to;
                if (dis[v] > dis[u] + e[i].w)
                    dis[v] = dis[u] + e[i].w, q.push(pii(dis[v], v));
            }
        }
    }
    namespace Polynomial
    {
        const int LEN = T * 8;
        const double PI = acos(-1);
        struct cpx
        {
            double x, y;
            cpx(const double _x = 0, const double _y = 0)
                : x(_x), y(_y) {}
            cpx operator + (const cpx &b) const
            {
                return cpx(x + b.x, y + b.y);
            }
            cpx operator - (const cpx &b) const
            {
                return cpx(x - b.x, y - b.y);
            }
            cpx operator * (const cpx &b) const
            {
                return cpx(x * b.x - y * b.y, x * b.y + y * b.x);
            }
        };
        cpx omega[B][LEN], winv[B][LEN];
        int rev[B][LEN];
        bool flag[B];
        void init(const int n, const int lg2)
        {
            if (flag[lg2])
                return;
            flag[lg2] = true;
            cpx w = cpx(cos(2 * PI / n), sin(2 * PI / n)), wi = cpx(cos(2 * PI / n), -sin(2 * PI / n));
            omega[lg2][0] = winv[lg2][0] = cpx(1, 0);
            for (int i = 1; i < n; i++)
            {
                omega[lg2][i] = omega[lg2][i - 1] * w;
                winv[lg2][i] = winv[lg2][i - 1] * wi;
            }
            for (int i = 0; i < n; i++)
                rev[lg2][i] = ((rev[lg2][i >> 1] >> 1) | ((i & 1) << (lg2 - 1)));
        }
        void fft(cpx *const a, const cpx *const w, const int *const rev, const int n)
        {
            for (int i = 0; i < n; i++)
                if (i < rev[i])
                    swap(a[i], a[rev[i]]);
            for (int l = 1; l < n; l <<= 1)
                for (int i = 0; i < n; i += (l << 1))
                    for (int k = 0; k < l; k++)
                    {
                        cpx x = a[i + k], y = a[i + l + k] * w[n / (l << 1) * k];
                        a[i + k] = x + y;
                        a[i + l + k] = x - y;
                    }
        }
        void mul(const double *const a, const double *const b, double *const c, const int n)
        {
            static cpx x[LEN], y[LEN];
            int m = 1, lg2 = 0;
            while (m < (n + n - 1))
                m <<= 1, ++lg2;
            init(m, lg2);
            for (int i = 0; i < n; i++)
                x[i] = a[i], y[i] = b[i];
            memset(x + n, 0, sizeof(cpx[m - n]));
            memset(y + n, 0, sizeof(cpx[m - n]));
            fft(x, omega[lg2], rev[lg2], m), fft(y, omega[lg2], rev[lg2], m);
            for (int i = 0; i < m; i++)
                x[i] = x[i] * y[i];
            fft(x, winv[lg2], rev[lg2], m);
            for (int i = 0; i < n; i++)
                c[i] = x[i].x / m;
        }
    }
    void solve(const int l, const int r)
    {
        if (l > t)
            return;
        if (l == r)
        {
            for (int i = 0; i < ecnt; i++)
                f[e[i].to][l] = min(f[e[i].to][l], g[i][l] + e[i].w);
            return;
        }
        int mid = (l + r) >> 1, len = r - l + 1;
        solve(mid + 1, r);
        for (int i = 0; i < ecnt; i++)
        {
            static double a[T << 1], b[T << 1];
            a[0] = 0;
            for (int j = 0; j < len; j++)
            {
                a[j] = (l + len - j - 1 > mid ? f[e[i].from][l + len - j - 1] : 0);
                b[j] = (j <= t ? p[i][j] : 0);
            }
            Polynomial::mul(a, b, a, len);
            for (int j = 0; j <= mid - l; j++)
                g[i][j + l] += a[len - j - 1];
        }
        solve(l, mid);
    }
    int work()
    {
        read(n), read(m), read(t), read(x);
        memset(head, -1, sizeof(int[n + 1]));
        for (int i = 0; i < m; i++)
        {
            int a, b, c;
            read(a), read(b), read(c);
            add(b, a, c);
            for (int j = 1; j <= t; j++)
                read(p[i][j]), p[i][j] /= 1e5;
        }
        Dijkstra();
        for (int i = 1; i <= n; i++)
        {
            for (int j = 0; j <= t; j++)
                f[i][j] = (i == n ? 0 : INFINITY);
            for (int j = t + 1; j <= (t << 1); j++)
                f[i][j] = dis[i] + x;
        }
        solve(0, t << 1);
        write(f[1][0]);
        return 0;
    }
}
int main()
{
    return zyt::work();
}

原文地址:https://www.cnblogs.com/zyt1253679098/p/12530827.html

时间: 2024-10-08 18:37:32

【Codeforces553E_CF553E】Kyoya and Train(概率_CDQ分治_FFT)的相关文章

●codeforces 553E Kyoya and Train

题链: http://codeforces.com/problemset/problem/623/E 题解: FFT,DP 题意: 一个有向图,给出每条边的起点u,终点v,费用c,以及花费每种时间的概率P[e][j](表示走第e条边花费时间为j的概率) 现在需要从1号点走到n号点,如果不能在T个单位时间内到达,则达到后还要另外支付X的费用. 求出所需支付的最小期望费用. 先是一个暴力的DP方案: (考虑到每条边的耗时至少为1,可以把状态设为类似分层图的形式) 定义$F[i][t]$为$t$时刻在

Codeforces 553E Kyoya and Train

题目大意 链接:CF533E 给一张\(n\)个点,\(m\)条边的图,起点\(1\)终点\(n\),如果不能在\(T\)的时间内到达则需支付\(X\)的代价. 走每条边都会支付一定代价,经过一条边\(i\)的时间有\(p_{i,j}\)的概率为\(j\),最小化期望代价. 题目分析 暴力方法:期望DP 设\(f_{i,j}\)表示在第\(j\)时刻,从\(i\)点出发,到达终点的期望花费, 设边为\(e\),边上两点为\(x,y\),边集为\(E\),则有 \[ f(x,t)=\min\lim

「PKUWC2018」猎人杀(分治NTT+概率期望)

Description 猎人杀是一款风靡一时的游戏"狼人杀"的民间版本,他的规则是这样的: 一开始有 \(n\) 个猎人,第 \(i\) 个猎人有仇恨度 \(w_i\) ,每个猎人只有一个固定的技能:死亡后必须开一枪,且被射中的人也会死亡. 然而向谁开枪也是有讲究的,假设当前还活着的猎人有 \([i_1,i_2,...,i_m]\),那么有 \(\frac{w_{i_k}}{\sum_{j=1}^nw_{i_j}}\) 的概率是向猎人 \(k\) 开枪. 一开始第一枪由你打响,目标的选

「PKUWC2018」猎人杀(概率+容斥+分治NTT)

https://loj.ac/problem/2541 很有意思的一道题目. 直接去算这题话,因为分母会变,你会发现不管怎么样都要枚举顺序. 考虑把题目转换,变成分母不会变的,即对于一个已经删过的,我们不把它从分母中剔除,但是,每一次的选择需要一直选直到选了一个没有被删过的. 然后再考虑怎么计算,这时就可以容斥了: 1既然要最后删除,我们枚举一个集合S一定在它之后被删,其它的随意. 设\(sw\)为\(\sum_{i\in S}w[i]\),\(W=\sum_{i=1}^n w[i]\) 最后答

基于并行化的神经网络和复旦中文语料库,构建中文概率语言模型

本文旨在基于复旦中文语料库和神经网络模型构建中文的概率语言模型. 统计语言模型的一个目标是找到句子中不同词汇的联合分布,也就是找到一个单词序列出现的概率,一个训练好的统计语言模型可以被应用于语音识别.中文输入法.机器翻译等领域.在神经网络方法被提出之前,一个非常成功的构建语言模型的方法是 n-gram,n-gram 模型学习出统计出给出特定的单词序列时某个单词出现的条件概率,并且通过把一系列重叠的短语拼接起来,获得了模型的泛化能力.然而 n-gram 模型也有很多不如人意的地方.第一, n 的数

bzoj 2152: 聪聪可可 树的点分治

2152: 聪聪可可 Time Limit: 3 Sec  Memory Limit: 259 MBSubmit: 485  Solved: 251[Submit][Status] Description 聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃.两个人都想玩儿电脑(可是他们家只有一台电脑)……遇到这种问题,一般情况下石头剪刀布就好了,可是他们已经玩儿腻了这种低智商的游戏.他们的爸爸快被他们的争吵烦死了,所以他发明了一个新游戏:由爸爸在纸上画n个“

codechef Prime Distance On Tree(树分治+FFT)

题目链接:http://www.codechef.com/problems/PRIMEDST/ 题意:给出一棵树,边长度都是1.每次任意取出两个点(u,v),他们之间的长度为素数的概率为多大? 树分治,对于每个根出发记录边的长度出现几次,然后每次求卷积,用素数表查一下即可添加答案. 1 #include<algorithm> 2 #include<cstdio> 3 #include<cmath> 4 #include<cstring> 5 #include

bzoj 2244: [SDOI2011]拦截导弹 cdq分治

2244: [SDOI2011]拦截导弹 Time Limit: 30 Sec  Memory Limit: 512 MBSec  Special JudgeSubmit: 237  Solved: 103[Submit][Status][Discuss] Description 某 国为了防御敌国的导弹袭击,发展出一种导弹拦截系统.但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度.并且能够拦截任意速度的导 弹,但是以后每一发炮弹都不能高于前一发的高度,其拦截的导弹的飞行速度

点分治2152 聪聪可可

/*点分治 将点一个一个讨论,找重心,讨论根节点,分别讨论子树.2152: 聪聪可可 Time Limit: 3 Sec Memory Limit: 259 MBSubmit: 1400 Solved: 703[Submit][Status][Discuss]Description 聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃.两个人都想玩儿电脑(可是他们家只有一台电脑)……遇到这种问题,一般情况下石头剪刀布就好了,可是他们已经玩儿腻了这种低智商的游戏