[ACOI2020] 课后期末考试滑溜滑溜补习班(单调队列优化dp)
题目
Description
潮田 渚(Shiota Nagisa)因为理科不大好,自然会被仔细观察学生的杀老师发现,于是渚同学只得加入杀老师举办的课后期末考试滑溜滑溜补习班。至于为什么叫这个名字,额,你不能问我啊。
在补习班上,因为多个学生会同时有需求,所以杀老师会制造分身用音速移动来回回答问题。
补习班上有 n 个同学,他们每一个人都有一个问题。杀老师为了有序回答学生的问题,把所有学生排成了一列。第 i 个学生的问题有一个困难值 ai,杀老师回答第 i 个学生的问题需要花费 ai 的精力。杀老师到了哪里,它就要解决那个学生的问题。杀老师最开始会解决序列中第一个同学的问题,他最后会去解决最后一个同学的问题。
杀老师每次解决完一个同学的问题到下一个同学的座位上就要花费 kk 点精力值。特殊的,如果杀老师想让自己轻松一点,可以不移动到下一个,可以直接到下两个,下三个,就不用解决跳过的同学的问题了。对应的,它会被学生调侃。受到打击的杀老师自然会花费格外的精力,花费的精力为 k+(q-p-1) \times d(当前位置为 p,跳到的位置为 q)。
当然的,杀老师也是有速度的啊,并且它想解决学生的一些问题,所以说杀老师最多只会跳过 x-1个学生,去解决下 x 个学生的问题。
Input
-
第一行五个整数 n,k,d,x,tpn,k,d,x,tp,表示有 nn 个学生,只按顺序去到下一个学生的座位需要花费 kk 点精力,每多跳过一个学生就要多花费 dd 点精力值,每一次最多只能跳过 x-1x−1 个学生,是否是特殊数据。
- tp=0,第二行 nn 个整数 a_{1\dots n}a1…n,a_iai 表示第 ii 个学生的问题的困难值为 a_iai。
- tp=1,第二行一个整数 SeedSeed,作为种子,然后调用 rndrnd 函数依次生成 nn 个整数,作为 aa 数组,a_iai 表示第 ii 个学生的问题的困难值为 a_iai。
inline int rnd () { static const int MOD = 1e9; return Seed = ( 1LL * Seed * 0x66CCFF % MOD + 20120712 ) % MOD; }
Output
一行一个整数,表示杀老师解决完最后一个同学的问题最少需要花费多少精力。
Sample Input1
5 3 4 1 0 1 2 3 4 5
Sample Output1
27
Sample Input2
10 30630 56910 2 0 7484 99194 86969 17540 29184 68691 91892 81564 93999 74280
Sample Output2
717318
Sample Input3
10000000 899999999 923456655 213111 1 1314520
Sample Output3
9231813656566921
Hint
样例1:杀老师每次不能跳过学生,因此他必须依次移动并解决所有问题,故答案为解决问题所需的精力 1+2+3+4+5=151+2+3+4+5=15 与移动所需的精力 4 \times 3=124×3=12,所以花费精力之和为 2727。
思路
首先很明显是一道动态规划的题;
我们设 $dp[i]$ 表示解决到第 $i$ 个同学的问题,需要花费的最小精力;
那么 $dp$ 转移方程就是
$dp[i]=min(dp[i],dp[j]+a[i]+k+(i-j-1)*d) $ ;
方程意思是从解决了第 $j$ 个同学到解决第 $i$ 个同学的问题所消耗的精力;
$a[i]$ 是第$i$ 个问题解决需要的精力 ,$(i-j-1)*d$ 表示跳过同学被吐槽消耗的精力;
具体跳过消耗的精力题目以给出求法,请仔细看题!!!;
那么我们很惊奇的发现:
方程可以转化一下,变成:
$dp[i]=min(dp[i],dp[j]-j*d+a[i]+i*d+k-d) $ ; (把 $ (i-j-1) \times d$ 分配开);
那么方程求最小值就与$j$ 有关了;
我们就可以把 $j$ 放到一个单调队列里,每次找一个 $min(dp[j]-j*d)$ ;
那么什么时候踢队头呢,题目限制跳过的同学不能超过 $x-1$ ;
所以如果 $i-q[head]-1~>~x-1$ 就要踢队头;
这样就 $ok$ 了,$AC$ 了;
代码
#include<bits/stdc++.h> #define re register//上面为啥没有注释 typedef long long ll; using namespace std; inline ll read() { ll a=0,f=1; char c=getchar(); while (c<'0'||c>'9') {if (c=='-') f=-1; c=getchar();} while (c>='0'&&c<='9') {a=a*10+c-'0'; c=getchar();} return a*f; } ll n,k,d,x,tp; ll Seed; ll a[10000010]; ll dp[10000010]; ll q[10000010]; inline ll rnd () { static const ll MOD = 1e9; return Seed = ( 1LL * Seed * 0x66CCFF % MOD + 20120712 ) % MOD; }//题目中给的生成每个 a[i] int main() { memset(dp,127/3,sizeof(dp));//初始 n=read(); k=read(); d=read(); x=read(); tp=read();//读入 if(tp==0)//读入 a[i] { for(re ll i=1;i<=n;i++) a[i]=read(); } else//生成 a[i] { Seed=read(); for(re ll i=1;i<=n;i++) a[i]=rnd(); } dp[0]=0; dp[1]=a[1];//初始化,题目要求第一个同学的必须解决 ll head=1,tail=0;// for(re ll i=1;i<=n;i++) { while(head<=tail&&i-q[head]-1>x-1)//跳过的同学超过限制 head++;//踢队头 if(i-q[head]-1>=0)//状态转移方程 dp[i]=min(dp[i],dp[q[head]]+a[i]+k+(i-q[head]-1)*d); while(head<=tail&&dp[q[tail]]-q[tail]*d>dp[i]-i*d)//寻找最小的 dp[j]-j*d) tail--;//踢队尾 q[++tail]=i;//入队 } printf("%lld\n",dp[n]);//输出 return 0;//AC }