题解

A - 地毯

标准的二维差分前缀和,定义 \(s_{i,j}\) 为当前格子的权值,然后根据题目模拟题意进行差分求和即可

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e3 + 10, mod = 1e9 + 7;
int s[N][N];
signed main()
{
    std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int n, m; cin >> n >> m;
    for(int i = 1; i <= m; i++){
        int a, b, c, d; cin >> a >> b >> c >> d;
        s[a][b] ++, s[a][d + 1] --, s[c + 1][b] --, s[c + 1][d + 1] ++;
    }
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= n; j++){
            s[i][j] = s[i][j] + s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
            cout << s[i][j] << ' ';
        }
        cout << '\n';
    }
    return 0;
}

B - FBI树

根据题意模拟,首先把节点数变成应该的长度即 \(2^n\),进行两边 \(dfs\),第一遍类似于线段树保存节点以及区间信息,这里注意如果两个儿子相同也有可能是 F,因为有一个是F那么就是F。接下来进行第二遍 \(dfs\),首先判断叶子节点,然后判断左右儿子,最后判断根节点。

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 10, mod = 1e9 + 7;
struct node{
    int l, r;
    char val;
};
signed main()
{
    std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int n; cin >> n;
    n = 1 << n;
    string s; cin >> s;
    s = " " + s;
    vector<node> tr(n * 4 + 1);
    function<void(int, int, int)> dfs1;
    dfs1 = [&](int u, int l, int r) -> void{
        if(l == r){
            if(s[l] == '1') tr[u] = {l, r, 'I'};
            else if(s[l] == '0') tr[u] = {l, r, 'B'};
            else tr[u] = {l, r, 'F'};
            return;
        }
        tr[u] = {l, r};
        int mid = l + r >> 1;
        dfs1(u << 1, l, mid), dfs1(u << 1 | 1, mid + 1, r);
        if(tr[u << 1].val == tr[u << 1 | 1].val){
            if(tr[u << 1].val == 'I') tr[u].val = 'I';
            else if(tr[u << 1].val == 'B') tr[u].val = 'B';
            else tr[u].val = 'F';
        }
        else tr[u].val = 'F';
    };
    function<void(int)> dfs2;
    dfs2 = [&](int u) -> void{
        if(tr[u].l == tr[u].r){
            cout << tr[u].val;
            return;
        }
        dfs2(u << 1), dfs2(u << 1 | 1);
        cout << tr[u].val; 
    };
    dfs1(1, 1, n);
    dfs2(1);
    return 0;
}

C - 牛牛

由于每头牛仰望的都是右边第一头比他高的牛,也就是说求从左向右数第一个递增的数字,那么我们可以考虑使用单调栈,从后往前做一个递增的单调栈,栈中存放的是一个递增牛的编号,也就是在栈中的相邻的两头牛中间隔着的牛们一定不会比这两头牛高,起到一个跳板的作用,若当前牛比栈顶高,则直接pop掉,利用单调栈这个跳板快速跳到下一头比栈顶牛高的牛牛,否则当前牛仰望的就是栈顶的牛,而这个牛一定是右边第一头比他高的牛,然后该牛进栈,依次类推即可

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6+10, mod = 1e9 + 7;
signed main()
{
    int n; cin >> n;
    vector<int> a(n + 1);
    for(int i = 1; i <= n; i++) cin >> a[i];
    stack<int> s;
    vector<int> res(n + 1);
    for(int i = n; i >= 1; i--){
        while(!s.empty() && a[i] >= a[s.top()]) s.pop();
        if(s.empty()) res[i] = 0;
        else res[i] = s.top();
        s.push(i);
    }
    for(int i = 1; i <= n; i++) cout << res[i] << '\n';
    return 0;
}

D - 想玩绝区零

考虑贪心,首先将数组进行排序,因为这样可以找到极值方便处理,要想最小化最大值与最小值之差,那么很明显的要从两边开始删除,那么最后留下的一定是一个连续的区间,枚举这个区间即可,假设我们当前在 \(i\) 位置,那么最终的位置就是 \(i + n - k - 1\) 即: $i + $ 连续区间的长度

\[\min_{1 \le i \le N, i + N - K - 1 \le N} (A_{i + N - K - 1} - A_{i}) \]

直接计算即可,时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10, mod = 1e9 + 7;
signed main()
{
    std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int n, k; cin >> n >> k;
    vector<int> a(n + 1);
    for(int i = 1; i <= n; i++) cin >> a[i];
    sort(a.begin() + 1, a.end());
    if(k >= a.size()) return cout << 0 << '\n', 0;
    int res = 1e10;
    for(int i = 1; i <= k + 1; i++){
        res = min(res, a[i + n - k - 1] - a[i]);
    }
    cout << res << '\n';
    return 0;
}

E - 班德尔城酿班德尔酒

总体看有点难做,直接考虑相邻路径,即细分 \(a ➡ b\) 所经过的经过的城市,由于经过的城市是确定的,那么我们即可利用前缀和来解决情况
对于每条路来说,判断其到相邻的城市所需要的花费,具体情况如图所示:

那么我们直接使用前缀和来维护这两个方向的区间和,对于一个区间来说我们直接判断是从左向右方向还是从右向左方向,随后直接输出其对应的前缀和即可

  • 若对应方向相邻的距离符合第一个条件,那么其花费是 \(1\)
  • 若不满足第一个条件,则花费为 \(|a_{i + 1} - a_i|\)
    这里拿从 \(2 ➡ 4\) 来举例,方向从左向右符合第一个,考虑从 \(2 ➡ 3\),然后考虑从 \(3 ➡ 4\) 即可,这样一步步拆分贪心就可以得到最优答案
    代码中两个数组,\(sum1\) 代表从左向右的前缀和,\(sum2\) 代表从右向左的前缀和
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6+10, mod = 1e9+7;
void solve(){
    int n; cin >> n;
    vector<int> a(n + 10), sum1(n + 10, 0), sum2(n + 10, 0);
    for(int i = 1; i <= n; i++) cin >> a[i];
    a[0] = a[1], a[n + 1] = 1e9;
    for(int i = 1; i < n; i++){
        if(i == 1){
            sum1[i] = 1;
            continue;
        }
        if(abs(a[i] - a[i + 1]) < abs(a[i] - a[i - 1])) sum1[i] = sum1[i - 1] + 1;
        else sum1[i] = sum1[i-1] + abs(a[i] - a[i + 1]);
    }
    for(int i = n; i > 1; i--){
        if(i == n){
            sum2[i] = 1;
            continue;
        }
        if(abs(a[i] - a[i - 1]) < abs(a[i] - a[i + 1])) sum2[i] = sum2[i + 1] + 1;
        else sum2[i] = sum2[i + 1] + abs(a[i - 1] - a[i]);
    }
    int m; cin >> m;
    while(m--){
        int a, b; cin >> a >> b;
        if(a < b) cout << sum1[b - 1] - sum1[a - 1] << '\n';
        else if(a > b) cout << sum2[b + 1] - sum2[a + 1] << '\n';
        else cout << '0' << '\n';
    }
    
}
signed main(){
    std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);int t;cin>>t;while(t--)solve();
}

F - 蒟蒻

归并排序方法

题解中大部分都是二分和双指针,这里给出一个逆序对的题解,其实由于每个工人都有自己的方向且不会变,那么很明显的我们走完路程之后,如果原来标号小的工人走到了标号大的工人的后面那么两者一定相遇,所以可以使用逆序对。

但是有以下几点需要注意:

  • 题目中工人的坐标不是升序的,不要被样例迷惑,一定要先排序
  • 对于坐标相同的,我们按照标号从大到小排序,方便统计。

顺便给出一组样例

5
26
00011
-28 -55 48 -4 89

下面是代码:

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 10, mod = 1e9 + 7;
struct node{
    int x, id, s;
    bool operator<(const node &w) const{
        if(x == w.x) return id > w.id;
        else return x < w.x;
    }
}a[N];
int c[N], res;
bool cmp(node a, node b){
    return a.x < b.x;
}
void merge_sort(int l, int r){
    if(l == r) return;
    int mid = l + r >> 1, i = l, j = mid + 1, num = l;
    merge_sort(l, mid), merge_sort(mid + 1, r);
    while(i <= mid && j <= r){
        if(a[i].id <= a[j].id) c[num++] = a[i++].id;
        else res += mid - i + 1, c[num++] = a[j++].id;
    }
    while(i <= mid) c[num++] = a[i++].id;
    while(j <= r) c[num++] = a[j++].id;
    for(int i = l; i <= r; i++) a[i].id = c[i];
}
signed main()
{
    std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int n, t; cin >> n >> t;
    string s; cin >> s;
    s = " " + s;
    for(int i = 1; i <= n; i++){
        int x; cin >> x;
        a[i] = {x, i, s[i] - '0'};  
    }
    sort(a + 1, a + 1 + n, cmp);
    for(int i = 1; i <= n; i++){
        a[i].id = i;
        if(a[i].s == 1) a[i].x += t;
        else a[i].x -= t;
    }
    sort(a + 1, a + 1 + n);
    merge_sort(1, n);
    cout << res << '\n';
    return 0;
}

二分或双指针方法

我们将蚂蚁进行排序,已知两个工人若同向则不可能相遇,所以我们把工人们分成两组,一组向左一组向,这样有:

  • 假设对于标号 \(x\) 的向左行驶的工人,那么要想向右行驶的工人与其相遇,则两者之差必然小于 \(2 * t\),故在二分查找小于等于其坐标 \(+ 2 * t\) 的向右行驶的工人的标号 \(y\),在这个 \(x\) ~ \(y\) 这个范围内的工人均会相遇到他,即 \(y - x\) 个工人

  • 设双指针 \(l = r = 1\),枚举向右行驶的工人标号 \(i\),若向左行驶的工人标号 \(l\)\(x_i > x_l\),则一直加到 \(x_l > x_i\),同样的对于 \(r\) 来说,我们要求其最多到达不超过 \(x_r - x_i <= 2 * t\) 的位置,这样对于当前工人 \(i\) 所对应的答案为 \(r - l\)

G - 良好的感觉

本题稍微有点怪,本来以为是 \(dp\),虽然列出方程之后

\[dp_i = dp_j + dp2_j * s(i, j) \]

\[dp_i = min(a_i...a_j) * s(a_i...a_j) \]

发现 \(1\)\(i\) 之间的最小值与区间和乘积较难维护,于是考虑贪心是否可行,我们可以直接考虑枚举最小值,这样的枚举次数为 \(O(n)\),定义一个结构体维护信息: 权值 \(val\) 和位置 \(id\),接下来考虑如何去贪心。

考虑由一个最小值组成的答案是一定的,也就是从最小值这个位置向左右扩展,左边和右边直到遇到一个比它小的位置停止,那么明显的,这个区间即是我们钦定这个最小值能做到的最大贡献,统计即可,这样做一定是对的,因为不存在另一个以这个数为最小值的更大区间。

那么我们从小到大排序,每枚举一个数就打上标记,那么我们在左右扩展中遇到标记过的就停止即可,那么一共打上了 \(n\) 个标记,时间复杂度为 \(O(nlogn + n)\),但是由于题目中的数值均不大于 \(10^6\),故我们可以使用桶排序优化到 \(O(n + n)\)

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 10, mod = 1e9 + 7;
struct node{
    int val, id;
    bool operator<(const node &w) const{
        return val < w.val;
    }
}a[N];
int s[N];
bool vis[N];
signed main()
{
    std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int n; cin >> n;
    for(int i = 1; i <= n; i++){
        int x; cin >> x; 
        a[i] = {x, i};
        s[i] = s[i - 1] + a[i].val;
    }
    sort(a + 1, a + 1 + n);
    int res = a[1].val * s[n];
    vis[0] = true, vis[a[1].id] = true;
    for(int i = 2; i <= n; i++){
        int l = a[i].id, r = a[i].id;
        while(l && !vis[l - 1]) l --;
        while(r < n && !vis[r + 1]) r ++;
        res = max(res, a[i].val * (s[r] - s[l - 1]));
        vis[a[i].id] = true;
    }
    cout << res << '\n';
    return 0;
}

H - 切蛋糕

推一下式子,设 \(dp_i\) 为当前在 \(i\) 位置能获得的最大贡献,\(s_i\) 为前缀和,那么有:

\[dp_i = max(s_i - s_j)|j∈[i - m, i - 1] \]

那么发现 \(s_i\) 是对 \(j\) 是没用的,我们可以提出来,那么就有:

\[dp_i = s_i - min(s_{j_1}, s_{j_2}, s_{j_3}....s_{j}) | j∈[i - m, i - 1] \]

那么也就是我们直接维护一个前缀和的静态区间最小值,可以用线段树, \(st\) 表, 树状数组, 单调队列等等,这里就拿我们的学习计划中的单调队列来解决:

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 10, mod = 1e9 + 7;
signed main()
{
    std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int n, m; cin >> n >> m;
    vector<int> a(n + 1), s(n + 1);
    for(int i = 1; i <= n; i++){
        cin >> a[i];
        s[i] = s[i - 1] + a[i];
    }
    vector<int> q(n + 1), dp(n + 1);
    int hh = 0, tt = -1;
    for(int i = 0; i <= n; i++){ // 这里计入 i = 0 的原因是需要统计出 s[0] = 0 这个元素, 因为我们的区间在[i - m, i - 1], 而 i 属于 [1, n], 故 j 可以取 0
        while(hh <= tt && q[hh] + m < i) hh ++;
        while(hh <= tt && s[i] <= s[q[tt]]) tt --;
        q[++tt] = i, dp[i] = q[hh];
    }
    int res = 0;
    for(int i = 1; i <= n; i++){
        res = max(res, s[i] - s[dp[i]]);
    }
    cout << res << '\n';
    return 0;
}
posted @ 2024-07-18 23:06  o-Sakurajimamai-o  阅读(18)  评论(0编辑  收藏  举报
-- --