贪心训练题

A - 活动安排

原题链接

思路

我们要求出最多能进行几场活动,可以考虑将所有活动按照结束时间排序,因为我们的活动越早结束,剩下的时间就越多,也就是将原问题转化为了剩下的时间,其他条件不变。这就很符合我们贪心的条件:

当前状态对后续状态没有影响(也叫做问题无后效性),这样一来,当前状态的最优解最终会造成全局的最优解,此时我们每一步(每个状态)都选取当前最优解(而不考虑之后的情况),即为贪心

所以我们就可以按照这种方式遍历结构体数组,得到我们贪心的结果。

#include <iostream>
#include <algorithm>
#define endl '\n'
#define ios ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
typedef long long LL;
using namespace std;

struct Act{
    int start;
    int finish;
}act[1010];

bool cmp(Act a, Act b) {
    return a.finish < b.finish;
}

int main() {
    ios;
    int n, ans = 0, now;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> act[i].start >> act[i].finish;
    }
    sort(act + 1, act + 1 + n, cmp);
    for (int i = 1; i <= n; i++) {
        if (act[i].start >= act[now].finish) {
            ans++;
            now = i;
        }
    }
    cout << ans << endl;
    return 0;
}

B - 种树

原题链接(洛谷)

原题链接(LOJ)

思路

我们需要求的是最小种树量,很显然我们会想到让居民们给出的区间相交处有尽可能多的树。注意到交界处肯定是某一区间的末尾,所以我们还是按照末尾对区间排序。因为区间可能已经有树了,我们先遍历这个区间,如果树的个数已经满足了,就不用管他,继续扫下一个区间。如果树的个数不够,我们倒着再扫一遍这个区间,在区间的后面添加树即可。

#include <iostream>
#include <algorithm>
#define endl '\n'
#define ios ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
typedef long long LL;
using namespace std;

const int N = 3e4 + 10;

struct Street{
    int b;
    int e;
    int t;
}street[5010];

bool road[N];

bool cmp(Street a, Street b){
    return a.e < b.e;
}

int main() {
    ios;
    int n, k, res = 0;
    cin >> n >> k;
    for (int i = 0; i < k; ++i) {
        cin >> street[i].b >> street[i].e >> street[i].t;
    }
    sort(street, street + k, cmp);
    for (int i = 0; i < k; ++i) {
        int tmp = 0;
        for (int j = street[i].b; j <= street[i].e; ++j) {
            if (road[j]) {
                tmp++;
            }
        }
        if (tmp >= street[i].t) continue;
        for (int j = street[i].e; j >= street[i].b; --j) {
            if (!road[j]) {
                road[j] = true;
                tmp++;
                res++;
                if (tmp == street[i].t) break;
            }
        }
    }
    cout << res << endl;
    return 0;
}

C - 喷水装置

原题链接(UVA)

原题链接(LOJ)

思路

这个草坪是有宽度的,但这题仍然是一个一维的题目,我们拿到的是喷头中心以及喷灌半径,我们要算的实际上是从喷头中心到草坪的一边的勾股距离,使用勾股定理来算。这题我们建立的结构体的左右端点是勾股完后的距离。随后我们需要对左端点排序,然后遍历一遍喷头结构体数组,每次都找既能覆盖前一个的右端点,自己的右端点又能伸的最远的那个喷头即可。

我还因为数组越界a[-1]WA一次就离谱

#include <iostream>
#include <cmath>
#include <algorithm>
#define endl '\n'
#define ios ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
using namespace std;

const int N = 15010;

struct Nozzle{
    double l;
    double r;
}nozzle[N];

bool cmp(Nozzle a, Nozzle b) {
    return a.l < b.l;
}

void solve() {
    int n, l, w, cntP = 1;
    cin >> n >> l >> w;
    for (int i = 1; i <= n; ++i) {
        int pos, r;
        cin >> pos >> r;
        if (2 * r > w) {
            nozzle[cntP].l = pos - sqrt(r * r - w * w / 4.0);
            nozzle[cntP++].r = pos + sqrt(r * r - w * w / 4.0);
        }
    }
    sort(nozzle + 1, nozzle + 1 + cntP, cmp);
    double rightCur, right = 0;
    int cnt = 0;
    int i = 1;
    while (right < l) {
        rightCur = right;
        for (/*开外面会快,但是我WA了啊*/; nozzle[i].l <= rightCur && i <= cntP; ++i) {
            right = max(nozzle[i].r, right);
        }
        if (rightCur == right) {
            cout << -1 << endl;
            return;
        }
        cnt++;
    }
    cout << cnt << endl;
}

int main() {
    ios;
    int t;
    cin >> t;
    while (t--) {
        solve();
    }
    return 0;
}

D - 加工生产调度

原题链接(洛谷)

原题链接(LOJ)

思路

这思路是因为题目在贪心一栏而猜出来的,我们可以猜到如下两点:

  1. 使在A车间加工时间最短的部件最先加工,这样使得B车间能更快开始加工。

  2. 使在B车间加工时间最短的部件最后加工,这样使得A车间的空闲时间最短。

按照这样排序我们就能把这题AC了。

但是,有句话是这么说的:

贪心不难,但证明贪心可能很难。 ---匿名

我便去搜了一下这题,发现很多人都在说Johnson规则

贪心杂谈

所以下面我们谈谈所谓的Johnson规则

Johnson 算法是用来解决在有负权重边图里的最短路径问题的。

咳咳,放错了

Johnson 规则是作业排序中的一种排序方法。

这种方法适用的条件是:n个工件经过二、三台设备(有限台设备)加工,所有工件在有限设备上加工的次序相同。

要证明贪心是正确的,我们需要用到交换论证的方法:

交换论证主要的思想也就是先假设存在一个最优的算法和我们的贪心算法最接近,
然后通过交换两个算法里的一个步骤(或元素),得到一个新的最优的算法,同时
这个算法比前一个最优算法更接近于我们的贪心算法,从而得到矛盾,原命题成立。
以上源自 博客园的一位大佬 这里!

\(Johnson\)的具体内容为:

  1. \(N_1=\{i|a_i \leq b_i\},N_2=\{i|a_i \geq b_i\}\)
  2. \(N_1\)中作业依照\(a_i\)增序排列,\(N_2\)中作业依\(b_i\)减序排列。
  3. \(N_1\)中作业接\(N_2\)中作业构成满足\(Johnson\)法则的最优调度。

它的证明如下:

发现自己敲LaTeX好累,于是找了一篇文章

借用一下别人题解的图文Johnson法则证明

下面就是大家喜闻乐见的代码啦:

#include <iostream>
#include <algorithm>
#define endl '\n'
#define ios ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
using namespace std;

const int N = 1e4 + 10;

int res[N], t[N], a[N], b[N];
struct AssemblyLine{
    int no;
    int min_;
}assemblyLine[N];

bool cmp(AssemblyLine x, AssemblyLine y) {
    return x.min_ < y.min_;
}

int main() {
    ios;
    int n;
    cin >> n;
    for (int i = 0; i < n; ++i) {
        cin >> a[i];
        assemblyLine[i].no = i;
    }
    for (int i = 0; i < n; ++i) {
        cin >> b[i];
        assemblyLine[i].min_ = min(a[i], b[i]);
    }
    sort(assemblyLine, assemblyLine + n, cmp);
    int j = -1, k = n;
    for (int i = 0; i < n; ++i) {
        res[(assemblyLine[i].min_ == a[assemblyLine[i].no] ? ++j : --k)] = assemblyLine[i].no;
    }
    for (int i = 1; i <= n; ++i) {
        t[i] = t[i - 1] + a[res[i - 1]];
    }
    int ans = t[1] + b[res[0]];
    for (int i = 1; i < n; ++i) {
        ans = max(t[i + 1], ans) + b[res[i]];
    }
    cout << ans << endl;
    for (int i = 0; i < n; ++i) {
        cout << res[i] + 1 << " \n"[i == n - 1];
    }
    return 0;
}

E - 智力大冲浪

原题链接(洛谷)

原题链接(LOJ)

思路

我们已经获得了一定量的资金,但是任务不完成就会被扣钱,如果这件事发生在现实当中,很显然我们会把扣钱最多的任务防止前面做。但是这样不对,为什么呢,因为每个任务有自己的时间限制,如果一味的追求扣钱的量反而会使他们的时间乱序,导致错解。再联系一下生活实际,各位作为DDL战神,我们可以把所有的任务都放在他的DDL的前一秒去做,这样就可以让时间效益最大化。如果我们的安排中最后一秒有占用,那么我们就再往前找没有事情干的时间去做,如果这一条时间线上没有时间可以用的,那只能乖乖罚款了。

#include <iostream>
#include <cmath>
#include <algorithm>
#define endl '\n'
#define ios ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
using namespace std;

const int N = 520;
const int M = 1510;

struct Game{
    int ddl;
    int penalty;
}game[N];

bool vis[M];

bool cmp(Game a, Game b) {
    return a.penalty > b.penalty;
}

int main() {
    ios;
    int m, n;
    cin >> m >> n;
    for (int i = 0; i < n; ++i) {
        cin >> game[i].ddl;
    }
    for (int i = 0; i < n; ++i) {
        cin >> game[i].penalty;
    }
    sort(game, game + n, cmp);
    for (int i = 0; i < n; ++i) {
        for (int j = game[i].ddl; j >= 1; --j) {
            if (!vis[j]) {
                vis[j] = true;
                game[i].penalty = 0;
                break;
            }
        }
    }
    for (int i = 0; i < n; ++i) {
        m -= game[i].penalty;
    }
    cout << m << endl;
    return 0;
}

F - 纪念品分组

原题链接(洛谷)

思路

我们需要做到让最后分组数目最少,可以考虑如下情形:

如果限制和为\(11\),我们的数列是:\(1, 2 ,3, 4 ,5, 6, 7, 8, 9, 10\),很容易想到一头一尾加在一起,这样肯定是最少的情况。但是这种情况下,一头一尾的和没有超过我们的限制。如果和为\(10\)呢?那我们可以把\(10\)单独分一组,剩下的十个元素一头一尾组合,这样就是最小的情况了。但这样的贪心并没有经过证明,所以下面来稍微证明一下。

贪心证明

我们采用反证法,设给定数组为\(a\),限制和为\(n\),考虑数组元素个数大于等于\(4\)的情况,而数组元素小于\(4\)时结论可以显然得到。设最大元素为\(a[max]\),最小元素为\(a[min]\),中间取任意元素\(a[i], a[j]\)。如果\(a[max]\)\(a[min]\)不在一组,而是与\(a[i]\)在一组,同时\(a[min]\)\(a[j]\)在一组。此时由于\(a[min] \leq a[i]\),所以\(a[max] + a[i] > n\)的可能性更大。如果不满足大于关系,整体分组数目不变,但如果满足大于关系,\(a[max]\)就需要单独成为一组,也就是最后结果的组数增多了,所以以上贪心为最优解。

#include <iostream>
#include <cmath>
#include <algorithm>
#define endl '\n'
#define ios ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
using namespace std;

const int N = 3e4 + 10;

int a[N];

int main() {
    ios;
    int w, n, cnt = 0;
    cin >> w >> n;
    for (int i = 0; i < n; ++i) {
        cin >> a[i];
    }
    sort(a, a + n);
    for (int i = 0, j = n - 1; i <= j;) {
        if (a[i] + a[j] > w) {
            j--;
        } else {
            i++;
            j--;
        }
        cnt++;
    }
    cout << cnt << endl;
    return 0;
}

G - 数列分段

Section I

原题链接(洛谷)

原题链接(LOJ)

思路

洛谷有这题的升级版(当然也有原题)点这里看升级题

对于这一题,我们直接考虑按照顺序分段,一旦出现段和超过要求的\(m\)时将分段数++,同时计数变量回到新输入的那个数的位置。

注意:我们的结果变量初始值应该是\(1\),因为没有分段的时候就已经算是一段了。

#include <iostream>
#define ios ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
using namespace std;

int main() {
    ios;
    int n, m;
    cin >> n >> m;
    int res = 1, tmp = 0;
    for (int i = 0; i < n; ++i) {
        int x;
        cin >> x;
        if (tmp + x <= m) {
            tmp += x;
        } else {
            res++;
            tmp = x;
        }
    }
    cout << res;
}

别急着看H题,我们顺带解决一下他的升级版:

Section II

题目描述

对于给定的一个长度为N的正整数数列 \(A_{1\sim N}\),现要将其分成 \(M\)\(M\leq N\))段,并要求每段连续,且每段和的最大值最小。

关于最大值最小:

例如一数列 \(4\ 2\ 4\ 5\ 1\) 要分成 \(3\) 段。

将其如下分段: $$[4\ 2][4\ 5][1]$$

第一段和为 \(6\),第 \(2\) 段和为 \(9\),第 \(3\) 段和为 \(1\),和最大值为 \(9\)

将其如下分段: $$[4][2\ 4][5\ 1]$$

第一段和为 \(4\),第 \(2\) 段和为 \(6\),第 \(3\) 段和为 \(6\),和最大值为 \(6\)

并且无论如何分段,最大值不会小于 \(6\)

所以可以得到要将数列 \(4\ 2\ 4\ 5\ 1\) 要分成 \(3\) 段,每段和的最大值最小为 \(6\)

思路

我相信大家肯定可以很顺利的看懂题意,实际上这题在我们的贪心主题下又增加了一个新的玩法:二分答案

由于时间限制只有\(1\)秒,我们很容易想到去二分求解(暴力匹配一定会TLE的)。

我们来考虑一下为什么这题可以使用二分去解——关键字:最大值最小

二分法可以解决上下有界且单调(满足于某一类性质),求最大值的最小值或最小值的最大值的问题,正好满足这一题的要求。

二分答案的板子很简单,我们直接处理\(check\)函数。注意到每次二分时,我们求的那部分实际上就是上面\(Section\ I\)的答案,即每次把不超过\(mid\)的元素放在一个分段中,直接再写一遍即可。

#include <iostream>
#include <algorithm>
#define endl '\n'
#define ios ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
using namespace std;

const int N = 1e5 + 10;

int n, m;
int a[N];

int check(int k) {
    int tmp = 0, res = 1;
    for (int i = 0; i < n; i++) {
        if (tmp + a[i] <= k) {
            tmp += a[i];
        } else {
            res++;
            tmp = a[i];
        }
    }
    return res > m;
}

int main() {
    ios;
    cin >> n >> m;
    int l = 0, r = 0, mid;
    for (int i = 0; i < n; i++) {
        cin >> a[i];
        r += a[i];
        l = max(l, a[i]);
    }
    while (l <= r) {
        mid = (l + r) >> 1;
        check(mid) ? l = mid + 1 : r = mid - 1;
    }
    cout << l << endl;
}

H - 线段

原题链接

思路

这题和A题其实没什么区别,我尝试了一下使用pair<int, int>代替结构体,可以让重复的题目不那么单调。

其实也可以不写\(cmp\),把\(first\)\(second\)反过来存,然后用\(greater\)排序就行。

#include <iostream>
#include <algorithm>
#define ios ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
using namespace std;
typedef pair<int, int> PII;

const int N = 1e6 + 10;

bool cmp(PII a, PII b) {
    return a.second < b.second;
}

PII dis[N];

int main() {
    ios;
    int n;
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> dis[i].first >> dis[i].second;
    }
    sort(dis, dis + n, cmp);
    int res = 0, k = 0;
    for (int i = 0; i < n; i++) {
        if (dis[i].first >= k) {
            k = dis[i].second;
            res++;
        }
    }
    cout << res << endl;
    return 0;
}

I - 家庭作业

原题链接

思路

这题乍一看和E题十分相似,甚至可以说是一模一样。但是仔细看看发现这个学校十分变态卷,作业数量能达到\(10^6\)之多,而E题的任务只有至多\(500\)个。所以我们要考虑对E题的代码思路优化。我们怎么提高找到适合赶DDL的作业的效率呢,答案是并查集

使用并查集可以很方便地让我们找到应该要安排的作业,其他思路按照E题思路即可。

居然有人取max写错变量名WA两次

#include <iostream>
#include <algorithm>
#define endl '\n'
#define ios ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
using namespace std;

const int N = 1e6 + 10;
const int M = 7e5 + 10;

struct Assignment{
    int ddl;
    int credit;
}assignment[N];

int ft[N];
bool vis[M];

bool cmp(Assignment a, Assignment b) {
    return a.credit == b.credit ? a.ddl > b.ddl : a.credit > b.credit;
}

int find(int x) {
    return !vis[x] ? x : (ft[x] = find(ft[x]));
}

int main() {
    ios;
    int n, max_ = 0, res = 0;
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> assignment[i].ddl >> assignment[i].credit;
        max_ = max(max_, assignment[i].ddl);
    }
    for (int i = 1; i <= max_; ++i) {
        ft[i] = i - 1;
    }
    sort(assignment + 1, assignment + n + 1, cmp);
    for (int i = 1; i <= n; ++i) {
        int father = find(assignment[i].ddl);
        if (!father) continue;
        vis[father] = true;
        res += assignment[i].credit;
    }
    cout << res << endl;
    return 0;
}

J - 钓鱼

原题链接(洛谷)

原题链接(LOJ)

思路

(这题其实是East Central North American 1999的题改的)

这题说的时间都是\(5\)的倍数,我们可以直接忽略这个\(5\),把他们当作一个时间单位,同时注意把题目给的小时换成分钟。

我们使用结构体存下每个池塘初始的 王飞亚 鱼的数量,并且把他们标号。重载小于号以备以后用大根堆维护。接下来我们搜索从第一个到第\(i\)个池塘中能获得的鱼的数量,循环取最大值,由于数据范围小的可怜,所以我们直接暴力循环查找是不会TLE的。大根堆的好处在于,我们可以十分方便地得到最大的鱼数而不需要不断\(sort\),因为题目实际上不要求我们钓鱼的顺序,所以我们在操作的时候可以直接把佳佳小朋友当成能随意tp到任何一个池塘的op玩家。也就是说可以一会在第一个池塘钓鱼然后瞬间到第八个池塘钓鱼之后再回到第二个,而实际操作的时候就是第一个钓\(n\)次之后再去下一个,也就是省略了一个隐含的排序过程,方便我们码出这题。

#include <iostream>
#include <queue>
#include <algorithm>
#define endl '\n'
#define ios ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
using namespace std;

const int N = 110;

struct Paddle {
    int fish, no;

    bool operator<(Paddle x) const {
        return fish < x.fish;
    }
}paddle[N];

int d[N], t[N];
priority_queue<Paddle> q;

int main() {
    ios;
    int n, h, res = 0;
    cin >> n >> h;
    h *= 12;
    for (int i = 1; i <= n; ++i) {
        cin >> paddle[i].fish;
        paddle[i].no = i;
    }
    for (int i = 1; i <= n; ++i) {
        cin >> d[i];
    }
    for (int i = 1; i < n; ++i) {
        cin >> t[i];
    }
    for (int i = 1; i <= n; ++i) {
        h -= t[i - 1];
        int now = 0;
        while (!q.empty()) q.pop();
        for (int j = 1; j <= i; ++j) {
            q.push(paddle[j]);
        }
        for (int j = 1; j <= h; ++j) {
            Paddle s = q.top();
            if (s.fish > 0) {
                now += s.fish;
            }
            s.fish -= d[s.no];
            q.pop();
            q.push(s);
        }
        res = max(res, now);
    }
    cout << res << endl;
    return 0;
}

K - 糖果传递

原题链接[洛谷]

原题链接[LOJ]

思路

乍一看,这题面是真的短,但是有了沈阳站B题的经验,短不代表一定好做。

来研究一下这道题,我们的目的是让所有人均分糖果,但是这题在洛谷从橙色普及-(均分纸牌)升级成了蓝色提高+/省选-,最大的问题在于,这次的小朋友们是环着坐的。

不过问题不大,我们依然可以通过求残差(residual)的方式计算,将残差数组排序后每次让+=当前残差与残差中位数差的绝对值即可。

贪心证明

想必你们也不知道上面的结论是怎么得出的,也不知道为什么这样就是最优解。

我们现在来考虑证明这个贪心结论:

我们把每个小朋友的糖果用\(a_i\)表示,每个小朋友需要向左传递的糖果数量记作\(l_i\)(实际上就是我们的残差),最后每个小朋友手上拿到的糖果数量(平均值)记为\(avg\),我们不难得出这样的一个方程组:

\[\begin{cases} a_i\ +\ l_{i+1}\ -\ l_i\ =\ avg\ (1\leq i < n)\\ a_n\ +\ l_1\ - \ l_n\ = avg \end{cases} \]

处理成对\(l_i\)的递推式如下:

\[\begin{cases} l_{i+1}\ =\ avg\ -\ a_i\ +\ l_i\ (1\leq i < n)\\ l_1\ = avg +\ l_n\ -\ a_n \end{cases} \]

然后我们再对他们代换求和得到:

\[l_i\ =\ (i-1)avg\ -\ \sum_{k = 1}^{i-1}a_k\ + l_1\ (1<i\leq n) \]

我们的目标是使得$$\sum_{i=1}^{n}|l_i|$$最小,代换等价于求:

\[min\{\sum_{i = 1}^{n}| (i-1)avg\ -\ \sum_{k=1}^{i-1}a_k\ +\ l_1| \} \]

这时候要拿出我们的绝对值不等式:

\[|a|\ +\ |b|\ \geq\ |a+b|\ \Rightarrow\ \sum_{i = 1}^{n}|a_i|\ \geq\ |\sum_{i=1}^{n}a_i| \]

等号成立条件是数列均为非负数,所以只要我们的\(l_1\)取得残差数列的中位数就可以成功求得最小值啦。

#include <iostream>
#include <queue>
#include <algorithm>
#define endl '\n'
#define ios ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
using namespace std;
typedef long long LL;

const int N = 1e6 + 10;

LL a[N], residual[N];

int main() {
    ios;
    LL n, avg = 0, res = 0;
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
        avg += a[i];
    }
    avg /= n;
    for (int i = 1; i <= n; ++i) {
        residual[i] = residual[i - 1] + avg - a[i - 1];
    }
    sort(residual + 1, residual + n + 1);
    for (int i = 1; i <= n; ++i) {
        res += abs(residual[(n + 1) >> 1] - residual[i]);
    }
    cout << res << endl;
    return 0;
}

L - 学习轨迹

原题链接

思路

很好,这题看起来我不会写,然后去参考了一下jiangly大佬的代码。十分可惜的是,蒋凌宇是用dp写的这题,代码贴在下面了:

#include <iostream>
#include <vector>
#include <cstring>
#define endl '\n'
#define ios ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
using namespace std;
typedef long long LL;

const LL INF = 1e18;
const int MAX_N = 1000000;

int n, m;
int pos[MAX_N], g[MAX_N], need[MAX_N], covered[MAX_N], perm[MAX_N], succ[MAX_N], pi[MAX_N], seq[MAX_N], ti[2][4 * MAX_N], ps[2][MAX_N];

LL w[MAX_N], dp[MAX_N], wv[MAX_N];
vector<int> vec[MAX_N];

auto work() {
    LL ans = INF;
    for (int i = 0; i < n; ++i) {
        pos[perm[i]] = i;
    }
    memset(covered, 0, n * sizeof(int));
    for (int i = 0; i < n; ++i) {
        if (pos[need[i]] > pos[i]) {
            ++covered[pos[i]];
            --covered[pos[need[i]]];
        }
    }
    for (int i = 1; i < n; ++i) {
        covered[i] += covered[i - 1];
    }
    LL mn = INF, mnp = -1;
    int end = -1;
    for (int i = 0; i < n; ++i) {
        if (2 * (w[perm[i]] - w[perm[0]]) < mn + 3 * w[perm[i]]) {
            dp[i] = 2 * (w[perm[i]] - w[perm[0]]);
            g[i] = -1;
        } else {
            dp[i] = mn + 3 * w[perm[i]];
            g[i] = mnp;
            mnp = i;
        }
        if (i > 0 && covered[i - 1] == 0) {
            if (dp[i] > dp[i - 1] + w[perm[i]] - w[perm[i - 1]]) {
                dp[i] = dp[i - 1] + w[perm[i]] - w[perm[i - 1]];
                g[i] = -2;
            }
            mn = dp[i] - 3 * w[perm[i]];
            mnp = i;
        }
        if (dp[i] + 2 * (w[perm[n - 1]] - w[perm[i]]) < ans) {
            ans = dp[i] + 2 * (w[perm[n - 1]] - w[perm[i]]);
            end = i;
        }
    }
    memset(succ, -1, n * sizeof(int));
    seq[0] = end;
    int tot = 1;
    if (end != n - 1) {
        seq[tot++] = n - 1;
        seq[tot++] = end;
    }
    for (int i = end;;) {
        if (g[i] == -1) {
            if (i != 0) {
                succ[i] = 0;
            }
            break;
        } else if (g[i] == -2) {
            seq[tot++] = i - 1;
            --i;
        } else {
            succ[i] = g[i];
            i = g[i];
        }
    }
    reverse(seq, seq + tot);
    int cnt = 0;
    for (int i = 0; i < tot; ++i) {
        int x = seq[i];
        pi[cnt++] = perm[x];
        for (int j = succ[x]; j != -1; j = succ[j]) {
            pi[cnt++] = perm[j];
        }
    }
    return make_pair(ans, vector<int>(pi, pi + cnt));
}

void rangeModify(int p, int l, int r, int x, int y, int v) {
    if (l >= y || r <= x) return;
    if (l >= x && r <= y) {
        ti[0][p] = min(ti[0][p], v);
        ti[1][p] = max(ti[1][p], v);
        return;
    }
    int mid = (l + r) >> 1;
    rangeModify(p << 1, l, mid, x, y, v);
    rangeModify(p << 1 | 1, mid, r, x, y, v);
}

void query(int p, int l, int r) {
    if (r - l == 1) {
        ps[0][l] = ti[0][p];
        ps[1][l] = ti[1][p];
        return;
    }
    int mid = (l + r) >> 1;
    ti[0][p << 1] = min(ti[0][p << 1], ti[0][p]);
    ti[1][p << 1] = max(ti[1][p << 1], ti[1][p]);
    ti[0][p << 1 | 1] = min(ti[0][p << 1 | 1], ti[0][p]);
    ti[1][p << 1 | 1] = max(ti[1][p << 1 | 1], ti[1][p]);
    query(p << 1, l, mid);
    query(p << 1 | 1, mid, r);
}

void solve() {
    cin >> n >> m;
    for (int i = 0; i < n; ++i) {
        cin >> w[i];
    }
    memset(need, -1, m * sizeof(int));
    for (int i = m; i < n; ++i) {
        cin >> need[i];
        --need[i];
    }
    for (int i = 0; i < n; ++i)
        perm[i] = i;
    sort(perm, perm + n, [&](int i, int j) {
        return w[i] < w[j];
    });
    auto ans = work();
    for (int i = 0; i < n; ++i) {
        w[i] = LL(1e12) - w[i];
    }
    reverse(perm, perm + n);
    auto ans1 = work();
    if (ans1.first < ans.first) {
        ans = ans1;
    }
    memcpy(wv, w, n * sizeof(LL));
    sort(wv, wv + n);
    int cnt = unique(wv, wv + n) - wv;
    for (int i = 0; i < n; ++i) {
        w[i] = lower_bound(wv, wv + cnt, w[i]) - wv;
    }
    memset(ti[0], 0x3f, sizeof(ti[0]));
    for (int i = 0; i + 1 < ans.second.size(); ++i) {
        LL x = w[ans.second[i]];
        LL y = w[ans.second[i + 1]];
        if (x > y) {
            swap(x, y);
        }
        rangeModify(1, 0, cnt, x, y + 1, i);
    }
    query(1, 0, cnt);
    for (int i = 0; i < n; ++i) {
        vec[ps[i >= m][w[i]]].push_back(i);
    }
    for (int i = 0; i + 1 < ans.second.size(); ++i) {
        int t = w[ans.second[i]] < w[ans.second[i + 1]];
        sort(vec[i].begin(), vec[i].end(), [&](int a, int b) {
            return (t ? w[a] < w[b] : w[a] > w[b]) || w[a] == w[b] && a < b;
        });
    }
    cout << ans.first << endl;
    for (int i = 0; i < n; ++i) {
        for (int j: vec[i]) {
            cout << j + 1 << " ";
        }
    }
    cout << endl;
}
int main() {
    ios;
    solve();
    return 0;
}

参考以下题解:题解一号 题解二号

我重新写了一份代码加上注释,并且尽可能改写成能看明白的方式如下:

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#define ios ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
#define endl '\n'
using namespace std;
typedef long long LL;
#define int LL
// 不开LL会寄
const int N = 1e6 + 10;

int father[N], rnk[N], val[N], idx[N], t[N];
// father数组存每个算法的前置算法编号
// rnk数组存最后算法的排序
// val数组存算法权值
// idx数组存每个算法的下标
// t数组维护前缀和序列
vector<int> anc;
int n, m;
LL res = 4e18;

bool cmp(int x, int y) {
    return val[x] < val[y];
}

inline void solve() {
    for (int i = 1; i <= n; ++i) {
        rnk[idx[i]] = i;
        t[i] = 0;
    }
    for (int i = m + 1; i <= n; ++i) {
        if (rnk[i] < rnk[father[i]]) { // i算法的位置比它依赖算法的位置要前,就把差分数组变动
            t[rnk[i]]++;
            t[rnk[father[i]]]--;
        }
    }
    for (int i = 1; i <= n; ++i) {
        t[i] += t[i - 1]; // 差分数组整理为前缀和
    }
    int s = -1, d, j = 0;
    LL vl = 0;
    // vl存放临时结果值,s和d实际上就是临时变量,便于下文处理端点值
    for (int i = 2; i <= n; ++i) {
        // 计算权值,赋值给vl
        LL tp1 = vl + (t[i - 1] ? 2 : 0) * (val[idx[i]] - val[idx[i - 1]]);
        LL tp2 = val[idx[i]] - val[idx[1]];
        if (tp1 < tp2) {
            vl = tp1;
        } else {
            vl = tp2;
            j = i;
        }
        LL now = vl + val[idx[n]] - val[idx[1]] + val[idx[n]] - val[idx[i]];
        // 动态更新结果值
        if (now < res) {
            res = now;
            s = j;
            d = i;
        }
    }
    if (s != -1) {
        if (s == d) { // 优化端点
            s = d = n;
        }
        anc.clear(); // 防止上次数据造成影响
        for (int i = s; i; i--) {
            if (idx[i] <= m) {
                anc.push_back(idx[i]);
            }
        }
        // 将前置算法先倒着输入,再将衍生算法输入
        for (int i = 1; i <= s; i++) {
            if (idx[i] > m) {
                anc.push_back(idx[i]);
            }
        }
        for (int i = d; i < n; ++i) {
            t[i] = 1;
        }
        // 前置算法调整
        for (int i = s + 1; i <= n; i++) {
            if (idx[i] <= m || rnk[father[idx[i]]] < i) {
                anc.push_back(idx[i]);
                if (!t[i]) {
                    for (j = i - 1; i > s && t[j]; j--) {
                        if (idx[j] > m && rnk[father[idx[j]]] > j) {
                            anc.push_back(idx[j]);
                        }
                    }
                }
            }
        }
    }
}

signed main() {
    ios;
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) {
        cin >> val[i];
        idx[i] = i;
    }
    for (int i = m + 1; i <= n; ++i) {
        cin >> father[i];
    }
    // 按照权值排序
    sort(idx + 1, idx + n + 1, cmp);
    solve();
    // 重新计算倒过来的情况,一定不能漏情况哦,否则会像沈阳一样寄。
    for (int i = 1; i <= n; ++i) {
        val[i] = -val[i];
    }
    reverse(idx + 1, idx + n + 1);
    solve();
    cout << res << endl;
    for (int &x : anc) {
        cout << x << " ";
    }
    return 0;
}
posted @ 2022-12-14 21:09  叁纔  阅读(92)  评论(2)    收藏  举报