连续段 dp
有些时候,为什么小蒟蒻就是想不到 dp 啊,小小蒟蒻自闭了
是个 dp 模型,用个题目讲一下吧qwq。
[CEOI2016] kangaroo
题目大意:一个 \(1 \sim n\) 的排列,要求开头为 \(s\),结尾为 \(t\),中间不出现长度大于 \(2\) 的递增或者递减子段。
想不到啊,积累一下,这种题就是连续段 dp 的应用之一,连续段 dp 大概在数排列的时候很方便。
具体思路是,依次插入每个元素(通常排序后从小到大或从大到小),考虑当前元素插入到哪个位置,状态记录就是当前插入到了哪个数,以及当前的连续段的数量。
这里的连续段的定义,是独立的一段,也就是把序列分成了若干段,其中每一个段都满足题目中提到的限制条件,但是注意相邻的两段合并到一起则不一定满足。
具体的状态也就是 \(dp_{i,j}\) 表示当前插入到了第 \(i\) 个数,当前有 \(j\) 个连续段的方案数。
连续段 dp 的转移有三种情况:\(1.\) 在一个已有的连续段的头或者尾插入一个数。,\(2.\) 用这个数创建一个新的连续段,\(3.\) 用这个数合并两个连续段。
对于这道题,我们先将数从小到大排序,然后我们分别考虑这三种情况的转移:
\(1.\) 由于数按从小到大排序了,那么对于新增加的数,他前面的数,都会是比他小的,而后面无论是新加入一个数,还是用数合并两个连续段,加进来的一定都是更大的一个数,那么这三个数就会构成一个递增子段,于题意矛盾,于是在这道题中,不能在已有的连续段的头或者尾加入一个数。
\(2.\) 用这个数创建一个新的连续段,容易发现不会矛盾,因为只有他一个元素,若现在状态为 \(dp_{i,j}\),那么原来就有 \(j - 1\) 个连续段,\(j - 1\) 个连续段中间一共有 \(j - 2 + 2 = j\) 个空位,于是 \(dp_{i,j} = dp_{i-1,j-1} \times j\),另外的,在本题如果当 \(i > s\) 或者 \(i > t\) 时,那么最开始的位置和最末尾的位置是不能插入的,因为本题要求排列以 \(s\) 开头,\(t\) 结尾 qwq
\(3.\) 合并两个连续段,因为没有插入连续段头尾元素的操作,连续块只能通过合并增加元素,而合并加入的元素都只会在新的连续段的中间,于是合并两个连续段的时候,交界处都是一个递减的子段,于是合并后合法,那么若现在状态为 \(dp_{i,j}\),那么原来就为 \(j + 1\) 个连续段,因为合并会减少一个连续段,然后合并一定要合并相邻两个块,所以一共有 \(j + 1 - 1 = j\) 中方案,于是 \(dp_{i,j} = dp_{i-1,j+1} \times j\)。
对于 \(i = s, t\) 的时候需要特判,只能加在最前面或者最后面,也可以独特的进行一次插入操作。
答案即为 \(dp_{n,1}\)。
为什么这样能够不重不漏的计算出所有的方案数呢?本质是一个W形置换,其具有的笛卡尔树形状足以说明这一点,详细可以见 Daltao dalao's blog /崇拜
具体实现看下面的代码!
// 德丽莎你好可爱德丽莎你好可爱德丽莎你好可爱德丽莎你好可爱德丽莎你好可爱
// 德丽莎的可爱在于德丽莎很可爱,德丽莎为什么很可爱呢,这是因为德丽莎很可爱!
// 没有力量的理想是戏言,没有理想的力量是空虚
#include <bits/stdc++.h>
#define LL long long
#define int long long
using namespace std;
char ibuf[1 << 15], *p1, *p2;
#define getchar() (p1 == p2 && (p2 = (p1 = ibuf) + fread(ibuf, 1, 1 << 15, stdin), p1==p2) ? EOF : *p1++)
inline int read() {
char ch = getchar(); int x = 0, f = 1;
while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
while (ch >= '0' && ch <= '9') x = (x << 1) + (x << 3) + (ch ^ 48), ch = getchar();
return x * f;
}
void print(LL x) {
if (x > 9) print(x / 10);
putchar(x % 10 + '0');
}
template<class T> bool chkmin(T &a, T b) { return a > b ? (a = b, true) : false; }
template<class T> bool chkmax(T &a, T b) { return a < b ? (a = b, true) : false; }
#define rep(i, l, r) for (int i = (l); i <= (r); i++)
#define repd(i, l, r) for (int i = (l); i >= (r); i--)
#define REP(i, l, r) for (int i = (l); i < (r); i++)
const int N = 3005;
const int mod = 1e9 + 7;
int n, s, t, dp[N][N];
void solve() {
n = read(), s = read(), t = read();
dp[0][0] = 1;
rep (i, 1, n) {
rep (j, 1, i) {
if (i == s || i == t) {
dp[i][j] = (dp[i - 1][j] + dp[i - 1][j - 1]) % mod;
} else {
int num = 0;
if (i > s) num ++;
if (i > t) num ++;
dp[i][j] = dp[i - 1][j - 1] * (j - num) % mod;
dp[i][j] += dp[i - 1][j + 1] * j % mod;
dp[i][j] %= mod;
}
}
}
int ans = dp[n][1];
cout << ans << "\n";
}
signed main () {
#ifdef LOCAL_DEFINE
freopen("1.in", "r", stdin);
freopen("1.ans", "w", stdout);
#endif
int T = 1; while (T--) solve();
#ifdef LOCAL_DEFINE
cerr << "Time elapsed: " << 1.0 * clock() / CLOCKS_PER_SEC << " s.\n";
#endif
return 0;
}
P7967 [COCI2021-2022#2] Magneti
题目大意:有 \(l\) 个空位,要求放入 \(n\) 个磁铁,第 \(i\) 个磁铁可以吸引距离小于 \(r_i\) 的其他磁铁,求放置 \(n\) 个磁铁,且所有磁铁互不吸引的方案数。