Loading

NOI 2010 做题笔记

NOI 2010 Day1 T1 能量采集

观察到 \((0, 0)\)\((x, y)\) 连线上的整点个数正好是 \(\gcd(x, y) - 1\)(不包括端点),于是总能量损失即为:

\[\begin{aligned}{} & \sum\limits_{i = 1} ^ n\sum\limits_{j = 1} ^ m (2\times \gcd(i, j) - 1) \\ = &\ 2\times \sum\limits_{i = 1} ^ n\sum\limits_{j = 1} ^ m \gcd(i, j) - n\times m \end{aligned}\]

考虑如何计算二维 \(\gcd\) 求和,根据 \(\varphi * 1 = \text{id}\),反演得:

\[\begin{aligned}{} & \sum\limits_{i = 1} ^ n\sum\limits_{j = 1} ^ m \gcd(i, j) \\ = &\ \sum\limits_{i = 1} ^ n\sum\limits_{j = 1} ^ m \sum\limits_{d \mid \gcd(i, j)} \varphi(d) \\ = &\ \sum\limits_{d = 1} ^ n \varphi(d) \sum\limits_{i = 1} ^ n\sum\limits_{j = 1} ^ m [d\mid \gcd(i, j)] \\ = &\ \sum\limits_{d = 1} ^ n \varphi(d) \lfloor\dfrac{n}{d}\rfloor \lfloor\dfrac{m}{d}\rfloor \end{aligned}\]

于是线性筛出 \(\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 成长快乐

不会。

posted @ 2024-02-10 13:45  xhgua  阅读(20)  评论(1编辑  收藏  举报