2.21 L5-NOIP训练23 测试题解

L5-NOIP训练23 - 比赛 - 码创未来

A. 嚎叫响彻在贪婪的厂房(gcd)

题目描述

机器人的哀嚎传遍了整座工厂,于是,Hobo 决定带着他们一起逃离这里。工厂的传送带上依次排列着 \(n\) 个机器人,其中,第 \(i\) 个机器人的质量为 \(a_i\)。Hobo 经过仔细观察,发现:

  1. 来自同一个家族的机器人, 在这 \(n\) 个机器人中一定是连续的一段。

  2. 如果从第 \(i\) 个机器人到第 \(j\) 个机器人都来自同一个家族,那么 \(a_i\)\(a_j\) 从小到大排序后一定是公差大于 \(1\) 的等差数列的子序列。

Hobo 发现,不同家族的个数越少,机器人就会越团结,成功逃离工厂的概率就会越高。Hobo 想知道,这 \(n\) 个机器人最少来自几个不同的家族呢?

简要题意

给定一个序列 \(a_1,a_2,\dots,a_n\),把它划分成若干段,使得每一段中的数都是一个公差大于 \(1\) 的等差数列的一部分(可以不连续或不按顺序),求最少能划分成几段。单独一个数也可以成为一段。

如 1 5 11 2 6 4 7 中,1 5 11 是等差数列 {1, 3, 5, 7, 9, 11} 的子序列,2 4 6 是等差数列 {2, 4, 6, 8} 的子序列,7 是等差数列 {7, 9, 11} 的子序列。

对于 \(100\%\) 的数据,满足 \(n\le10^5\)\(1\le a_i\le10^9\)

思路

首先,我们发现如果一个子段 \(a_i,\dots,a_j\) 合法,那么一定满足 \(\gcd(a_{i+1}-a_i,a_{i+2}-a_{i+1},\dots,a_j-a_{j-1})\ge2\),于是处理出差分数组,这样比较好表示。

并且对于相邻两个极大的合法子段,它们所被包含的两个等差数列的公差 \(d_1,d_2\)\(\gcd\) 一定为 \(1\),否则就可以合并成一段(以 \(\gcd(d_1,d_2)\) 为公差)。

而且,如果连续几个数可以被同时包含在两个子段里,那么其实被包含在哪一个子段里是无关紧要的。如 \(1,5,3,7,13,16,4\) 中,\(7,13\) 可以被包含在前面或者后面,即 \(\{1,5,3,7,13\},\{16,4\}\)\(\{1,5,3\},\{7,13,16,4\}\)\(\{1,5,3,7\},\{13,16,4\}\)。但是答案是相同的,都是分成 \(2\) 个子段。

因此我们可以贪心地使当前的子段增大,直到子段的差分数组的 \(\gcd\)\(1\),这时则使这最后一个数划分到下一个子段。

还有一个问题就是一个子段中出现的数字不能有重复。我这里用 std::unordered_map 解决。

代码

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <unordered_map>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define FILENAME "factory"
#define gcd __gcd
#define abs __builtin_abs
using namespace std;
int const N = 1e5 + 10;
int n, a[N], b[N], ans = 1;
unordered_map<int ,bool> ck;

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    // freopen(FILENAME".in", "r", stdin);
    // freopen(FILENAME".out", "w", stdout);
    
    cin >> n;
    int t = 0;
    f(i, 1, n) cin >> a[i], b[i] = a[i] - a[i - 1];
    ck[a[1]] = true;
    f(i, 2, n) {
        int g = gcd(b[i], t);
        g = abs(g);
        if (g == 1 || ck[a[i]]) {
            ++ans;
            t = 0;
            ck.clear();
            ck[a[i]] = true;
        } else t = g, ck[a[i]] = true;
    }
    cout << ans << '\n';
    
    return 0;
}

B. 主仆见证了 Hobo 的离别(建树,特殊性质)

题目描述

Eddie 终于意识到了改造计划的本质,恨自己没能阻止这一切的发生。在 Millie 和 Simon 的帮助下,他拆开了 Hobo 的电路板,决定帮助 Hobo 找回记忆。

一开始,Hobo 的中央处理器有 \(n\) 个元件(编号为 \(1\)\(n\)),每一个元件都存储了一段时间的记忆。也就是说,每个元件都可以看做一个集合。

为了唤醒 Hobo 的回忆,他会时不时地在当前的所有元件中选择 \(k\) 个进行一次融合,组成一个新的元件,新元件的编号等于融合之前元件的总个数加一。

当然,参与融合的 \(k\) 个元件融合之后依然存在,并且 每个元件至多参与一次融合

由于元件的容量有限,Eddie 没有能力唤醒 Hobo 全部的回忆,所以他会用下列两种方式来融合元件:

  1. 集合的交:一段记忆存储在新的元件中,当且仅当这段记忆在参与融合的 \(k\) 个元件中都有储存。

  2. 集合的并:一段记忆存储在新的元件中,当且仅当这段记忆在参与融合的至少一个元件中有储存。

在融合元件的过程中,Eddie 迫切地想知道:凡是存储在 \(x\) 号元件中的记忆,是否一定也存储在 \(y\) 号元件中?

简要题意

现在有编号为 \(1\)\(n\)\(n\) 个集合。我们不关心集合中的具体元素,只关心集合之间的包含关系。

对这些集合进行 \(m\) 次操作或查询。操作可能是「交」或者「并」。查询即为:查询集合 \(x\) 是否一定为集合 \(y\) 的子集。

对于每次操作,选定 \(k\) 个集合,生成它们的「交」或者「并」作为新的集合,添加到原来的集合序列中,编号设为 \(n+1\),同时令 \(n\gets n+1\)

考试时眼瞎没看见的重要条件:对于所有操作,每个集合至多参与其中一次。「参与」指生成该集合与其他若干集合的「交」或「并」(而不是被生成)。

测试点 \(n,m\)= \(\sum k\le\) 备注
1 2000 2000 \(k=1\)
2 2000 2000 \(k=1\)
3 2000 2000 前 1999 个操作均不含询问操作
4 2000 2000 前 1000 个操作不含询问操作,
后 1000 个操作都是询问操作
5 5000 10000 只存在"集合的交”和询问操作
6 5000 10000 只存在"集合的交”和询问操作
7 5000 10000 只存在"集合的并”和询问操作
8 100000 200000 所有的询问操作中 \(x\le y\)
9 100000 300000 所有的询问操作中 \(x\ge y\)
10 250000 500000

思路

据说暴力也能过??

由于「对于所有操作,每个集合至多参与其中一次」的特殊限制,每个新生成的点的度数一定等于 \(k+1\),其中 \(k\) 为生成这个点时所操作的集合数量。如果这个点是原有的,那么度数小于等于 \(1\)。并且 后面的操作永远不会对前面产生影响

我们发现,向上 \(1\) 条边,向下 \(k\) 条边,这正好符合树的性质。

于是我们离线所有操作,建出 两棵树(或者说森林),分别记录所有交操作和并操作,具体方式为由选定的 \(k\) 个点向新生成的点连无向边。

在交树上,父亲是儿子的子集;在并树上,儿子是父亲的子集。

所以, \(x\)\(y\) 的子集当且仅当:在交树上 \(x\)\(y\) 的祖先,或者在并树上 \(y\)\(x\) 的祖先。

这样问题就转化为了如何在树上查询 \(p\) 是否在 \(q\) 的子树中。

我们知道,树(以及森林)的 dfn 序有一个性质:一个子树中所有节点的 dfn 序都是连续的一段区间。于是我们可以在 dfs 过程中处理出一个子树的 dfn 序范围,然后判断是否在范围内就可以了。

注意特判 \(k=1\) 的情况,此时所操作的集合和所生成的集合相等,在交树和并树上都要连边。

代码

  • LR 数组表示子树的 dfn 序范围,L 数组就是子树根节点的 dfn 序(即最小的),R 数组是子树中最大的 dfn 序。

  • 由于未知原因,if (op1) ++cnt, cin >> q[cnt].x >> q[cnt].y; 这行代码,如果换成 if (op1) cin >> q[++cnt].x >> q[cnt].y;,用 C++ (NOI) [GCC 4.8.4 (NOILinux 1.4.1)] 编译器会 WA 0,但 C++ 17 (Clang) [Clang 7.0.1] 则没问题。

#include <cstdio>
#include <iostream>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define g(x, y, z) for (int x = (y); (x) >= (z); --(x))
#define FILENAME "friendship"
using namespace std;
int const N = 5e5 + 10;
int n, m;

struct Tree {

    int head[N], cnt;
    struct Edge {
        int to, nxt;
    } e[N << 1];
    inline void add(int from, int to) {
        e[++cnt].to = to, e[cnt].nxt = head[from], head[from] = cnt;
        e[++cnt].to = from, e[cnt].nxt = head[to], head[to] = cnt;
        return;
    }

    int L[N], R[N], tot;
    void dfs(int u, int fa) {
        L[u] = ++tot;
        for (int i = head[u]; i; i = e[i].nxt) {
            int v = e[i].to;
            if (v == fa) continue;
            dfs(v, u);
        }
        R[u] = tot;
    }

} t1, t2; //交和并

bool check(int x, int y) {
    if (t1.L[x] <= t1.L[y] && t1.R[x] >= t1.R[y]) return 1;
    if (t2.L[y] <= t2.L[x] && t2.R[y] >= t2.R[x]) return 1;
    return 0;
}

int cnt;
struct Query {
    int x, y;
} q[250010];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    // freopen(FILENAME".in", "r", stdin);
    // freopen(FILENAME".out", "w", stdout);
    
    cin >> n >> m;
    f(i, 1, m) {
        int op1;
        cin >> op1;
        if (op1) ++cnt, cin >> q[cnt].x >> q[cnt].y;
        else {
            int op2, k, a;
            cin >> op2 >> k;
            ++n;
            if (k == 1) cin >> a, t1.add(n, a), t2.add(n, a);
            else if (!op2) f(i, 1, k) cin >> a, t1.add(n, a);
            else f(i, 1, k) cin >> a, t2.add(n, a);
        }
    }
    g(i, n, 1) {
        if (!t1.L[i]) t1.dfs(i, 0);
        if (!t2.L[i]) t2.dfs(i, 0);
    }
    f(i, 1, cnt) cout << check(q[i].x, q[i].y) << '\n';
    
    return 0;
}

C. 征途堆积出友情的永恒(堆优化 DP)

题目描述

为了说服珍娜女王停止改造计划,Eddie 和 Hobo 踏上了去往王宫的征程。Sunshine Empire 依次排列着 \(n+1\) 座城市,\(0\) 号城市是出发地,\(n\) 号城市是王宫。

火车是 Sunshine Empire 的主要交通工具。Eddie 和 Hobo 可以在当前的城市上车,并且在之后的某一座城市下车。从第 \(i-1\) 座城市乘坐到第 \(i\) 座城市需要花费 \(a_i\) 的费用。同时,在第 \(i\) 座城市上车需要缴纳 \(b_i\) 的税款。其中,税款属于额外支出,不属于乘坐这段火车的费用。

珍娜女王为了促进 Sunshine Empire 的繁荣发展,下令:如果连续地乘坐一段火车的费用大于这次上车前所需缴纳的税款,则这次上车前的税款可以被免除,否则免去乘坐这段火车的费用。

然而,为了保证火车的正常运行,每一次乘坐都不能连续经过超过 \(k\) 座城市(不包括上车时所在的城市),并且,Eddie 和 Hobo 的移动范围都不能超出 Sunshine Empire。

Eddie 想知道,到达王宫的最小总花费是多少?

\(100\%\) 的数据满足,\(n\le5\times10^5\)\(k\le10^5\)\(1\le a_i,b_i\le10^5\)

思路

\(f(x)\) 表示从起点 \(0\) 到城市 \(x\) 的最小总花费。初值 \(f(0)=b_0,f(i)=+\infty(i\ge1)\)。答案为 \(f(n)\)

写出状态转移方程

\[f(i)=\min_{i-k\le j\le i-1}\left\{f(j)+\max\left\{b_j,\sum_{p=j+1}^ia_p\right\}\right\}. \]

预处理出 \(a\) 的前缀和 \(s\),则有

\[f(i)=\min_{i-k\le j\le i-1}\left\{f(j)+\max\left\{b_j,s_i-s_j\right\}\right\}. \]

暴力枚举是 \(O(n^2)\),无法通过。我们考虑如何优化。

一眼看上去像是单调队列优化,因为给了窗口范围 \(k\)。不过对于 \(\max\) 值的选择则不好处理。

由于最外层取 \(\min\),我们可以用小根堆来优化,快速进行决策。

由于 \(\forall\ i\ge1,a_i>0\),所以前缀和数组 \(s\) 是单调递增的。于是我们在发现某个时刻 \(f_j+b_j\) 已经小于 \(f_j+s_i-s_j\) 时,一定会将前者舍弃,用后者进行决策。

我们建立两个小根堆,在第一个中放入之前所有的 \(f_j+b_j\),在第二个中放入所有某时刻满足 \(f_j+b_j<f_j+s_i-s_j\)\(f_j-s_j\)

尝试用两个堆的堆顶更新答案,如果堆顶对应的 \(j\) 不满足 \(i-k\le j\le i-1\),那么就弹出堆顶,舍去 \(j\),继续尝试。

如果第一个堆顶对应的 \(j\) 满足 \(f_j+b_j<f_j+s_i-s_j\),就把第一个堆顶弹出,同时把 \(f_j-s_j\) 压入第二个堆。

直到两个堆都不需要弹出堆顶,我们把两个堆顶取 \(\min\),更新答案。

代码

(别忘了开 long long

#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define FILENAME "empire"
#define int ll
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
int const N = 5e5 + 10;
int n, k, a[N], b[N], s[N], f[N];
priority_queue< pii, vector<pii>, greater<pii> > q1, q2;

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    // freopen(FILENAME".in", "r", stdin);
    // freopen(FILENAME".out", "w", stdout);
    
    cin >> n >> k;
    f(i, 1, n) cin >> a[i], s[i] = s[i - 1] + a[i];
    f(i, 0, n - 1) cin >> b[i];
    memset(f, 0x3f, sizeof f);
    f[0] = 0;
    q1.emplace(b[0], 0);
    f(i, 1, n) {
        while (!q1.empty()) {
            pii t = q1.top();
            int j = t.second;
            if (j >= i - k) {
                if (t.first <= f[j] + s[i] - s[j])
                    q2.emplace(f[j] - s[j], j);
                else break;
            }
            q1.pop();
        }
        while (!q2.empty()) {
            if (q2.top().second >= i - k) break;
            q2.pop();
        }
        if (!q1.empty()) f[i] = min(f[i], q1.top().first);
        if (!q2.empty()) f[i] = min(f[i], q2.top().first + s[i]);
        q1.emplace(f[i] + b[i], i);
    }
    cout << f[n] << '\n';
    
    return 0;
}
posted @ 2023-02-23 09:36  f2021ljh  阅读(113)  评论(0)    收藏  举报