题意

  1. 大小为的数组
  2. 填入 四个数字
  3. 的个数为偶数,的个数也为偶数的方案数。(这里题意不是很明确,通过看样例可以知道不满足题意)

方法

遍历+模拟

我们可以直接深度搜索所有的位置,填入,然后统计的个数

然而这种搜索所有的方案复杂度为状态数,无法在时间复杂度内完成

通过合并两个值,可以把复杂度降低到,但是依然无法在时间复杂度内完成

动态规划

基于上面的想法,我们同样把1和3进行合并。

令状态 表示,从最开始到第个位置, 2出现的次数为次,4出现的次数为次的方案数

然而这种做法,空间复杂度,是无法接受的。注意到只关心2和4出现次数是否是偶数次,也就是次数模2后是否为0,因此可以对的状态进行修改。

令状态 表示,从最开始到第个位置, 2出现的次数模2后次,4出现的次数模2后次的方案数

这样空间复杂度为 下标取模的状态数(常数) 为,

时间复杂度为 空间复杂度状态转移代价(常数) 为 ,这题目的n最大达到 依然无法在时间复杂度内完成

矩阵乘法

上面的方法虽然没有完成在时间复杂度内完成,但给出了相邻项的常数倍数的递推式

我们把 枚举展开

图片说明

把它写成矩阵的形式

有矩阵乘法可得

由此,通过快速幂,我们可以在的时间内计算出,也就是要求的答案

只需要常数大小的空间

代码

const int mod = 1000000007;
// a00 a01 a10 a11 
//                 2 1 1 0
//                 1 2 0 1
//                 1 0 2 1
//                 0 1 1 2
class Solution {
public:
    vector<vector<long long> > mul(vector<vector<long long>> &m1,vector<vector<long long>> &m2){
        vector<vector<long long> > r = vector<vector<long long>>(m1.size(),vector<long long>(m2[0].size(),0));
        for(int i = 0;i< m1.size();i++){
            for(int j = 0;j< m2[0].size();j++){
                for(int k = 0;k<m1[0].size();k++){
                    (r[i][j] += m1[i][k]*m2[k][j]%mod)%=mod;
                }
            }
        }
        return r;
    }
    vector<vector<long long> > mypow(int p){
        vector<vector<long long> > r = vector<vector<long long>>(4,vector<long long>(4,0));
        for(int i = 0;i<4;i++){
            r[i][i] = 1;
        }
        vector<vector<long long> > v = {
            {2, 1, 1, 0},
            {1, 2, 0, 1},
            {1, 0 ,2, 1},
            {0, 1, 1, 2}
        };

        while(p){
            if(p%2){
                r = mul(r,v);
            }
            v = mul(v,v);
            p/=2;
        }
        return r;
    }
    /**
     * 
     * @param n int整型 
     * @return int整型
     */
    // 要求每个偶数出现的次数也是偶数次
    int GetNumberOfSchemes(int n) {
        // write code here
        vector<vector<long long> > r = mypow(n);
        return r[0][0];
    }
};

递推变形

这里还可以合并相同值的项来简化递推关系

我们注意到中间两项的和是可以合并的

这样对应的矩阵可以简化为

通过快速幂,我们可以在的时间内计算出,也就是要求的答案

只需要常数大小的空间

代码

代码变为

const int mod = 1000000007;
// a00 a01+a10 a11 
//                2 2 0
//                1 2 1
//                0 2 2
class Solution {
public:
    vector<vector<long long> > mul(vector<vector<long long>> &m1,vector<vector<long long>> &m2){
        vector<vector<long long> > r = vector<vector<long long>>(m1.size(),vector<long long>(m2[0].size(),0));
        for(int i = 0;i< m1.size();i++){
            for(int j = 0;j< m2[0].size();j++){
                for(int k = 0;k<m1[0].size();k++){
                    (r[i][j] += m1[i][k]*m2[k][j]%mod)%=mod;
                }
            }
        }
        return r;
    }
    vector<vector<long long> > mypow(int p){
        vector<vector<long long> > r = vector<vector<long long>>(3,vector<long long>(3,0));
        for(int i = 0;i<3;i++){
            r[i][i] = 1;
        }
        vector<vector<long long> > v = {
            {2, 2, 0},
            {1, 2, 1},
            {0, 2 ,2}
        };

        while(p){
            if(p%2){
                r = mul(r,v);
            }
            v = mul(v,v);
            p/=2;
        }
        return r;
    }
    /**
     * 
     * @param n int整型 
     * @return int整型
     */
    // 要求每个偶数出现的次数也是偶数次
    int GetNumberOfSchemes(int n) {
        // write code here
        vector<vector<long long> > r = mypow(n);
        return r[0][0];
    }
};

注意到 递推式中 是满足轮换关系的,从技术上讲我们还可以继续合并简化,甚至推出一个非矩阵的表达式。但是这里不论是上面简化成三阶矩阵,还是继续简化,从思路来源上都是矩阵乘法,从时间复杂度上也没有差异,对于在给定时间内解题来说,在得到四阶矩阵递推时就已经可以了。

这里三阶和四阶矩阵是通过手推计算出的具体值,但是对于一些递推关系更复杂的题目,你可以用代码枚举1到4来计算这个具体的递推矩阵,从时间复杂度上只会多个常数倍数。最后只用关心一下初始矩阵的值。你需要保证的是相邻项的递推关系是与项数无关的常数即可。

数学/打表/OEIS

不论你通过上面反复简化递推式,还是简单的把前面几项打表出来(2,6,20,72,272,1056)搜一下OEIS

你可以得到

对于

那么剩下的就是快速幂

这道题在能推出的情况还是建议用矩阵写,或者手动推出数学表达式。对于一些难以推出递推式的题目,通过打表+观察或者OEIS,也许能给你带来解题的思路

通过快速幂,我们可以在的时间内计算出要求的答案

只需要常数大小的空间

代码

class Solution {
public:
    /**
     * 
     * @param n int整型 
     * @return int整型
     */
    // 要求每个偶数出现的次数也是偶数次
    int GetNumberOfSchemes(int n) {
        const int mod = 1000000007;
        n-=1; // 起始项不同
        long long r = 1;
        long long v = 2;
        while(n){
            if(n%2)(r*=v)%=mod;
            (v*=v)%=mod;
            n/=2;
        }
        return r*(r+1)%mod;
    }
};

知识点

数学建模建立递推关系

在进入矩阵的推导之前,需要通过将题意数学建模,推导出一个动态规划的递推关系,这样才有助于我们发现其相邻项是常数倍数的加和关系,进而想到使用矩阵

快速幂与矩阵乘法

快速幂不论是矩阵乘法也好,还是普通的数值(例如上面我们得到的数学公式中2的幂次),复数等等也罢,核心也就这么10行不到的代码,对于需要取mod的时候记住取模,

剩下的,实现其乘法部分即可。比如这里乘法部分是矩阵乘法。

        result = 1
        while(power){
            if(p%2){
                result = mul(result,base);
            }
            base = mul(base,base);
            power/=2;
        }
        return result;

乘法溢出

虽然这里出入参都是int,但是在乘法过程中很容易超过int,在计算过程中使用long long,并随时取模即可