中山纪念中学游记

啊,神仙学校的神仙环境

自从去年学长们去纪中培训后,我就已经听闻这所学校的优美环境了。这所学校的建筑非常吸引我,色调和复古的设计都非常符合我的口味。

新的科学馆比旧科学馆要大气不少

不得不承认,在如此优美的环境下学习和生活,是何等的美事啊!我有幸在这个地方停留了三个星期,呆在机房里和全国的神仙们一起学习,无论是学习气氛还是说校园环境,我觉得都是非常让人满意的,这也可能就是为什么这个学校神仙这么多的原因吧,神仙都待在神仙之所。

这边的训练还是非常高强度的,每天早上 7:30 的比赛一直打到 11:30 ,中间屁股都不挪一下。晚自习要等到 22:00 才放,中途没有任何休息,全都是连贯的,又加上题目神仙,经常会感到疲惫。机房的外景些许可以缓解一下视觉的压力。

每天做题累了就会往外看

优秀的食堂体验

纪中有三个巨大的食堂:连在一起的一、二食堂,还有在西边的、靠近男生宿舍的三食堂。每一个食堂至少都有两层,其中二食堂还设有茶餐厅,体验非常出众,排队的同学挺多的。

一二食堂的壹加壹离我们宿舍比较近,所以一般我都会在晚自习结束后,趁着壹加壹还没关门就赶紧去买泡面或者是其他的可以用作夜宵的食物。

¥31的晚饭,茶餐厅还可以,比那些什么都没有的学校不知道高到哪里去了

糟糕的宿舍体验

作为一个在南方生活了16年的小朋友,自从来了中山,我才知道什么才叫“南方风情”:老鼠、飞蛾、蚊子和蟑螂随时在宿舍迎接你;屋内过重的“地气”非常的难闻,被子里有动物尿液味道,而且晚上睡觉时非常的湿,有种粘稠的感觉;变换无常的天气随时会让你的袜子处在一个“明天才能干”的状态。总之,推荐短期集训的 OIer 尽量选择酒店。不过附近的酒店也好奇怪,空调有越吹越热的毛病,可能是个例吧。

我不放图了,免得大家呕吐。

图集

这是我见过的体育场中设计最好的
靠西南门的一个休闲去处
从宿舍到初中部的一条路径

U63113:入侵 – 又名 「XG 的数学题」

题目背景

众所周知,XG_Zepto 是一位来自 FZOI 的神仙。他觉得眼下的比赛太简单了,就用扫雷的时间出了一道数学题给 kal0rona。kal0rona 太菜了,看不懂题也不会写,之后求救于你了。

题目思路

很好的一道计数 DP,出自神仙组长 XG_Zepto 之手。首先,设\(f[i][j]\)为长度为\(i\),换弹次数为\(j\)时满足条件的排列数量。之后,考虑枚举第\(i\)个数字出现的位置\(k\),对于位置\(k\)后面的数字是不会有特殊贡献的(贡献为\((i-k)!\)),而对于前面的数,可以进行枚举前一段进行转移,一共有\({k-1}\choose{i-1}\)种选数字的方案和\(f[i-1][j-1\)种排列,乘起来即可,也就是:

\[ F[i][j] = \sum_{k = 1}^{i}f[k-1][j-1]* {{k-1} \choose {i-1}} *(i-1)! \]

可以化简,并用前缀和优化:

\[ F[i][j] = (i-1)!\sum_{k=1}^{i}\frac{f[k-1][j-1]}{(k-1)!} \]

代码

// U63113.cpp
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAX_N = 5050, mod = 998244353;
ll f[MAX_N], prefix[MAX_N], level[MAX_N], inv[MAX_N], n, m;
ll quick_pow(ll bas, ll tim)
{
    ll ans = 1;
    while (tim)
    {
        if (tim & 1)
            ans = ans * bas % mod;
        bas = bas * bas % mod;
        tim >>= 1;
    }
    return ans;
}
int main()
{
    scanf("%lld%lld", &n, &m);
    level[0] = 1, inv[0] = 1;
    for (int i = 1; i <= n; i++)
        level[i] = level[i - 1] * i % mod;
    inv[n] = quick_pow(level[n], mod - 2);
    for (int i = n; i >= 1; i--)
        inv[i - 1] = inv[i] * i % mod;
    for (int i = 0; i <= n; i++)
        prefix[i] = 1;
    for (int j = 1; j <= m; j++)
    {
        for (int i = j; i <= n; i++)
            f[i] = prefix[i - 1] * level[i - 1] % mod;
        for (int i = j; i <= n; i++)
            prefix[i] = f[i] * inv[i] % mod;
        for (int i = j + 1; i <= n; i++)
            prefix[i] = (prefix[i] + prefix[i - 1]) % mod;
    }
    printf("%lld", f[n]);
    return 0;
}

P1072:Hankson 的趣味题题解

超级简洁的做法

看到这篇题解之后立马 A 掉了,真的很简洁的一个做法。

我们有\(gcd(x,a_0)=a_1\),根据结论\(gcd(a,b)=c \to gcd(\frac{a}{c},\frac{b}{c})=1\),可以推出:

\[gcd(\frac{x}{a_1},\frac{a_0}{a_1})\]

另一个式子\(lcm(x,b_0)=b_1\)可以转换为:

\[ lcm(x,b_0)=b_1 \\ gcd(x,b_0)*lcm(x,b_0)=xb_0 \\ gcd(x,b_0)=\frac{xb_0}{b_1} \\ gcd(\frac{b_1}{b_0},\frac{b_1}{x})=1 \]

可以看出,\(x\)为\(b_1\)的一个约数,\(a_1\)是\(x\)的一个约束,根据这两个条件和两个\(gcd\)条件,进行枚举即可。

代码

// CH3201.cpp
#include <bits/stdc++.h>
using namespace std;
int n, a0, a1, b0, b1;
int gcd(int a, int b) { return (b == 0) ? a : gcd(b, a % b); }
int main()
{
    scanf("%d", &n);
    while (n--)
    {
        int ans = 0;
        scanf("%d%d%d%d", &a0, &a1, &b0, &b1);
        int p = b1 / b0, q = a0 / a1;
        for (int x = 1; x * x <= b1; x++)
            if (b1 % x == 0)
            {
                if (x % a1 == 0 && gcd(p, b1 / x) == 1 && gcd(q, x / a1) == 1)
                    ans++;
                int y = b1 / x;
                if (x == y)
                    continue;
                if (y % a1 == 0 && gcd(p, b1 / y) == 1 && gcd(q, y / a1) == 1)
                    ans++;
            }
        printf("%d\n", ans);
    }
    return 0;
}

BZOJ1257:「CQOI2007」余数之和题解

神仙思路与证明

这道题可以将式子化简为:

\[ nk-\sum_{i=1}^{n} \lfloor \frac{k}{i} \rfloor * i \]

第一部分乘积即可,第二部分直觉告诉我们\(\lfloor \frac{k}{i} \rfloor\)在一段连续的\(i\)上会相等,也就是说,是分段的。每一段的和可以直接用等差数列来求就行。我们来探究一下段区间的左右端点。我们假设目标区间是\([i,?]\),那么这一段区间的值肯定就是\(\lfloor \frac{k}{i} \rfloor\),反求一下那么右端点就是\( \lfloor \frac{k}{\lfloor \frac{k}{i} \rfloor} \rfloor \)。可以证明右端点大于等于左端点。等差数列求和即可。

代码

// BZ1257.cpp
#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll n, k;
int main()
{
    scanf("%lld%lld", &n, &k);
    ll ans = n * k, gx;
    for (int x = 1; x <= n; x = gx + 1)
    {
        gx = (k / x) ? min(k / (k / x), n) : n;
        // x ~ gx same;
        ans -= (k / x) * (gx - x + 1) * (gx + x) / 2;
    }
    printf("%lld", ans);
    return 0;
}

「Fortuna OJ」绕圈跑题解

思路

好神仙啊。

这道题思路非常的巧妙。答案很容易可以知道,为

\[ ans = \lfloor \sum_{i=1}^{n} \frac{L*C*speed[i]}{maxSpeed*C} \rfloor \]

化简得:

\[ ans = \lfloor \sum_{i=1}^{n} \frac{L*speed[i]}{maxSpeed} \rfloor \]

如果我们统计括号内的实数集和的话,会出现一些只有在计算机上才会出现的恶心误差,所以我们可以考虑分开取整再求和。这里有一个小技巧,对于两个数而言:

\[ \lfloor a – b \rfloor = \begin{cases} \lfloor a \rfloor – \lfloor b \rfloor, a的小数部分 \leq b的小数部分 \\ \lfloor a \rfloor – \lfloor b \rfloor – 1, a的小数部分>b的小数部分 \end{cases} \]

在判断小数部分的时候,我们可以把小数部分之间的大小关系转化为余数之间的大小关系进行判断,答案默认减掉那个取整的1,然后用树状数组补全那些被误删的取整项。

代码

// FOJ2930.cpp
#include <bits/stdc++.h>
#define ll long long
#define lowbit(p) (p & -p)
using namespace std;
const ll MAX_N = 1e5 + 200;
ll n, l, c, spd[MAX_N], mxspd, f[MAX_N], tree[1000100];
void update(int num)
{
    num++;
    while (num <= mxspd)
        tree[num] += 1, num += lowbit(num);
}
ll sum(int num)
{
    num++;
    ll ans = 0;
    while (num)
        ans += tree[num], num -= lowbit(num);
    return ans;
}
int main()
{
    scanf("%lld%lld%lld", &n, &l, &c);
    for (int i = 1; i <= n; i++)
        scanf("%lld", &spd[i]);
    sort(spd + 1, spd + 1 + n), mxspd = spd[n];
    for (int i = 1; i <= n; i++)
        f[i] = (l * spd[i]) / mxspd;
    update((l * spd[1]) % mxspd);
    ll prefix = f[1], ans = 0;
    for (int i = 2; i <= n; i++)
    {
        prefix += f[i];
        ans += i * f[i] - prefix - i;
        update((l * spd[i]) % mxspd);
        ans += sum((l * spd[i]) % mxspd);
    }
    printf("%lld", ans);
    return 0;
}

P2501:「HAOI2006」数字序列题解

主要思路

这道题算是非常的毒瘤了。

首先我们来看第一问,根据 XG 巨佬的话,是非常明显的。我们在序列读入时减去元素自己的序号,找 LIS 长度即可。代码:

scanf("%lld", &n);
for (ll i = 1; i <= n; i++)
    scanf("%lld", &arr[i]), arr[i] -= i, dst[i] = INF;
dst[0] = -INF, dp[1] = 1, len = 1, dst[1] = arr[1], arr[0] = -INF, arr[++n] = INF;
for (ll i = 2; i <= n; i++)
{
    ll addr = upper_bound(dst, dst + 1 + len, arr[i]) - dst;
    len = max(len, addr);
    dp[i] = addr;
    dst[addr] = min(dst[addr], arr[i]);
}
printf("%lld\n", n - dp[n]);

第二问我们引出一个小的结论:如果要把区间\([l\dots r]\)之间变得“好看”,那么这个子区间内一定分为两段:高度为\(arr[l]\)的一段和高度\(arr[r]\)的一段。注意,此时\(arr[]\)内的元素已经剪去了元素编号。

所以这第二问就是一道区间 DP,时间复杂度为\(O(n^3)\)。不过好在序列随机,且我们可以使用邻接表预先存好那些可以处理的区间,然后进行 DP。

代码

// P2501.cpp
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define ll long long
using namespace std;
const ll MX_N = 35020, INF = 0x3f3f3f3f;
ll head[MX_N], arr[MX_N], n, len, dst[MX_N], dp[MX_N], M_curt, f[MX_N], sum1[MX_N], sum2[MX_N];
struct edge
{
    ll to, nxt;
} edges[MX_N << 1];
void addpath(ll src, ll dst)
{
    edges[M_curt].to = dst, edges[M_curt].nxt = head[src];
    head[src] = M_curt++;
}
int main()
{
    memset(head, -1, sizeof(head));
    scanf("%lld", &n);
    for (ll i = 1; i <= n; i++)
        scanf("%lld", &arr[i]), arr[i] -= i, dst[i] = INF;
    dst[0] = -INF, dp[1] = 1, len = 1, dst[1] = arr[1], arr[0] = -INF, arr[++n] = INF;
    for (ll i = 2; i <= n; i++)
    {
        ll addr = upper_bound(dst, dst + 1 + len, arr[i]) - dst;
        len = max(len, addr);
        dp[i] = addr;
        dst[addr] = min(dst[addr], arr[i]);
    }
    printf("%lld\n", n - dp[n]);
    for (ll i = n; i >= 0; i--)
        addpath(dp[i], i), f[i] = INF;
    f[0] = 0;
    for (ll i = 1; i <= n; i++)
        for (ll e = head[dp[i] - 1]; e != -1; e = edges[e].nxt)
        {
            ll v = edges[e].to;
            if (v > i)
                break;
            if (arr[v] > arr[i])
                continue;
            for (ll k = v; k <= i; k++)
                sum1[k] = abs(arr[k] - arr[v]), sum2[k] = abs(arr[k] - arr[i]);
            for (ll k = v + 1; k <= i; k++)
                sum1[k] += sum1[k - 1], sum2[k] += sum2[k - 1];
            for (ll k = v; k <= i - 1; k++)
                f[i] = min(f[i], f[v] + sum1[k] - sum1[v] + sum2[i] - sum2[k]);
        }
    printf("%lld", f[n]);
    return 0;
}