|
马上注册,结交更多好友,享用更多功能^_^
您需要 登录 才可以下载或查看,没有账号?立即注册
x
本帖最后由 柿子饼同学 于 2024-8-23 23:21 编辑
总结与反思
E (数位DP 状压DP)
对于 DP 的转移顺序, 要看阶段, 也要看什么状态不能被重复更新, 确定正序/倒序
F (树形DP)
树形 DP 中常用的思想就是对于每个子树, 计算它的贡献并且合并进来, 常常设两个数组一个是贡献, 一个是方案数
G (树形DP)
和树上路径相关的一般可以考虑枚举 lca , 拆成好维护的路径再拼接起来
H (二进制枚举 树形DP)
数量比较少的时候考虑二进制枚举, 对于 DP 数组的意义一定要严谨搞清楚, 本题中一些点的取和不取会强制让一些状态不合法, 直接砍掉, 答案相等时方案数记得加起来 总的来说遇到树上让你求 所有 什么什么的贡献就想着求一个子树的答案, 再加一些辅助数组互相计算, 要考虑大一点的贡献, 方便计算
E - Wonderhoy子集
魔术师有 n 张卡牌,编号从 1 到 n (n < 1e9)
小梦每次从中抽出若干张,无论她怎么抽,她抽到的卡牌中 0 到 9 这个十个数字最多出现一次
小梦觉得这魔术非常的 Wonderhoy! 她想知道有多少种卡牌组合是 Wonderhoy 的
只需要用数位dp把小于 n 中的满足条件的数字按照含有的数位 (二进制) 分类求和
dp 的时候刷表, 但是必须注意对于一个状态, 每种数只能用一次, 必须倒序循环, 不然一个数可能把状态更新两次
- #include <bits/stdc++.h>
- using namespace std;
- const int N = 10;
- const int P = 1e9 + 7;
- int n, num[N], ans;
- int cnt[(1 << N)];
- int f[(1 << N)];
- void dfs(int pos, int st, bool lim, bool lead0) {
- if (!pos) {
- if (!lead0)
- cnt[st]++;
- return;
- }
- int up = 9;
- if (lim)
- up = num[pos];
- for (int i = 0; i <= up; i++) {
- if ((st >> i) & 1)
- continue;
- if (!i && lead0)
- dfs(pos - 1, st, lim && (i == up), lead0);
- else
- dfs(pos - 1, st | (1 << i), lim && (i == up), lead0 && !i);
- }
- }
- void work(int x) {
- int len = 0;
- while (x) {
- num[++len] = x % 10;
- x /= 10;
- }
- dfs(len, 0, 1, 1);
- }
- int main() {
- ios::sync_with_stdio(0);
- cin.tie(0);
- cin >> n;
- work(n);
- f[0] = 1;
- // i枚举数字, j枚举状态, 阶段是选取这个数字, j倒序循环不会重复计算
- for (int i = 1; i < (1 << 10); i++) {
- for (int j = (1 << 10) - 1; j >= 0; j--) {
- if (i & j)
- continue;
- f[i | j] = (f[i | j] + f[j] * cnt[i]) % P;
- }
- }
- for (int i = 1; i < (1 << 10); i++) {
- ans = (ans + f[i]) % P;
- }
- cout << ans;
- return 0;
- }
复制代码
F - 子树
给出 n ( < 1e5) 个节点的树, 求所有能选出子树的方案的节点数量和
80分 O(n^2) 代码 f[ i][j] 表示 i 中取 j 个点的方案数, 注意要倒序循环, 否则会重复
100分 O(n) 代码 设 f(i) 代表以 i 为根子树的答案, cnt(i) 代表所有以 i 为根的子树取法方案数, 还在遍历中的 f u 相当于把之前算过的子树并在自己身上了
两个数组相互计算, 要有这种思想
- for (int i = siz[u]; i >= 1; i--) {
- for (int j = 1; j < i; j++) {
- f[u][i] += f[u][j] * f[ed][i - j] % P;
- f[u][i] %= P;
- }
- }
复制代码- #include <bits/stdc++.h>
- using namespace std;
- typedef long long ll;
- const int N = 1e5 + 5;
- const int P = 1e9 + 7;
- vector<int> e[N];
- int n;
- // f[i] 以 i 为根的子树的答案
- // cnt[i] 以 i 为根的子树中的取法数
- ll cnt[N], f[N], ans;
- void dfs(int u, int pa) {
- cnt[u] = f[u] = 1;
- for (auto ed : e[u]) {
- if (ed == pa)
- continue;
- dfs(ed, u);
- // 取或不取, 共 cnt[ed] + 1 种选择, 也就是子树选择多了这么多
- // u 自己的选法一定要选, 所以增加了子树的贡献
- f[u] = (((cnt[ed] + 1) * f[u] % P + cnt[u] * f[ed] % P) % P) % P;
- cnt[u] = cnt[u] * (cnt[ed] + 1) % P;
- }
- ans = (ans + f[u]) % P;
- }
- int main() {
- ios::sync_with_stdio(0);
- cin.tie(0);
- cin >> n;
- for (int i = 1; i < n; i++) {
- int a, b;
- cin >> a >> b;
- e[a].emplace_back(b);
- e[b].emplace_back(a);
- }
- dfs(0, 0);
- cout << ans;
- return 0;
- }
复制代码
G - Bear and Tree Jumps (CF771C)
有一棵 n ( < 2e5) 个节点的树,树上住着一只熊。
若熊在节点 i,熊跳一次可以从 i 跳到离 i 树上距离 <= k ( < 5) 的任意节点 。
求所有路径(i, j) , i < j 跳跃数的总和
k 很小, 想到给路径分类
求所有的路径不好想, 先想着枚举 lca , 然后把两个路径拼起来即可
设 f(u) 表示 u 的子树中所有节点到 u 跳跃次数总和, 考虑转移, 每次新加了一条边, 增加的跳跃数只有 %k 等于 k-1 的点
数组含义要大胆设计, 不要局限于一个点一条边的贡献
好好利用值小的好性质进行转移
- #include <bits/stdc++.h>
- using namespace std;
- const int N = 2e5 + 5;
- typedef long long ll;
- // sum[i][j] 节点 i 的子树中, 到 i 距离 %k 为 j 的点的数量, 给路径分类
- // f[i] 节点 i 的子树中的点到 i 的跳跃次数, 每次计算子树会增加一条边, 只有子树中 sum[i][k - 1] 的点需要更新
- // siz[i] 子树大小
- ll sum[N][5], f[N];
- int siz[N];
- int n, k;
- ll ans;
- vector<int> e[N];
- void dfs(int u, int pa) {
- sum[u][0] = siz[u] = 1;
- for (auto ed : e[u]) {
- if (ed == pa)
- continue;
- dfs(ed, u);
- // 当前处理过的子树和 u 构成的树 和 现在的 ed 这个子树进行计算
- // 先计算 ed 和 u 之间的路径总贡献, 再接上去
- // 先把粗略的都算一遍(没有连接, 因为有余数)
- // u 到 ed 的每个节点的跳跃次数和(从 u 的子树跳到 u 的总数 + 从 u 跳到 ed 子树的总数)
- ans += f[u] * siz[ed] + f[ed] * siz[u];
- // 检查连接处需要跳 1 还是 2 次
- for (int i = 0; i < k; i++) {
- for (int j = 0; j < k; j++) {
- ans += sum[u][i] * sum[ed][j] * ((i + j) / k + 1);
- }
- }
- // 每次把 ed 接上去会增加 1 个边, 所有点的跳跃距离只有余数是 k-1 的点会增加 1
- // 其他的原样加上即可
- f[u] += f[ed] + sum[ed][k - 1];
- for (int i = 0; i < k; i++) sum[u][(i + 1) % k] += sum[ed][i];
- siz[u] += siz[ed];
- }
- }
- int main() {
- ios::sync_with_stdio(0);
- cin.tie(0);
- cin >> n >> k;
- for (int i = 1; i < n; i++) {
- int a, b;
- cin >> a >> b;
- e[a].emplace_back(b);
- e[b].emplace_back(a);
- }
- dfs(1, 0);
- cout << ans;
- return 0;
- }
复制代码
H - 吸血鬼 【DP】SRM674D2L3 VampireTreeDiv2
给定一个吸血鬼族谱,包含了A,B两个数组,每个吸血鬼由以下方式生成:
0、0号吸血鬼是吸血鬼始祖。
1、B[ i]=-1表示i号吸血鬼被A[ i]号吸血鬼感染。构成一条关系(i,A[ i])。
2、B[ i]!=-1表示i号吸血鬼由A[ i]号吸血鬼和B[ i]号吸血鬼生出来。构成两条关系(i,A[ i])和(i,B[ i])(这类生成不会超过15个)
现在要求选取最少数目的吸血鬼,使得家谱中给出的所有关系中,至少有一只吸血鬼被选到(即每条边中至少有一点被选中,且选取点数最少)。
问满足如上条件的选取方案数
两个边的点不超过 15 考虑枚举每个点的取/不取情况然后 dp
一定注意在 dp 过程中一个点强制不取, 那另一个路径上的点就必须强制取, 有的状态就不合法需要设极大值了
我做题的时候一直在思考取不取对答案的贡献, 其实不需要, 因为 求一次 dp 的时间复杂度完全够用
不要总考虑局部
- #include <bits/stdc++.h>
- using namespace std;
- typedef long long ll;
- const int N = 1005;
- const ll P = 1e9 + 7;
- // ef 专门存第二条边, 如果这些边的子节点不取, 那父节点必须取
- vector<int> e[N], ef[N];
- int n;
- // 存带有两个边的节点对应的编号, 不是两个边的就是 -1
- int id[N], tot;
- // f[ i][0/1] i这个点取不取的最优解, cnt[ i][0/1] 方案数
- // 注意 f[ed][0] == f[ed][1] 的时候 cnt[u][1] *= (cnt[ed][0] + cnt[ed][1])
- // 初始状态 f[u][0] = 0, 其余 3 个等于 1
- int st;
- ll f[N][2], cnt[N][2];
- // a, b 是 f 值, ac, bc 是 cnt 值
- void Add(ll& s, ll a, ll b, ll ac, ll bc) {
- if (a == b)
- s = (s + ac + bc) % P;
- if (a < b)
- s = (s + ac) % P;
- if (a > b)
- s = (s + bc) % P;
- }
- void dfs(int u, int pa) {
- f[u][0] = 0;
- f[u][1] = cnt[u][0] = cnt[u][1] = 1;
- // 不取可不可以, 取可不可以
- bool ban = 1, pick = 1;
- if (~id[u]) {
- // 枚举的是强制取 u
- if (((st >> id[u]) & 1)) {
- cnt[u][0] = 0;
- f[u][0] = INT_MAX;
- ban = 0;
- } else {
- cnt[u][1] = 0;
- f[u][1] = INT_MAX;
- pick = 0;
- }
- }
- for (auto ed : e[u]) {
- if (ed == pa)
- continue;
- dfs(ed, u);
- // u 可以不取
- if (ban) {
- f[u][0] += f[ed][1];
- cnt[u][0] = cnt[u][0] * cnt[ed][1] % P;
- }
- // u 可以取
- if (pick) {
- f[u][1] += min(f[ed][0], f[ed][1]);
- ll t = 0;
- Add(t, f[ed][0], f[ed][1], cnt[ed][0], cnt[ed][1]);
- cnt[u][1] = cnt[u][1] * t % P;
- }
- }
- // 强制转换, 如果 ed 不取那么 f[u][0] 是无效值
- for (auto ed : ef[u]) {
- if ((st >> id[ed]) & 1)
- continue;
- f[u][0] = INT_MAX, cnt[u][0] = 0;
- }
- }
- int main() {
- ios::sync_with_stdio(0);
- cin.tie(0);
- cin >> n;
- for (int i = 1; i <= n; i++) {
- int a;
- cin >> a;
- e[a].emplace_back(i);
- e[i].emplace_back(a);
- }
- cin >> n;
- for (int i = 1; i <= n; i++) {
- int b;
- cin >> b;
- id[i] = -1;
- if (~b) {
- id[i] = tot++;
- ef[b].emplace_back(i);
- }
- }
- id[0] = -1;
- // st 表示每个两条边的点的 取/不取 状态, 0不取 1取
- // res 表示取的最少的点数, ans 是这样取的方案数
- // 注意如果相等 ans 是要加上这个方案数的
- ll res = INT_MAX, ans = 0;
- for (st = 0; st < (1 << tot); st++) {
- memset(f, 0x3f, sizeof(f));
- memset(cnt, 0, sizeof(cnt));
- dfs(0, 0);
- ll temp = min(f[0][0], f[0][1]);
- if (temp > res)
- continue;
- // 如果小于现有的点最小值就更新答案, 方案数清零
- if (temp < res) {
- res = temp;
- ans = 0;
- }
- // 等于就不管
- // 在基础上加上这次的方案数
- Add(ans, f[0][0], f[0][1], cnt[0][0], cnt[0][1]);
- }
- cout << ans;
- return 0;
- }
复制代码
|
评分
-
查看全部评分
|