传送门:Vjudge
这道题的dp想法其实是需要两次滚动的思想的.(虽然卡滚动我也觉得很zz,但是平时练习的话我还是忍了)
首先看完题面,我们肯定会发现最后的花费和最后两个朋友的位置有关(显然对于最后m个位置,最后两个朋友的位置不同会导致我们的答案不同).所以我们的dp不难应该想到和最后两个朋友的位置有关.
我们不难想到应该定义 d p [ i ] [ p o s 1 ] [ p o s 2 ] dp[i][pos1][pos2] dp[i][pos1][pos2]为前 i i i个位置最后两个朋友分别选在 p o s 1 , p o s 2 pos1,pos2 pos1,pos2位置的最小花费.首先这个dp方程思想肯定是没有问题的.但是我们会发现这个朴素的dp方程需要 2 e 5 ∗ 2 e 5 ∗ 2 e 5 2e5*2e5*2e5 2e5∗2e5∗2e5的位置(注意这个是没办法用map离散化的,因为我们的位置是可以在任意的m个位置的,所以离散化并不能优化),所以我们得考虑优化dp方程.
在优化之前先考虑这个dp方程该如何进行递推.
因为连续m个位置需要2个朋友,那么我们现在一个在 p o s 1 pos1 pos1,一个在 p o s 2 pos2 pos2(这里不妨设pos1
d p [ i ] [ p o s 1 ] [ p o s 2 ] = m a x ( d p [ j ∈ [ p o s 1 , i − 1 ] ] [ x ] [ p o s 1 ] ) , x ∈ [ p o s 2 − m , p o s 1 − 1 ] dp[i][pos1][pos2]=max(dp[j\in[pos1,i-1]\ ][x][pos1]),x\in[pos2-m,pos1-1] dp[i][pos1][pos2]=max(dp[j∈[pos1,i−1] ][x][pos1]),x∈[pos2−m,pos1−1]此时我们就发现了,我们的第一维可以跟着我们的后两维随便取的,更加具体的来说,我们的第一维是没有任何用处的,所以我们可以直接优化掉.此时我们有:
d p [ p o s 1 ] [ p o s 2 ] = m a x ( d p [ x ] [ p o s 1 ] ) , x ∈ [ p o s 2 − m , p o s 1 − 1 ] dp[pos1][pos2]=max(dp[x][pos1]),x\in[pos2-m,pos1-1] dp[pos1][pos2]=max(dp[x][pos1]),x∈[pos2−m,pos1−1]但是此时我们发现虽然此时我们的dp方程进行了一维的滚动,但是依旧需要 2 e 5 ∗ 2 e 5 2e5*2e5 2e5∗2e5,依旧是无法接受,所以需要继续进行优化才行.
此时我们会发现对于一个位置来说,我们此时的位置最多只能由在它之前的m个位置递推而来,也就是说在这个位置m之前的位置对于后面的所有位置来说都是没有用的,所以这里又可以进行滚动.考虑直接进行%m来进行滚动.(但是需要注意的是,我们此时并不能对pos1和pos2同时进行%m滚动,因为我们同时进行取模滚动的话会导致相对大小随机发生变化,导致影响线性性质,很难维护).所以此时我们对pos2进行滚动,然后对于pos1来说,我们没必要维护pos1的具体位置,可以使用pos1和pos2的相对位置来代替这个,这样我们就同时对于两个都进行优化了.此时我们的 d p dp dp方程就变成了:
d p [ p o s % m ] [ d i s ] = m a x ( d p [ ( p o s − d i s ) % m ] [ d i s ∈ [ 1 , p o s − d i s − ( p o s − m ) ] ] ) dp[pos\%m][dis]=max(dp[(pos-dis)\%m][dis\in[1,pos-dis-(pos-m)]]) dp[pos%m][dis]=max(dp[(pos−dis)%m][dis∈[1,pos−dis−(pos−m)]])因为此时的我们的 p o s 1 pos1 pos1变成了 p o s 2 − d i s pos2-dis pos2−dis,那么此时的选取区间就变成了 [ p o s 2 − m , p o s 2 − d i s − 1 ] [pos2-m,pos2-dis-1] [pos2−m,pos2−dis−1].
现在我们的问题就是如何快速求出最大的那一个 d p dp dp值了,对于区间最小值,第一想法显然是线段树,但是此时需要二维线段树,一是内存不够,而来码量也较大,所以考虑使用前缀最大值来进行维护.
具体来说就是在跑出 d p [ p o s ] [ d i s ] dp[pos][dis] dp[pos][dis]的时候,同时用一个数组 m i n n [ p o s ] [ d i s ] minn[pos][dis] minn[pos][dis]来不断的从前往后进行取 m i n min min,这样就能求出了前缀最小值了,又因为我们每次需要是 d p [ p o s ] [ 1 dp[pos][1 dp[pos][1~ x ] x] x]的最小值,很符合我们的前缀最小值,此时我们直接进行调用即可.
至此本题结束
下面是具体的代码部分(此题卡map,pos数组不能用map映射):
#include
using namespace std;
typedef long long ll;
#define root 1,n,1
#define ls rt<<1
#define rs rt<<1|1
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
inline ll read() {
ll x=0,w=1;char ch=getchar();
for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;
for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+ch-'0';
return x*w;
}
inline void print(__int128 x){
if(x<0) {putchar('-');x=-x;}
if(x>9) print(x/10);
putchar(x%10+'0');
}
#define maxn 1000000
const double eps=1e-8;
#define int_INF 0x3f3f3f3f
#define ll_INF 0x3f3f3f3f3f3f3f3f
int dp[2010][2010];int minn[2010][2010];int a[maxn];int pos[maxn];
//dp[i][j]的定义最后一个朋友的位置为i,倒二个朋友所处位置距离最后一个朋友i的距离为j的最小花费
//minn[i][j]的定义对于位置i来说,所有dp[i][j]的前缀最小值(j>=1&&j<=m-1)
//简单来说也就是以i为最后一个朋友,倒二个朋友所处位置距离i为j的dp[i][j]的前缀最小值(j从小到大)
int main() {
int T=read();
while (T--) {
int n=read();int m=read();
for(int i=0;i<=m;i++) {
for(int j=0;j<=m;j++) {
dp[i][j]=minn[i][j]=int_INF;
}
}
for (int i=1;i<=n;i++) {
a[i]=read();
pos[i]=i%m;
}
int ans=int_INF;
for (int i=2;i<=m;i++) {
for(int j=1;j<=i-1;j++) {//距离
dp[pos[i]][j]=a[i]+a[i-j];
minn[pos[i]][j]=min(minn[pos[i]][j-1],dp[pos[i]][j]);
}
}
for(int i=m+1;i<=n;i++) {
for(int j=1;j<=m-1;j++) {
//注意此时的滚动数组和01背包那种是不同的,此时的滚动区间刚好没有交叉
//所以完全没必要倒着枚举,不知道std为什么故作玄虚
int kk=i-j;
dp[pos[i]][j]=a[i]+minn[pos[kk]][kk-(i-m)];
//此时倒一位置为i,倒二位置为kk,所以倒三位置只能是[i-m,kk-1],所以距离为kk-(i-m)
minn[pos[i]][j]=min(minn[pos[i]][j-1],dp[pos[i]][j]);
}
}
for(int i=n-m+2;i<=n;i++) {
//最后的答案区间一定是[n-m+2,n],因为我们必须要有两个朋友才行
//所以最后的朋友的位置一定要为倒二留一个位置,不知道为什么std是n-m+1,竟然还过了,真是奇妙
int st=n-m+1;
for(int j=1;j<=i-st;j++) {
ans=min(ans,dp[pos[i]][j]);
}
}
cout<<ans<<"\n";
}
}