题目链接:https://jzoj.net/senior/#main/show/5415
首先可以推出n^2的DP方程:F[i]=min{F[j]+(i-j)/C[i]*V[i]} (C[i]|(i-j))。
我们发现靠前的 dp 值对之后 dp 值的贡献是一个形如一条直线(一次函数)的形式。这样我们在计算后面的 dp 值时,就是找到之前所有可能转移到这个点的所有直线在当前横坐标的最小值。我们发现 C [i]和 i%C[i] 相同的两点可以转移到之后车站的集合是相同的。所以可以维护 maxc*maxc 个栈来维护。
转移方程简化为 F[i]=min{F[j]+dis*V[i]},dis为i到j要坐几站。
问题转化为维护栈里的下凸壳,每次询问一个横坐标(满足询问递增),求与下凸壳交点的纵坐标(与直线集交点的最小值)。
栈里的直线是随着i递增而加入的,首先要保证斜率(V[i])递增。
若存在x < y,V[x] > V[y],一直乘坐x车不如到y之后换乘y车。
接着就是要满足下凸壳性质,新加一条直线时假如会把栈顶的直线从凸壳中去除,那么就要退栈。
算答案的时候,假如栈顶的答案大于栈顶-1的答案,那么栈顶的直线再也不会成为最有决策(因为斜率递增),所以也要退栈,知道小于才更新答案。
WA点:求直线交点写成整除,直线方程几次没写对。
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=1000010;
int n,maxc,f[maxn],c[maxn],v[maxn],st[11][10][20100],top[11][10];
int G(int x,int y){return f[y]+(x-y)/c[y]*v[y];}
double Q(int x,int y){return (v[y]==v[x])?1e18:(double)(f[x]-f[y]-v[x]*(x/c[x])+v[y]*(y/c[y]))/(v[y]-v[x]);}
int main()
{
scanf("%d%d",&n,&maxc);
for(int i=0;i<n;i++)
scanf("%d%d",&c[i],&v[i]);
memset(top,0,sizeof(top));
memset(f,0x3f,sizeof(f));
f[0]=0;
for(int i=0;i<n;i++)
{
int k=c[i],l=i%c[i];
while(top[k][l]>0&&(v[i]<=v[st[k][l][top[k][l]]])) {top[k][l]--;}
while(top[k][l]>1&&Q(i,st[k][l][top[k][l]-1])>=Q(st[k][l][top[k][l]],st[k][l][top[k][l]-1])) {top[k][l]--;}
st[c[i]][i%c[i]][++top[c[i]][i%c[i]]]=i;
for(int d=1;d<=maxc;d++)
{
l=(i+1)%d;
while(top[d][l]>1&&G(i+1,st[d][l][top[d][l]])>=G(i+1,st[d][l][top[d][l]-1])) top[d][l]--;
if(top[d][l]>0) f[i+1]=min(f[i+1],G(i+1,st[d][l][top[d][l]]));
}
}
for(int i=1;i<=n;i++)
if(f[i]>1e9) printf("-1 ");
else printf("%d ",f[i]);
return 0;
}