题解:AT_abc419_e [ABC419E] Subarray Sum Divisibility
前言
本文同步自洛谷专栏
这篇题解中,会先讲解 \(O(LM^2)\) 的做法,再讨论 \(O(NM)\) 的做法。
下文出现的变量,默认有属于自然数集的限制。
题意
给定一个长为 \(N\) 的序列,一次操作可以给其中一个数 \(+1\) ,问使其所有长为 \(L\) 的连续子序列之和都是 \(M\) 的倍数的最小操作数。
解题思路
设原序列为 \(A\),操作结束后的序列为 \(B\)。
先根据题意几个式子看一下(在此贴出前两个):
发现两个式子仅有两个数不同,作差得到:
对所有式子进行处理,可以得到:
也即是说,\(B\) 序列中,下标模 \(L\) 同余的,原数模 \(M\) 同余。为了方便,对于模 \(L\) 同余的下标,称它们为强关联下标(每个下标都是其本身的强关联下标)。
由此,我们有这样一个思路:对于一个下标 \(i\le L\),枚举 \(B_i\) 的取值,考虑其所有强关联下标 \(j\) 由 \(A_j\) 改为 \(B_j\) 所需的操作数,统计到同一个预处理数组中。可以结合代码理解:
$\red{\text{参考代码}}$
for(int i=1;i<=n;i++){//cost 即是预处理数组
int p=i%l;//意味着后面下标中将会出现 0
A[i]=read();
for(int j=0;j<A[i];j++) cost[p][j]+=j-A[i]+m;
//操作数的取值有属于自然数的限制
for(int j=A[i];j<m;j++) cost[p][j]+=j-A[i];
}
预处理结束后,我们就只用考虑前 \(L\) 个下标之间的限制,仅有一个限制:
由于下标对 \(L\) 求余过,不妨认为下标 \(L\) 为下标 \(0\),那么改写过来即是:
结合预处理,可知这样一个方案的操作数(下称花费)是:
容易想到 dp,设 \(dp_{i,j}\) 为考虑了下标 \(\le i\) 的部分(下标从 \(0\) 开始),\((\sum_{k=0}^{k\le i}B_k)\bmod M=j\) 的最小花费,转移方程:
\(dp\) 数组先初始化为无穷大,然后 \(dp_{-1,0}\xleftarrow{}0\),最终得到的 \(dp_{L-1,0}\) 即为答案。
由于出现了负数作为下标,我们可以使用滚动数组来避免此问题,代码如下:
$\red{\text{code}}$
#include<bits/stdc++.h>
using namespace std;
#define N 505
int n,m,l,cost[N][N];
int dp[N],A[N],H[N];
inline int read(){
int a=0;char ch=getchar();
while(!isdigit(ch)) ch=getchar();
while(isdigit(ch)) a=10*a+ch-'0',ch=getchar();
return a;
}
inline void checkmin(int &a,int b){a=min(a,b);}
int main(){
n=read(),m=read(),l=read();
for(int i=1;i<=n;i++){
int p=i%l;
A[i]=read();
for(int j=0;j<A[i];j++) cost[p][j]+=j-A[i]+m;
for(int j=A[i];j<m;j++) cost[p][j]+=j-A[i];
}
memset(dp,0x3f,sizeof(dp)),dp[0]=0;
for(int i=0;i<l;i++){
memset(H,0x3f,sizeof(H));
for(int j=0;j<m;j++){
for(int k=0;k<m;k++){
int c=j+k;(c>=m)&&(c-=m);//相当于取模
checkmin(H[c],dp[j]+cost[i][k]);
}
}
for(int j=0;j<m;j++) dp[j]=H[j];
}
printf("%d",dp[0]);
return 0;
}
预处理部分的时间复杂度为 \(O(NM)\),dp 转移部分的时间复杂度为 \(O(LM^2)\),总时间复杂度为 \(O(LM^2)\)。
更低的时间复杂度
事实上,由于 \(cost\) 的值有一定性质,我们可以做到 \(O(NM)\) 的时间复杂度。观察预处理部分的代码:
int p=i%l;
A[i]=read();
for(int j=0;j<A[i];j++) cost[p][j]+=j-A[i]+m;
for(int j=A[i];j<m;j++) cost[p][j]+=j-A[i];
发现 \(j<A_i\) 时,\(cost_{p,j}\) 要多加上一个 \(M\),尝试总结(其中 \(p\) 与 \(i\) 为强关联下标,\(p<L\)):
基于上式,设 \(siz_p=\sum_{i}1\),\(t=\sum_{i}[j<A_i]\),\(c=\sum_{i}A_i\),则:
对于一个固定的 \(p\),随着 \(j\) 的增长,\(t\) 单调不增,\(siz_p\) 和 \(c\) 都是定值,视 \(cost_{p,j}\) 为一个关于 \(j\) 的函数,那么,在平面直角坐标系中,\(t\) 的取值随 \(j\) 变化,使函数分段,每一段内都是一个一次函数。
那么,在转移的时候,我们只需要在分段的点上去使用 \(cost\) 数组转移,其余部分可以利用 \(siz\) 快速转移,
由于 \(t\) 至多有 \(siz\) 种取值,即只用特殊转移 \(siz\) 处,
又 \(\sum_{p<L}siz_p=N\),故只用特殊转移 \(N\) 处,每处按原方法转移,时间复杂度 \(O(NM)\)。
快速转移部分,由于模意义下数轴会形成一个环,需要破环成链,那么对于所有 \(0\le j<2\times M\),进行如下转移:
注意应先进行特殊转移,利用特殊转移的结果来快速转移,事实上,快速转移可以看作对特殊转移中使用的 \(cost\) 数组的一种延续,这是基于一次函数的。
总时间复杂度降至 \(O(NM)\)。
$\red{\text{code}}$
#include<bits/stdc++.h>
using namespace std;
#define N 505
int n,m,l,cost[N][N];
int dp[N],A[N],H[N];
vector<int>sp[N];//special,存特殊点
inline int read(){
int a=0;char ch=getchar();
while(!isdigit(ch)) ch=getchar();
while(isdigit(ch)) a=10*a+ch-'0',ch=getchar();
return a;
}
inline void checkmin(int &a,int b){a=min(a,b);}
int main(){
n=read(),m=read(),l=read();
for(int i=1;i<=n;i++){
int p=i%l;
A[i]=read(),sp[p].push_back(A[i]);
for(int j=0;j<A[i];j++) cost[p][j]+=j-A[i]+m;
for(int j=A[i];j<m;j++) cost[p][j]+=j-A[i];
}
memset(dp,0x3f,sizeof(dp)),dp[0]=0;
for(int i=0;i<l;i++){
memset(H,0x3f,sizeof(H));
int siz=sp[i].size();
for(int j=0;j<m;j++){
for(auto &v:sp[i]){
checkmin(H[(v+j)%m],dp[j]+cost[i][v]);
}
}
for(int j=0;j<2*m;j++){
checkmin(H[(j+1)%m],H[j%m]+siz);
}
for(int j=0;j<m;j++) dp[j]=H[j];
}
printf("%d",dp[0]);
return 0;
}
总结
遇到一些和数学有一定关系的题目,要先推一些式子,不要和蒟蒻一样在那里毫无根据乱想用什么算法,导致后面式子推出来了但差 30 秒调好代码。
做优化的时候,注意挖掘性质,这一点需要多加练习。

浙公网安备 33010602011771号