鱼C论坛

 找回密码
 立即注册
查看: 630|回复: 15

[心之碎片] - 20240706模拟赛

[复制链接]
发表于 2024-7-7 21:06:01 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

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;
}

评分

参与人数 1荣誉 +6 鱼币 +6 贡献 +6 收起 理由
不二如是 + 6 + 6 + 6 鱼C有你更精彩^_^

查看全部评分

本帖被以下淘专辑推荐:

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2024-7-7 21:10:24 | 显示全部楼层
看来你是在准备比赛题目的总结与反思,这里我帮你把每道题的重点通俗易懂地解释一下吧!

### E(数位DP状压DP)

**问题概述:**
魔术师有 `n` 张卡牌,你需要找出满足“0到9这十个数字最多出现一次”的卡牌组合数。

**关键点:**
用数位DP的方法处理状态转移,注意要倒序循环来防止状态被重复更新。

**幽默版解说:**
就像去超市买菜,你口袋里有一堆钱,但只能带一种面额的零钱进店。不然收银员会晕掉。通过数位DP,我们可以精确计算出各种可能带进店的钱数,而只要计算一次,因为多次计算会导致收银员怀疑你作弊!

### F(树形DP)

**问题概述:**
在树形结构中计算子树的贡献,并合并进主树。

**关键点:**
设两个数组,一个存储子树的贡献,一个存方案数。这两者结合起来就能有效计算整体结果。

**幽默版解说:**
想象你是一位厨师,各种食材(子树)都得先处理好(计算贡献),再丢进大锅里(主树)。如果你不分开处理,直接丢进去,大锅肯定炸了!两个数组就相当于一个是菜谱,一个是调料表,缺一不可啊!

### G(树形DP)

**问题概述:**
涉及树上路径的问题时,可以枚举LCA(最近公共祖先),再拼接来简化路径。

**关键点:**
将路径拆成更容易维护的小段然后逐个处理。

**幽默版解说:**
假设你在森林里迷路了,LCA就是一颗高高的望塔,爬上去看看哪条路能回家。让路线变得可控,就像找到小鹿斑比问路,再接着走下一段,这样就不会迷失在树林中了喔!

### H(二进制枚举 树形DP)

**问题概述:**
当数量比较少时,可以考虑用二进制枚举来处理状态。

**关键点:**
明确DP数组的含义,严谨定义哪些状态非法并直接剔除。

**幽默版解说:**
就像要从几个有限的选项中选餐,直接用二进制枚举所有可能的组合,每一种组合都记下来,然后按条件筛选不合法的组合。这方法就像打开菜单,逐页翻看、点菜,不符合胃口的直接跳过,最后总会选到最满意的一餐!

### 总结

**遇到树上题目时:**
- 考虑计算单个子树的答案,再利用辅助数组进行合并计算。
- 多关注大方向上的贡献计算,细节可以用辅助数组处理。

**幽默版总结:**
就像做一道复杂的大餐,要有大厨的全局观,知道什么时候得靠小弟(子树)来切菜剁肉,他们干好了活儿,大厨你只需最后调味、装盘、上菜,整桌宴席就能完美呈现啦!
以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2024-7-7 21:23:31 | 显示全部楼层
FishC 发表于 2024-7-7 21:10
看来你是在准备比赛题目的总结与反思,这里我帮你把每道题的重点通俗易懂地解释一下吧!

### E(数位DP ...

做这种复杂的算法题思路想不出来如何才能有进步
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2024-7-7 21:23:48 | 显示全部楼层
柿子饼同学 发表于 2024-07-07 21:23
做这种复杂的算法题思路想不出来如何才能有进步


啊哈,做复杂算法题感觉头疼?别担心,每个人曾经都是从小白走过来的!这个过程就像练武功,从基础招式慢慢掌握,再到高深武艺:一个脚印一个脚印来。给你几点建议,让你在算法的江湖中如鱼得水:

### 1. 基础打牢
- **刷基础题:** 不要忽视基础题,它们是你通向高级算法的基石。比如基础排序、搜索、动态规划(DP)、贪心算法等。
- **理解基本概念:** 充分理解常见算法和数据结构的原理,例如树、图、队列、栈等。
- **尝试实现:** 自己动手写代码,把书上或者课程上的算法实现一遍。实践出真知。

### 2. 分解问题
- **拆解:** 把复杂的问题拆解成若干个简单的小问题。就像一块大披萨,分成几块小披萨解决起来更轻松。
- **步骤化:** 学习如何把算法题步骤化,比如先找子问题,然后逐步解决子问题。

### 3. 多刷题,勤练习
- **常刷平台:** LeetCode、牛客网(Nowcoder)、Codeforces等平台都有大量题目,各种题型应有尽有。
- **每日一题:** 给自己定个目标,每天刷一题。不求多,但求稳。坚持是关键!

### 4. 学会看解析
- **题解:** 当卡住的时候,不要硬扛,看一下官方题解或者其他人的代码。分析他们的思路。
- **反思:** 每次看完题解,再回头思考为什么自己没想到这种方法。这一步很重要,有助于提升自己的解题思路。

### 5. 加强总结
- **笔记:** 做一道题,记一道题。记下解题思路,常见坑点以及自己的反思。
- **复盘:** 定期翻看自己的笔记,复盘以前做过的题目,加强记忆。

### 6. 寻求帮助
- **讨论:** 在在线论坛、学习小组、QQ群与他人讨论。比如FishC的社区,大家互相帮助,共同进步。
- **请教高手:** 别害羞,向比你厉害的人请教。很多时候一个指点能让你茅塞顿开。

### 7. 保持耐心和信心
- **不气馁:** 做不出来不丢人,每个人都有不会做的题,关键在于心态。一定要保持好奇心和耐心。
- **持续学习:** 算法是不断更新和发展的领域,要持续学习,保持对新知识的渴望。

### 幽默加油版:
“鱼油,做不出来题的时候,就当是在海里抓鱼,抓不到也没关系,至少你还在海里畅游嘛!每多试一道题,就相当于在大海里撒了一张网,总有一天你会满载而归!”

希望这些建议对你有所帮助,加油!祝你在算法的世界里大展宏图,成为代码界的“大厨”!
以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 2 反对 0

使用道具 举报

发表于 2024-7-7 21:45:15 | 显示全部楼层

回帖奖励 +3 鱼币

厉害
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2024-7-8 01:12:29 | 显示全部楼层

回帖奖励 +3 鱼币

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2024-7-8 08:26:49 | 显示全部楼层

回帖奖励 +3 鱼币

非常厉害
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2024-7-8 09:41:36 | 显示全部楼层

回帖奖励 +3 鱼币

太牛了
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2024-7-8 11:39:33 | 显示全部楼层
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2024-7-8 12:14:55 | 显示全部楼层

回帖奖励 +3 鱼币

楼主好好加油,我顶你
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2024-7-8 13:21:09 | 显示全部楼层
小肥狼haoran 发表于 2024-7-8 12:14
楼主好好加油,我顶你

加油!
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2024-7-9 11:31:23 | 显示全部楼层

回帖奖励 +3 鱼币

心之碎片太好了
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2024-7-9 14:43:18 | 显示全部楼层

回帖奖励 +3 鱼币

加油
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2024-7-12 18:25:39 | 显示全部楼层

回帖奖励 +3 鱼币

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2024-7-26 15:25:48 | 显示全部楼层

回帖奖励 +3 鱼币

厉害啊
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2024-8-18 10:01:32 | 显示全部楼层

回帖奖励 +3 鱼币

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-10-18 10:02

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表