40%:直接 DP
刚读题的时候第一感觉:这不是很像我在 《<数谜>解题报告》 里面提到的弱化版问题嘛,搞个无脑 DP,做法很显然。然而我却忽略了最重要的数据范围。
记 f[i][j] 为前 i 层组成的数 X′ 使 X′modY=j 的方案数,容易推出状态转移方程:
直接根据这个方程进行转移,时间复杂度是 O(NBY) 的。
参考代码:
//2217.cpp
#include
#include
#include
#include
#include
using namespace std;
const long long Aspe = 1000000007;
const long long MAXN = 5e4 + 100;
const long long MAXY = 100 + 10;
long long N, B, Z, Y;
long long A[MAXN], f[2][MAXY];
pair C[MAXY];
int main(void) {
freopen("2217.in", "r", stdin);
freopen("2217.out", "w", stdout);
// scanf("%d%d%d%d", &N, &B, &Z, &Y);
cin >> N >> B >> Z >> Y;
for (long long i = 0; i < N; i++) { cin >> A[i]; /*scanf("%d", &A[i]);*/ ++f[0][A[i] %= Y]; }
sort(A, A + N);
long long cur = 0, cnt = 0, M = 0;
for (long long i = 0; i < N; i++)
if (A[i] == A[cur]) ++cnt; else { C[M++] = make_pair(A[cur], cnt); cur = i; cnt = 1; }
C[M++] = make_pair(A[cur], cnt);
// f[0][0] = 1;
for (long long i = 0; i + 1 < B; i++)
for (long long j = 0; j < Y; j++)
if (f[i & 1][j]) {
for (long long k = 0; k < M; k++) (f[(i + 1) & 1][(j * 10 + C[k].first) % Y] += f[i & 1][j] * C[k].second % Aspe) %= Aspe;
f[i & 1][j] = 0;
}
// printf("%d\n", f[(B - 1) & 1][Z]);
cout << f[(B - 1) & 1][Z] << endl;
return 0;
}
100%:
上面的 DP 并没有大的问题,但就是效率低下,需要考虑对其进行优化。常见的 DP 优化手段?降维?不能再压了。单调性和斜率优化,这题显然不满足。
此时就需要用到矩阵乘法。接下来以样例 1 进行说明。
不妨将 f[i] 的值保存在 1 行 Y 列的矩阵 A 中:
类似地,将 f[i+1] 的值保存在矩阵 C 中:
如果能够找到矩阵 D ,使 AD=C ,那么 DP 的转移只是不断重复这个过程,即最终代表 f[B] 的矩阵 ans=DB−1E ,其中 E 为代表 f[1] 的矩阵。
这样的话,就可以利用矩阵快速幂,迅速得到答案了。接下来试着推导这个矩阵。
显然,根据矩阵乘法的定义, A 和 C 都是 1 行 Y 列的,又因为有 AD=C ,因此 D 应该是 Y 行 Y 列的矩阵,形如
根据矩阵乘法的运算法则, A 的第 i 列总会与 D 的第 i 行每一列的值相乘,再累加入 C 的该列。
首先考虑 j=0 的情况(即 A 的第一列),看它会如何决定 D 的第一行的值。
A[] 数组为 {3, 5, 6, 7, 8, 9, 5, 1, 1, 1, 1, 5},则 (0×10+Ai)modY 的取值分布为:1 会出现 4 次,3 会出现 1 次,5 会出现 3 次,6、7、8 和 9 各出现 1 次。也就意味着, C 的第二列中( f[i+1][1] )包含着 4 个 f[i][0] , C 的第四列中包含着 1 个 f[i][0] ,依此类推。这样就可以把对应系数填进 D 的第一行:
类似地,当 j=1 时, f[i][1] 也会相应地对 f[i+1] 作出一定的贡献。 (1×10+Ai)modY 的取值分布与上面是完全一样的,因此 D 的第二行与第一行相同。同理,下面所有行都相同。但要注意这是因为样例一中 Y=10 的特殊情况,导致了只需考虑末位,而普遍情况不是这样的。
一般地,推出 D 矩阵的方法可归纳为:枚举 j ,根据 A[] 的值算出 (10j+Ak)modY 的分布情况。若算得 x 会出现 y 次,则 D 第 j 行 x 列的值为 y 。
得到这个矩阵之后,对其进行矩阵快速幂,再与 E 相乘,即可得到 ans 。
总结一下,这题可以利用矩阵乘法优化,很重要的一点就是它满足“线性常系数递推”。什么是“线性”?可以理解为“一次”。至于“常系数”,我刚学习矩阵乘法的时候没有理解好,认为总是一个不变的常数(与具体数据没有关系)。但事实上是指,在转移过程中的这个矩阵是一定的,因此通过一定的方法推出这个矩阵之后,就可以利用矩阵乘法的结合律(注意,交换律是不成立的),进行矩阵快速幂,从而使递推的效率得到了飞跃。
参考代码:
//2217.cpp
#include
#include
#include
#include
#include
using namespace std;
const long long MAXN = 5e4 + 100;
const long long MAXY = 100 + 5;
const long long Ghastlcon = 1000000007;
struct Matrix {
long long val[MAXY][MAXY];
long long row, col;
Matrix () {
for (long long i = 0; i < MAXY; i++)
for (long long j = 0; j < MAXY; j++) val[i][j] = 0;
}
void load_from_array(long long src[][MAXY], long long r, long long c) {
row = r; col = c;
for (long long i = 0; i < r; i++)
for (long long j = 0; j < c; j++) val[i][j] = src[i][j];
}
Matrix operator * (const Matrix x) { //重载运算符
Matrix res; res.row = row; res.col = x.col;
for (int i = 0; i < res.row; i++)
for (int j = 0; j < res.col; j++)
for (int k = 0; k < col; k++)
(res.val[i][j] += val[i][k] * x.val[k][j] % Ghastlcon) %= Ghastlcon;
return res;
}
void debug_output() {
for (long long i = 0; i < row; i++) {
for (long long j = 0; j < col; j++) printf("%I64d ", val[i][j]);
putchar('\n');
}
}
} basic, tmp1, tmp2, ans;
long long N, B, Z, Y;
long long A[MAXN], arr_b[MAXY][MAXY], f[1][MAXY];
Matrix matrix_pow(Matrix cur, long long k) { //矩阵快速幂
if (k == 1) return cur;
Matrix tmp = matrix_pow(cur, k >> 1);
if (k & 1) return tmp * tmp * cur; else return tmp * tmp;
}
int main(void) {
freopen("2217.in", "r", stdin);
freopen("2217.out", "w", stdout);
scanf("%I64d%I64d%I64d%I64d", &N, &B, &Z, &Y);
for (long long i = 0; i < N; i++) { scanf("%I64d", &A[i]); ++f[0][A[i] % Y]; }
if (B == 1) { printf("%I64d\n", f[0][Z]); return 0; } //边界
for (long long i = 0; i < Y; i++)
for (long long j = 0; j < N; j++)
++arr_b[i][(i * 10 + A[j]) % Y]; //推出基础矩阵
basic.load_from_array(arr_b, Y, Y); //basic.debug_output();
tmp1 = matrix_pow(basic, B - 1); tmp2.load_from_array(f, 1, Y); //tmp2.debug_output();
ans = tmp2 * tmp1; /*ans.debug_output();*/ printf("%I64d\n", ans.val[0][Z]);
//注意矩阵乘法不满足交换律,我一开始把这里写成了 tmp1 * tmp2,还调了好久才发现
return 0;
}