CF577B Modulo Sum(背包+抽屉原理)
给出一个长度为 \(n\) 的序列和一个正整数 \(m\)。问这个原序列中是否存在非空子序列,使其元素之和能被 \(m\) 整除。\(1\le n\le 10^6,1\le m\le 2\times 10^3,1\le a_i\le 10^9\)。
看到子序列,我们首先想到 01 背包。设计 \(f_{i,j}\) 表示在前 \(i\) 个元素中选,其加和对 \(m\) 取模结果为 \(j\) 是否可行。转移部分代码如下:
//f[][]为 dp 数组(bool),a[]为原序列(int)
if (f[i - 1][j] == true)
{
f[i][j] == true;
f[i][(j + a[i]) % m] = true;
}
这样做时间复杂度是 \(O(n\times m)\) 的,无法通过本题,空间也会爆炸。
这道题有一个不太寻常的优化:\(n>m\) 时,原序列中一定存在非空子序列,使其元素之和能被 \(m\) 整除。证明基于抽屉原理。对原序列 \(a\) 求前缀和为 \(s\),因为对 \(m\) 取模,则一定有 \(i\in [1,n],j\in [1,n],i\neq j\) 使得 \(s_i=s_j\)。只需取出 \(i,j\) 之间的子段,它的和一定能被 \(m\) 整除。因此,原序列一定满足题意。
下面给出 AC 代码(核心部分):
if (n >= m)
{
puts("YES");
return 0;
}
for (int i = 1; i <= n; i++)
{
scanf("%d", &a[i]);
a[i] %= m;//联系 13 行,保证 a[i] 也在 [1,m] 之内
}
for (int i = 1; i <= m; i++)
{
f[i][a[i]] = true;
for (int j = 0; j < m; j++)//枚举取模结果
{
if(f[i - 1][j] == true)
{
f[i][j] = true;
f[i][(j + a[i]) % m] = true;
}
}
}
if (f[n][0] == true)//对于前 n 个数,取模结果为 0(整除)可行
puts("YES");
else
puts("NO");