[SMOJ2217]摩天楼

40%:直接 DP
刚读题的时候第一感觉:这不是很像我在 《<数谜>解题报告》 里面提到的弱化版问题嘛,搞个无脑 DP,做法很显然。然而我却忽略了最重要的数据范围。
f[i][j] 为前 i 层组成的数 X 使 XmodY=j 的方案数,容易推出状态转移方程:

f[i+1][(10j+Ak)modY]=f[i+1][(10j+Ak)modY]+f[i][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 中:

A=[f[i][0]f[i][1]f[i][2]f[i][3]f[i][4]f[i][5]f[i][6]f[i][7]f[i][8]f[i][9]]

类似地,将 f[i+1] 的值保存在矩阵 C 中:

C=[f[i+1][0]f[i+1][1]f[i+1][2]f[i+1][3]f[i+1][4]f[i+1][5]f[i+1][6]f[i+1][7]f[i+1][8]f[i+1][9]]

如果能够找到矩阵 D ,使 AD=C ,那么 DP 的转移只是不断重复这个过程,即最终代表 f[B] 的矩阵 ans=DB1E ,其中 E 为代表 f[1] 的矩阵。
这样的话,就可以利用矩阵快速幂,迅速得到答案了。接下来试着推导这个矩阵。

显然,根据矩阵乘法的定义, A C 都是 1 行 Y 列的,又因为有 AD=C ,因此 D 应该是 Y Y 列的矩阵,形如

a1,0a2,0...aY2,0aY1,0a1,1a2,1...aY2,1aY1,1...............a1,Y2a2,Y2...aY2,Y2aY1,Y2a1,Y1a2,Y1...aY2,Y1aY1,Y1

根据矩阵乘法的运算法则, 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 的第一行:

[0401031111]

类似地,当 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;
}


你可能感兴趣的:(解题报告,SMOJ,矩阵乘法,递推DP)