T1. 青蛙棋

皮皮制作了一个玩具叫做青蛙棋,它是一个单人的益智棋类游戏,青蛙棋的棋盘只有一行,共 \(n\) 个格子,从左到右编号从 \(1 \sim n\),青蛙从 \(1\) 号格子出发,需要到达
\(n\) 号格子才算胜利。
青蛙棋只能进行跳跃,第一次只能跳一格,跳到2号格子,接下来的跳跃必须满足以下条件:如果它向右跳,那么每次必须比上一次多跳一个格子;如果它向左跳,那么每次必须和上一次的跳跃距离完全相同。
例如,当青蛙第一次跳到 \(2\) 号格子后,下一步它可以跳到 \(4\) 号或者 \(1\) 号格子。
棋盘上每个格子都有费用,当青蛙跳到第 \(i\) 个格子时,需要支付 \(a_i\) 的费用,因此玩家需要在到达 \(n\) 号格的前提下,尽可能少花钱。请你求出这个最小值。

限制:

  • \(1 \leqslant n \leqslant 1000\)
  • \(1 \leqslant a_i \leqslant 500\)

算法分析

假设 \(f(x, d)\) 表示当前在 \(x\) 号格,上一次走的长度为 \(d\) 的状态到达 \(n\) 号格的最小花费
下一次可以向左走,走到 \(f(x-d,d)\);或向右走,走到 \(f(x+d+1, d+1)\)
记忆化搜索即可

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;

int main() {
    int n;
    cin >> n;
    
    vector<int> a(n);
    rep(i, n) cin >> a[i];
    
    vector<vector<int>> memo(n, vector<int>(n));
    auto dfs = [&](auto& f, int x, int d) -> int {
        if (x < 0 or x >= n) return 1e9;
        if (x == n-1) return a[x];
        if (memo[x][d]) return memo[x][d];
        return memo[x][d] = min(f(f, x-d, d), f(f, x+d+1, d+1)) + a[x];
    };
    
    int ans = dfs(dfs, 1, 1);
    cout << ans << '\n';
    
    return 0;
}

T2. 庄园

皮皮有一个庄园,我们可以把他的庄园理解为一个 \(n×m\) 的矩阵。
由于他太久没有打理庄园,庄园的田地现在长满了杂草,他现在想要把杂草清理干净。他可以做两种操作,分别是:

  • 使用法术把草的高度从 \(x_i\) 变成 \(y_i\),消耗的体力值为 \(z_i\),法术可以使用无限次
  • 使用镰刀把草的高度从 \(x\) 变成 \(x−1\),消耗的体力值为 \(1\)

他现在需要修剪他的草地,把草地中草的高度全部变为 \(1\),皮皮现在想要知道他最少需要消耗的体力值是多少。

数据保证一定有一种方法可以把草地中草的高度变为 \(1\)

限制:

  • \(1 \leqslant n, m \leqslant 100\)
  • \(1 \leqslant v \leqslant 25000\)
  • \(1 \leqslant x_i, y_i \leqslant 300\)
  • \(1 \leqslant z_i \leqslant 10^9\)
  • \(1 \leqslant a_{i,j} < 10^9\)

算法分析

对于 \(60\) 分做法,floyd算出任意两个高度变化的最小体力
对于 \(100\) 分做法,可以把高度用镰刀割到 \(300\) 以内,然后再考虑它们变成 \(1\) 的最小代价

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using ll = long long;

inline void chmin(int& x, int y) { if (x > y) x = y; }

int main() {
    int n, m, v;
    cin >> n >> m >> v;
    
    vector<vector<int>> a(n, vector<int>(m));
    rep(i, n)rep(j, m) cin >> a[i][j], a[i][j]--;
    
    const int M = 305;
    const int INF = 1001001001;
    vector<vector<int>> f(M, vector<int>(M, INF));
    int s = 0;
    rep(i, v) {
        int x, y, z;
        cin >> x >> y >> z;
        --x; --y;
        chmin(f[x][y], z);
        s = max(s, max(x, y));
    }
    
    rep(i, s+1)rep(j, i) chmin(f[i][j], i-j);
    rep(k, s+1)rep(i, s+1)rep(j, s+1) chmin(f[i][j], f[i][k]+f[k][j]);
    
    ll ans = 0;
    rep(i, n)rep(j, m) {
        int now = a[i][j];
        rep(k, min(a[i][j], s)+1) {
            chmin(now, a[i][j]-k+f[k][0]);
        }
        ans += now;
    }
    
    cout << ans << '\n';
    
    return 0;
}

T3. 银行排队

皮皮通过玩具赚了很多钱,因此他去银行升级自己的账户。当他办理完业务后,发现 \(M\) 个柜台都排了好多的人,由于皮皮今天特别无聊,于是决定观察大家的排队方法。

在皮皮开始观察的时候,所有柜台前一共有 \(K\) 个人在排队或办理业务,办理业务的人有一个剩余办理业务时间,排队的人有一个预计办理业务时间。

在接下来的时间里,一共进来了 \(N\) 个人,其中第 \(i\) 个人是在皮皮开始观察后的第 \(X_i\) 秒进入银行的。每个人都会选择人数最少的一个队伍。如果有多个队伍人数一样,则会选择编号较小的一个。

每一个人会有性别、年龄、预计办理业务时间等信息。如果有多个人同时进来,女士优先选择队伍。性别相同时年龄小的优先选择队伍,年龄相同时办理业务时间较短的优先选择队伍。

我们默认一个人办完业务离开队列是不花时间的,也就是说每个时刻都是办理完业务的人先出队,然后新的顾客才到来。

皮皮认为,即使每个顾客都按照这个规律,也会出现有些柜台排队时间明显更久的情况,为了证明他的想法是正确的,你需要帮他求出每个柜台的最后一个人办理完业务的具体时间(皮皮开始观察的时间记为 \(0\))。

限制:

  • \(1 \leqslant N, K \leqslant 2 \times 10^5\)
  • \(1 \leqslant M \leqslant 10^5\)
  • \(1 \leqslant X_i \leqslant 10^6\)
  • \(1 \leqslant T_i \leqslant 1000\)
  • \(1 \leqslant A_i \leqslant 100\)

算法分析

对于 \(70\) 分做法,可以每次用循环去找最短的队伍
对于 \(100\) 分做法,可以使用小根堆将这个循环去掉

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using P = pair<int, int>;

struct People {
    int x, t, a;
    char e;
    People() {}
    People(int x, int t, int a, char e): x(x), t(t), a(a), e(e) {}
    bool operator<(const People& o) const {
        if (x != o.x) return x < o.x;
        if (e != o.e) return e == 'F';
        if (a != o.a) return a < o.a;
        return t < o.t;
    }
};

int main() {
    cin.tie(nullptr) -> sync_with_stdio(false);
    
    int n, m, k;
    cin >> n >> m >> k;
    
    // sum[i]: 记录第 i 个队伍此时的人数
    // tim[i]: 记录第 i 个队伍最后一个完成业务的时间
    vector<int> sum(m), tim(m);
    priority_queue<P, vector<P>, greater<P>> q1, q2; // q1: 最早完成业务的人 q2:当前最短的时间
    rep(i, k) {
        int c, t;
        cin >> c >> t;
        --c;
        sum[c]++;
        tim[c] += t;
        q1.emplace(tim[c], c);
    }
    rep(i, m) q2.emplace(sum[i], i);
    
    vector<People> ps(n);
    rep(i, n) cin >> ps[i].x >> ps[i].t >> ps[i].a >> ps[i].e;
    
    sort(ps.begin(), ps.end());
    
    rep(i, n) {
        while (q1.size()) { // 把第 i 个人进来之前完成业务的人都踢掉
            auto [t, c] = q1.top();
            if (t <= ps[i].x) {
                q1.pop();
                sum[c]--;
                q2.emplace(sum[c], c);
            }
            else break;
        }
        int c = q2.top().second; // 找到最短队伍的编号
        while (sum[c] != q2.top().first) {
            q2.pop();
            c = q2.top().second;
        }
        q2.pop();
        sum[c]++;
        // 如果人都走完了,开始时间就是 ps[i].x
        tim[c] = max(tim[c], ps[i].x) + ps[i].t;
        q1.emplace(tim[c], c);
        q2.emplace(sum[c], c);
    }
    
    rep(i, m) cout << tim[i] << '\n';
    
    return 0;
}

T4. 楼顶跳跃

一条东西向道路旁有 \(N\) 个建筑,以某个点为坐标原点,从左到右第 \(i\) 个建筑(以下简称建筑 \(i\))坐标为 \(X_i\),高度为 \(H_i\)

当你处于建筑 \(i\) 的楼顶时,你能移动到建筑 \(j\) 的楼顶,当且仅当这两个建筑之间不存在高度超过 \(\max(H_i,H_j)\) 的建筑。严谨地来说,当 \(i<j\) 时不存在 \(i<k<j\) 使得 \(H_k > \max(H_i,H_j)\);当 \(i>j\) 时不存在 \(j<k<i\) 使得 \(H_k > \max(H_i,H_j)\)

你从建筑 \(i\) 楼顶移动到建筑 \(j\) 楼顶需要花费体力 \(∣X_i−X_j∣×∣H_i−H_j∣\)

给出 \(S\),对于 \(i=1,2,⋯,N\),求从建筑 \(S\) 楼顶移动到建筑 \(i\) 楼顶所需的最少体力。

限制:

  • \(1 \leqslant N \leqslant 2 \times 10^5\)
  • \(1 \leqslant X_1 < X_2 < \cdots < X_N \leqslant 10^9\)
  • \(1 \leqslant H_i \leqslant 10^9\)

算法分析

\(10\%\)\(N \leqslant 16\),记忆化搜索
\(20\%\)\(N \leqslant 500\),Floyd
\(30\%\)\(N \leqslant 4000\),Dikstra,注意这个图是近似完全图,不要用堆优化
另外 \(10\%\)\(H\) 递增,每次只会移动到相邻的楼顶,线性递推,\(O(N)\)
另外 \(25\%\),左右横跳,启发正解
\(100\%\),在楼顶 \(i\),往高处跳,或往低处跳
\(i\) 跳到 \(j\)\(\Leftrightarrow\) \(i, j\) 之间不存在中间高度的建筑
从楼顶 \(i\) 往高处跳,只会跳到左边或右边第一个高度大于 \(j\) 的楼顶,连双向边即可
从楼顶 \(i\) 往低处跳,若跳到 \(j\),则 \(i\)\(j\) 左/右侧第一个高度大于 \(j\) 的楼顶,无需考虑
然后在这个图里面跑最短路即可
时间复杂度为 \(O(N\log N)\)

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using ll = long long;
using P = pair<ll, int>;

struct Edge {
    int to; ll co;
    Edge(int to, ll co): to(to), co(co) {}
};

int main() {
    cin.tie(nullptr) -> sync_with_stdio(false);
    
    int n, s;
    cin >> n >> s;
    --s;
    
    vector<int> x(n);
    rep(i, n) cin >> x[i];
    
    vector<int> h(n);
    rep(i, n) cin >> h[i];
    
    stack<int> stk;
    vector<vector<Edge>> g(n); 
    rep(i, n) { // 维护单调递减的栈
        while (stk.size() and h[stk.top()] < h[i]) {
            int j = stk.top(); stk.pop();
            ll co = (ll)abs(x[i]-x[j])*abs(h[i]-h[j]);
            g[i].emplace_back(j, co);
            g[j].emplace_back(i, co);
        }
        if (stk.size()) {
            int j = stk.top();
            ll co = (ll)abs(x[i]-x[j])*abs(h[i]-h[j]);
            g[i].emplace_back(j, co);
            g[j].emplace_back(i, co);
        }
        stk.push(i);
    }
    
    const ll INF = 1e18;
    vector<ll> dist(n, INF);
    priority_queue<P, vector<P>, greater<P>> q;
    auto push = [&](int v, ll d) {
        if (dist[v] <= d) return;
        dist[v] = d;
        q.emplace(d, v);
    };
    push(s, 0);
    while (q.size()) {
        auto [d, v] = q.top(); q.pop();
        if (dist[v] != d) continue;
        for (auto [u, w] : g[v]) {
            push(u, d+w);
        }
    }
    
    rep(i, n) cout << dist[i] << ' ';
    
    return 0;
}

T5. 士兵排队

\(N\) 名士兵排成一列进行队列训练,第 \(i\) 名士兵身高为 \(H_i\)
为了更高效地训练士兵,军官对士兵发出了 \(M\) 次命令。第 \(i\) 次命令可以使用整数 \(A_i\) 来描述。

  • 队列中所有身高不超过 \(A_i\) 的士兵出队,按照原来的顺序排成一排。
  • 出列的士兵通过交换按照身高从小到大排成一排,为了保证美观性,不至于出现混乱,每次只能交换相邻的两个士兵。
  • 最后出列的士兵按照原来的顺序回到队列中。

你作为监督员,需要监督士兵按质按量完成任务,你想要统计士兵是否进行了冗余的交换操作,因此你需要计算出每次命令下达之后士兵最少需要进行多少次交换。

限制:

  • \(1 \leqslant N \leqslant 5 \times 10^5\)
  • \(1 \leqslant M \leqslant N\)
  • \(1 \leqslant H_i \leqslant 10^9\)

算法分析

\(10\%\)\(N \leqslant 100\),拿出身高 \(\leqslant A_i\) 的士兵,冒泡排序的交换次数 \(O(N^3)\)
\(30\%\)\(N \leqslant 10^3\),拿出身高 \(\leqslant A_i\) 的士兵,归并排序/树状数组求逆序对 \(O(N^2\log N)\)
更好的做法:考虑 \(i < j\),若 \(H_i \leqslant H_j\),任何时候不会产生逆序对;若 \(H_i > H_j\),则当第 \(i, j\) 个士兵同时出队时会产生逆序对,也就是若 \(A \geqslant H_i\),会产生一个逆序对,相当于是一个后缀和,可以差分,然后用前缀和来还原
\(H_i\) 可能很大:

  • \(H\) 离散化 \(O(N^2)\)

  • map \(O(N^2\log N)\)

另外 \(20\%\),特殊 \(A\)\(H_1 \sim H_N\)\(1 \leqslant A_i \leqslant N\) ,不必离散化了
特殊 \(B\):每次出队必然是一个前缀,记 ans[i] 表示 \(H_1 \sim H_i\) 的逆序对数量
\(ans[i] = ans[i-1] + H_1 \sim H_{i-1}\) 中大于 \(H_i\) 的元素数量
\(H_1 \sim H_{i-1}\) 中大于 \(H_i\) 的元素数量是非常经典的树状数组 \(O(N\log N)\)
另外 \(20\%\),特殊 \(B\):将 \(H_1 \sim H_N\) 全部离散化,\(H \to H'\) 离散化后
\(1 \sim K(K \leqslant N)\)
\(ans[i] = ans[i-1] + H_1’ \sim H_{i-1}’\) 中大于 \(H_i'\) 的元素数量,可以用树状数组做到 \(O(N\log N)\)
\(100\%\)

考虑 \(i < j\),若 \(H_i \leqslant H_j\),任何时候不会产生逆序对;若 \(H_i > H_j\),则当第 \(i, j\) 个士兵同时出队时会产生逆序对,也就是若 \(A \geqslant H_i\),会产生一个逆序对,相当于是一个后缀和,可以差分,然后用前缀和来还原

考虑这个东西能不能优化

枚举 \(i\),对于 \(j = i+1, i+2, \cdots, N\) 一起处理
对于 \(H_i < H_j\),无事发生
对于 \(H_i > H_j\),每一对会使 \(A \geqslant H_i\) 时产生一个逆序对,总共产生等同于 \(H_i > H_j\)\(j\) 数量个逆序对
ans[Hi] += j数量,离散化之后树状数组

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using ll = long long;

// Coodinate Compression
template<typename T=int>
struct CC {
  bool initialized;
  vector<T> xs;
  CC(): initialized(false) {}
  void add(T x) { xs.push_back(x);}
  void init() {
    sort(xs.begin(), xs.end());
    xs.erase(unique(xs.begin(),xs.end()),xs.end());
    initialized = true;
  }
  int operator()(T x) {
    if (!initialized) init();
    return upper_bound(xs.begin(), xs.end(), x) - xs.begin() - 1;
  }
  T operator[](int i) {
    if (!initialized) init();
    return xs[i];
  }
  int size() {
    if (!initialized) init();
    return xs.size();
  }
};

template<typename T>
struct BIT {
  int n;
  vector<T> d;
  BIT(int n=0):n(n),d(n+1) {}
  void add(int i, T x=1) {
    for (i++; i <= n; i += i&-i) {
      d[i] += x;
    }
  }
  T sum(int i) {
    T x = 0;
    for (i++; i; i -= i&-i) {
      x += d[i];
    }
    return x;
  }
  T sum(int l, int r) {
    return sum(r-1) - sum(l-1);
  }
};

int main() {
    int n, m;
    cin >> n >> m;
    
    vector<int> h(n);
    rep(i, n) cin >> h[i];
    
    CC cc;
    rep(i, n) cc.add(h[i]);
    rep(i, n) h[i] = cc(h[i]);
    int k = cc.size();
    
    vector<ll> ans(k);
    BIT<int> t(k);
    for (int i = n-1; i >= 0; --i) {
        ans[h[i]] += t.sum(h[i]-1);
        t.add(h[i]);
    }
    
    rep(i, k-1) ans[i+1] += ans[i];
    
    rep(i, m) {
        int x;
        cin >> x;
        x = cc(x);
        if (x == -1) cout << 0 << '\n';
        else cout << ans[x] << '\n';
    }
    
    return 0;
}