• AcWing 361 观光奶牛


    \(AcWing\) \(361\) 观光奶牛

    题目传送门

    一、题目描述

    背景
    作为对奶牛们辛勤工作的回报,\(Farmer\) \(John\)决定带她们去附近的大城市玩一天。
    旅行的前夜,奶牛们在兴奋地讨论如何最好地享受这难得的闲暇。
    很幸运地,奶牛们找到了一张详细的城市地图,上面标注了城市中所有\(L(2⩽L⩽1000)\)座标志性建筑物(建筑物按\(1…L\)顺次编号),以及连接这些建筑物的\(P(2⩽P⩽5000)\)条道路。按照计划,那天早上\(Farmer\) \(John\)会开车将奶牛们送到某个她们指定的 建筑物 旁边,等奶牛们完成她们的整个旅行并回到出发点后,将她们接回农场。由于大城市中总是寸土寸金,所有的道路都很窄,政府不得不把它们都设定为通行方向固定的单行道。
    尽管参观那些标志性建筑物的确很有意思,但如果你认为奶牛们同样享受穿行于大城市的车流中的话,你就大错特错了。与参观景点相反,奶牛们把走路定义为无趣且令她们厌烦的活动。对于编号为\(i\)的标志性建筑物,奶牛们清楚地知道参观它能给自己带来的乐趣值\(F_i\)(\(1⩽F_i⩽1000\))。相对于奶牛们在走路上花的时间,她们参观建筑物的耗时可以忽略不计。
    奶牛们同样仔细地研究过城市中的道路。她们知道第\(i\)条道路两端的建筑物\(L1_i\)\(L2_i\)(道路方向为\(L1_i \rightarrow L2_i\) ),以及她们从道路的一头走到另一头所需要的时间\(Ti(1⩽Ti⩽1000)\)
    为了最好地享受她们的休息日,奶牛们希望她们 在一整天中平均每单位时间内获得的乐趣值最大 。当然咯,奶牛们不会愿意把同一个建筑物参观两遍,也就是说,虽然她们可以两次经过同一个建筑物,但她们的乐趣值只会增加一次。顺便说一句,为了让奶牛们得到一些锻炼,\(Farmer\) \(John\)要求奶牛们参观至少\(2\)个建筑物。
    请你写个程序,帮奶牛们计算一下她们能得到的最大平均乐趣值。

    \(AcWing\) 抽象出的题意

    给定一张 \(L\) 个点、\(P\) 条边的有向图,每个点都有一个权值 \(f[i]\),每条边都有一个权值 \(t[i]\)

    求图中的一个环,使 环上各点的权值之和 除以 环上各边的权值之和 最大

    输出这个最大值。

    注意:数据保证至少存在一个环

    输入格式
    第一行包含两个整数 \(L\)\(P\)

    接下来 \(L\) 行每行一个整数,表示 \(f[i]\)

    再接下来 \(P\) 行,每行三个整数 \(a,b,t[i]\),表示点 \(a\)\(b\) 之间存在一条边,边的权值为 \(t[i]\)

    输出格式
    输出一个数表示结果,保留两位小数。

    二、\(01\)分数规划

    \(01\)分数规划 还是不太好理解的,有几个细节需要仔细考虑,单开一个专题文章讲解一下,点这里

    三、本题思路

    分析

    题目要求$\displaystyle \frac{\sum {f_i}}{\sum{w_i}} $的最大值,这种问题称为 \(01\)分数规划,通俗点说,就是一堆的和除以一堆的和,要求比值最大。

    对于本题

    我们可以通过二分来做,二分啥呢?就是对于一个环,二分一个\(mid\)值,判断是否满足\(\displaystyle \frac{\sum{f_i}}{\sum{w_i}} \geq mid\),然后我们就可以来不断更改二分的区间,直到找到\(\displaystyle \frac{\sum{f_i}}{\sum{w_i}}\)的最大值。

    思路确定了,那么具体如何实现呢?

    \(\displaystyle \frac{\sum{f_i}}{\sum{w_i}} \geq mid\) 由于这里的边都是正权边,所以可以移项,变成

    \[\displaystyle \sum f_i ≥mid× \sum w_i \]


    再变一下:

    \[\sum f_i −mid\times \sum w_i ≥0 \]

    将求和符号提出,亦等价于:

    \[\sum(f_i −mid×w_i)≥0 \]

    如下建图:

    • 题目明确告诉我们有环,让我们找出环,但没有明说是正环还是负环
    • 求最长路,如果路径不断的增长,则说明存在正环
    • 求最短路,如果路径不断的减小,则说明存在负环

    上面我们得到了一个 柿子

    \[\large \sum (f_i - mid \times w_i) \geq 0 \]

    妥妥的边长不断累加增长,是正环!用最长路办法即可

    当然,也可以变形一下 柿子

    \[\large \sum (mid \times w_i -f_i) \leq 0 \]

    妥妥的边长不断累加减少,是负环!用最短路办法即可

    \(Q\):有没有可能是 零环 呢?

    \(A\):上面的分析过程是带等号的,所以会有零环,在真正的代码实现中,我们没有用等号,零环不会被检查。

    总结一下\(01\)分数规划的套路

    1. 二分一个定值
    2. 整理表达式,重新定义边权
    3. 套用常规的图论算法(负环、最小生成树等等)

    由于是求正环,与求负环类似,只不过最短路变成最长路。

    而且,边权需要自定义,如上图,边权\(f[u] - mid * w[i]\),其中\(f[u]\)表示\(u\)点的点权,\(w[i]\)表示从\(u\)\(j\)的边权,这样合起来作为该边的边权,相当于\(spfa\)求最长路的边权\(w[i]\).

    四、正环解法

    #include <bits/stdc++.h>
    using namespace std;
    const int N = 1010, M = 5010;
    int n, m;
    int f[N], cnt[N];
    double dist[N];
    bool st[N];
    const double eps = 1e-4;
    //邻接表
    int idx, h[N], e[M], w[M], ne[M];
    void add(int a, int b, int c) {
        e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
    }
    
    bool check(double mid) {
        queue<int> q;
        memset(cnt, 0, sizeof cnt);
        memset(dist, 0x3f, sizeof dist);
        memset(st, false, sizeof st);
        for (int i = 1; i <= n; i++) {
            q.push(i);
            st[i] = true;
        }
        while (q.size()) {
            int u = q.front();
            q.pop();
            st[u] = false;
            for (int i = h[u]; ~i; i = ne[i]) {
                int j = e[i];
                //正环,最长路
                if (dist[j] < dist[u] + f[u] - w[i] * mid) {
                    dist[j] = dist[u] + f[u] - w[i] * mid;
                    //判环
                    cnt[j] = cnt[u] + 1;
                    if (cnt[j] >= n) return true;
                    if (!st[j]) {
                        q.push(j);
                        st[j] = true;
                    }
                }
            }
        }
        return false;
    }
    int main() {
        cin >> n >> m;
        for (int i = 1; i <= n; i++) cin >> f[i]; //每个点都有一个权值f[i]
        //初始化邻接表
        memset(h, -1, sizeof h);
        int a, b, c;
        for (int i = 0; i < m; i++) {
            cin >> a >> b >> c;
            add(a, b, c);
        }
        //浮点数二分
        double l = 0, r = 1000;
        //左边界很好理解,因为最小是0;
        //Σf[i]最大1000*n,Σt[i]最小是1*n,比值最大是1000
        //当然,也可以无脑的设置r=INF,并不会浪费太多时间,logN的效率你懂的
        //因为保留两位小数,所以这里精度设为1e-4
        while (r - l > eps) {
            double mid = (l + r) / 2;
            if (check(mid))
                l = mid;
            else
                r = mid;
        }
        printf("%.2lf\n", l);
        return 0;
    }
    

    五、\(SPFA+dfs\)解法 【推荐】

    个人理解,\(SPFA+dfs\)的方法才是判负环的正解,可以做到线性时间复杂度,原理见:这里

    补充于 \(2022-11-11\)

    #include <bits/stdc++.h>
    using namespace std;
    const int N = 1010, M = 5010;
    int n, m;
    int f[N], cnt[N];
    double dist[N];
    bool st[N];
    const double eps = 1e-4;
    //邻接表
    int idx, h[N], e[M], w[M], ne[M];
    void add(int a, int b, int c) {
        e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
    }
    
    // dfs 判环 Accepted	35 ms
    bool dfs(int u, double mid) {
        if (st[u]) return true; //如果又见u,说明有环
        bool flag = false;      //我的后代们是不是有环?
        st[u] = true;           // u我出现过一次了~
        for (int i = h[u]; ~i; i = ne[i]) {
            int j = e[i];
            //更新最小值,判负环
            if (dist[j] > dist[u] + w[i] * mid - f[u]) {
                dist[j] = dist[u] + w[i] * mid - f[u];
                //检查一下我的下一个节点j,它要是有负环检查到,我也汇报
                flag = dfs(j, mid);
                if (flag) break;
            }
        }
        st[u] = false; //回溯
        return flag;
    }
    
    bool check(double mid) {
        memset(dist, 0, sizeof dist);
        for (int i = 1; i <= n; i++)
            if (dfs(i, mid)) return true;
        return false;
    }
    
    int main() {
        cin >> n >> m;
        for (int i = 1; i <= n; i++) cin >> f[i]; //每个点都有一个权值f[i]
        //初始化邻接表
        memset(h, -1, sizeof h);
        int a, b, c;
        for (int i = 0; i < m; i++) {
            cin >> a >> b >> c;
            add(a, b, c);
        }
        //浮点数二分
        double l = 0, r = 1000;
        //左边界很好理解,因为最小是0;
        //Σf[i]最大1000*n,Σt[i]最小是1*n,比值最大是1000
        //当然,也可以无脑的设置r=INF,并不会浪费太多时间,logN的效率你懂的
        //因为保留两位小数,所以这里精度设为1e-4
        while (r - l > eps) {
            double mid = (l + r) / 2;
            if (check(mid))
                l = mid;
            else
                r = mid;
        }
        printf("%.2lf\n", l);
        return 0;
    }
    
  • 相关阅读:
    【树状数组套权值线段树】bzoj1901 Zju2112 Dynamic Rankings
    【权值线段树】bzoj3224 Tyvj 1728 普通平衡树
    【转载】【树形DP】【数学期望】Codeforces Round #362 (Div. 2) D.Puzzles
    ReStart
    Good-Bye
    【分块打表】bzoj1662 [Usaco2006 Nov]Round Numbers 圆环数
    【分块打表】bzoj1026 [SCOI2009]windy数
    【分块打表】bzoj3798 特殊的质数
    【分块打表】bzoj3758 数数
    【线段树】bzoj3995 [SDOI2015]道路修建
  • 原文地址:https://www.cnblogs.com/littlehb/p/16058048.html
Copyright © 2020-2023  润新知