洛谷

洛谷题单指南-状态压缩动态规划-P1357 花园

原题链接:https://www.luogu.com.cn/problem/P1357

题意解读:长度为n的0/1环形数组,连续m长度的子串中1的数量不超过k个,求一共有多少种可能的数组方案,结果对10^9+7取模。

解题思路:

1、可行解

由于m不超5,可以对连续m个子串的状态进行压缩,压缩成二进制整数。

设f[i][j]表示前i个元素,从i-m+1~i的子串状态是j的所有方案数。

不难发现,前i-1个元素的最后m个元素与前i个元素的最后m个元素,有m-1个元素是重叠的,

设前i-1个元素的最后m个元素状态是k,前i个元素的最后m个元素状态是j,k和j的关系有两种可能:

第一种,k等于j的前m-1个二进制位然后在最前面加一个0,k = j >> 1;

第二种,k等于j的前m-1个二进制位然后在最前面加一个1,k = j >> 1 | 1 << m - 1;

因此,可以得到递推关系:

f[i][j] = f[i-1][j >> 1] + f[i-1][j >> 1 | 1 << m - 1]

由于是环形数组,要处理一圈,从第0个到第n个即可,

对于所有状态s,进行初始化f[0][s] = 1,然后执行dp过程,结果就是所有f[n][s]的累加。

对结果的解释:即对某一个初始状态s,执行一圈dp,回到起始位置时状态仍然是s,这就是最后m个元素状态为s的方案数,枚举所有的s累加结果即为所有的方案数。

注意:考虑所有状态时,要判断是否合法,也就是其中1个个数是否不超过k,可以提前预处理。

80分代码:

#include <bits/stdc++.h>
using namespace std;

const int N = 100005, MOD = 1e9 + 7;
int f[N][1 << 5]; //f[i][j]表示前i个元素,从i-m+1~i的子串状态是j的所有方案数
int ones[1 << 5]; //二进制有多少个1
int n, m, k, ans;

void init()
{
    for(int i = 0; i < 1 << m; i++)
    {
        int cnt = 0;
        for(int j = 0; j < m; j++)
            if(i >> j & 1) 
                cnt++;
        ones[i] = cnt;
    }
}

int main()
{
    cin >> n >> m >> k;
    init();
    for(int s = 0; s < 1 << m; s++)
    {
        if(ones[s] > k) continue;
        memset(f, 0, sizeof(f));
        f[0][s] = 1;
        for(int i = 1; i <= n; i++)
        {
            for(int j = 0; j < 1 << m; j++)
            {
                int k1 = j >> 1;
                int k2 = j >> 1 | 1 << m - 1;
                if(ones[k1] <= k) f[i][j] = (f[i][j] + f[i - 1][k1]) % MOD;
                if(ones[k2] <= k) f[i][j] = (f[i][j] + f[i - 1][k2]) % MOD;
            }
        }
        ans = (ans + f[n][s]) % MOD;
    }
    cout << ans;
    return 0;
}

2、矩阵乘法优化

由于n最大到10^15,必须用logn的算法才可能通过,可以想到将递推优化成矩阵乘法。

来分析一下

f[i][j] = f[i-1][j >> 1] + f[i-1][j >> 1 | 1 << m - 1]

这里j >> 1,j >> 1 | 1 << m - 1都是可以转移到j的合法状态,更一般的,我们可以枚举所有状态k,看k是否能转移到j

设q[k][j]=1表示状态k可以转移到j,0表示不可以,那么上面递推式可以转换为:

对于这段代码:

由于要枚举所有的状态j,设size = 1<<m可以直接定义1*size矩阵:

p[i] = [ f[i][0], f[i][1]……f[i][(1<<m)-1]] ,而q是size*size矩阵

因此有p[i] = p[i-1] * q

再来看上一层代码:

要从1递归到n,因此有p[n] = p[0] * q^n

再看上一层代码,由于会枚举所有状态s,且初始化f[0][s]=1

这样,就是对于所有的状态s,依次计算:

p[n] = [ f[n][0], f[n][1]……f[n][(1<<m)-1]] = [f[0][0], f[0][1]…f[0][s]…f[0][(1<<m)-1]] * q^n

然后将每次计算之后的矩阵p[n]的第0行第s列的数值累加到答案即可。

对于矩阵q可以预计算,q^n可以用矩阵快速幂。

100分代码:

#include <bits/stdc++.h>
using namespace std;

const int M = 1 << 5, MOD = 1e9 + 7;
long long n, m, k, ans;
int ones[M]; //二进制有多少个1
struct Matrix
{
    int a[M][M];

    Matrix()
    {
        memset(a, 0 ,sizeof(a));
    }

    Matrix operator*(const Matrix &to) const 
    {
        Matrix res;
        for(int i = 0; i < 1 << m; i++)
            for(int j = 0; j < 1 << m; j++)
                for(int k = 0; k < 1 << m; k++)
                    res.a[i][j] = (res.a[i][j] + 1ll * a[i][k] * to.a[k][j] % MOD) % MOD;
        return res;
    }
} q;

void init_ones()
{
    for(int i = 0; i < 1 << m; i++)
    {
        int cnt = 0;
        for(int j = 0; j < m; j++)
            if(i >> j & 1) 
                cnt++;
        ones[i] = cnt;
    }
}

void init_q()
{
    for(int j = 0; j < 1 << m; j++)
    {
        int k1 = j >> 1;
        int k2 = j >> 1 | 1 << m - 1;
        if(ones[k1] <= k) q.a[k1][j] = 1;
        if(ones[k2] <= k) q.a[k2][j] = 1;
    }
}

Matrix qmi(Matrix &x, long long y)
{
    Matrix res;
    for(int i = 0; i < 1 << m; i++) res.a[i][i] = 1;
    while(y)
    {
        if(y & 1) res = res * x;
        x = x * x;
        y = y >> 1;
    }
    return res;
}

int main()
{
    cin >> n >> m >> k;
    init_ones();
    init_q();
    q = qmi(q, n);
    for(int s = 0; s < 1 << m; s++)
    {
        if(ones[s] > k) continue;
        Matrix p;
        p.a[0][s] = 1;
        p = p * q;
        ans = (ans + p.a[0][s]) % MOD;
    }
    cout << ans;
    return 0;
}

3、更进一步

在上面方法中,用p[i]表示1*size矩阵,每一行矩阵只有第s列初始值是1,且1的位置随着s的遍历而右移。

结果也是对第s列的值进行累加。

根据矩阵乘法的特性,何不直接定义p为size*size的矩阵,每一行初始值如下:

[1,0,0…0]

[0,1,0…0]

….

[0,0,0…1]

即是一个单位矩阵,用这个单位矩阵乘上q^n,既可以得到结果矩阵,而结果矩阵所有对角线的值之和即是答案。

因此结果矩阵就是q^n,将对角线值累加即可。

100分代码:

#include <bits/stdc++.h>
using namespace std;

const int M = 1 << 5, MOD = 1e9 + 7;
long long n, m, k, ans;
int ones[M]; //二进制有多少个1
struct Matrix
{
    int a[M][M];

    Matrix()
    {
        memset(a, 0 ,sizeof(a));
    }

    Matrix operator*(const Matrix &to) const 
    {
        Matrix res;
        for(int i = 0; i < 1 << m; i++)
            for(int j = 0; j < 1 << m; j++)
                for(int k = 0; k < 1 << m; k++)
                    res.a[i][j] = (res.a[i][j] + 1ll * a[i][k] * to.a[k][j] % MOD) % MOD;
        return res;
    }
} q;

void init_ones()
{
    for(int i = 0; i < 1 << m; i++)
    {
        int cnt = 0;
        for(int j = 0; j < m; j++)
            if(i >> j & 1) 
                cnt++;
        ones[i] = cnt;
    }
}

void init_q()
{
    for(int j = 0; j < 1 << m; j++)
    {
        int k1 = j >> 1;
        int k2 = j >> 1 | 1 << m - 1;
        if(ones[k1] <= k) q.a[k1][j] = 1;
        if(ones[k2] <= k) q.a[k2][j] = 1;
    }
}

Matrix qmi(Matrix &x, long long y)
{
    Matrix res;
    for(int i = 0; i < 1 << m; i++) res.a[i][i] = 1;
    while(y)
    {
        if(y & 1) res = res * x;
        x = x * x;
        y = y >> 1;
    }
    return res;
}

int main()
{
    cin >> n >> m >> k;
    init_ones();
    init_q();
    q = qmi(q, n);
    for(int i = 0; i < 1 << m; i++) 
        ans = (ans + q.a[i][i]) % MOD;
    cout << ans;
    return 0;
}