第十五届蓝桥杯C\C++A组省赛第六题零食采购详解

一、朴素做法:图的DFS

​#include <iostream>
#include <vector>
#include <set>

using namespace std;

vector<vector<int>> adj;  // 邻接表表示树
vector<int> c;            // 每个星球的零食种类
vector<int> path;         // 存储从起点到终点的路径
bool found = false;       // 标记是否找到路径

// DFS查找从u到v的路径
void dfs(int u, int v, vector<int>& currentPath, vector<bool>& visited) {
    visited[u] = true;
    currentPath.push_back(u);

    if (u == v) {
        path = currentPath;  // 找到路径
        found = true;
        return;
    }

    for (int neighbor : adj[u]) {
        if (!visited[neighbor]) {
            dfs(neighbor, v, currentPath, visited);
            if (found) return;  // 如果找到路径,直接返回
        }
    }
    currentPath.pop_back();  // 回溯。由于是树,vis可以不用回溯
}

int main() {
    int n, q;
    cin >> n >> q;
    adj.resize(n + 1);
    c.resize(n + 1);

    // 输入每个星球的零食种类
    for (int i = 1; i <= n; ++i) {
        cin >> c[i];
    }

    // 输入n-1条边,构建树
    for (int i = 1; i < n; ++i) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    // 处理每个查询
    for (int i = 0; i < q; ++i) {
        int s, t;
        cin >> s >> t;

        // 初始化
        path.clear();
        found = false;
        vector<bool> visited(n + 1, false);
        vector<int> currentPath;

        // 查找从s到t的路径
        dfs(s, t, currentPath, visited);

        // 统计路径上的零食种类
        set<int> kinds;
        for (int node : path) {
            kinds.insert(c[node]);
        }

        // 输出结果
        cout << kinds.size() << endl;
    }

    return 0;
}

可通过20%,其他的超时。

二、更优解:LCA

1.LCA基本模版 

LCA,即最近公共祖先。参考:https://www.bilibili.com/video/BV1N7411G7JD/?spm_id_from=333.337.search-card.all.click&vd_source=0252428a85afe838b167a93feafd7c42

对于两个点,找他们的最近公共祖先有两种情况:

  1. 让深度较大的跳到与深度小的同一深度,如果已重合,重合点即为LCA,结束
  2. 再让它们同时尽可能向上跳,跳到同一深度,但不能重合跳后所在两点的父亲即为LCA

那么我们如何实现“尽可能向上跳但不能重合”?——我们可以先试探最大的跳跃高度,然后慢慢缩小。实质上,是把跳的高度表示成2的次方的和的形式(与快速幂、ST表的思想类似)。为了实现快速跳跃,我们需要准备2个东西:

  1. 各点的深度
  2. 跳跃表

跳跃表的递推式:

i表示目前所在的点,j表示跳跃的高度的log2,表格的值记作f[i][j]表示跳后到达的点。 

f[i][j]=f[f[i][j-1]][j-1]

这两个东西可以通过一次dfs得到:

void dfs(int v,int u) {//u是v的父节点
    depth[v] = depth[u] + 1;
    f[v][0] = u;
    for (int j = 1; (1 << j) < depth[v]; j++) {//1 << j相当于2的j次方
        f[v][j] = f[f[v][j - 1]][j - 1];
    }

    for (auto it : g[v]) {
        if (it == u) continue;
        f[it][0] = v;
        dfs(it, v);
    }
}

准备好后就可以上LCA

int LCA(int x,int y) {
    //x赋成深度大的点
    if (depth[x] < depth[y]) swap(x, y);

    //x跳到y的同一深度
    for (int i = 20; i >= 0; i--) {//题中所给的n对应的最大深度是20
        if (depth[f[x][i]] <= depth[y]) x = f[x][i];
    }
    if (x == y) return x;

    //一起跳
    for (int i = 20; i >= 0; i--) {
        if (f[x][i] != f[y][i])
            x = f[x][i], y = f[y][i];
    }
    return f[x][0];
}

2.详细解法

cnt[x][i] 表示从根节点到节点x的路径上零食种类i的数量,注意题干规定:i的范围是1到20。如果找到了起点、终点的lca,我们就可以统计起点a到终点b的路径上每一种零食的数量:

cnt[a][i]+cnt[b][i]-cnt[lca][i]-cnt[f[lca][0]]

而cnt在DFS时就可得到:每个点的各种类的零食数都与其父节点的相同,除了它拥有的那一种要+1。

完整题解:

#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 5;

vector<int> g[N];  // 邻接表表示树
int c[N], depth[N], f[N][21], cnt[N][21];//cnt[x][i] 表示从根节点到节点x的路径上零食种类i的数量,注意题干规定:i的范围是1到20

void dfs(int v,int u) {//u是v的父节点
    depth[v] = depth[u] + 1;
    f[v][0] = u;
    for (int i = 1; i <= 20; i++) {
        cnt[v][i] = cnt[u][i];
    }
    cnt[v][c[v]]++;

    for (int j = 1; (1 << j) < depth[v]; j++) {//1 << j相当于2的j次方
        f[v][j] = f[f[v][j - 1]][j - 1];
    }

    for (auto it : g[v]) {
        if (it == u) continue;
        dfs(it, v);
    }
}

int LCA(int x,int y) {
    //x赋成深度大的点
    if (depth[x] < depth[y]) swap(x, y);

    //x跳到y的同一深度
    for (int i = 20; i >= 0; i--) {
        if (depth[f[x][i]] >= depth[y]) x = f[x][i];
    }
    if (x == y) return x;

    //一起跳
    for (int i = 20; i >= 0; i--) {
        if (f[x][i] != f[y][i])
            x = f[x][i], y = f[y][i];
    }
    return f[x][0];
}

int main() {
    int n, q;
    cin >> n >> q;

    // 输入每个星球的零食种类
    for (int i = 1; i <= n; ++i) {
        cin >> c[i];
    }

    // 输入n-1条边,构建树
    for (int i = 1; i < n; ++i) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    dfs(1, 0);
    int a, b;
    while (q--) {
        int ans = 0;
        cin >> a >> b;
        int lca = LCA(a, b);
        for (int i = 1; i <= 20; i++) {
            int p = cnt[a][i] + cnt[b][i] - cnt[lca][i] - cnt[f[lca][0]][i];
            if (p) ans++;
        }
        cout << ans << endl;
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值