NOIP2021 数列
算法一
最暴力的爆搜,枚举每个位置所有填值的情况,时间复杂度 \(O(n^m)\)。可以拿到20分。
算法二
没那么暴力的爆搜,注意到填数的具体位置不重要,只关系每种数的出现次数。
考虑暴力枚举每个数出现了多少次,记数字 \(i\) 出现了 \(c_i\) 次。所求即为下面这个不定方程解的个数:
\[c_0 + c_1 + c2 + \dots + c_m = n
\]
利用隔板法可以分析出,时间复杂度为 \(O(C_{n + m}^{m})\)。
这样的一种方案对答案的贡献为
\[\binom{n}{c_0 c_1 \dots c_m} \times \prod_{i = 0}^{m} v_i^{c_i}
\]
可以拿35~45分。
算法三
对于 \(m \le 12\),考虑状压dp。
记 \(f[i][j][s]\) 表示长度为 \(i\),用了 \(0 \sim j\) 这些值(可以不用),权值和为 \(s\),方案数。
有转移
\[f[i][j][s] = \sum_{0 \le k \le i} f[i - k][j- 1][s - 2^j \times k] \times\binom{i}{k}
\]
时间复杂度 \(O(2^mn^3m)\)。可以拿50分。
#include<bits/stdc++.h>
#define F(i,l,r) for(int i(l);i<=(r);++i)
#define G(i,r,l) for(int i(r);i>=(l);--i)
using namespace std;
using ll = long long;
const int mod = 998244353;
const int N = 105;
int n, m, lim, ans = 0;
int v[N], f[32][15][122900], c[32][32]; // f[i][j][s] : 长度为 i, 用了 0 ~ j 这些值, 权值和为 s
inline ll quickmod(ll x, int y){
ll ret = 1;
while(y){
if(y & 1) ret = ret * x % mod;
x = x * x % mod, y >>= 1;
}
return ret;
}
inline int popcount(int x){ return __builtin_popcount(x); }
signed main(){
// freopen("sequence2.in","r",stdin);
// freopen("sequence.out","w",stdout);
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n >> m >> lim;
F(i, 0, m) cin >> v[i];
F(i, 0, n) c[i][i] = c[i][0] = 1;
F(i, 2, n) F(j, 1, i - 1) c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % mod;
F(j, 0, m) F(i, 1, n) f[i][j][(1 << j) * i] = quickmod(v[j], i); //长度为 i,全用一种数j
F(j, 0, m){ // 用了 0 ~ j 这些值
F(i, 1, n){ //长度
F(s, 0, (1 << j) * i){ // 权值和
F(k, 0, i - 1){ // 用了 k 个 j
f[i][j][s] += quickmod(v[j], k) * f[i - k][j - 1][s - ((1 << j) * k)] % mod * c[i][k] % mod;
if(f[i][j][s] >= mod) f[i][j][s] -= mod;
}
}
}
}
F(s, 0, (1 << m) * n) if(popcount(s) <= lim) (ans += f[n][m][s]) %= mod;
cout << ans << '\n';
return fflush(0), 0;
}
算法四
位数太多存不下,考虑数位dp。
还是得枚举目前最大使用的值 \(j\),注意到如果 \(0 \sim j\) 的个数都已经确定,那么后面无论怎么填数,\(0 \sim j\) 位都不会再变化了。
所以我们从低位到高位dp,转移时维护进位就可以。
关于进位,一次可能不止进一位,但我们多开一维状态记录最高位填了多少,我们最后再处理这些进位,这样就很方便了。
另外可能一位都进不了,但进位这一位的原理本质是个桶,所以可以直接当成最高位填了 0 个来进位。
记 \(f[i][j][k][l]\) 表示长度为 \(i\),当前进位最高位到了第 \(j\) 位(因此填数只填到了 \(j - 1\)),第 \(j\) 位有 \(k\) 个,已经有 \(l\) 个位是 1,序列方案数。
有转移(采用刷表法,假设新填 \(num\) 个 \(j\))
\[f[i][j + 1][(k + num) / 2][l + (k + num) \bmod 2] += f[i][j][k][l] \times v_j^{num} \times \binom{i + num}{num}
\]
答案:
\[ans = \sum_{k, l}f[n][m + 1][k][l] \times [popcount(k) + l \le K]
\]
\(K\) 是读入的。
初始化:\(f[0][0][0][0] = 1\)。
时间复杂度 \(O(mn^4)\)。
#include<bits/stdc++.h>
#define F(i,l,r) for(int i(l);i<=(r);++i)
#define G(i,r,l) for(int i(r);i>=(l);--i)
using namespace std;
using ll = long long;
const int N = 35, M = 105;
const int mod = 998244353;
ll f[N][M][N][M], v[M], c[N][N], ans = 0;
int n, m, lim;
int popcount(int x){
return __builtin_popcount(x);
}
int quickmod(ll x, int y){
ll ret = 1;
while(y){
if(y & 1) ret = ret * x % mod;
x = x * x % mod;
y >>= 1;
}
return ret;
}
signed main(){
// freopen("sequence.in","r",stdin);
// freopen("sequence.out","w",stdout);
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n >> m >> lim;
F(i, 0, m) cin >> v[i];
F(i, 0, n) c[i][0] = c[i][i] = 1;
F(i, 2, n) F(j, 1, i - 1) c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % mod;
// F(i, 0, n) { F(j, 0, i) cout << c[i][j] << ' '; cout << '\n';}return 0;
f[0][0][0][0] = 1;
F(i, 0, n) F(j, 0, m) F(k, 0, i) F(l, 0, i) F(num, 0, n - i) (f[i + num][j + 1][(k + num) / 2][l + (k + num) % 2] += f[i][j][k][l] * quickmod(v[j], num) % mod * c[i + num][num] % mod) %= mod;
F(k, 0, n) F(l, 0, lim) if(popcount(k) + l <= lim) {
ans += f[n][m + 1][k][l];
if(ans >= mod) ans -= mod;
}
cout << ans << '\n';
return fflush(0), 0;
}