HNOI2010 公交线路
题目链接:戳我
看到k,p的范围这么小,显然要状压DP啊!
但是要怎么状压DP呢。
我们先注意到每p个公交站,这k辆公交车都要至少出现一次。因为答案是按集合算的,所以公交车之间不做区别,换句话说就是我们可以讲题目简化一下——
1-n的n个元素,k个集合,保证一个元素只出现在一个集合中(不能多余一个也不能少于一个),任意一个集合中的元素从小到大排序之后,两两之间的编号相差不能超过p,问方案数?
我们设状态\(dp[i][j]\)表示前i个数,从i(包括i)往前p个数的状态,方案数。因为要状压,所以肯定要想出来如何用01表示状态——0表示这个位置不是任何一个集合当前选的最后一个数,1表示是。
那么就是每p个数中,一定要有k个数是1。
我们预处理出来合法的状态,然后再处理出哪些状态之间可以互相转移——i可以转移到j,当且仅当\((i<<1)\&j==k-1\)(j因为最后一位必须为1)。这样算起来第二维状态数最多也是\(2^{k-1}\),不是很大。
然后就是\(dp[i][j]=\sum dp[i-1][k]\),其中k状态能够转移到j。
发现n很大,所以要矩阵快速幂。
具体实现可以看一下代码。
最后额外解释一下:
1、为什么矩阵快速幂的次数为n-k?因为开始k个是出发站,所以每一位一定是1。我们从\([k+1,k+p]\)开始,而且它们肯定都能从k个1转移过来。
2、为什么最后输出的是[1][1]?因为矩阵快速幂里面记录的都是预处理出来的合法状态,而我们处理出来的第一个合法状态是0011...11(一共p位,后k位为1),考虑一下位运算的顺序,这是很显然的。题目中要求的也是最后k为必须为终点站,所以我们直接输出第一个的答案就行了。
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<cstdio>
#define MAXN 155
#define mod 30031
using namespace std;
int n,k,p,cnt;
int c[MAXN];
struct Node{int t[MAXN][MAXN];}a,init;
inline Node calc(Node x,Node y)
{
Node cur;
for(int i=1;i<=cnt;i++)
for(int j=1;j<=cnt;j++)
cur.t[i][j]=0;
for(int i=1;i<=cnt;i++)
for(int j=1;j<=cnt;j++)
for(int k=1;k<=cnt;k++)
cur.t[i][j]=(cur.t[i][j]+x.t[i][k]*y.t[k][j]%mod)%mod;
return cur;
}
inline Node fpow(Node x,int y)
{
Node cur;
for(int i=1;i<=cnt;i++)
for(int j=1;j<=cnt;j++)
cur.t[i][j]=0;
for(int i=1;i<=cnt;i++) cur.t[i][i]=1;
while(y)
{
if(y&1) cur=calc(cur,x);
x=calc(x,x);
y>>=1;
}
return cur;
}
inline bool check(int x,int y)//x是否能够转移到y
{
int cur_ans=((c[x]<<1)&c[y]),cur_sum=0;
for(int i=0;i<p;i++)
if(cur_ans&(1<<i))
cur_sum++;
if(cur_sum==k-1) return true;
else return false;
}
int main()
{
#ifndef ONLINE_JUDGE
freopen("ce.in","r",stdin);
#endif
scanf("%d%d%d",&n,&k,&p);
for(int i=0;i<(1<<p);i++)
{
int cur_sum=0;
for(int j=0;j<p;j++)
if(i&(1<<j))
cur_sum++;
if(cur_sum==k&&(i&1)) c[++cnt]=i;
}
for(int i=1;i<=cnt;i++)
for(int j=1;j<=cnt;j++)
if(check(i,j)&&(c[j]&1))
init.t[i][j]=1;
init=fpow(init,n-k);
printf("%d\n",init.t[1][1]);
return 0;
}