首先,拿到题面,\(n \le 30\),\(k \le 8\),这不就暴搜吗。再想想,紫题会给你暴搜的机会吗?所以进一步思考,发现这其实是一道 DP,而且数据这么小,肯定是给状压 DP 的样子。
经过一定思考,发现我们可以直接线性枚举 \([1,n]\)。
为什么呢?因为每一个点只能和前 \(k\) 个点连边,而且每一个点只能有偶数条连边。那么我们枚举到 \(i\),它可以改变的点最前面是 \(i-k\),它下一个就是 \(i+1\),那么枚举到 \(i+1\) 时肯定没办法再改变 \(i-k\),所以,到了 \(i\) 是改变 \(i-k\) 最后一次机会,所以再此时必须改变 \(i-k\) 变成偶数。
于是,我们可以设计 dp 状态 \(dp_{i,j,l,z}\) 代表枚举点数 \(i\),枚举边 \(j\),前 \(k\) 个点中的第 \(l\) 个,\(z\) 为状态压缩,代表着每一个点的奇偶性。
因此我们可以推出转移方程(见代码)。
注意事项:状压 DP 用到位运算,因为位运算的优先级复杂,建议勤快的加括号,我就是因为这个括号的问题调了很久。
AC CODE:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,k;
int dp[40][40][20][550];
const int p=1e9+7;
//定义:dp[i][j][l][z]
//表示枚举到第i个点,第j条边,z为i个点与前k个点的状压连边状态,已经连了l条边
signed main(){
cin >> n >> m >> k;
dp[2][0][0][0]=1;
for(int i = 2;i <= n;i++){
//枚举每一个点
for(int j = 0 ;j <= m;j++){
//枚举每一条边
for(int z=0;z<(1<<(k+1));z++){
//枚举状态
for(int l = 0;l <= k;l++){
//枚举已经连的边数
if(l!=min(i-1,k)){
dp[i][j][l+1][z]=(dp[i][j][l+1][z] + dp[i][j][l][z])%p;
dp[i][j+1][l][z^(1<<(l+k-min(k,i-1)))^(1<<k)] = (dp[i][j+1][l][z^(1<<(l+k-min(k,i-1)))^(1<<k)] + dp[i][j][l][z]) % p;
}
if(l==min(i-1,k) && !(z & 1))
dp[i+1][j][0][z>>1]=(dp[i+1][j][0][z>>1]+dp[i][j][l][z])%p;
}
}
}
}
cout<<dp[n+1][m][0][0] % p;
return 0;
}