题解:AT_abc419_e [ABC419E] Subarray Sum Divisibility

前言

本文同步自洛谷专栏

题目传送门官方题解更低复杂度的官方题解

这篇题解中,会先讲解 \(O(LM^2)\) 的做法,再讨论 \(O(NM)\) 的做法。

下文出现的变量,默认有属于自然数集的限制。

题意

给定一个长为 \(N\) 的序列,一次操作可以给其中一个数 \(+1\) ,问使其所有长为 \(L\) 的连续子序列之和都是 \(M\) 的倍数的最小操作数。

解题思路

设原序列为 \(A\),操作结束后的序列为 \(B\)

先根据题意几个式子看一下(在此贴出前两个):

\[(\sum_{i=1}^{i\le L}B_i)\bmod M=0 \]

\[(\sum_{i=2}^{i\le L+1}B_i)\bmod M=0 \]

发现两个式子仅有两个数不同,作差得到:

\[(B_1-B_{L+1})\bmod M=0 \]

对所有式子进行处理,可以得到:

\[\forall (i\le L)\land (j\times L+i\le N),(B_i- B_{j\times L+i})\bmod M=0 \]

也即是说,\(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\) 个下标之间的限制,仅有一个限制:

\[(\sum_{i=1}^{i\le L}B_i)\bmod M=0 \]

由于下标对 \(L\) 求余过,不妨认为下标 \(L\) 为下标 \(0\),那么改写过来即是:

\[(\sum_{i=0}^{i<L}B_i)\bmod M=0 \]

结合预处理,可知这样一个方案的操作数(下称花费)是:

\[\sum_{i=0}^{i<L}cost_{i,B_i} \]

容易想到 dp,设 \(dp_{i,j}\) 为考虑了下标 \(\le i\) 的部分(下标从 \(0\) 开始),\((\sum_{k=0}^{k\le i}B_k)\bmod M=j\) 的最小花费,转移方程:

\[dp_{i,(j+k)\bmod M} \xleftarrow{checkmin} dp_{i-1,j}+cost_{i,k} \]

\(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\)):

\[cost_{p,j}=\sum_{i}j-A_i+[j<A_i]\times M \]

基于上式,设 \(siz_p=\sum_{i}1\)\(t=\sum_{i}[j<A_i]\)\(c=\sum_{i}A_i\),则:

\[cost_{p,j}=siz_p\times j-c+t\times M \]

对于一个固定的 \(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\),进行如下转移:

\[dp_{i,(j+1)\bmod M} \xleftarrow{checkmin} dp_{i,j\bmod M}+siz_i \]

注意应先进行特殊转移,利用特殊转移的结果来快速转移,事实上,快速转移可以看作对特殊转移中使用的 \(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 秒调好代码。

做优化的时候,注意挖掘性质,这一点需要多加练习。

posted @ 2026-06-05 09:46  Wxb2010  阅读(11)  评论(0)    收藏  举报