Luogu SCP-J Mock Contest 2024 Review

0. 闲话

1.5h 完成所有题目,通过所有大样例。

这是赛时写的,看看能不能完成结论证明。

1. 带余除法

\(k=0\),则答案为 \(1\)

否则,答案为 \(\lfloor \frac{n}{k}\rfloor-\lfloor\frac{n}{k+1}\rfloor\)

\(k=0\) 时显然 \(q>k\),则 \(r=n\)

将题目中的式子转写得 \(n=qk+r\),其中 \(r\) 满足 \(0\le r<q\)。此时令 \(q=\lfloor\frac{n}{k}\rfloor\),则可取得 \(q\) 的最大值。为了使得上述式子不符合 \(r\) 的取值范围,不难发现需要找到最大的 \(q\) 使得 \(r\ge q\)。那么,令 \(n=q'(k+1)+r'\),其中 \(0\le r'<q'\),按照上述方法求出的 \(q'\) 则为最大的不符合条件的 \(q\)。不难发现这是一个左开右闭区间,直接相减即可得出答案。

时间复杂度 \(O(1)\),期望得分 \(100\) 分。

#include <cstdio>
using namespace std;
const int N = 1e5 + 10;
using ll = long long;
ll x, y, a, b;
void init_global()
{
}
void init_local()
{
    scanf("%lld%lld", &x, &y);
}
void run()
{
    a = (y ? x / y : x + 1);
    b = x / (y + 1);
    printf("%lld\n", a - b);
}
int main()
{
    int T = 1;
    scanf("%d", &T);
    init_global();
    while (T--)
    {
        init_local();
        run();
    }
}

2. 奖牌排序

难度略低于第一题。

按照题面,使用三种排序方式分别为所有小朋友排序,在进行 for 循环时顺便处理平局即可。

时间复杂度 \(O(n\log n)\),期望得分 \(100\) 分。

#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 2e5 + 10;
struct st
{
    int x, y, z, id;
} stu[N];
int n, res[N];
bool cmp1(st &a, st &b)
{
    if (a.x != b.x)
        return a.x > b.x;
    return a.id < b.id;
}
bool cmp2(st &a, st &b)
{
    if (a.y != b.y)
        return a.y > b.y;
    return a.id < b.id;
}
bool cmp3(st &a, st &b)
{
    if (a.z != b.z)
        return a.z > b.z;
    return a.id < b.id;
}
int main()
{
    // freopen("medal.in", "r", stdin);
    // freopen("medal.out", "w", stdout);
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
    {
        scanf("%d%d%d", &stu[i].x, &stu[i].y, &stu[i].z);
        stu[i].id = i;
        res[i] = n + 1;
    }
    sort(stu + 1, stu + n + 1, cmp1);
    for (int i = 1, p = 1; i <= n; i++)
    {
        if (stu[i].x != stu[i - 1].x)
            p = i;
        res[stu[i].id] = min(res[stu[i].id], p);
    }
    sort(stu + 1, stu + n + 1, cmp2);
    for (int i = 1, p = 1; i <= n; i++)
    {
        if (stu[i].y != stu[i - 1].y)
            p = i;
        res[stu[i].id] = min(res[stu[i].id], p);
    }
    sort(stu + 1, stu + n + 1, cmp3);
    for (int i = 1, p = 1; i <= n; i++)
    {
        if (stu[i].z != stu[i - 1].z)
            p = i;
        res[stu[i].id] = min(res[stu[i].id], p);
    }
    for (int i = 1; i <= n; i++)
    {
        printf("%d\n", res[i]);
    }
}

3. 三目运算

首先通过读题可以得到以下结论:

  • 分段常数表达式一定以数字结尾。
  • : 可以被认定为一个分段常数表达式不符合前置条件的部分的开始或是一个分段常数表达式的结尾。

通过上述的两个结论,我们可以方便地构建条件树,且该条件树是二叉树。

具体的,声明如下结构体:

struct nd
{
    bool var, gr;
    int ls, rs, inv;
} tr[M];

其中,var 代表该分段常数表达式是不是常量,inv 代表该分段常数表达式的值或条件值。

var\(0\) 的情况下gr 代表条件是大于/小于号,lsrs 分别表示满足/不满足条件时链接的分段常数表达式。

以下是构建条件树的过程,读者自行理解不难。

stk[++tp] = ++idx;
for (int i = 0; i < k; i++)
{
    if (s[i] == 'x')
    {
        tr[stk[tp]].var = true;
        continue;
    }
    if (s[i] == '<' or s[i] == '>')
    {
        tr[stk[tp]].gr = (s[i] == '>');
        continue;
    }
    if (isdigit(s[i]))
    {
        tps = ~tps ? tps : i;
        continue;
    }
    if (s[i] == '?')
    {
        tr[stk[tp]].inv = calc(tps, i);
        tps = -1;
        tr[stk[tp]].ls = ++idx;
        stk[++tp] = idx;
        continue;
    }
    tr[stk[tp]].inv = calc(tps, i);
    tps = -1;
    while (!tr[stk[tp]].var or tr[stk[tp]].rs)
        tp--;
    tr[stk[tp]].rs = ++idx;
    stk[++tp] = idx;
}
tr[stk[tp]].inv = calc(tps, k);

其中,calc(l,r) 的返回值是字符串下标在 \([l,r)\) 范围内的数字组成的数值。

对于每一个输入的整数 \(x\),我们可以从根节点开始根据节点信息往下遍历,若遍历到叶子节点,则答案为该叶子节点存储的数。

然而,根据题目条件,不难构造出一个所有条件全部嵌套在符合/不符合条件的分段常数表达式使得树高为 \(O(n)\),故暴力遍历无法通过此题。怎么办呢?

由于题目并不要求强制在线,所以考虑离线。

具体的,我们将所有的询问按 \(x\) 值从大到小排序。

不难发现,不管节点上存储的是大于或小于号,都有一个分界点,使得分界点一侧的询问会遍历到左子树,而另一侧的询问会遍历到右子树。可以利用二分搜索确定分界点位置,随后分治即可。

时间复杂度 \(O(|s|+n\log q+q)\),期望得分 \(100\) 分。

#include <algorithm>
#include <cctype>
#include <cstdio>
#include <iostream>
#include <string>
#include <utility>
using namespace std;
const int N = 1e5 + 10, M = 5e5 + 10;
using pii = pair<int, int>;
int n, m, res[N], stk[M], tp, idx, k, tps = -1;
pii qry[N];
struct nd
{
    bool var, gr;
    int ls, rs, inv;
} tr[M];
string s;
int calc(int l, int r)
{
    int res = 0;
    for (int i = l; i < r; i++)
    {
        res = res * 10 + s[i] - '0';
    }
    return res;
}
void rg(int x, int l, int r)
{
    if (l > r)
        return;
    if (!tr[x].var)
    {
        for (int i = l; i <= r; i++)
        {
            res[qry[i].second] = tr[x].inv;
        }
        return;
    }
    int mid = lower_bound(qry + l, qry + r + 1, pii{tr[x].inv + tr[x].gr, 0}) - qry;
    if (tr[x].gr)
    {
        rg(tr[x].ls, mid, r);
        rg(tr[x].rs, l, mid - 1);
        return;
    }
    rg(tr[x].ls, l, mid - 1);
    rg(tr[x].rs, mid, r);
}
int main()
{
    // freopen("expr.in", "r", stdin);
    // freopen("expr.out", "w", stdout);
    scanf("%d%d", &n, &m);
    cin >> s;
    k = s.size();
    for (int i = 1; i <= m; i++)
    {
        scanf("%d", &qry[i].first);
        qry[i].second = i;
    }
    sort(qry + 1, qry + m + 1);
    stk[++tp] = ++idx;
    for (int i = 0; i < k; i++)
    {
        if (s[i] == 'x')
        {
            tr[stk[tp]].var = true;
            continue;
        }
        if (s[i] == '<' or s[i] == '>')
        {
            tr[stk[tp]].gr = (s[i] == '>');
            continue;
        }
        if (isdigit(s[i]))
        {
            tps = ~tps ? tps : i;
            continue;
        }
        if (s[i] == '?')
        {
            tr[stk[tp]].inv = calc(tps, i);
            tps = -1;
            tr[stk[tp]].ls = ++idx;
            stk[++tp] = idx;
            continue;
        }
        tr[stk[tp]].inv = calc(tps, i);
        tps = -1;
        while (!tr[stk[tp]].var or tr[stk[tp]].rs)
            tp--;
        tr[stk[tp]].rs = ++idx;
        stk[++tp] = idx;
    }
    tr[stk[tp]].inv = calc(tps, k);
    rg(1, 1, m);
    for (int i = 1; i <= m; i++)
        printf("%d\n", res[i]);
}

4. 配对序列

本场比赛最有难度的题,但是比较无脑。

\(d_{i,0/1}\) 表示结尾为 \(i\) 的不完整/完整配对序列的最大长度除以二向下取整的值。其中,定义不完整配对序列为长度为奇数但满足配对序列其余要求的序列。

\(m=\max a_i\)

以下的转移前提为从前向后遍历到一个 \(a_x=i\)

首先考虑如何得到 \(d_{i,1}\) 的值。显然,\(d_{i,1}\) 只能由 \(d_{i,0}\) 转移,因为此时处在偶数位,前一位必须与 \(i\) 相等。所以,\(d_{i,1}\leftarrow \max(d_{i,1},d_{i,0}+1)\)

接下来考虑如何得到 \(d_{i,0}\)。此时处在奇数位,根据题目定义,这个位不能与前一位相同。并且,一个不完整配对序列去掉最后一个元素后必须为空序列或一个完整配对序列。

所以,

\[d_{i,0}=\max_{j=1}^m d_{j,1} \]

以上两个转移需同时进行

然而,注意到 \(d_{i,0}\) 的转移速度过慢,需要优化。

利用 multiset 容器的自动排序特性,我们只需要将所有的 \(d_{x,1}\) 值存储到 multiset 中,再查询除了 \(d_{i,1}\) 外的所有值的最大值即可。

时间复杂度 \(O(n\log m)\),期望得分 \(100\) 分。

#include <algorithm>
#include <cstdio>
#include <functional>
#include <set>
using namespace std;
const int N = 5e5 + 10;
int n, dp[N][2], res;
multiset<int, greater<>> ms;
bool vis[N];
int main()
{
    // freopen("pairing.in", "r", stdin);
    // freopen("pairing.out", "w", stdout);
    scanf("%d", &n);
    ms.emplace(0);
    for (int i = 1, x, tmp; i <= n; i++)
    {
        scanf("%d", &x);
        if (!vis[x])
        {
            dp[x][0] = *ms.begin();
            ms.emplace(0);
            vis[x] = true;
            continue;
        }
        tmp = max(dp[x][0], (*ms.begin() == dp[x][1] ? *next(ms.begin()) : *ms.begin()));
        ms.erase(ms.find(dp[x][1]));
        dp[x][1] = max(dp[x][0] + 1, dp[x][1]);
        ms.emplace(dp[x][1]);
        dp[x][0] = tmp;
        res = max(res, dp[x][1]);
    }
    printf("%d\n", res << 1);
}

5. 后记

确实是 \(400\) 分,没有挂分。

评价是这次 J 还是太保守了。

posted @ 2024-10-13 12:03  丝羽绫华  阅读(161)  评论(0编辑  收藏  举报