《信息学奥赛一本通·高手专项训练》集训 Day 0

二叉堆

本来 Day 1 才正式开始,没想到 Day 0 就已经有比赛了,晚上听讲评才知道,于是直接按题解做了题目。

A. 航班延误

题目

目前机场有 nn 趟被延误航班,编号为 11nn。机场只有一条起飞跑道,所有的航班需按某个顺序依次起飞(称这个顺序为起飞序列)。定义一个航班的起飞时间为该航班在起飞序列中的位置,即是第几个起飞的航班。

起飞序列必须满足以下条件:

  • 限制一:编号为 ii 的航班起飞时间不得超过 kik_i
  • 限制二:存在一些相对起飞顺序限制 (a,b)(a,b),表示航班 aa 必须在航班 bb 之前起飞。

有两个问题:

  • 在考虑两类限制条件的情况下,是否可以计算出一个可行的起飞序列。
  • 在考虑两类限制条件的情况下,如何求出每个航班在所有可行的起飞序列中的最小起飞时间。

题解

显然,把 (a,b)(a,b) 当做有向边 aba\rightarrow b,这就是一个点带权 DAG\text{DAG},对于第一问,使用拓扑排序求,要把常用的队列改成优先队列。

对于第二问,建反图,每个点的点权由 kik_i 变为 nkin-k_i,原来每个点的点权表示最晚第 kik_i 个起飞,现在表示最早第 nkin-k_i 个起飞,求原来每个点最早起飞时间等同于求现在每个点最晚起飞时间,我们仍然进行 nn 次拓扑排序,只是要把所求的点尽量往后放,直到不能再放为止。

建反图的好处就是可以保证所求点放的位置是符合要求的,如果是正图就无法考虑到之后的节点。

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;
long long read() {
    long long x = 0, f = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - 48;
        ch = getchar();
    }
    return x * f;
}
void write(long long x) {
    if (x < 0)
        putchar('-'), x = -x;
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}
const int N = 2e3 + 10, M = 1e4 + 10;
int n, m, k[N], in[N], ru[N], ans[N], cnt;
int head[N], ver[M << 1], nxt[M << 1], tot;
void add(int x, int y) {
    ver[++tot] = y;
    nxt[tot] = head[x];
    head[x] = tot;
}
priority_queue<pair<int, int> > q;
int work(int p) {
    while (q.size()) q.pop();
    for (int i = 1; i <= n; i++) {
        in[i] = ru[i];
        if (!in[i])
            q.push(make_pair(k[i] - n, i));
    }
    cnt = 0;
    while (q.size()) {
        int x = q.top().second;
        q.pop();
        if (x == p)
            continue;
        if (n - cnt > k[x])
            return n - cnt;
        cnt++;
        for (int i = head[x]; i; i = nxt[i]) {
            int y = ver[i];
            in[y]--;
            if (!in[y])
                q.push(make_pair(k[y] - n, y));
        }
    }
    return n - cnt;
}
int main() {
    freopen("airport.in", "r", stdin);
    freopen("airport.out", "w", stdout);
    n = read();
    m = read();
    for (int i = 1; i <= n; i++) k[i] = read();
    for (int i = 1; i <= m; i++) {
        int u, v;
        u = read();
        v = read();
        add(v, u);
        ru[u]++;
    }
    for (int i = 1; i <= n; i++) {
        in[i] = ru[i];
        if (!in[i])
            q.push(make_pair(k[i] - n, i));
    }
    while (q.size()) {
        int x = q.top().second;
        q.pop();
        ans[++cnt] = x;
        for (int i = head[x]; i; i = nxt[i]) {
            int y = ver[i];
            in[y]--;
            if (!in[y])
                q.push(make_pair(k[y] - n, y));
        }
    }
    for (int i = n; i >= 1; i--) {
        write(ans[i]);
        putchar(' ');
    }
    puts("");
    for (int i = 1; i <= n; i++) {
        write(work(i));
        putchar(' ');
    }
    puts("");
    return 0;
}

B.环上选数

题目

给定一个环,环上有 nn 个数 AiA_i,要求选出其中不相邻的 mm 个数,使得 mm 个数的和最大。

若不能选出不相邻的 mm 个数,输出 Error!\texttt{Error!}

题解

把所有的 AiA_i 扔进大根堆,选择 mm 次,当选了一个数 AiA_i 后,我们可能会选择 AliA_{l_i}AriA_{r_i}(表示环中 ii 左右的数),注意是一起选的,不可能单独选一个,因为 AiA_i 能被先选说明 AiA_iAliA_{l_i}AriA_{r_i} 大,若只选 AliA_{l_i},那么 Alli,Ai,AriA_{l_{l_i}},A_i,A_{r_i} 均不选,则存在一种在四者中只选 AiA_i 的方案比该方案的和大,对于 AriA_{r_i},同理可证。于是 AliA_{l_i}AriA_{r_i} 若选必须一起选。

选择 AiA_i 后,我们把二者合并,将 Ai,AliA_i,A_{l_i}AriA_{r_i} 从环和堆中删去,将合并后的点放入环和堆中,由于再次选他们后,AiA_i 不能选了,所以合并后的新和应为 Ali+AriAiA_{l_i}+A_{r_i}-A_i

环可以用双链表动态维护。

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;
long long read() {
    long long x = 0, f = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - 48;
        ch = getchar();
    }
    return x * f;
}
void write(long long x) {
    if (x < 0)
        putchar('-'), x = -x;
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}
const int N = 2e5 + 10;
int n, m, a[N], l[N], r[N], ans, v[N];
priority_queue<pair<int, int> > q;
int main() {
    freopen("ring.in", "r", stdin);
    freopen("ring.out", "w", stdout);
    n = read();
    m = read();
    for (int i = 1; i <= n; i++) {
        a[i] = read();
        q.push(make_pair(a[i], i));
    }
    if (n < 2 * m) {
        puts("Error!");
        return 0;
    }
    for (int i = 2; i < n; i++) l[i] = i - 1, r[i] = i + 1;
    l[1] = n;
    r[1] = 2;
    l[n] = n - 1;
    r[n] = 1;
    for (int i = 1; i <= m; i++) {
        while (v[q.top().second]) q.pop();
        pair<int, int> x = q.top();
        q.pop();
        ans += x.first;
        int L = l[x.second], R = r[x.second];
        r[l[L]] = r[L];
        l[x.second] = l[L];
        l[r[R]] = l[R];
        r[x.second] = r[R];
        v[L] = v[R] = 1;
        a[x.second] = a[L] + a[R] - a[x.second];
        x.first = a[x.second];
        q.push(x);
    }
    write(ans);
    return 0;
}

C. 图上删点

题目

给定一张有向无环图(DAG),点数为 nn,边数为 mm

你可以在图中删去一个点,并且删去与该点相连的边,并且得到剩下部分最长路的长度。其中一条路径的长度被定义为这条路径经过的边数。

你需要求出 nn 个删点方案中,剩下部分最长路长度的最小值,以及对应的其中一种删点方案。

题解

将原图拓扑排序,求出以每个点为起点和终点的最长路。这样每条边就覆盖了拓扑序上的一段区间,删点的最长路就是拓扑排上该点前面部分的最长路、后面部分的最长路和横跨删点的最长路,删去一个点就相当于删去了以这个点为右端点的区间,以及以这个点为左端点的最长路,删去后加入这个点就加入了以这个点为左端点的区间和以这个点为右端点的最长路。使用堆动态维护即可,因为堆不能任意删除,所以要加个堆维护已删除的值。

位运算写错调了很久,改完后还是一直 0 分,一直到早上才发现原来这 SPJ 答案的文末必须要换行,和 UVA 一样坑人……

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;
long long read() {
    long long x = 0, f = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - 48;
        ch = getchar();
    }
    return x * f;
}
void write(long long x) {
    if (x < 0)
        putchar('-'), x = -x;
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}
const int N = 5e5 + 10, M = 1e6 + 10;
int n, m, in[N], f[N], g[N], topo[N], t, ans0, ans1 = 1e9;
int head[N], ver[M << 1], nxt[M << 1], tot = 0;
void add(int x, int y) {
    ver[++tot] = y;
    nxt[tot] = head[x];
    head[x] = tot;
}
queue<int> q;
priority_queue<int> h1, h2;
int main() {
    freopen("graph.in", "r", stdin);
    freopen("graph.out", "w", stdout);
    n = read();
    m = read();
    for (int i = 1; i <= m; i++) {
        int u, v;
        u = read();
        v = read();
        add(u, v);
        add(v, u);
        in[v]++;
    }
    for (int i = 1; i <= n; i++)
        if (!in[i])
            q.push(i);
    while (q.size()) {
        int x = q.front();
        q.pop();
        topo[++t] = x;
        for (int i = head[x]; i; i = nxt[i]) {
            if ((i ^ 1) & 1)
                continue;
            int y = ver[i];
            in[y]--;
            if (!in[y])
                q.push(y);
        }
    }
    for (int i = n; i >= 1; i--) {
        int x = topo[i];
        for (int j = head[x]; j; j = nxt[j]) {
            if (j & 1)
                continue;
            int y = ver[j];
            f[y] = max(f[y], f[x] + 1);
        }
    }
    for (int i = 1; i <= n; i++) {
        int x = topo[i];
        for (int j = head[x]; j; j = nxt[j]) {
            if ((j ^ 1) & 1)
                continue;
            int y = ver[j];
            g[y] = max(g[y], g[x] + 1);
        }
    }
    for (int i = 1; i <= n; i++) h1.push(f[i]);
    for (int i = 1; i <= n; i++) {
        int x = topo[i];
        h2.push(f[x]);
        for (int j = head[x]; j; j = nxt[j]) {
            if (j & 1)
                continue;
            int y = ver[j];
            h2.push(g[y] + 1 + f[x]);
        }
        while (h1.size() && h2.size() && h1.top() == h2.top()) h1.pop(), h2.pop();
        if (h1.size() && h1.top() < ans1) {
            ans1 = h1.top();
            ans0 = x;
        }
        h1.push(g[x]);
        for (int j = head[x]; j; j = nxt[j]) {
            if ((j ^ 1) & 1)
                continue;
            int y = ver[j];
            h1.push(g[x] + 1 + f[y]);
        }
    }
    write(ans0);
    putchar(' ');
    write(ans1);
    putchar('\n');
    return 0;
}
posted @ 2022-08-01 07:49  luckydrawbox  阅读(70)  评论(0)    收藏  举报  来源