题目传送门
https://www.luogu.org/problemnew/show/P1357
思路
显然的状压DP,
F[i][S]+=F[i−1][S>>1]+F[i−1][(S>>1)+(1<<(m−1))]
转移的条件是状态皆合法,这个可以预处理。
直接递推肯定不行,我们开一个32*32的矩阵,同时有一个状态列向量,表示达到各个状态的方案数。
矩阵左乘列向量,矩阵(i,j)代表着j->i状态的转移。我们要破环为链,对应直接由初始1~m转移n次,求回到原来的状态的方案。一种暴力的方法是枚举1~m的初始状态,然后对每个状态转移n次统计答案,但这样的时间是O(32^4*log(10^15))的,不够优。
我们发现,每次枚举初始状态,相当于在初始向量的某一位填上1,我们直接将转移矩阵自乘n-1次,对角线的和就是所有的方案数。
时间复杂度O(32^3*log(10^15)),stable.
代码
#include <bits/stdc++.h>
#define MOD 1000000007
using namespace std;
typedef long long LL;
LL n, ans;
int m, K, cnt;
bool Legal[32];
struct Mat{
LL a[32][32];
void Clear(){
memset(a, 0LL, sizeof(a));
}
friend Mat operator * (Mat A, Mat B){
Mat C;
C.Clear();
for(int i = 0; i < cnt; i++)
for(int j = 0; j < cnt; j++)
for(int k = 0; k < cnt; k++)
C.a[i][j] = (C.a[i][j] + A.a[i][k] * B.a[k][j] % MOD) % MOD;
return C;
}
};
Mat Pow(Mat X, LL Y){
Mat Z = X;
while(Y){
if(Y & 1) Z = X * Z;
X = X * X;
Y >>= 1;
}
return Z;
}
int main(){
scanf("%lld%d%d", &n, &m, &K);
cnt = 1 << m;
Mat F;
F.Clear();
for(int i = 0; i < cnt; i++){
int temp = 0;
for(int j = 0; j < m; j++) if((1<<j) & i) temp ++;
if(temp <= K) Legal[i] = true;
}
for(int i = 0; i < cnt; i++){
if(Legal[i] && Legal[i>>1]) F.a[i][i>>1] = 1;
if(Legal[i] && Legal[(i>>1)+(1<<(m-1))]) F.a[i][(i>>1)+(1<<(m-1))] = 1;
}
F = Pow(F, n-1);
for(int i = 0; i < cnt; i++)
ans = (ans + F.a[i][i]) % MOD;
printf("%lld\n", ans);
return 0;
}