题解:P3757 [CQOI2017] 老C的键盘
思路
一篇好的题解,不只是讲这道题怎么做,更应该讲清楚这道题怎么想到这么做。
第一眼的误区:拓扑排序
首先,题目给出的所有边的关系,构成了一棵完全二叉树。每条边给了我们大于或者小于的关系,所以我们可以把这棵树上每条边看成是一条有向边,那么我们可以对这棵树进行拓扑排序。然后按照拓扑排序的顺序,依次填入
我相信很多同学,包括我,看到这道题以后,第一思路都是这样的。因此,开始考虑怎么求拓扑排序方案数,即把这个问题完全看成是一个图论问题了。思考半天以后发现没有特别好的方法可以解决。因此需要换一个思路来考虑。
充分利用信息:树上 DP
考虑到题目给我们的是树,树其实是比一张普通的图有更多性质的,如果我们放弃了树的性质,用图来做题,其实就是浪费了题目中的关键信息。因为题目中要求方案数,因此是树上计数类 DP 问题,状态定义套路一般是“以树上某个点为根的子树,合法的方案数有多少”,然后进行转移。
状态的设计
前面提到的状态,只有一维,显然是不够的。我们要考虑状态的第二个维度是什么。注意本题解是按照思考的思路进行展开,所以先不直接跳转到正确答案,而是先讲述一下同学们可能的第一想法。
按照一般情况下做树上 DP 的经验,我们容易想到这样的状态设计:
用一个二维的状态
然后我们考虑状态转移,还是按照经验,一般的树上 DP,都是先初始化子树的树根的状态,然后把孩子的状态求好,在把孩子一个一个加进来,和之前的已经加进来的树根和子树进行转移。如果你对这个套路不熟悉,可以先做 P1272 重建道路。如下图所示:
以
仔细思考一下发现上述状态定义并不好转移,因为原来的状态里面,比如
关键的思考:相对排名
关键点在于,我们不应该在状态里面指定树根上的点放哪个具体数字,而是应该去指定树根上的点,上面放的数字在整棵树里面的排名是多少。
比如说上图当中,左边的那棵树里面,树根上面放的数字
因此,我们修改一下状态的定义,新的定义为:
用一个二维的状态
状态转移
还是以这张图为例:
首先我们定义一个数组
然后我们考虑把
边是大于号的情况
在这里我们首先讨论
那么,我们可以简单地得到一个状态转移方程吗?类似:
不止如此,我们还忽略了一个细节。在原来的树当中,我们有
合并以后,比树根小的,总共是
边是小于号的情况
同理,当子树上的点比父亲小的时候,我们发现状态转移规则是不变的,只不过
代码实现细节
预处理组合数
因为在状态转移的过程中使用了组合数,所以不如预处理一个杨辉三角,把组合数都算出来。
for (ll i = 0; i <= n; ++i) {
c[i][0] = c[i][i] = 1;
for (ll j = 1; j < i; ++j) {
c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % MOD;
}
}
因为每次转移都是在父子之间进行,所以我写了一个函数它的两个参数分别是
完整的代码如下。时间复杂度
#include <iostream>
using namespace std;
typedef long long ll;
const ll MAXN = 105;
const ll MOD = 1e9 + 7;
//dp[i][j]表示以i点为树根的子树中,i这个点上填写的数字在子树中排名第j大,方案数
ll n, dp[MAXN][MAXN], sz[MAXN], f[MAXN];
ll c[MAXN][MAXN];
char s[MAXN];
void cal(ll u, ll v) {
// 父结点是u,孩子是v
if (v > n) {
return;
}
for (ll i = 1; i <= sz[u]; ++i) {
f[i] = dp[u][i];
dp[u][i] = 0;
}
for (ll i = 1; i <= sz[u]; ++i) {
// 之前的所有孩子里面,u排名第i,v排名第j
for (ll j = 1; j <= sz[v]; ++j) {
// 再枚举一个k表示v所在的子树里面有多少个点排在u前面
if (s[v] == '>') {
// v上的数字大于u,则k最大只能取j-1
for (ll k = 0; k < j; ++k) {
ll t = f[i] * dp[v][j] % MOD;
// 总共有i+k-1个点排在u前面,取k个放在子树里面
t = t * c[i + k - 1][k] % MOD;
// 总共有 sz[u]+sz[v]-i-k个点排在u后面,取sz[v]-k个放在子树里面
t = t * c[sz[u] + sz[v] - i - k][sz[v] - k] % MOD;
dp[u][i + k] = (dp[u][i + k] + t) % MOD;
}
} else {
// v上的数字小于u,则k从j开始
for (ll k = j; k <= sz[v]; ++k) {
ll t = f[i] * dp[v][j] % MOD;
// 总共有i+k-1个点排在u前面,取k个放在子树里面
t = t * c[i + k - 1][k] % MOD;
// 总共有 sz[u]+sz[v]-i-k个点排在u后面,取sz[v]-k个放在子树里面
t = t * c[sz[u] + sz[v] - i - k][sz[v] - k] % MOD;
dp[u][i + k] = (dp[u][i + k] + t) % MOD;
}
}
}
}
sz[u] += sz[v];
}
int main() {
cin >> n;
cin >> (s + 2);
// 预处理杨辉三角数组
for (ll i = 0; i <= n; ++i) {
c[i][0] = c[i][i] = 1;
for (ll j = 1; j < i; ++j) {
c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % MOD;
}
}
// 是完全二叉树,可以倒着依次处理每个点
for (ll u = n; u >= 1; --u) {
// 首先不考虑孩子,自己一个点排名第一,方案有一种
dp[u][1] = 1;
sz[u] = 1;
cal(u, u * 2);
cal(u, u * 2 + 1);
}
ll ans = 0;
for (int i = 1; i <= n; ++i) {
ans = (ans + dp[1][i]) % MOD;
}
cout << ans << endl;
return 0;
}