谁说 \(n\le5\times 10^5\),\(k\le100\),\(p\le100\) 只能 \(O(nk)\)?我今天就要用 \(O(nk\log p)\) 过这个题!
定义 \(f_{i,j}\) 表示前 \(j\) 个数,分成 \(i\) 段的最小价值和,\(s_i\) 表示前缀和(对 \(p\) 取模),转移就是 \(f_{i,j}=\min\limits_{l=1}^{j-1}\left\{f_{i-1,l}+\left(s_j-s_l\right)\bmod p\right\}\)。朴素转移是 \(O(n^2k)\) 的。
但是我们发现,我们不需要枚举具体从哪个位置转移过来,只要知道那个位置的前缀和对 \(p\) 取模的值即可。更具体地,我们在转移的时候维护一个数组 \(g\),\(g_i\) 表示所有前缀和对 \(p\) 取模为 \(i\) 的转移中最小的,显然这个可以在转移的同时 \(O(1)\) 更新,复杂度就优化到了 \(O(nkp)\)。
这样肯定还是过不了的,但我们可以利用数据结构再次优化,用树状数组维护 \(g\) 数组:对于 \(\left(s_j-s_l\right)\bmod p\) 这一项,发现它在 \(s_l\le s_j\) 的时候等于 \(s_j-s_l\),反之等于 \(s_j-s_l+p\)。所以我们可以开两个树状数组,分别维护前/后缀 \(f_{i-1,l}-s_l\) 的最小值。时间复杂度 \(O(nk\log p)\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int inf=1e9;
inline int read(){
int x=0,f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;
}
int n,k,p,f[105][500005],s[500005],c[105],d[105];
inline void addp(int x,int y){for(x++;x<=p;x+=x&-x)c[x]=min(c[x],y);}
inline void adds(int x,int y){for(x=p-x;x<=p;x+=x&-x)d[x]=min(d[x],y);}
inline int askp(int x){int res=inf;for(x++;x;x-=x&-x)res=min(res,c[x]);return res;}
inline int asks(int x){int res=inf;for(x=p-x;x;x-=x&-x)res=min(res,d[x]);return res;}
signed main(){
n=read(),k=read(),p=read();
for(int i=1;i<=n;++i)s[i]=(s[i-1]+read())%p,f[1][i]=s[i];
for(int i=2;i<=k;++i){
for(int j=0;j<p;++j)c[j+1]=d[p-j]=inf;
for(int j=1;j<=n;++j)f[i][j]=min(askp(s[j]),asks(s[j]+1)+p)+s[j],addp(s[j],f[i-1][j]-s[j]),adds(s[j],f[i-1][j]-s[j]);
}
printf("%d\n",f[k][n]);
return 0;
}