题目描述:给出一个长度为 m的序列A, 请你求出有多少种1……n的排列, 满足A是它的一个LIS. (1<=m<=n<=15)
题面异常的简洁,身为蒟蒻我也是第一次接触这种三进制状压dp(考场上当然只打了暴力
稍微转化一下问题,就是求序列A在1……n的排列中并且n的lis等于m;
看到15的数据范围再加上计数问题很容易就能想到状压dp,而我考试时只往二进制方面去想了,甚至想去设一个n^3*2^n的状态。。。
首先考虑平常怎么做最长上升子序列问题,一般的nlog算法是有一个f数组(因为之后还有一个dp数组所以先起名叫f,这好像也算是dp套dp?),f[i]表示长度为i的子序列里末尾最小能是多少,然后就能二分nlog出解了;
然后很容易就能想到一个性质,这个dp数组里在任何时刻都是单调不降的,而且如果是求排列中的,那也一定是递增的;
所以考虑利用这种性质,设dp[sta]为当前的状态下的方案数,在三进制下,如果第j位为0,那么j还没有被选,如果第j位为1,那么j是在求lis过程中处于f数组中,如果第j位为2那么就不在f数组中;
然后就类似lis的做法每次找f数组中第一个比当前要插入的数小的进行插入;
!!注意,这里处于f数组中必须状态设为1,因为有可能当前插入的数会取代其中一个f数组中的数,这时候要考虑dp无后效性的问题,只能进行sta的加法操作,所以不能由2减回1,所以只能由1加到2.。。
这样,如果当前三进制状态下有cnt个1,那么当前状态的lis长度就为cnt;
当前状态都不为0时,就可以计入答案;
而当前如果cnt等于m,则不能再继续使其lis增加;
这样看起来似乎复杂度是3^n*n^2的,但是由于很多状态不满,实际能跑过;
具体转移细节见代码,感觉只要理解这个状态的设置转移就很好写;
#include<cstdio>
#include<algorithm>
#include<cstring>
#define LL long long
LL dp[14348907];
int n,m;
int id[16];
int v[16];
int zt[16];
int st[16];
LL ans=0;
void write(int ta)//输出当前状态在3进制下的表示,调试用的qwq
{
int a[16];
for(int i=1;i<=n;i++){
a[i]=ta%3;
printf("%d ",a[i]);
ta/=3;
}
printf("\n");
}
int main()
{
freopen("arg.in","r",stdin);
freopen("arg.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d",&v[i]);
id[v[i]]=i;
}
st[0]=1;
dp[0]=1;
for(int i=1;i<=15;i++)st[i]=st[i-1]*3;
for(int sta=0;sta<st[n];sta++){
if(!dp[sta])continue;
int ta=sta;
int cnt=0,num=0;// cnt:当前状态被选了的数目 num:当前状态中属于最长上升子序列f数组中的数
for(int i=1;i<=n;i++){
zt[i]=ta%3;
ta/=3;
if(zt[i])cnt++;
if(zt[i]==1)num++;
}
if(cnt==n){
ans+=dp[sta];
continue;
}
for(int i=1;i<=n;i++)if(zt[i])continue;
else{
if(id[i]>1&&!zt[v[id[i]-1]])continue;
int tot=0;//tot:当前数插进去后f数组中之前有多少比它小的
for(int j=1;j<i;j++){
if(!zt[j])continue;
else if(zt[j]==1)tot++;
}
if(tot==num){
if(tot==m)continue;//lis长度不能超过m
int ns=sta+st[i-1];
dp[ns]+=dp[sta];
}else{
int ns=sta+st[i-1];
for(int j=i+1;j<=n;j++)if(zt[j]==1){
ns+=st[j-1];//替换掉f数组中一个不优的数
break;
}
dp[ns]+=dp[sta];
}
}
}
printf("%lld\n",ans);
return 0;
}