洛谷 P1357 花园
题目传送门
暴力
思路
状态设计
设 d p i , j dp_{i, j} dpi,j 表示目前放到第 i i i 个花圃,且最近 m m m 个花圃的状态是 j j j( C C C 形花圃用 1 1 1, P P P 形花圃用 0 0 0 表示)的方案数。
状态转移
当前状态为
j
j
j 时,上一个状态就是
j
>
>
1
j >> 1
j>>1 或
(
j
>
>
1
)
∣
(
1
<
<
(
m
−
1
)
)
(j >> 1) \ | \ (1 << (m - 1))
(j>>1) ∣ (1<<(m−1))。
因为当上一个状态转移到当前状态时,它会左移一位,并且挤掉最左边一位的状态。那么上一个状态的最左边的那一位就有
0
/
1
0/1
0/1 两种可能。
转移时再判断上一个状态是否合法,即
1
1
1 的个数是否不超过
k
k
k 个。
边界条件
由于它是一个环形花园,因此我们要枚举最初始 m m m 个花圃的状态。假设其状态为 s t a r t start start,那么边界就是 d p m , s t a r t = 1 dp_{m, start} = 1 dpm,start=1。
答案统计
还是因为它是圆形花园,所以当初始 m m m 个花圃的状态固定时,它的贡献就是 d p n + m , s t a r t dp_{n + m, start} dpn+m,start(相当于转一圈转回来了),将其累加即可。
时间复杂度
O ( n × 2 2 m ) O(n \times 2 ^ {2m}) O(n×22m),可以通过 n ≤ 1 0 5 n \leq 10 ^ 5 n≤105 的数据。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 7;
const int maxs = (1 << 5) + 7;
const int mod = 1e9 + 7;
ll n;
int m, k, ms;
bool vis[maxs];
int dp[maxn][maxs], ans;
int main() {
scanf("%lld%d%d", &n, &m, &k);
ms = (1 << m) - 1;
for (int st = 0; st <= ms; ++st) { // 枚举最初始 m 个的状态
// __builtin_popcount 用来得到 st 的二进制中 1 的个数
if (__builtin_popcount(st) > k) continue;
memset(dp, 0, sizeof(dp));
// 由于初始时 dp[m][st] = 1, 最后统计也是 dp[n + m][st]
// 所以直接将整个数组向左平移 m 个位置
dp[0][st] = 1;
for (int i = 1; i <= n; ++i) {
for (int s = 0; s <= ms; ++s) {
if (__builtin_popcount(s) > k) continue;
int pre1 = (s >> 1), pre2 = (s >> 1) | (1 << (m - 1));
if (__builtin_popcount(pre1) <= k)
dp[i][s] = (dp[i][s] + dp[i - 1][pre1]) % mod;
if (__builtin_popcount(pre2) <= k)
dp[i][s] = (dp[i][s] + dp[i - 1][pre2]) % mod;
}
}
ans = (ans + dp[n][st]) % mod;
}
printf("%d\n", ans);
return 0;
}
优化
100
%
100 \%
100% 的数据范围时是
n
≤
1
0
15
n \leq 10 ^ {15}
n≤1015,肯定需要
O
(
l
o
g
(
n
)
)
O(log(n))
O(log(n)) 级别的算法。
这又是在转移方面优化,所以可以想到用矩阵快速幂转移。
假设我们用
t
r
a
n
s
j
,
k
trans_{j, k}
transj,k 表示状态
j
j
j 是否可以转移到状态
k
k
k(就是转移矩阵),那么转移方程就可以转换为:
d
p
i
,
j
=
∑
k
=
0
2
m
−
1
d
p
i
−
1
,
k
×
t
r
a
n
s
k
,
j
dp_{i, j} = \sum_{k = 0}^{2^m-1} dp_{i - 1, k} \times trans_{k, j}
dpi,j=k=0∑2m−1dpi−1,k×transk,j
又发现当前行状态只与上一行状态有关,所以这就相当于
d
p
i
=
d
p
i
−
1
×
t
r
a
n
s
dp_i = dp_{i - 1} \times trans
dpi=dpi−1×trans。
更进一步地,
d
p
n
=
d
p
0
×
t
r
a
n
s
n
dp_n = dp_0 \times trans^n
dpn=dp0×transn。
因此先计算好转移矩阵的
n
n
n 次方后,再乘以初始状态矩阵就好了。
时间复杂度
O ( l o g ( n ) × 2 3 m ) O(log(n) \times 2^{3m}) O(log(n)×23m)
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 7;
const int maxs = (1 << 5) + 7;
const int mod = 1e9 + 7;
ll n;
int m, k, ms;
int ans;
struct matrix {
int a[maxs][maxs];
void init() {memset(a, 0, sizeof(a));}
matrix operator * (const matrix& x) const {
matrix res; res.init();
for (int i = 0; i <= ms; ++i)
for (int j = 0; j <= ms; ++j)
for (int k = 0; k <= ms; ++k)
res.a[i][j] = (res.a[i][j] + 1ll * a[i][k] * x.a[k][j] % mod) % mod;
return res;
}
} trans;
matrix qpow(matrix x, ll y) {
matrix res; res.init();
for (int i = 0; i <= ms; ++i)
res.a[i][i] = 1;
for (; y; y >>= 1, x = x * x)
if (y & 1) res = res * x;
return res;
}
int main() {
scanf("%lld%d%d", &n, &m, &k);
ms = (1 << m) - 1;
for (int s = 0; s <= ms; ++s) {
if (__builtin_popcount(s) > k) continue;
int pre1 = (s >> 1), pre2 = (s >> 1) | (1 << (m - 1));
if (__builtin_popcount(pre1) <= k)
trans.a[pre1][s] = 1;
if (__builtin_popcount(pre2) <= k)
trans.a[pre2][s] = 1;
}
trans = qpow(trans, n);
for (int s = 0; s <= ms; ++s) {
if (__builtin_popcount(s) > k) continue;
matrix dp; dp.init(), dp.a[0][s] = 1;
dp = dp * trans;
ans = (ans + dp.a[0][s]) % mod;
}
printf("%d\n", ans);
return 0;
}