[贪心] [dp] CF1152D Neko and Aki's Prank
posted on 2024-04-03 05:37:35 | under | source
吃饭时想出来的,挺有意思。
先让 \(n=n*2\)。
先思考对于一颗树,怎么求出其最大独立边集。从上往下不太现实,但是从下往上就能找到性质了。只要每次选取叶子上的边(相同祖先选哪个都行),就能保证最优。
为什么?对叶子上的边 \(e(u,v)\) 讨论,\(v\) 是叶子,\(u\) 是另一个点,\(fa\) 是 \(u\) 的父亲。我们发现不选 \(e(u,v)\),选取 \(e(fa,u)\) 的话,它们的贡献相等都是 \(1\),但是选 \(e(fa,u)\) 会导致 \(v\)、\(fa\) 都被占用,不如 \((u,v)\)。
换句话说,每次选取叶子上的边,可以使得对未来的影响最小,同时对答案的贡献还不劣。
回到 \(\rm trie\) 上,我们当然不可能建出树然后跑一遍。如何优化这个过程?由于相同 \(u\) 的边,只会选取其一,这启示我们用 \(u\) 计数。又因为 \(\rm trie\) 上任意时刻叶子节点的深度都相同,所以答案等价于 \(n,n-2,n-4...\) 层节点数目和。
怎么计数呢?回到括号序列,尝试记 \(f_i\) 表示长度 \(i\) 的括号串数量。但是这些串不一定要合法,因为一些左括号可以之后再被匹配。因此加一维记 \(f_{i,j}\) 表示长度 \(i\)、待匹配左括号数量为 \(j\) 的串数量(除待匹配括号外,这个串合法)。转移很简单。
最后计算答案,考虑 \(f_{i,j}\)。这其实是可行性问题,只要存在任意合法串以 \(f_{i,j}\) 为前缀,就可以算入答案。如果这 \(j\) 个待匹配括号能在此后被匹配,并且剩下位置可以被填充为合法串,那么就将 \(f_{i,j}\to ans\)。这句话对应下来就是:\(n-i-j\ge 0\) 且 \(2\mid n-i-j\)。
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e3 + 5, mod = 1e9 + 7;
int n, f[N][N], ans;
signed main(){
cin >> n, n <<= 1;
f[1][1] = 1;
for(int i = 2; i <= n; ++i)
for(int j = 0; j <= i; ++j){
if(j) f[i][j] = (f[i][j] + f[i - 1][j - 1]) % mod; //左括号
f[i][j] = (f[i][j] + f[i - 1][j + 1]) % mod; //右括号
}
for(int i = 1; i <= n; i += 2)
for(int j = 0; j <= i; ++j)
if(n - i - j >= 0 && (n - i - j) % 2 == 0) ans = (ans + f[i][j]) % mod;
cout << ans;
return 0;
}

浙公网安备 33010602011771号