题目大意
给一个有向图,有一个人要从 1 1 1走到 n n n,第 i i i号边花费的钱为 c i c_i ci,花费的时间为 1 1 1~ T T T中随机的值,每种时间的概率为 p i , j p_{i,j} pi,j,如果这个人在 T T T时刻之后走到 n n n,就要交 X X X的罚款,求这个人花钱的最小期望。
题解
令
d
p
[
u
]
[
t
]
dp[u][t]
dp[u][t]表示当前走到了u号结点,已经花费的时间为t,走到终点的最小期望代价。
d
p
[
u
]
[
t
]
=
m
i
n
{
c
[
u
→
v
]
+
∑
k
=
1
T
d
p
[
v
]
[
t
+
k
]
×
P
[
u
→
v
]
[
k
]
}
dp[u][t]=min \left\{ c[u\rightarrow v]+\sum_{k=1}^{T} dp[v][t+k]\times P[u\rightarrow v][k] \right\}
dp[u][t]=min{c[u→v]+k=1∑Tdp[v][t+k]×P[u→v][k]}
可以发现如果将dp[v]翻转,
∑
k
=
1
T
d
p
[
v
]
[
(
2
T
−
t
)
−
k
]
×
P
[
u
→
v
]
[
k
]
\sum_{k=1}^{T} dp[v][(2T-t)-k]\times P[u\rightarrow v][k]
∑k=1Tdp[v][(2T−t)−k]×P[u→v][k],即成为卷积的形式,可以用FFT优化
然而这个图并不是有向无环图,直接用FFT优化dp实际上是有后效性的,一个结点可能会被多次更新DP。
但是仔细观察发现,把所有dp的状态拆出来,转移实际上不存在环(时间永远会增加),即时间小的状态一定由时间大的状态转移过来。
于是我们可以分段FFT,先计算一段时间大的,用它去更新时间小的,这就可以利用CDQ分治来完成。
一些细节:
初始值:dp[N][1~T]=0
如果dp[u][t]中t>T,则dp[u][t]=dis[u][n]+X(反正都迟到了,找一条最便宜的路慢慢走。。。)
所以,一开始每个点的T+1~2T部分都已被计算,可用这些先做FFT更新前面的答案。
用FFT计算出一次dp后,不能马上与原来的dp值取min更新(dp是求和,此时还没有把所有的和加进来),应再开一个数组存储当前计算后,累计的dp值的和为多少。
因为已经保证dp更新顺序由时间大的到时间小的状态,就不需要再管原图遍历的顺序了,计算dp时,直接枚举每一条边进行计算即可。
代码
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int MAXN=52,MAXM=105,MAXT=20005;
const double PI=acos(-1);
struct cpx
{
double r,i;
cpx(){}
cpx(double a,double b):r(a),i(b){}
cpx operator + (const cpx &t)const
{return cpx(r+t.r,i+t.i);}
cpx operator - (const cpx &t)const
{return cpx(r-t.r,i-t.i);}
cpx operator * (const cpx &t)const
{return cpx(r*t.r-i*t.i,r*t.i+t.r*i);}
cpx operator / (double k)const
{return cpx(r/k,i/k);}
};
void FFT(cpx A[],int n,int mode)
{
for(int i=0,j=0;i<n;i++)
{
if(i<j)
swap(A[i],A[j]);
int k=n>>1;
while(k&j)
j^=k,k>>=1;
j^=k;
}
for(int i=1;i<n;i<<=1)
{
cpx w1=cpx(cos(PI/i*mode),sin(PI/i*mode)),w;
for(int j=0;j<n;j+=(i<<1))
{
w=cpx(1,0);
for(int l=j,r=j+i;l<j+i;l++,r++,w=w*w1)
{
cpx tmp=A[r]*w;
A[r]=A[l]-tmp;
A[l]=A[l]+tmp;
}
}
}
if(mode==-1)
for(int i=0;i<n;i++)
A[i]=A[i]/n;
}
int N,M,T,X;
int E[MAXM][3];
double P[MAXM][MAXT];
int dis[MAXN][MAXN];
double dp[MAXN][MAXT*2],S[MAXM][MAXT*2];
void DP(int L,int mid,int R)
{
static cpx A[MAXT*10],B[MAXT*10];
int t=min(T,R-L+1);
for(int e=1;e<=M;e++)
{
int len=1;
while(len<R-mid+t)
len<<=1;
for(int i=0;i<len;i++)
A[i]=B[i]=cpx(0,0);
for(int i=0;i<R-mid;i++)
A[i]=cpx(dp[E[e][1]][R-i],0);//翻转
for(int i=0;i<=t;i++)
B[i]=cpx(P[e][i],0);
FFT(A,len,1);
FFT(B,len,1);
for(int i=0;i<len;i++)
A[i]=A[i]*B[i];
FFT(A,len,-1);
for(int i=L;i<=mid;i++)//用S记录当前转移累计的和
S[e][i]+=A[R-i].r;
}
}
void solve(int L,int R)
{
if(L==R)
{//L后面的时间已全部累计进入S,可以转移了
for(int e=1;e<=M;e++)
dp[E[e][0]][L]=min(dp[E[e][0]][L],S[e][L]+E[e][2]);
return;
}
int mid=(L+R)/2;
solve(mid+1,R);
DP(L,mid,R);
solve(L,mid);
}
int main()
{
scanf("%d%d%d%d",&N,&M,&T,&X);
for(int i=1;i<=N;i++)
{
for(int j=1;j<=N;j++)
dis[i][j]=0x3F3F3F3F;
dis[i][i]=0;
}
for(int i=1;i<=M;i++)
{
scanf("%d%d%d",&E[i][0],&E[i][1],&E[i][2]);
dis[E[i][0]][E[i][1]]=min(dis[E[i][0]][E[i][1]],E[i][2]);
for(int j=1;j<=T;j++)
{
scanf("%lf",&P[i][j]);
P[i][j]/=100000.0;
}
}
//Floyd求最便宜的路
for(int k=1;k<=N;k++)
for(int i=1;i<=N;i++)
for(int j=1;j<=N;j++)
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
//dp初始值
for(int i=1;i<=N;i++)
for(int j=0;j<=T;j++)
{
dp[i][j]=(i==N?0:1e100);
dp[i][j+T]=X+dis[i][N];
}
//用T+1~2T的值更新0~T的值
DP(0,T,2*T+1);
//CDQ分治
solve(0,T);
printf("%.6f\n",dp[1][0]);
return 0;
}