CF Div2.B 竟然在洛谷评到蓝色……不过抽屉原理确实很难想到,或许不是优化,而是分类讨论。

给出一个长度为 \(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");

THE END