一类基于笛卡尔树的排列计数DP

· · 个人记录

P5999 [CEOI2016]kangaroo

题意 : 有 n 个草丛排成一排,编号 1\sim n

有一只袋鼠,从 s 出发,每次跳一步跳到一个其他的草丛,经过每个草丛恰好一次,最终到达 t。显然他会跳跃 n-1 次。

为了不被人类发现,袋鼠每次跳跃的方向必须与前一次不同。

求方案数模 10^9+7 的结果。

------------ 按访问顺序把编号记下,得到排列 $P$ ,则约束为 : - $P_1=s,P_n=t$。 - 对于 $1<i<n$ ,$P_{i-1}<P_i>P_{i+1}$ 或 $P_{i-1}>P_i<P_{i+1}$ ,排列形如“波浪”。 对于排列计数 DP 问题,一种经典的手段是从小到大插入元素,但此题中不合法的序列可能在插入后变得合法,故不能采用插入法。 从小到大考虑元素,观察排列的表现。已加入的元素会形成若干个连续段,这些连续段内部一定是合法的。 设 $dp[i][j]$ 表示考虑了数 $1\sim i$ ,形成 $j$ 个连续段(考虑的是以连续段为元素的有序序列,不考虑中间的空位有几个等等),内部合法,段的左右都是波谷(不包括 $s,t$),的方案数。 插入 $i$ 时,讨论如下: - 当 $i\neq s,t$ 时 - 可以作为新的一段插入。若 $i>s$ ($s$ 已经插了)不能插在首,若 $i>t$ 不能插在尾。 记 $c=[i>s]+[i>t]$ ,$f_{i,j+1}\leftarrow (j+1-c)\times f_{i-1,j}

可以发现,每种合法的排列都恰会被计数一次。

首先显然被计数的排列都是合法的,只需证每种合法排列恰只有一种计数方式。

对于给定的合法排列 P ,从小到大考虑其中元素,维护两侧为波谷的连续段,每次插入可以得到对应的操作。

不难发现这类似于水淹笛卡尔树并维护森林,不同排列对应不同的笛卡尔树,自然,水淹森林的过程也就不同。

复杂度 O(n^2)

#include<algorithm>
#include<cstdio>
#define MaxN 2050
using namespace std;
const int mod=1000000007;
int n,s,t,f[MaxN][MaxN];
int main()
{
  scanf("%d%d%d",&n,&s,&t);
  f[0][0]=1;
  for (int i=1;i<=n;i++)
    if (i==s||i==t){
      for (int j=1;j<=i;j++)
        f[i][j]=(f[i-1][j]+f[i-1][j-1])%mod;
    }else {
      int c=(i>s)+(i>t);
      for (int j=1;j<=i;j++)
        f[i][j]=(1ll*j*f[i-1][j+1]+1ll*(j-c)*f[i-1][j-1])%mod;
    }
  printf("%d",f[n][1]);
  return 0;
}

CF704B Ant Man

题意 : 有 n 个元素,第 i 个元素有五个参数x_i,a_i,b_i,c_i,d_i

------------ 令 $a_i\leftarrow a_i+x_i,\ b_i\leftarrow b_i-x_i,\ c_i\leftarrow c_i+x_i,\ d_i\leftarrow d_i-x_i$ ,则有 $$f(i,j)=\begin{cases}c_i+b_j&(i>j)\\d_i+a_j&(i<j)\end{cases}$$ 设 $dp_{i,j}$ 表示考虑了前 $i$ 个数,形成 $j$ 个连续段的方案数。 加入 $i$ 时,讨论 $i$ 两侧贡献的四种情况,计算关于 $a_i,b_i,c_i,d_i$ 的贡献。 - $i\neq s,e

复杂度 O(n^2)

#include<algorithm>
#include<cstdio>
#define ll long long
#define MaxN 5050
using namespace std;
const ll INF=1ll<<60;
#define chkmin(a,b) a=min(a,b)
int n,s,e,x[MaxN],a[MaxN],b[MaxN],c[MaxN],d[MaxN];
ll f[2][MaxN];
int main()
{
  scanf("%d%d%d",&n,&s,&e);
  for (int i=1;i<=n;i++)scanf("%d",&x[i]);
  for (int i=1;i<=n;i++){scanf("%d",&a[i]);a[i]+=x[i];}
  for (int i=1;i<=n;i++){scanf("%d",&b[i]);b[i]-=x[i];}
  for (int i=1;i<=n;i++){scanf("%d",&c[i]);c[i]+=x[i];}
  for (int i=1;i<=n;i++){scanf("%d",&d[i]);d[i]-=x[i];}
  for (int i=1;i<=n;i++)f[01][i]=INF;
  for (int i=1,cur=0;i<=n;i++){
    cur^=1;
    for (int j=0;j<=i;j++)f[cur][j]=INF;
    if (i!=s&&i!=e){
      for (int j=0;j<i;j++){
        if (i<max(s,e)||j>1)chkmin(f[cur][j+1],f[cur^1][j]+b[i]+d[i]);
        if (j>1)chkmin(f[cur][j-1],f[cur^1][j]+a[i]+c[i]);
        if ((i<e||j>1)&&j>0)chkmin(f[cur][j],f[cur^1][j]+a[i]+d[i]);
        if ((i<s||j>1)&&j>0)chkmin(f[cur][j],f[cur^1][j]+b[i]+c[i]);
      }
    }else if (i==s){
      for (int j=0;j<i;j++){
        chkmin(f[cur][j+1],f[cur^1][j]+d[i]);
        if (j>0)chkmin(f[cur][j],f[cur^1][j]+c[i]);
      }
    }else if (i==e){
      for (int j=0;j<i;j++){
        chkmin(f[cur][j+1],f[cur^1][j]+b[i]);
        if (j>0)chkmin(f[cur][j],f[cur^1][j]+a[i]);
      }
    }
  }printf("%lld",f[n&1][1]);
  return 0;
}

Loj#2743. 「JOI Open 2016」摩天大楼

题意 对于 A_{1\sim n} 的排列 P ,定义其“波动度”为:

\sum\limits_{i=1}^{n-1}|P_i-P_{i+1}|

求波动度 \leq c 的排列数目。答案对 10^9+7 取模。

------------ 显然绝对值可以拆开,变成讨论相邻两个数的大小关系。但这样贡献可能有负数,求和不单调,复杂度较差。 一个使得贡献单调的技巧是**分层**。将 $A$ 排序,观察 $A_i-A_{i-1}$ 的贡献系数,是满足一个 $\leq A_{i-1}$ ,另一个 $\geq A_i$ 的对子 $(P_j,P_{j+1})$ 的个数。 每次插入 $A_i$ 时,计算 $A_i-A_{i-1}$ 的贡献系数(显然为正)。这样的贡献是单调不降的,于是可以应用 $c$ 的限制。 记 $dp_{i,j,k,0\sim 2}$ 表示前 $i$ 个数,形成了 $j$ 个连续段,目前贡献和为 $k$ ,选择了 $0\sim 2$ 个边界元素的方案数。 对于新形成的边,显然有贡献,对于未形成的边和已形成的边则无贡献。 讨论有五类:自成一段,自成一边界段,贴在段的一侧,贴在段的一侧并作为边界,连接两个段。 转移见代码。 最后 $\sum_{k\leq c} dp_{n,1,k,2}$ 就是答案。 将 $i$ 滚动。时间复杂度 $O(n^2c)$ ,空间复杂度 $O(nc)$。 ```cpp #include<algorithm> #include<cstring> #include<cstdio> #define MaxN 105 #define MaxK 1050 using namespace std; const int mod=1000000007; #define add(a,b) a=(a+b)%mod int n,c,a[MaxN],f[2][MaxN][MaxK][3]; int main() { scanf("%d%d",&n,&c); if (n==1){puts("1");return 0;} for (int i=1;i<=n;i++)scanf("%d",&a[i]); sort(a+1,a+n+1); f[1][1][0][0]=1;f[1][1][0][1]=2; for (int i=2,cur=1;i<=n;i++){ cur^=1; memset(f[cur],0,sizeof(f)>>1); int det=a[i]-a[i-1]; for (int j=1;j<i;j++) for (int k=0;k<=c;k++) for (int o=0;o<=2;o++) if (f[cur^1][j][k][o]){ int buf=f[cur^1][j][k][o],k2=k+(2*j-o)*det; if (k2>c)continue; add(f[cur][j+1][k2][o],1ll*(j+1-o)*buf);//自成一中段 if (o<2)add(f[cur][j+1][k2][o+1],1ll*(2-o)*buf);//自成一边段 if (j>=2)add(f[cur][j-1][k2][o],1ll*(j-1)*buf);//连两段 add(f[cur][j][k2][o],1ll*(2*j-o)*buf);//贴 if (o<2)add(f[cur][j][k2][o+1],1ll*(2-o)*buf);//贴,并产生边段 } } int ans=0; for (int k=0;k<=c;k++) add(ans,f[n&1][1][k][2]); printf("%d",ans); return 0; } ``` # [CF1515E Phoenix and Computers](https://www.luogu.com.cn/problem/CF1515E) **题意** : 给定 $n$ ,有 $n$ 个电脑排成一排。 初始时所有电脑都关机,你要**按某种顺序**手动开电脑,使得最终所有电脑都被开启位置。 若某电脑两侧的电脑都开机,这台电脑会立刻自动开机。不能开一个已经开了的电脑。 将手动开的电脑编号依次记下,求有多少种可能的编号序列。 答案对给定的质数 $mod$ 取模。 $n\leq 400$ ,时限$\texttt{3s}$。 ------------ 设 $f_{i,j}$ 表示已开机(包括手动和自动) $i$ 台,形成了 $j$ 个连续段,且钦定连续段间距离 $>1$ 的情况。 对于 $f_{i,j}$ ,新开一台,可能的转移如下: - 新增一段 : $f_{i,j}\times (j+1)\rightarrow f_{i+1,j+1}

最终 f_{n,1} 是答案。

复杂度 O(n^2)

#include<algorithm>
#include<cstdio>
#define MaxN 405
using namespace std;
#define add(a,b) a=(a+b)%mod
int n,mod,a[MaxN],f[MaxN][MaxN];
int main()
{
  scanf("%d%d",&n,&mod);
  f[0][0]=1;
  for (int i=0;i<n;i++){
    for (int j=0;j<=i;j++){
      add(f[i+1][j+1],1ll*f[i][j]*(j+1));
      if (j>=2){
        add(f[i+2][j-1],1ll*f[i][j]*2*(j-1));
        add(f[i+3][j-1],1ll*f[i][j]*(j-1));
      }
      if (j>=1){
        add(f[i+1][j],1ll*f[i][j]*2*j);
        add(f[i+2][j],1ll*f[i][j]*2*j);
      }
    }
  }printf("%d",f[n][1]);
  return 0;
}