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;
}
浙公网安备 33010602011771号