NOI 2010 做题笔记
NOI 2010 Day1 T1 能量采集
观察到 \((0, 0)\) 与 \((x, y)\) 连线上的整点个数正好是 \(\gcd(x, y) - 1\)(不包括端点),于是总能量损失即为:
考虑如何计算二维 \(\gcd\) 求和,根据 \(\varphi * 1 = \text{id}\),反演得:
于是线性筛出 \(\varphi\) 即可,复杂度 \(O(n)\)。
#include <bits/stdc++.h>
using i64 = long long;
constexpr int N = 1e5 + 5;
int n, m, tot;
int prime[N], phi[N];
bool isp[N];
void sieve() {
memset(isp, true, sizeof(isp));
isp[1] = false; phi[1] = 1;
for (int i = 2; i < N; i++) {
if (isp[i]) prime[++tot] = i, phi[i] = i - 1;
for (int j = 1; j <= tot && 1ll * i * prime[j] < N; j++) {
int p = prime[j];
isp[i * p] = false;
if (i % p == 0) {
phi[i * p] = phi[i] * p;
break;
}
else phi[i * p] = phi[i] * (p - 1);
}
}
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
sieve();
std::cin >> n >> m;
i64 ans = 0;
for (int d = 1; d <= std::min(n, m); d++)
ans += 1ll * phi[d] * (n / d) * (m / d);
std::cout << 2 * ans - 1ll * n * m << "\n";
return 0;
}
NOI 2010 Day1 T2 超级钢琴
先考虑 \(k = 1\) 的情况,此时相当于求长度在 \([l, r]\) 范围内的最大子段和,不难发现对于一个左端点 \(i\),右端点的合法范围即为 \([i + l - 1, i + r - 1]\),所以只需求出该范围内的前缀和最大值即可得出左端点 \(i\) 对应的最大子段和。
不妨将其推广,刚开始对于每个左端点 \(i\),将四元组 \((val, l, r, i)\) 放进堆里,表示左端点 \(i\) 在右端点范围为 \([l, r]\) 时的最大子段和为 \(val\)。当取出一个四元组后,我们令 \(t\) 为让子段和取到最大值的右端点,然后将其分裂为 \((val_l, l, t - 1, i)\) 和 \((val_r, t + 1, r, i)\) 再放回堆中,不难发现这样第 \(k\) 次取出的 \(val\) 就是第 \(k\) 大。对于区间最大值及其位置,使用 ST 表维护即可。
由于每次堆内元素最多增加一个,所以总时间复杂度为 \(O(n\log n + k\log n)\)。
#include <bits/stdc++.h>
using i64 = long long;
constexpr int N = 5e5 + 5;
template <typename T>
struct ST {
static constexpr int N = 5e5 + 5, LOG = 20;
int n;
T st[LOG][N];
std::function<T(T, T)> op;
ST() {}
ST(int _n) { init(_n); }
void init(int _n) {
n = _n;
for (int i = 1; i <= n; i++) st[0][i] = i;
for (int j = 1; j <= std::__lg(n); j++)
for (int i = 1; i + (1 << j) - 1 <= n; i++)
st[j][i] = op(st[j - 1][i], st[j - 1][i + (1 << (j - 1))]);
}
inline T query(int l, int r) {
int k = std::__lg(r - l + 1);
return op(st[k][l], st[k][r - (1 << k) + 1]);
}
};
struct Node {
int x, i, l, r;
bool operator < (const Node &rhs) const { return x < rhs.x; }
};
int n, k, L, R;
int a[N], sum[N];
ST<int> stMax;
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
stMax.op = [&](int lhs, int rhs) { return sum[lhs] > sum[rhs] ? lhs : rhs; };
std::cin >> n >> k >> L >> R;
for (int i = 1; i <= n; i++) {
std::cin >> a[i];
sum[i] = sum[i - 1] + a[i];
}
stMax.init(n);
std::priority_queue<Node> q;
for (int i = 1; i <= n; i++) {
int l = L + i - 1, r = std::min(n, R + i - 1);
if (l > r) continue;
q.push({sum[stMax.query(l, r)] - sum[i - 1], i, l, r});
}
i64 ans = 0;
while (k--) {
int x = q.top().x, i = q.top().i, l = q.top().l, r = q.top().r;
ans += x; q.pop();
int p = stMax.query(l, r);
if (p != l) q.push({sum[stMax.query(l, p - 1)] - sum[i - 1], i, l, p - 1});
if (p != r) q.push({sum[stMax.query(p + 1, r)] - sum[i - 1], i, p + 1, r});
}
std::cout << ans << "\n";
return 0;
}
NOI 2010 Day1 T3 海拔
结论:每个点的海拔只会是 \(0\) 或 \(1\),且所有海拔为 \(0\) 的点形成一个连通块,所有海拔为 \(1\) 的点形成一个连通块。
证明:待补。
然后问题就转变为合理安排海拔为 \(0\) 的点和海拔为 \(1\) 的点,使得交界处的人流量最小。
显然这是一个最小割模型,而由于点数过大,所以普通的网络流无法通过,而观察到该图为一个平面图,所以平面图转对偶图跑最短路即可。
#include <bits/stdc++.h>
using i64 = long long;
constexpr int N = 3e5 + 5, INF = (1 << 30);
int n, tot;
int dis[N];
bool vis[N];
std::vector<std::pair<int, int>> G[N];
int id(int i, int j) {
return (i - 1) * n + j;
}
void addEdge(int u, int v, int w) {
G[u].push_back({v, w});
}
void dijkstra(int s) {
for (int i = 0; i <= n * n + 1; i++) dis[i] = INF, vis[i] = false;
dis[s] = 0;
std::priority_queue<std::pair<int, int>> q;
q.push({0, s});
while (!q.empty()) {
int u = q.top().second; q.pop();
if (vis[u]) continue;
vis[u] = true;
for (auto e : G[u]) {
int v = e.first, w = e.second;
if (dis[u] + w < dis[v]) {
dis[v] = dis[u] + w;
q.push({-dis[v], v});
}
}
}
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cin >> n;
int s = 0, t = n * n + 1;
for (int i = 0; i <= n; i++)
for (int j = 1; j <= n; j++) {
int w; std::cin >> w;
if (i == 0) addEdge(id(j, 1), t, w);
if (i == n) addEdge(s, id(j, n), w);
if (i > 0 && i < n) addEdge(id(j, i + 1), id(j, i), w);
}
for (int j = 1; j <= n; j++)
for (int i = 0; i <= n; i++) {
int w; std::cin >> w;
if (i == 0) addEdge(s, id(1, j), w);
if (i == n) addEdge(id(n, j), t, w);
if (i > 0 && i < n) addEdge(id(i, j), id(i + 1, j), w);
}
for (int i = 0; i <= n; i++)
for (int j = 1; j <= n; j++) {
int w; std::cin >> w;
if (i == 0) addEdge(t, id(j, 1), w);
if (i == n) addEdge(id(j, n), s, w);
if (i > 0 && i < n) addEdge(id(j, i), id(j, i + 1), w);
}
for (int j = 1; j <= n; j++)
for (int i = 0; i <= n; i++) {
int w; std::cin >> w;
if (i == 0) addEdge(id(1, j), s, w);
if (i == n) addEdge(t, id(n, j), w);
if (i > 0 && i < n) addEdge(id(i + 1, j), id(i, j), w);
}
dijkstra(0);
std::cout << dis[n * n + 1] << "\n";
return 0;
}
NOI 2010 Day2 T1 航空管制
题目中的起飞序号不得超过 \(k_i\) 这一条件有些棘手,我们尝试转化一下。
考虑时光倒流,即从后往前安排航班,这样上述条件便被我们转化成了航班 \(i\) 在时刻 \(k_i\) 后即可起飞,这样子我们处理问题就变得容易了。
对于第一问,我们倒序枚举时间,每次解锁一些航班,并在反图(因为时光倒流所以 \(a\) 早于 \(b\) 要变成 \(b\) 早于 \(a\))上用类似拓扑排序的做法取出航班即可。
对于第二问,我们枚举航班 \(i\),考虑在某一时刻取出航班时,我们希望航班 \(i\) 尽量晚点被取出(还是因为时光倒流),所以对于候选队列里的合法航班,我们尽量先不去选我们指定的航班 \(i\),而是选择别的航班,直到某时刻只能选择航班 \(i\),此时该时刻即为要求的最早时刻。
用双端队列维护合法航班,只需看队头与队尾是否不为 \(i\) 即可,时间复杂度 \(O(nm)\)。
#include <bits/stdc++.h>
using i64 = long long;
constexpr int N = 2e3 + 5;
int n, m;
int a[N], deg[N], rest[N], ans1[N], ans2[N];
bool unlock[N];
std::vector<int> G[N], tim[N];
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cin >> n >> m;
for (int i = 1; i <= n; i++) std::cin >> a[i], tim[a[i]].push_back(i);
for (int i = 1; i <= m; i++) {
int u, v;
std::cin >> u >> v;
deg[u]++;
G[v].push_back(u);
}
std::queue<int> q1;
for (int i = 1; i <= n; i++) unlock[i] = false, rest[i] = deg[i];
for (int t = n; t >= 1; t--) {
for (auto v : tim[t]) {
unlock[v] = true;
if (!rest[v]) q1.push(v);
}
int u = q1.front(); q1.pop();
ans1[t] = u;
for (auto v : G[u]) {
--rest[v];
if (unlock[v] && !rest[v]) q1.push(v);
}
}
for (int i = 1; i <= n; i++) {
std::deque<int> q2;
for (int j = 1; j <= n; j++) unlock[j] = false, rest[j] = deg[j];
for (int t = n; t >= 1; t--) {
for (auto v : tim[t]) {
unlock[v] = true;
if (!rest[v]) q2.push_back(v);
}
int u;
if (q2.front() != i) u = q2.front(), q2.pop_front();
else if (q2.back() != i) u = q2.back(), q2.pop_back();
else {
ans2[i] = t;
break;
}
for (auto v : G[u]) {
--rest[v];
if (unlock[v] && !rest[v]) q2.push_back(v);
}
}
}
for (int i = 1; i <= n; i++) std::cout << ans1[i] << " \n"[i == n];
for (int i = 1; i <= n; i++) std::cout << ans2[i] << " \n"[i == n];
return 0;
}
NOI 2010 Day2 T2 旅行路线
不会插头 dp。
NOI 2010 Day2 T3 成长快乐
不会。