排名

Solve: 5 (ADEFJ)

Penalty: 767

Rank: 122/343 (35%)

题解

D - Exam Results

题意

给定 $n$ 个学生,第 $i$ 个学生的分数要么为 $a_i$,要么为 $b_i$。

给定整数 $P \in [1,100]$,如果最高分为 $x$,则分数 $\geq x * \frac{P}{100}$ 的学生可以及格。

求所有可能的情况中,及格学生的最大数量?

其中,$1 \leq n \leq 2 \times 10^5, 1 \leq a_i \leq b_i \leq 10^9$。

题解

直接枚举可能出现的最高分,我们知道要保证 $x$ 为最高分,必须有 $x \geq \max \{a_i\}$。

如果我们从小到大枚举 $x$,会发现 $[x * \frac{P}{100}, x]$ 是一个滑动的窗口。

所以本题只要先把所有的 $a_i, b_i$ 放在一起,然后 sort 一下,从 $\max \{a_i\}$ 开始枚举 $x$,在窗口滑动的过程中,利用类似于莫队的思想维护及格人数即可。

代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5+7;

ll n,p,ans = 0;
struct Point {
    ll x, id;
} arr[maxn*2];

int cnt[maxn];
ll cur = 0;

void add(int x) {
    cnt[x]++;
    if (cnt[x] == 1) cur++;
}

void minu(int x) {
    cnt[x]--;
    if (!cnt[x]) cur--;
}

int main() {
    int T; read(T);
    for (int t = 1; t <= T; t++) {
        ans = 0;
        read(n), read(p);
        cur = 0;

        ll lmax = 0;
        for (int i = 1; i <= n; i++) {
            ll a,b; read(b); read(a);
            a *= 100LL, b *= 100LL;
            lmax = max(lmax, a);
            arr[i*2 - 1] = {a, i}, arr[i*2] = {b, i};
        }
        sort(arr+1, arr+2*n+1, [](auto a, auto b) {
            return a.x < b.x;
        });

        int st = 0;
        for (int i = 1; i <= 2*n; i++) {
            if (arr[i].x >= lmax) {
                st = i;
                break;
            }
        }

        int lptr = 0, rptr = 0;
        for (int i = st; i <= 2*n; i++) {
            ll L = arr[i].x / 100 * p, R = arr[i].x;
            while (rptr + 1 <= 2*n && arr[rptr+1].x <= R) {
                rptr++;
                add(arr[rptr].id);
            }
            while (lptr + 1 <= rptr && arr[lptr+1].x < L) {
                lptr++;
                minu(arr[lptr].id);
            }
            ans = max(ans, cur);
        }
        printf("Case #%d: %lld\n", t, ans);
        fill(cnt, cnt+n+5, 0);
    }
}

J - Kingdom’s Power

题意

给定一棵 $n$ 个节点的有根树,根为 $1$。$1$ 的位置有无限个飞船。

每一步操作中,可以选定任意一个飞船,让它走向它的一个邻居。

求最少操作数使得所有节点被访问至少一次?

其中,$n \leq 10^6$

题解

猜想 $1$:

任意时刻,只有一个飞船是 有用的

要么我们动这个飞船,要么我们动 $1$ 里面的无限个飞船。如果我们在访问某个子树时没有使用这个飞船,那么这个飞船就再也不会被用到了。

这个猜想是正确的(然而我也不确定怎么严格证明)。

猜想 $2$:

我们可以利用贪心,从 $1$ 开始 dfs,根据 最大深度 的顺序,从小到大 dfs 它的 child。

这个贪心也是正确的,感性理解的话可以考虑最简单的情况:

img

这里从 $2$ 出发的话应该是先访问 $3,4$ ,最后访问 $5$。


利用以上贪心就可以写出一个 $O(n\log n)$ 的解法:

直接维护当前 有用 飞船的位置 $x$,然后每次要移动到下一个点 $y$ 的时候,比较一下 $d(x,y)$ 和 $d(1,y)$ 的距离即可,然后更新 $x=y$。

比赛的时候就是这样写的,然后愉快的T了。


$O(n)$ 正解:

我们发现,对于一个节点 $u$,我们定义 $dp[u]$ 为:访问完 $u$ 的整个子树,并且飞船不回来(即停留在访问时的哪个节点)所需要的步数。

$dp[u]$ 怎么转移?

对于 $u$ 的每个child $v$,除了最后一个访问的 $v$ 以外,其他的 child 都需要访问结束以后,再回到 $u$。

回到 $u$ 的飞船,要么是访问完 $v$ 的那个飞船再走回来,要么我们直接从 $1$ 派一个飞船到 $u$,看哪个距离更近就好了。

所以有:

$$dp[u] = \sum\limits_v \{1 + dp[v] + \min \{maxdep[v] - dep[u], dep[u]\} \}$$

注意到这个转移把 最后一个child $v$ 需要回到 $u$ 的贡献也算上了,且我们知道最后一个child $v$ 访问完毕时,停留的节点深度为 $maxdep[u]$,所以我们需要减掉这个贡献:

$$dp[u] = dp[u] - \min \{maxdep[u] - dep[u], dep[u]\}$$

代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6+3;

int T, n;
int dep[maxn], maxdep[maxn], head[maxn], ecnt = 1;
ll dp[maxn];
struct Edge {
    int to, nxt;
} edges[maxn];
void addEdge(int u, int v) {
    Edge e = {v, head[u]};
    head[u] = ecnt;
    edges[ecnt++] = e;
}

void dfs1(int u) {
    maxdep[u] = dep[u];
    for (int e = head[u]; e; e = edges[e].nxt) {
        int to = edges[e].to;
        dep[to] = dep[u] + 1;
        dfs1(to);
        maxdep[u] = max(maxdep[u], maxdep[to]);
    }
}

void dfs2(int u) {
    for (int e = head[u]; e; e = edges[e].nxt) {
        int to = edges[e].to;
        dfs2(to);
        dp[u] += (dp[to] + min(maxdep[to] - dep[u], dep[u]) + 1);
    }
    dp[u] -= (min(maxdep[u] - dep[u], dep[u]));
}

int main() {
    read(T);
    for (int t = 1; t <= T; t++) {
        read(n);
        for (int u = 2; u <= n; u++) {
            int v; read(v);
            addEdge(v, u);
        }

        dep[1] = 0;
        dfs1(1);
        dfs2(1);

        printf("Case #%d: %lld\n",t, dp[1]);
        fill(maxdep, maxdep+n+2, 0);
        fill(dp, dp+n+2, 0);
        ecnt = 1;
        fill(head, head+n+2, 0);
    }

}

H - Interstellar Hunter

题意

在一个无限大的二维平面中,我们的初始位置为 $(0,0)$,现在给定 $Q$ 个询问,每次询问为两种格式:

$1 ~ x ~ y$:获得 $(x,y)$ 的跳跃能力。

$2 ~ x ~ y ~ w$:在 $(x,y)$ 的位置出现一个价值为 $w$ 的宝藏,可以选择走到这里拿宝藏。

当我们拥有 $(a,b)$ 的跳跃能力时,可以将我们的坐标 $(x,y)$ 变为 $(x+a,y+b)$ 或者 $(x-a,y-b)$。

求最大的宝藏价值总和?

其中,$Q \leq 10^6$。

题解

获得一个跳跃能力以后,我们每次移动就多了一种移动方式。

注意到我们当前坐标为多少不会影响答案,因为这个获得的跳跃能力,相当于在一个 整数域 中维护一些向量组成的 $span$。每次都看作是从 $(0,0)$ 出发就好了。

现在问题就在于如何维护向量集合了。

有一个结论:

如果集合中有 $\geq 2$ 个不共线向量,则可以用 $(d,0), (x_2,y_2)$ 来表示这个向量集合的 $span$,其中 $d \leq x_2$。

证明?我也不会。

现在考虑一下如果我们加入了一个新的向量 $(x,y)$,怎么更新这些向量?

在整数域下,就需要保证 $d$ 尽量小,而 $y_2$ 也尽量小。

所以我们可以先用 $(x_2,y_2)$ 与 $(x,y)$ 构造出一个 $(x’,0)$ 的向量,然后更新 $d = \gcd(d,x’)$。

要令 $y_2$ 尽量小,则我们用 $(x_2,y_2)$ 与 $(x,y)$ 构造出 $(x’', \gcd(y_2,y))$。

这个构造的过程,就是一个 linear combination 的过程。

那么怎么构造呢?注意到是在整数域下,所以用 $exgcd$!

所以设:

$$ay_2 + by = 0$$

令 $g = \gcd(y_2,y)$,就有:

$$a\frac{y_2}{g} + b\frac{y}{g} = 0$$

所以 $a = -\frac{y}{g}, b = \frac{y_2}{g}$,就有:

$$a(x_2,y_2) + b(x,y) = (x’, 0)$$

然后同理,解出

$$a(x_2,y_2) + b(x,y) = (x’', \gcd(y_2,y))$$

解出来以后,令 $x_2 = x’', y_2 = \gcd(y_2,y)$ 即可。

• 注意需要单独处理 $d,x,y=0$ 的情况。

代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5+3;

int T, Q;
ll curx, cury;
ll exgcd(ll a, ll b, ll& x, ll& y) {
    if (!b) {
        x = 1, y = 0;
        return a;
    }
    ll g = exgcd(b, a%b, x, y);
    ll x2 = x, y2 = y;
    x = y2, y = x2 - a/b * y2;
    return g;
}

ll gcd(ll a, ll b) {
    if (!b) return a;
    return gcd(b, a%b);
}

int cnt = 0;
ll d = 0, x2, y2;

int main() {
    read(T);
    for (int tt = 1; tt <= T; tt++) {
        read(Q);
        d = x2 = y2 = 0;
        ll ans = 0;
        while (Q--) {
            int t; ll x,y,w;
            read(t);
            read(x); read(y);

            if (t == 1) {
                if (y == 0) {
                    d = gcd(d, x);
                    continue;
                }
                ll a,b;
                ll g = exgcd(y, y2, a, b);  // 因为 y2 = 0 无所谓,所以 g != 0
                ll xx = abs(-y2 * x + y * x2) / g;
                d = gcd(d, xx);

                y2 = g;
                x2 = a * x + b * x2;

                if (d)
                    x2 = (x2 % d + d) % d;
            } else {
                read(w);
                if (y == 0) {
                    if (!d && !x) ans += w;
                    if (d > 0 && x % d == 0) ans += w;
                } else {
                    if (y2 && y % y2 == 0) {
                        ll c = y / y2;
                        x -= c * x2;
                        if (d && x % d == 0) ans += w;
                        if (!d && !x) ans += w;
                    }
                }
            }
        }
        printf("Case #%d: %lld\n", tt, ans);
    }
}