Harris
发布于 2026-03-24 / 14 阅读
0
0

矩阵快速幂与环形状压 DP:洛谷 P1357 花园详解

矩阵快速幂与环形状压 DP:洛谷 P1357 花园 详解

题目简述:有一个周长为 N 的环形花园,每个位置可以种 'C' 或 'P' 两种花。要求任意连续的 M 个位置中,'C' 的数量不能超过 K 个。求合法的种植方案数,对 10^9 + 7 取模。 数据范围4 \leq N \leq 10^{15}2 \leq M \leq 51 \leq K \leq M

一、 算法嗅觉:如何一眼看透这道题?

当我们在考场上看到这组数据范围时,应该立刻触发两个条件反射:

  1. ​M 极小:任意时刻只需要关注连续的 ​M 个位置,暗示这一定是一个 状态压缩 DP,总状态数最多只有 ​2^M 种!
  2. ​N 极大:要求 ​O(N) 的递推是绝对不可能的,必须用 ​O(\log N) 的算法。再结合 DP 的线性递推性质,这一定是 矩阵快速幂 的绝对领域!

二、 核心推导:降维打击三步曲

第一步:状态定义 (State Representation)

我们把连续的 M 个花圃看作一个滑动窗口。规定种 'C' 为 1,种 'P' 为 0。 那么长度为 M 的窗口可以用一个 M 位的整数 S 来表示。

  • 合法状态的约束:只有当二进制数 ​S1 的个数不超过 ​K 时,这个状态才是合法的。我们可以使用 GCC 内置函数 __builtin_popcount(S) <= K 瞬间完成判定。

第二步:状态转移 (Transition Matrix)

假设当前窗口的状态是 S(代表第 ii+M-1 个花圃)。我们现在要种第 i+M 个花圃。 随着窗口向右滑动一格,最左边(最老)的花会被挤出窗口,最右边(最新)的花会进入窗口。

这在位运算中如何表现?

  1. 左移腾出空位S << 1
  2. 种下新的花:种 'P' 就是末尾加 0,种 'C' 就是末尾加 1(即 | 1)。
  3. 截断保持长度:只保留低 ​M 位,即 & ((1 << M) - 1)

如果旧状态 S 和生成的新状态 S' 都是合法的(1 的个数都 \leq K),那么这就是一次合法的转移,我们在邻接矩阵中记作:T[S][S'] = 1

第三步:环形约束与“矩阵的迹 (Trace)”

普通的矩阵快速幂通常是求起点到一个特定终点的路径数,但这道题是环形的!

这意味着什么? 想象我们在环上随机选一段长度为 M 的起始状态 S。我们绕着花园走一圈,发生 N 次转移后,由于是环,最后这 M 盆花必须严丝合缝地变回状态 S

在矩阵乘法中,设转移矩阵为 T

  • ​(T^N)[S][S] 的物理意义:从状态 ​S 出发,经过 ​N 步转移,最终又回到状态 ​S 的方案数。

因此,所有合法的环形花园总数,就是枚举所有合法的起始状态 S,把它们绕一圈回到自己的方案数全部加起来:

\text{Answer} = \sum_{\text{合法 } S} (T^N)[S][S]

在线性代数中,矩阵主对角线元素之和被称为“矩阵的迹 (Trace)”。 这道题的终极答案,就是转移矩阵 TN 次方的迹!

三、 C++ 极简最优解代码

书上的预处理逻辑使用递归 DFS 构建矩阵,显得有些繁琐。由于 M \leq 5,我们直接用两个 for 循环和几行位运算,就能极其清晰地把状态转移矩阵构建出来。

#include <iostream>
#include <vector>

using namespace std;

const int MOD = 1000000007;

// 定义矩阵结构体,封装矩阵乘法
struct Matrix {
    int size;
    long long mat[35][35]; // M<=5,状态最多32个

    // 构造函数:初始化为全0矩阵
    Matrix(int s) : size(s) {
        for (int i = 0; i < size; ++i)
            for (int j = 0; j < size; ++j)
                mat[i][j] = 0;
    }

    // 设置为单位矩阵 (主对角线为1,其余为0)
    void setIdentity() {
        for (int i = 0; i < size; ++i) 
            mat[i][i] = 1;
    }

    // 重载乘号,实现矩阵乘法
    Matrix operator*(const Matrix& other) const {
        Matrix res(size);
        for (int i = 0; i < size; ++i) {
            for (int k = 0; k < size; ++k) {
                if (mat[i][k] == 0) continue; // 常数级优化
                for (int j = 0; j < size; ++j) {
                    res.mat[i][j] = (res.mat[i][j] + mat[i][k] * other.mat[k][j]) % MOD;
                }
            }
        }
        return res;
    }
};

// 矩阵快速幂:求 base 的 exp 次方
Matrix power(Matrix base, long long exp) {
    Matrix res(base.size);
    res.setIdentity(); // 答案矩阵初始为单位矩阵
  
    while (exp > 0) {
        if (exp & 1) res = res * base;
        base = base * base;
        exp >>= 1;
    }
    return res;
}

int main() {
    // 优化输入输出流
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    long long n;
    int m, k;
    if (!(cin >> n >> m >> k)) return 0;

    int maxS = 1 << m; // 状态总数为 2^m
    int mask = maxS - 1; // 用于截断高位,保留最低的 m 位
  
    Matrix A(maxS);

    // 【核心一】构建一次转移矩阵 A
    for (int S = 0; S < maxS; ++S) {
        // 如果当前状态 S 不合法('C' 的数量超过 k),直接跳过
        if (__builtin_popcount(S) > k) continue;

        // 尝试种一盆 'P' (即在末尾加 0)
        int next0 = (S << 1) & mask;
        if (__builtin_popcount(next0) <= k) {
            A.mat[S][next0] = 1; // 转移合法,标记为 1
        }

        // 尝试种一盆 'C' (即在末尾加 1)
        int next1 = ((S << 1) | 1) & mask;
        if (__builtin_popcount(next1) <= k) {
            A.mat[S][next1] = 1; // 转移合法,标记为 1
        }
    }

    // 【核心二】计算矩阵 A 的 n 次方
    Matrix An = power(A, n);

    // 【核心三】由于是环形,起始状态必须等于最终状态
    // 我们将所有合法状态 S 的 An[S][S] (即主对角线元素) 加起来,这就是答案
    long long total_valid_gardens = 0;
    for (int S = 0; S < maxS; ++S) {
        if (__builtin_popcount(S) <= k) {
            total_valid_gardens = (total_valid_gardens + An.mat[S][S]) % MOD;
        }
    }

    cout << total_valid_gardens << "\n";

    return 0;
}



评论