loj2743「JOI Open 2016」摩天大楼

· · 个人记录

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

前言

此题使用的 dp 我称为线头 dp (不喜轻喷,主要是为了区分插头 dp),在我很久以前就有这个想法,不过一直没有找到题目,最近找到了,于是决定写这篇题解。

题目简意

给一个长度为 n ,元素互不相同的序列 A ,求有多少个长度为 n 的排列 p 使得 \sum_{i=1}^{n-1}|A_{p_i}-A_{p_{i+1}}|\le L ,对 10^9 + 7 取模。

n\le 100, A_i,L\le 1000

题解

为了方便,先将 A 排序。显然,这样做不会改变答案。

考虑把题目里限制的式子 \sum_{i=1}^{n-1}|A_{p_i}-A_{p_{i+1}}| 拍到数轴上,其含义就是从 A_{p_1} 出发,依次走到 A_{p_i}(2\le i\le n) 经过的总长度。不妨把走的路线画出来,我们规定每“调一次头”就把路线切换到下一层,并在每次走到 A_{p_i} 的时候在路线上把 A_{p_i}标注出来,这样子画出来的路线和排列是一一对应的,只需要对合法的路线计数即可。

举个例子,设 A=\{2,3,5,6,7\},p=\{2,1,5,4,3\} ,那么画出来的路线长这样:

考虑对这条路线 dp 。先不考虑最终路线的端点,可以发现路线的每一个纵截面形如若干个并列的线头,并且我们只需要关心线头的数量。于是我们像扫描线一样,从左到右对线头的形态 dp ,设 f_{i,j,k} 表示扫到 A_i ,当前纵截面有 j 个线头,截止到现在,路线总长为 k 的方案数。

(一个线头长这样,中间的折线省略了路线的形态)

容易发现,每个 A_i 对线头的形态的改变只有 3 种情况:

  1. 新增一个线头
  2. 合并两个线头
  3. 不改变

每种情况在原排列上其实表现为 (这里写出来帮助理解)

  1. A_{p_{i-1}}> A_{p_i}, A_{p_{i+1}}>A_{p_i}
  2. A_{p_{i-1}}< A_{p_i}, A_{p_{i+1}}<A_{p_i}
  3. A_{p_{i-1}}< A_{p_i}<A_{p_{i+1}}$ 或 $A_{p_{i+1}}< A_{p_i}<A_{p_{i-1}}

这三种情况的转移都是容易的,转移式在最后加上端点限制后一起写。

现在考虑最终路径端点的情况:首先可以发现,起点的线头一定在最上面,终点的线头一定在最下面,并且它们本质一样,所以这里只讨论起点。起点的形态显然只有两种,往左走和往右走。

往左走的情况可以转化成把最上面的线头的上端点封住。

而往右走的情况可以转化成在上面添加一个虚线头后,把虚线头的上端点封住。

自然地,我们需要在状态里加一维 d 表示边界确定了即可,或者说最上面和最下面的线头有几个的端点被封了。

现在的状态为 f_{i,j,k,d} 表示扫到 A_i ,当前纵截面有 j 个线头,截止到现在,路线总长为 k ,有 d 个边界确定了的方案数。

转移:

  1. 新建一个线头:

    f_{i+1,j+1,k+(A_{i+1}-A_i)(2j-d),d}\leftarrow (j+1-d)f_{i,j,k,d}

    解释两个地方:

    1. 总共有 j 个线头,每个线头两个端点,但有 d 个端点被封了,所以路径延长的总数是 (A_{i+1}-A_i)(2j-d) ,下面的同理。
    2. 关于转移系数 j+1-d 即是新建的线头可以插在任意两个相邻的线头中间或者最上面和最下面,总共 j+1-d 个可以插的位置,但每确定一个边界会少一个可以插的位置。
  2. 合并两个线头:

    f_{i+1,j-1,k+(A_{i+1}-A_i)(2j-d),d}\leftarrow (j-1)f_{i,j,k,d} (j>1)

    显然需要至少两个线头才能合并,选择相邻两个线头合并,所以总共有 j-1 种方式。

  3. 不改变:

    f_{i+1,j,k+(A_{i+1}-A_i)(2j-d),d}\leftarrow (2j-d)f_{i,j,k,d}
  4. 作为其中一个边界:

    1. 往左走:

      f_{i+1,j,k+(A_{i+1}-A_i)(2j-d),d+1}\leftarrow(2-d)f_{i,j,k,d}
    2. 往右走:

      f_{i+1,j+1,k+(A_{i+1}-A_i)(2j-d),d+1}\leftarrow(2-d)f_{i,j,k,d}

边界:f_{0,0,0,0}=1

答案:\sum_{k=0}^{L}f_{n,1,k,2}

特判:n=1 时需要特判,因为上述转移中默认两个最终路线的端点不同。

时间复杂度:\mathcal{O}(n^2L)

代码

#include <bits/stdc++.h>
#define re(i, x, y) for (int i = (x); i < (y); ++i) 
#define rep(i, x, y) for (int i = (x); i <= (y); ++i) 
#define repp(i, x, y, d) for (int i = (x); i <= (y); i += (d)) 
#define reep(i, x, y) for (int i = (x); i <= (y); i <<= 1) 
#define pe(i, x, y) for (int i = (x) - 1; i >= (y); --i) 
#define per(i, x, y) for (int i = (x); i >= (y); --i) 
#define rep_e(i, u) for (int i = head[(u)]; i; i = e[i].nxt) 
#define vi vector<int> 
#define vL vector<LL>
#define vii vector<pii> 
#define viL vector<piL>
#define vLi vector<pLi> 
#define vLL vector<pLL>
#define eb emplace_back
#define pb pop_back
#define mp make_pair
#define pii pair<int, int>
#define piL pair<int, LL>
#define pLi pair<LL, int>
#define pLL pair<LL, LL>
#define lowbit(x) ((x) & (-(x)))
#define fi first
#define se second
#define all(x) x.begin(), x.end()
#define debug(x) cout << #x << " = " << x << endl
using namespace std;
typedef unsigned int ui;
typedef long long LL;
typedef unsigned long long ULL;
typedef double db;
#define getchar() (SB == TB && (TB = (SB = BB) + fread(BB, 1, 1 << 16, stdin), SB == TB) ? EOF : *SB++)
char BB[1 << 16], *SB = BB, *TB = BB;
template<typename T> void read(T &n) {
    T w = 1;
    n = 0;
    char ch = getchar();
    for ( ; !isdigit(ch); ch = getchar()) {
        if (ch == '-') {
            w = -1;
        }
    }
    for ( ; isdigit(ch); ch = getchar()) {
        n = n * 10 + (ch & 15);
    }
    n *= w;
}
template<typename T> void chkmn(T &a, const T &b) { 
    a = a > b ? b : a; 
}
template<typename T> void chkmx(T &a, const T &b) { 
    a = a < b ? b : a;  
}

const int MOD = 1e9 + 7;
int adt(const LL &a) { 
    return (a % MOD + MOD) % MOD; 
} 
int inc(const int &a, const int &b) { 
    return a + b >= MOD ? a + b - MOD : a + b; 
}
int dec(const int &a, const int &b) { 
    return a - b < 0 ? a - b + MOD : a - b; 
}
int mul(const int &a, const int &b) { 
    return 1LL * a * b % MOD; 
}
int sqr(const int &a) { 
    return 1LL * a * a % MOD; 
}
void Adt(LL &a) {
    a = (a % MOD + MOD) % MOD;
}
void Inc(int &a, const int &b) { 
    a = a + b >= MOD ? a + b - MOD : a + b; 
}
void Dec(int &a, const int &b) { 
    a = a - b < 0 ? a - b + MOD : a - b; 
}
void Mul(int &a, const int &b) { 
    a = 1LL * a * b % MOD; 
}
void Sqr(int &a) { 
    a = 1LL * a * a % MOD;
}
int fsp(int a, int x = MOD - 2) {
    int res = 1;
    for ( ; x; x >>= 1, Sqr(a)) {
        if (x & 1) {
            Mul(res, a);
        }
    }
    return res;
}

int n, L;
int a[105], f[2][105][1005][3];

int main() {
#ifdef sword 
    freopen("test.in", "r", stdin);
#endif

    read(n), read(L);
    rep(i, 1, n) {
        read(a[i]);
    }
    if (n == 1) {
        puts("1");
        return 0;
    }
    sort(a + 1, a + n + 1);
    int now = 1, las = 0;
    f[1][0][0][0] = 1;
    rep(i, 1, n) {
        swap(now, las);
        memset(f[now], 0, sizeof(f[now]));
        rep(j, 0, n) {
            rep(k, 0, L) {
                rep(t, 0, 2 * (j != 0)) {
                    int nk = k + (a[i] - a[i - 1]) * (j * 2 - t);
                    if (nk <= L && f[las][j][k][t]) {
                        const int x = f[las][j][k][t];
                        if (t < 2) {
                            if (j) { 
                                Inc(f[now][j][nk][t + 1], mul(2 - t, x));
                            }
                            Inc(f[now][j + 1][nk][t + 1], mul(2 - t, x));
                        }
                        Inc(f[now][j][nk][t], mul(j * 2 - t, x));
                        Inc(f[now][j + 1][nk][t], mul(j + 1 - t, x));
                        if (j) {
                            Inc(f[now][j - 1][nk][t], mul(j - 1, x));
                        }
                    }
                }
            }
        }
    }
    int ans = 0;
    rep(k, 0, L) {
        Inc(ans, f[now][1][k][2]);
    }
    printf("%d\n", ans);
    return 0;
}