题目链接
(mathcal{Solution})
作为一道 NOIP 题,当然要考虑从部分分到正解,一是拿稳分防止正解写挂,二是可以拿不同部分分的程序对拍。
分析:看到样例大胆猜想,或者考虑这个问题的本质。
可以通过反证法证明新货币系统的集合一定是原货币系统的子集。
考虑先把 (A) 系统的数全部选上,然后删去一些,则这些删去的一定可以被剩下的数都表示出来,否则这个不能被新货币系统表示出来的原货币系统的数本身不能被表示出来,而这个数所拓展出去的数也不一定能被表示出来。
下面设 (m=max{a_i})
(nleq 13):
直接状压选哪些数,然后跑一遍 (mathcal{O}(nm^2)) 的 (dp) (当然实现的好的话可以做到 (mathcal{O}(nm))),然后遍历不在当前货币系统的数,看是否能被表示出来,可以的话就更新答案。
减少常数的方法:类似最优性剪枝,如果当前的状态的二进制数本来就大于等于答案,一定不会比答案更优,可以选择不跑 (dp)。
总时间复杂度 (mathcal{O}(T2^nnm^2)) 或 (mathcal{O}(T2^nnm))。
(nleq 100)
毕竟和位置没有关系,所以先排一下序看看有什么结论。
这个时候发现,(a_1) 是一定要选的,因为 (a_1) 是最小的数,因为新货币系统中选的最小的数一定小于等于它,如果选比它更小的数,会使得比它更小的数能被新货币系统表示出来,不符合题意,所以一定选 (a_1)。
然后对于 (a_1),去更新它的倍数,也就是它的倍数在新货币系统中已经不需要了。然后对于后面的 (a_i),如果可以被前面的数表示过了,就一定不会出现在新货币系统中。否则在所有可以被表示的数上加上它的倍数去转移一个数是否能被表示出来。
时间复杂度 (mathcal{O}(Tnm^2)),考虑优化,在转移的时候 (mod a_i) 相等的位置会被同样的转移多次,所以只对每个不同的 (mod a_i) 去 (dp) 转移即可,一次转移是 (frac{m}{a_i}),一共 (a_i) 次,这样对于一个 (a_i) 的单次转移复杂度就到 (mathcal{O}(m)) 了=w=
总时间复杂度 (mathcal{O}(Tnm))
(mathcal{Code})
#include<iostream>
#include<cstdio>
#include<algorithm>
inline int Min(int x, int y) { return x < y ? x : y; }
inline int Max(int x, int y) { return x > y ? x : y; }
inline int read() {
int r = 0; bool w = 0; char ch = getchar();
while(ch < '0' || ch > '9') w = ch == '-' ? 1 : w, ch = getchar();
while(ch >= '0' && ch <= '9') r = (r << 3) + (r << 1) + (ch ^ 48), ch = getchar();
return w ? ~r + 1 : r;
}
const int N = 110;
const int K = 25010;
int n, a[N];
int b[N], cnt;
bool vis[K], vis2[K];
int main() { int T = read(); while(T--) {
for(int i = 0; i < K; ++i) vis[i] = 0;
cnt = 0;
n = read();
for(int i = 1; i <= n; ++i) a[i] = read();
std::sort(a + 1, a + n + 1);
if(a[1] == 1) {
puts("1");
continue;
}
for(int i = 1; i <= n; ++i) {
if(vis[a[i]]) continue;
b[++cnt] = a[i];
vis[a[i]] = 1;
for(int j = 1; j < K; ++j) {
if(!vis[j]) continue;
if(vis2[j % a[i]]) continue;
for(int k = 1; k * a[i] + j < K; ++k)
vis[k * a[i] + j] = 1;
vis2[j % a[i]] = 1;
}
for(int j = 0; j < a[i]; ++j) vis2[j] = 0;
}
printf("%d
", cnt);
}return 0; }