文章标题:
图论算法在P9487小熊游览景点问题中的应用
文章内容:## 本文涉及的基础知识点
有关C++图论的知识链接:C++图论相关
C++深度优先搜索(DFS)的相关内容链接:C++DFS讲解
C++广度优先搜索(BFS)算法的相关链接:C++BFS算法剖析
此题目实际难度较高,比官网标注的难度至少高出一个层级,甚至可能高出两个层级
P9487 「LAOI-1」小熊游景点
题目描述
小熊的地图上存在 n 个景点,每个景点都有对应的分数 sᵢ。
有 n-1 对节点之间通过双向的公交线路直接相连,每条线路的收费为 wᵢ。
当下,小熊位于 a 景点,总司令处于 b 景点,他们需要沿着简单路径在 a 到 b 的路径上的 p 景点会合,之后再沿着简单路径一同前往 q 景点(q 为任意节点,且每个人不会重复游览 p 景点)。
针对 m 次询问,每次给定 a 和 b,需找出 p 和 q,使得在小熊与总司令花费之和最小的条件下,他们所经过的景点分数之和最大,并输出这个最大的分数之和(即小熊经过的景点分数之和加上总司令经过的景点分数之和)。需要注意的是,重复经过的线路的收费会被重复计算,重复经过的景点的分数也会被重复计算。
输入格式
第一行有两个整数 n 和 m,分别代表景点的数量以及询问的次数。
紧接着的一行包含 n 个整数,其中第 i 个整数对应第 i 个景点的权值 sᵢ。
随后的 n-1 行,每行包含三个整数 u、v 和 w,表示 u 节点和 v 节点之间存在一条收费为 w 的双向公交路线。
最后有 m 行,每行包含两个整数 a 和 b,代表小熊和总司令所在的景点位置。
输出格式
对于每一组询问,每行输出一个整数作为结果。
代码
template<class TSave>
class CMultMin {
public:
    CMultMin(const vector<int>& next, CParents& pars, const vector<TSave>& vals) :m_vNext(next), m_pars(pars) {
        const int N = next.size();
        const int M = pars.GetBitCnt();
        m_data.assign(N, vector<TSave>(M));
        for (int i = 0; i < N; i++) {
            m_data[i][0] = vals[i];
        }
        for (int i = 0; i + 1 < M; i++) {
            const int len = 1 << i;
            for (int j = 0; j < N; j++) {
                m_data[j][i + 1] = m_data[j][i];
                int j2 = pars.GetPow2Parent(j, i);
                if (-1 != j2) {
                    m_data[j][i + 1] = min(m_data[j][i], m_data[j2][i]);
                }
            }
        }
    }
    TSave Query(int cur, int len, TSave def) {
        for (int iBit = 0; iBit < m_data[0].size(); iBit++)
        {
            if (len & (1 << iBit))
            {
                def = min(def, m_data[cur][iBit]);
                cur = m_pars.GetPow2Parent(cur, iBit);
                if (-1 == cur) { return def; }
                len -= (1 << iBit);
            }
        }
        return def;
    };
    const vector<int>& m_vNext;
    vector<vector<TSave>> m_data;
    CParents& m_pars;
};
class Solution {
public:
    vector<long long> Ans(vector<int>& pw, vector<tuple<int, int, int>>& edge, vector<pair<int, int>>& que) {
        const int N = pw.size();
        vector<vector<int>> neiBo(N);
        for (auto& [n1, n2, w] : edge) {
            n1--, n2--;
        }
        for (auto& [a, b] : que) {
            a--, b--;
        }
        for (const auto& [n1, n2, w] : edge) {
            neiBo[n1].emplace_back(n2);
            neiBo[n2].emplace_back(n1);
        }
        auto leves = CBFSLeve::Leve(neiBo, { 0 });
        vector<int> pars(N, -1), ew(N);//pars[cur]记录cur父节点,ew[cur]记录边cur、pars[cur]的权。
        for (int i = 0; i < N; i++) {
            for (const auto& j : neiBo[i]) {
                if (leves[j] < leves[i]) { continue; }
                pars[j] = i;
            }
        }
        for (const auto& [n1, n2, w] : edge) {
            if (leves[n1] < leves[n2]) {
                ew[n2] = w;
            }
            else {
                ew[n1] = w;
            }
        }
        auto nodeByLeveSort = CBFSLeve::LeveSort(leves);
        vector<long long> pw0(pw.begin(), pw.end()), ew0(N);//d[i]记录i到0的简单路径的点权之和,e记录边权。
        for (const auto& node : nodeByLeveSort)
        {
            if (-1 == pars[node]) { continue; }
            pw0[node] += pw0[pars[node]];
            ew0[node] += ew0[pars[node]];
        }
        vector<vector<tuple<long long, long long, int>>> pc(N);
        vector<pair<long long, long long>> pp(N);
        for (int cur = 0; cur < N; cur++) {
            pp[cur] = make_pair(0, -pw[cur]);
            pc[cur].emplace_back(make_tuple(0, -pw[cur], -1));
        }
        for (auto it = nodeByLeveSort.rbegin(); it != nodeByLeveSort.rend(); ++it) {
            auto& v = pc[*it];
            if (v.size() > 1) {//只保留最优次优
                nth_element(v.begin(), v.begin() + 1, v.end());
                v.erase(v.begin() + 2, v.end());
            }
            const auto& t = pc[*it].front();
            if (-1 == pars[*it]) { continue; }
            pc[pars[*it]].emplace_back(make_tuple(get<0>(t) + ew[*it], get<1>(t) - pw[*it] - pw[pars[*it]], *it));
        }
        for (const auto& cur : nodeByLeveSort)
        {
            const int iPar = pars[cur];
            if (-1 == iPar) { continue; }
            auto t = make_pair(ew[cur] + get<0>(pp[iPar]), get<1>(pp[iPar]) - pw[iPar] - pw[cur]);
            pp[cur] = min(pp[cur], t);//经过父节点,不经过兄弟节点
            for (const auto& [f, s1, i] : pc[iPar]) {
                if ((-1 == i) || (i == cur)) { continue; }
                t = make_pair(get<0>(pc[i].front()) + ew[i] + ew[cur], get<1>(pc[i].front()) - pw[i] - 2 * pw[iPar] - pw[cur]);
                pp[cur] = min(pp[cur], t);//经过父节点,经过兄弟节点
                break;
            }
        }
        vector<pair<long long, long long>> ppc(N);
        for (int i = 0; i < N; i++) {
            ppc[i] = min(pp[i], make_pair(get<0>(pc[i].front()), get<1>(pc[i].front())));
        }
        C2Parents par2s(pars, leves);
        /*{
            vector<pair<long long, long long>> ppc1(N,make_pair(LLONG_MAX/2,0));
            for (int a = 0; a < N; a++) {
                for (int b = 0; b < N; b++) {
                    const int ab = par2s.GetPublicParent(a, b);
                    pair<long long, long long> abr(0,0);
                    for (int i = a; i != ab; i = pars[i]) {
                        abr.first += ew[i];
                        abr.second += -2 * pw[i];
                    }
                    for (int i = b; i != ab; i = pars[i]) {
                        abr.first += ew[i];
                        abr.second += -2 * pw[i];
                    }
                    abr.second += pw[a];
                    abr.second -= 2 * pw[ab];
                    ppc1[a] = min(ppc1[a], abr);
                }
            }
            for (int i = 0; i < N; i++) {
                AssertEx(-ppc1[i].second, ppc[i]);
            }
        };*/
        CMultMin<pair<long long, long long>> mult(pars, par2s, ppc);
        vector<long long> ans;
        for (const auto& [a, b] : que) {
            const int ab = par2s.GetPublicParent(a, b);
            const auto curAns1 = mult.Query(a, leves[a] - leves[ab], ppc[ab]);
            const auto curAns2 = mult.Query(b, leves[b] - leves[ab], ppc[ab]);
            const auto curAns3 = min(curAns1, curAns2);
            //AssertEx(curAns3, curAns);
            ans.emplace_back(pw0[a] + pw0[b] - 2 * pw0[ab] + pw[ab] - curAns3.second);
        }
        return ans;
    }
};
单元测试
vector<int> a;
vector<tuple<int, int, int>> edge;
vector<pair<int, int>> que;
TEST_METHOD(TestMethod1)
{
    a = { 1,1,1,1,1,1,1 },edge = { {1,2,3},{3,6,-4},{2,5,2},{1,3,6},{2,4,1},{3,7,5} },
        que = { {4,7} };
    auto res = Solution().Ans(a, edge,que);
    AssertV(vector<long long>{ 8 }, res);
}
TEST_METHOD(TestMethod12)
{
    a = { 786755,-687509,368192,154769,647117,-713535,337677,913223,-389809,-824004 },
        edge = { {1,2,-785875},{1,3,-77082},{1,4,-973070},{3,5,-97388},{2,6,-112274},{3,7,657757},{4,8,741733},{3,9,5656},{4,10,-35190} };
    que = { {3,3},{3,10},{7,3},{5,1},{2,10},{10,10},{1,6},{7,2},{8,9},{9,1} };
    auto res = Solution().Ans(a, edge, que);
    AssertV(vector<long long>{ 971424,- 1257332,1309101,3420605,-2313033,- 2567048, - 2467802,
        352646,759321,1368370 }, res);
}
扩展阅读
我想对大家说的一些话
工作中遇到问题时,可按类别查阅我的算法相关文章,点击查看《算法与数据汇总》。
学习算法时,建议按章节学习《喜缺全书算法册》,其中包含大量题目和测试用例,可通过打包下载获取。有效学习需具备明确目标、及时反馈以及处于难度合适的拉伸区,同时要保持专注。“闻缺陷则喜”是一种良好心态,尽早发现并解决问题能为老板节省成本。正如子墨子所言“事无终始,无务多业”,这也意味着专业的人应专注于专业之事。若把程序比作一条龙,那么算法便是它的眼睛。失败加上反思会走向成功,成功加上反思也会走向成功。
视频课程
若想先学习基础课程,可前往CSDN学院,聆听由白银讲师(即本人)讲解的课程。相关课程链接:https://edu.csdn.net/course/detail/38771
若希望快速提升能力以分担工作,可学习C#入职培训、C++入职培训等课程,相关讲师页面:https://edu.csdn.net/lecturer/6176
测试环境
操作系统:win7 开发环境: VS2019 C++17
或者 操作系统:win10 开发环境: VS2022 C++17
如无特殊说明,本算法 用C++实现。
