「CEOI2016」袋鼠
statement
一行共有 (N) 个格子,从左到右从 (1) 到 (N) 编号。一只袋鼠从 (c_s) 出发,经过恰好 (N - 1) 次跳跃,到达 (c_f) ,并且经过所有的 (N) 个格子,每次跳跃的方向都与上一次不同。如果袋鼠当前所在的格子为 ( m current) ,来自 ( m prev) ,则其下一个格子 ( m next) 满足:
- 如果 ( m prev < current),则 ( m next < current) ;
- 如果 ( m current < prev),则 ( m current < next) 。
求不同的跳跃方案数除以 (10 ^ 9 + 7) 的余数。
Hints
(2le Nle 2000),(c_s eq c_f) .
solution
线头 DP ...
其实就是一个求排列个数的问题,这里的排列指的就是跳跃的下标序列。
然后开始线头,设 (f(i, j)) 表示填了前 (i) 个数,产生 (j) 个没有接的向上的线头的方案数,转移:
其实这个转移方程就是接几个散乱的线头的过程。
对于 (c_s) 和 (c_f) ,默认它们的线头位于最左边和最右边,然后答案就是 (dp(n - 1, 0)) ,因为这个时候最后一个数字的摆放位置是唯一的。
考虑这个东西为什么可以不重不漏,因为在一个排列中,每个线头可以看成一个笛卡尔树,合并相当于两颗二叉树用根节点接牢,增加一个线头可以看成新建一个节点的笛卡尔树,而笛卡尔树的形态和一个排列可以构成双射,所以保证了每次的不同。
时间复杂度 (mathcal O (n^2)) 。
int n, s, t, dp[N][N];
int main() {
scanf("%d%d%d", &n, &s, &t);
dp[0][0] = 1;
forn(i,1,n) forn(j,0,i) {
if(i != s && i != t) {
dp[i][j] = 1ll * dp[i - 1][j + 1] * (j + (i > s) + (i > t)) %Mod * (j + 1) %Mod;
if(j) dp[i][j] = (dp[i][j] + dp[i - 1][j - 1]) %Mod;
} else {
dp[i][j] = dp[i - 1][j];
dp[i][j] = (dp[i][j] + 1ll * dp[i - 1][j + 1] * (j + 1) %Mod) %Mod;
}
}
printf("%d
", dp[n - 1][0]);
return 0;
}
Thinks
使用这种解决排列问题的 DP 的充要条件。
这个模型的特点:
- 不重不漏
- 无后效性
而且这两个特点都是基于每个小集合的笛卡尔树,所以能使用这个东西的时候,序列中的数不一定是排列,但是要保证不同序列都只能对应一颗笛卡尔树,所以序列中的数字必须两两不同,并且枚举的时候从小到大或从大到小。
HDU6764 Blood Pressure Game
statement
给一个长度为 (n) 的数组 (a),定义一个数组的权值为
求在 (a) 的每一种排列中,权值严格前 (k) 大方案的权值,以及每种方案的方案数,方案数对 (10 ^ 9 + 7) 取模。
Hints
(nle 600) ,(kle 20) , 数组 (a) 中整数大小互不相同 .
solution
套路地把贡献拆成排序后的数组 (a) ,数 (|a_{i + 1} - a_i|) 的个数。
由于元素互不相同,采用上述模型,设 (dp(i, j, k)) 表示从小到大加入了前面 (i) 个数,构成的集合个数为 (j) ,是否确定首尾的状态。
其中 (k = 0) 表示没有确定首或尾,(k = 1) 表示确定了首尾中的一个,(k = 2) 表示首尾全部确定了。
由于要记录前 (k) 大,所以每个状态除了计数,还要记录转移后序列的每一种权值的集合。
初始状态 (dp(1, 1, 0)) 和 (dp(1, 1, 1)) 。
首先是权值的转移,发现从 (dp(i,,) ightarrow dp(i + 1,,)) 的贡献是相同的,都是 ((a_i - a_{i - 1}) imes (2j - k)) 。
因为有 (j - k) 个集合需要连左边和右边,(k) 个集合只需要接一边,加之每次加入的都是比当前元素大的元素,这样的过程自动完成了一个差分的前缀和。
对于每种集合的构造,一定不能破坏上述的权值计算的过程,所以,确定为首尾的集合只能在一边进行构造,不能在另一边构造,这会影响到对于方案数转移的过程。
分类讨论方案数的转移:
- 新建一个集合,不作为首尾,(dp(i + 1, j + 1, k)leftarrow dp(i, j, k)) ,方案数为 (j - k + 1) 。
- 新建一个集合,作为首尾,(dp(i + 1, j + 1, k + 1)leftarrow dp(i, j, k)) ,方案数为 (2 - k) .
- 合并两个集合,(dp(i + 1, j - 1, k)leftarrow dp(i, j, k)) ,因为求最大权值的特殊性,集合在序列中的排列顺序已经确定,方案数为 (j - 1) .
- 新建一个集合,并且在这一轮就与另一个集合合并,不作为最后的 (a_1) 或 (a_n) ,(dp(i + 1, j, k)leftarrow dp(i, j, k)) ,方案数为 (2j - k) .
- 新建一个集合,并且在这一轮就作为 (a_1) 或 (a_n) ,与其他集合合并,(dp(i + 1, j, k + 1) leftarrow dp(i, j, k)) ,方案数为 (2 - k) .
实现的时候,对于每种状态都记录前 (k) 最大权值的方案,每种方案记为二元组 ((val, num)) ,分别为这种排列的权值以及对应的方案数。
对于每个状态记录的二元组对于关键字 (val) 先排序,记录前 (k) 个后,再进行转移。
时间复杂度 (mathcal O (n ^ 2 klog k)) 。
int n, m; i64 a[N];
struct state {
node U[M * 5]; int num;
state() {num = 0;}
inline void reset() {num = 0;}
inline void ins(i64 val, Mint t) {U[++num] = node(val, t);}
inline void init() {
sort(U + 1, U + num + 1);
int sem = 0;
forn(l,1,num) {
int r = l; Mint sum = U[l].num;
while(r < num && U[r + 1].val == U[l].val) sum += U[++r].num;
U[++sem] = node(U[l].val, sum);
l = r;
}
num = min(m, sem);
}
} dp[3][2][N];
inline void solve() {
scanf("%d%d", &n, &m);
forn(i,1,n) scanf("%lld", a + i);
sort(a + 1, a + n + 1);
dp[0][1][1].ins(0, Mint(1)), dp[1][1][1].ins(0, Mint(2));
int now = 1;
rep(i,1,n) {
forn(j,1,n) rep(k,0,3) if(dp[k][now][j].num) {
dp[k][now][j].init();
forn(t,1,dp[k][now][j].num) {
i64 val = dp[k][now][j].U[t].val + (a[i + 1] - a[i]) * (2 * j - k);
Mint num = dp[k][now][j].U[t].num;
if(j - k + 1 > 0) dp[k][now ^ 1][j + 1].ins(val, num * Mint(j - k + 1));
if(2 * j - k > 0) dp[k][now ^ 1][j].ins(val, num * Mint(2 * j - k));
if(j > 1) dp[k][now ^ 1][j - 1].ins(val, num * Mint(j - 1));
if(k < 2) {
dp[k + 1][now ^ 1][j].ins(val, num * Mint(2 - k));
dp[k + 1][now ^ 1][j + 1].ins(val, num * Mint(2 - k));
}
}
dp[k][now][j].reset();
}
now ^= 1;
}
dp[2][now][1].init();
forn(i,1,min(m, dp[2][now][1].num)) printf("%lld %d
", dp[2][now][1].U[i].val, dp[2][now][1].U[i].num.res);
forn(i,1,m - dp[2][now][1].num) puts("-1");
forn(j,1,n) rep(k,0,3) if(dp[k][now][j].num) dp[k][now][j].reset();
}