avatar

目录
洛谷 P1357 花园

题目相关

题目链接:洛谷 P1357 花园

题目描述:

  小L有一座环形花园,沿花园的顺时针方向,他把各个花圃编号为1~N(2≤N≤10^15)。他的环形花园每天都会换一个新花样,但他的花园都不外乎一个规则,任意相邻M(2≤M≤5,M≤N)个花圃中有不超过K(1<=K<M)个C形的花圃,其余花圃均为P形的花圃。
  例如,N=10,M=5,K=3。则
  CCPCPPPPCC 是一种不符合规则的花圃;
  CCPPPPCPCP 是一种符合规则的花圃。
  请帮小L求出符合规则的花园种数Mod 1000000007。请您编写一个程序解决此题。

输入输出格式:

输入格式:
  一行,三个数N,M,K。
输出格式:
  花园种数Mod 1000000007

输入输出样例:

编号 输入 输出
#1 10 5 3 458
#2 6 2 1 18

说明:

40%的数据中,N<=20;
60%的数据中,M=2;
80%的数据中,N<=$10^5$。
100%的数据中,N<=$10^{15}$。



博主乱搞

蒟蒻解析1(前80%):

一定注意这是个环形花园否则就自闭查错吧。

前40%的数据:

  应该挺暴力的博主没有尝试。

前60%的数据:

  M=2,K=1,即所有的C形都会被分开。可以尝试dp。博主没有尝试。
  $f[i][1/0]$代表前i位以及这一位是否是C形,由$f[i-1][1/0]$转移而来。
  是不是组合数也可以做博主懒得思考了。。

前80%的数据:

  注意到M≤5,说明我们可以进行状态压缩了。
  设$f[i][v]$,i代表目前在第几位,v代表前面M位的状态(2进制)。则:

$$f[i][v]=\sum_{u=0}^{2^M-1}{f[i-1][u]*ok[u][v]}$$

  u,v分别表示两种状态,$ok[u][v]$当且仅当状态u能一次转移到状态v时成立。转移n次即可,取状态相同结果。

question 1:怎样处理环?

answer 1:我们枚举前M个的位置的情况s,设f[M][s]=1,然后递推到M+N个位置。此时M+N位置其实就相当于第M个位置(因为是个环)。所以只需和初始状况相同即可,即答案贡献为f[M+N][s]。最终结果为所有合法f[M+N][s]的sum。

question 2:怎样处理ok[][]?怎样判断状态是否能转移?

answer 2:我们当然可以在运算中解决。当尝试加入新的一位状态v时,原状态s整体左移一位(移出去的就不管了),然后在空出的一位决定是0还是1。当然为了方便,博主这里采用的是预处理所有情况。所以用了ok[i][j]。

  具体代码如下(只有70,开O2就80了,我也不知道为什么。。。)

蒟蒻代码(80%)

c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
#define ll long long
const int p=1e9+7;
int f[2][40]; //可以滚动
bool ok[70][70];
bool s_ok[70]; //判断该状态本身是否合法
int n,m,k,mx;
bool xx[10]; //生成及处理状态
int tot[10];
ll ans;
void check(){
int tmp1=0,tmp2=0;
for (int i=1;i<=m;++i) tmp1=(tmp1<<1)+((xx[i])?1:0);
for (int i=2;i<=m+1;++i) tmp2=(tmp2<<1)+((xx[i])?1:0);
for (int i=1;i<=m+1;++i) tot[i]=tot[i-1]+((xx[i])?1:0);
for (int i=m;i<=m+1;++i) if (tot[i]-tot[i-m]>k) return;
ok[tmp1][tmp2]=true; s_ok[tmp1]=s_ok[tmp2]=true;
}
void work_ok(int x){
if (x>m+1){ //improtant!!!为了比较是否能通过移一位解决,我们要多一位。
check(); return;
}
xx[x]=true; work_ok(x+1);
xx[x]=false; work_ok(x+1);
}
int main(){
scanf("%d%d%d",&n,&m,&k);
mx=(1<<m)-1; //状态数之和
work_ok(1);
for (int s=0;s<=mx;++s){
if (!s_ok[s]) continue;
int tp=0;
memset(f,0,sizeof(f)); f[tp][s]=1;
for (int i=m+1;i<=n+m;++i){
tp^=1;
for (int j=0;j<=mx;++j){
f[tp][j]=0;
for (int t=0;t<=mx;++t){
if (!ok[j][t]) continue;
f[tp][j]=(f[tp][j]+f[tp^1][t])%p;
}
}
}
ans=(ans+f[tp][s])%p;
}
printf("%lld",ans);
return 0;
}

蒟蒻解析2(前100%):

  $10^{15}$的范围让上述算法时空都无法接受。由于是dp,我们可能会联想到矩阵快速幂。所以这个矩阵怎么构造?
  我们尝试换一种思路。
  设$w[i][u][v]$,表示转移i次,从u状态到v状态的方案数。
  当$i=1$时,w[1][u][v]不是1就是0。
  当$i=2$时,

$$w[2][u][v]=\sum_{k=0}^{2^M-1}{w[1][u][k]*w[1][k][v]}$$

  (是不是和矩阵乘法有相似之处?)
  我们不妨可以做的更加普遍一些:
  由第x-1次转移到第x状态怎么做?

$$w[x][u][v]=\sum_{k=0}^{2^M-1}{w[x-1][u][k]*w[x-1][k][v]}$$

  排除第一位不看,我们可以知道:
每转移一次,就相当于做了一次矩阵乘法。
  这样,转移N次得到最终状态的方案数,即w[N][u][v],就可以由N次矩阵乘法求出。这样就可以愉快地快速幂了。

question 3:所以答案是什么?

answer 3:我们需要的是转移N次后状态与初始状态相同的个数且该状态合法。所以最终答案为:(转移N次后的矩阵为A

$$Ans=\sum_{i=0}^{2^M-1}{A[i][i]}$$ (一定注意i为合法状态)

question 4:初始矩阵是什么?

answer 4:我们一开始处理的ok[u][v]代表两状态可以一次到达。我们可以把初状态设置为ok[u][v]的矩阵,然后转移N-1次。

  然后这道题就可以结束了。

蒟蒻代码:

c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
#define ll long long
const int p=1e9+7;
//bool ok[70][70];
bool s_ok[70];
ll n;
int m,k,mx;
bool xx[10];
int tot[10];
ll trueans;
struct matrix{
ll f[40][40];
void prep(){
memset(f,0,sizeof(f));
}
}u,ans;
matrix operator * (matrix a,matrix b){
matrix tmp; tmp.prep();
for (int i=0;i<=mx;++i){
for (int j=0;j<=mx;++j){
for (int t=0;t<=mx;++t){
tmp.f[i][j]+=a.f[i][t]*b.f[t][j];
tmp.f[i][j]%=p;
}
}
}
return tmp;
}
void check(){
int tmp1=0,tmp2=0;
for (int i=1;i<=m;++i) tmp1=(tmp1<<1)+((xx[i])?1:0);
for (int i=2;i<=m+1;++i) tmp2=(tmp2<<1)+((xx[i])?1:0);
for (int i=1;i<=m+1;++i) tot[i]=tot[i-1]+((xx[i])?1:0);
for (int i=m;i<=m+1;++i) if (tot[i]-tot[i-m]>k) return;
//ok[tmp1][tmp2]=true;
u.f[tmp1][tmp2]=1;
s_ok[tmp1]=s_ok[tmp2]=true;
}
void work_ok(int x){
if (x>m+1){ //improtant!!!
check(); return;
}
xx[x]=true; work_ok(x+1);
xx[x]=false; work_ok(x+1);
}
int main(){
scanf("%lld%d%d",&n,&m,&k);
mx=(1<<m)-1; u.prep();
work_ok(1); ans=u;
n--; //初始状态为转移一次后的状态,所以只需转移N-1次
while (n){
if (n&1) ans=ans*u;
n>>=1; u=u*u;
}
for (int i=0;i<=mx;++i){
if (s_ok[i]){
trueans+=ans.f[i][i];
trueans%=p;
}
}
printf("%lld",trueans);
return 0;
}
文章作者: lonlyn
文章链接: https://lonlyn.gitee.io/blog/2019/07/06/luoguP1357/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 lonlyn's blog

评论