Maximum Element(dp)
模拟赛出来这道,自己写出来了,虽然花了挺久的,开心!开心!
首先看到 \(n\leq10^6\) 不是线性 dp 就是数学。
数往后 \(k\) 个数小于自己实在是太蠢了,不好计数,但是反过来,输出正确答案的情况十分优美。
输出正确的情况大致可以抽象为这样一个形式,对于一个原序列的上升子序列 \(S\), \(S\) 的相邻两项之间的数小于它们,子序列最后一项后面的数也小于它,并且子序列的相邻两项在原列的位置之差不超过 \(k\)。
现在随便胡一个二维状态应该可以做到 \(O(n^3)\) 或者 \(O(n^2)\),可是 \(n\leq 10^6\),考虑设一维状态。
由于题目要求是一个序列,所以我们不关心一个数实际的大小,我们只关心相对大小,我们可以设一个状态 \(f_i\) 表示对于一个 \(n = i\) 的数列, 我们钦定最后一个数是 \(n\),满足上述条件的方案数。转移可以枚举一个转移点,然后把新增的数插板插进原数列中,即:
\[f_i = \sum_{j = i - k}^{i - 1} f_j \binom{i - 2}{j - 1}(i - j - 1)!
\]
简单拆一下:
\[f_i = \sum_{j = i - k}^{i - 1} f_j \dfrac{(i - 2)!}{(j - 1)!(i - j - 1) !}(i - j - 1)!
\]
\[f_i = (i - 2)!\sum_{j = i - k}^{i - 1} \dfrac{f_j}{(j - 1)!}
\]
前缀和优化转移即可。
答案只需要枚举 \(n\) 在哪个位置,然后乘上 \(n\) 后面的数带来的贡献,即:
\[ans = n! - \sum_{i = 1}^{n} f_i \binom{n - 1}{i - 1}(n - i)
\]
我真聪明。
code:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 1e6 + 10;
const int mod = 1e9 + 7;
ll Pow(ll a, ll b, ll mod) {
ll res = 1;
while(b) {
if(b & 1) res = res * a % mod;
a = a * a % mod, b >>= 1;
}
return res;
}
int f[maxn];
int pre[maxn];
int fac[maxn];
int ifac[maxn];
int C(int n, int m) {
return 1ll * fac[n] * ifac[m] % mod * ifac[n - m] % mod;
}
int n, k;
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> k;
ifac[0] = fac[0] = 1;
for(int i = 1; i <= n; i++) fac[i] = 1ll * fac[i - 1] * i % mod;
ifac[n] = Pow(fac[n], mod - 2, mod);
for(int i = n - 1; i; i--) ifac[i] = 1ll * ifac[i + 1] * (i + 1) % mod;
f[1] = 1;
pre[1] = 1;
for(int i = 2; i <= n; i++) {
int l = max(1, i - k);
f[i] = 1ll * fac[i - 2] * ((1ll * pre[i - 1] - pre[l - 1] + mod) % mod) % mod;
pre[i] = (pre[i - 1] + 1ll * f[i] * ifac[i - 1] % mod) % mod;
}
int ans = 0;
for(int i = n; i >= 1; i--) {
ans = ans + 1ll * f[i] * fac[n - i] % mod * C(n - 1, i - 1) % mod;
ans %= mod;
}
cout << (fac[n] - ans + mod) % mod;
}