题目
思路
观察到 n n n 大而 m m m 小,肯定考虑设计只跟 m m m 有关的算法。
背包题,不难想到如下优化方式:确保每次更新都至少有一个新的数字能被凑出来。这样的话就只用更新 O ( m ) \mathcal O(m) O(m) 次,复杂度最高也就 O ( m 2 ) \mathcal O(m^2) O(m2) 了。
这是容易做到的。用 d i s ( x ) dis(x) dis(x) 表示,对于当前的数字种类 a a a ,在 x x x 处最多能递推多少次。如果用 S S S 表示已经能够凑出的字符,则
∀ i ∈ [ 0 , d i s ( x ) ) , x + i a ∉ S \forall i\in[0,dis(x)),\;x+ia\not\in S ∀i∈[0,dis(x)),x+ia∈S
且
x + d i s ( x ) ⋅ a ∈ S x+dis(x)\cdot a\in S x+dis(x)⋅a∈S
边界是 ∀ x ∈ S , d i s ( x ) = 0 \forall x\in S,\;dis(x)=0 ∀x∈S,dis(x)=0 。当然你也可以是别的写法。理解到我的目标就行。
尽管 x + k a x+ka x+ka 在模 m m m 意义下是个环,但是仍然可以转移,因为其值实际上是意义唯一的。简单的实现方式是循环两遍。
那么,我们在背包转移时,对于一个 x ∈ S x\in S x∈S ,我们将其后方的 min [ d i s ( x + a ) , c n t a ] \min[dis(x+a),cnt_a] min[dis(x+a),cnta] 个 x + k a x+ka x+ka 格子都赋值为 t r u e \tt true true 即可。不难证明这是 O ( m 2 ) \mathcal O(m^2) O(m2) 的算法。并且可以推广到余数为任意值。
代码
在我的代码实现中,规定了 d i s ( 0 ) = 1 dis(0)=1 dis(0)=1 ,否则无法走到 n o w = 0 now=0 now=0 的情况。
写法因人而异。
#include <cstdio>
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long int_;
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
inline void writeint(int x){
if(x > 9) writeint(x/10);
putchar((x%10)^48);
}
const int MaxM = 1000;
int gcd[MaxM]; // gcd[x] = gcd(x,m)
bool dp[MaxM+5];
int cnt[MaxM], dis[MaxM];
int main(){
int n = readint(), m = readint();
for(int i=0; i<n; ++i)
++ cnt[readint()%m];
for(int i=1; i<m; ++i)
if(m%i == 0) // 可能是 gcd
for(int j=1; j<m/i; ++j)
gcd[i*j] = i;
dp[0] = true; // empty set
bool ans = cnt[0];
for(int a=1; a<m&&!ans; ++a){
int d = gcd[a];
for(int i=0; i<d; ++i)
for(int j=2*m/d-1; ~j; --j){
int now = (i+j*a)%m;
dis[now] = dis[(now+a)%m]+1;
if(dp[now]) dis[now] = !now;
}
for(int i=0; i<d; ++i)
for(int j=0; j<m/d; ++j){
int now = (i+j*a)%m;
if(dp[now]){
int t = dis[(now+a)%m];
t = min(t,cnt[a]);
for(; t; --t,++j){
now = (now+a)%m;
dp[now] = true;
if(now == 0)
ans = true;
}
}
}
// printf("dp:");
// for(int i=0; i<m; ++i)
// printf("%d",dp[i]);
// putchar('\n');
}
puts(ans ? "YES" : "NO");
return 0;
}
后记
对于特殊的情况,只求为 m m m 的倍数时, n > m n>m n>m 则 前缀和 根据抽屉原理必然有重复。故直接输出 Y E S \tt YES YES 即可。